diff --git a/.github/ArgumentUsage.xlsx b/.github/ArgumentUsage.xlsx
index 6a64457..9fb1cf4 100644
Binary files a/.github/ArgumentUsage.xlsx and b/.github/ArgumentUsage.xlsx differ
diff --git a/.github/scripts/update_argument_usage.R b/.github/scripts/update_argument_usage.R
new file mode 100644
index 0000000..cfbd0e6
--- /dev/null
+++ b/.github/scripts/update_argument_usage.R
@@ -0,0 +1,675 @@
+#!/usr/bin/env Rscript
+# Refresh .github/ArgumentUsage.xlsx from the current package source.
+# The workbook is treated as a template so row order, column order, widths,
+# styles, and legacy labels are preserved where possible. Function signatures
+# come from R/, argument descriptions come from man/*.Rd, and the workbook is
+# only rewritten when the generated XML actually changes.
+
+args <- commandArgs(trailingOnly = TRUE)
+xlsx_path <- if (length(args) >= 1) args[[1]] else ".github/ArgumentUsage.xlsx"
+
+normalize_ws <- function(x) {
+ trimws(gsub("[[:space:]]+", " ", x))
+}
+
+normalize_arg_key <- function(x) {
+ x <- normalize_ws(x)
+ x[x %in% c("...", "\u2026", "\\dots", "\\dots{}")] <- "..."
+ x
+}
+
+xml_escape <- function(x) {
+ x <- gsub("&", "&", x, fixed = TRUE)
+ x <- gsub("<", "<", x, fixed = TRUE)
+ x <- gsub(">", ">", x, fixed = TRUE)
+ x <- gsub("\"", """, x, fixed = TRUE)
+ x <- gsub("'", "'", x, fixed = TRUE)
+ x
+}
+
+xml_unescape <- function(x) {
+ x <- gsub("<", "<", x, fixed = TRUE)
+ x <- gsub(">", ">", x, fixed = TRUE)
+ x <- gsub(""", "\"", x, fixed = TRUE)
+ x <- gsub("'", "'", x, fixed = TRUE)
+ x <- gsub("&", "&", x, fixed = TRUE)
+ x
+}
+
+read_text <- function(path) {
+ paste(readLines(path, warn = FALSE, encoding = "UTF-8"), collapse = "")
+}
+
+write_text <- function(path, text) {
+ writeLines(text, path, useBytes = TRUE)
+}
+
+extract_matches <- function(text, pattern) {
+ hits <- gregexpr(pattern, text, perl = TRUE)
+ if (hits[[1]][1] == -1) {
+ return(character())
+ }
+ regmatches(text, hits)[[1]]
+}
+
+extract_first <- function(text, pattern) {
+ match <- regexec(pattern, text, perl = TRUE)
+ groups <- regmatches(text, match)[[1]]
+ if (length(groups) < 2) {
+ return(NA_character_)
+ }
+ groups[[2]]
+}
+
+col_from_number <- function(index) {
+ stopifnot(index >= 1)
+ result <- character()
+ while (index > 0) {
+ remainder <- (index - 1) %% 26
+ result <- c(intToUtf8(65 + remainder), result)
+ index <- (index - 1) %/% 26
+ }
+ paste(result, collapse = "")
+}
+
+col_to_number <- function(label) {
+ chars <- utf8ToInt(label)
+ total <- 0L
+ for (char in chars) {
+ total <- total * 26L + (char - 64L)
+ }
+ total
+}
+
+get_named_value <- function(values, name) {
+ if (name %in% names(values)) {
+ return(unname(values[[name]]))
+ }
+ ""
+}
+
+rd_flatten <- function(node) {
+ if (is.null(node)) {
+ return("")
+ }
+ if (is.character(node)) {
+ return(paste(node, collapse = ""))
+ }
+ if (!is.list(node)) {
+ return(as.character(node))
+ }
+ paste(vapply(node, rd_flatten, character(1)), collapse = "")
+}
+
+parse_function_specs <- function(r_dir = "R") {
+ # Discover top-level package functions directly from source without
+ # evaluating their bodies. Dot-prefixed lifecycle hooks are skipped.
+ files <- sort(list.files(r_dir, pattern = "[.][Rr]$", full.names = TRUE))
+ specs <- list()
+
+ for (file in files) {
+ expressions <- as.list(parse(file = file, keep.source = FALSE))
+ for (expr in expressions) {
+ if (!is.call(expr) || length(expr) < 3) {
+ next
+ }
+ if (!identical(as.character(expr[[1]]), "<-")) {
+ next
+ }
+ if (!is.symbol(expr[[2]]) || !is.call(expr[[3]])) {
+ next
+ }
+ if (!identical(expr[[3]][[1]], as.name("function"))) {
+ next
+ }
+
+ function_name <- as.character(expr[[2]])
+ if (startsWith(function_name, ".")) {
+ next
+ }
+
+ function_object <- eval(expr[[3]], envir = baseenv())
+ formal_names <- names(formals(function_object))
+ if (is.null(formal_names)) {
+ formal_names <- character()
+ }
+ specs[[function_name]] <- formal_names
+ }
+ }
+
+ specs
+}
+
+parse_rd_arguments <- function(function_names, man_dir = "man") {
+ # Extract the canonical argument text from each generated Rd file.
+ docs <- list()
+
+ for (function_name in function_names) {
+ rd_path <- file.path(man_dir, paste0(function_name, ".Rd"))
+ if (!file.exists(rd_path)) {
+ stop("Missing Rd file for function: ", function_name)
+ }
+
+ rd <- suppressWarnings(tools::parse_Rd(rd_path))
+ rd_tags <- vapply(rd, function(node) attr(node, "Rd_tag"), character(1))
+ argument_section_index <- which(rd_tags == "\\arguments")
+
+ if (!length(argument_section_index)) {
+ docs[[function_name]] <- stats::setNames(character(), character())
+ next
+ }
+
+ argument_section <- rd[[argument_section_index[[1]]]]
+ item_tags <- vapply(argument_section, function(node) attr(node, "Rd_tag"), character(1))
+ items <- argument_section[item_tags == "\\item"]
+
+ descriptions <- character()
+ argument_names <- character()
+
+ for (item in items) {
+ argument_name <- normalize_arg_key(rd_flatten(item[[1]]))
+ description <- normalize_ws(rd_flatten(item[[2]]))
+
+ if (!nzchar(argument_name)) {
+ next
+ }
+
+ argument_names <- c(argument_names, argument_name)
+ descriptions <- c(descriptions, description)
+ }
+
+ if (anyDuplicated(argument_names)) {
+ stop("Duplicate argument documentation in ", rd_path)
+ }
+
+ docs[[function_name]] <- stats::setNames(descriptions, argument_names)
+ }
+
+ docs
+}
+
+validate_documentation <- function(function_specs, rd_docs) {
+ # Fail fast if a function signature and its Rd file disagree.
+ for (function_name in names(function_specs)) {
+ formal_keys <- normalize_arg_key(function_specs[[function_name]])
+ doc_keys <- names(rd_docs[[function_name]])
+
+ missing_docs <- setdiff(formal_keys, doc_keys)
+ extra_docs <- setdiff(doc_keys, formal_keys)
+
+ if (length(missing_docs) || length(extra_docs)) {
+ message("Formal arguments: ", paste(formal_keys, collapse = ", "))
+ message("Documented args: ", paste(doc_keys, collapse = ", "))
+ stop(
+ "Function documentation mismatch for ", function_name,
+ if (length(missing_docs)) paste0("; missing docs: ", paste(missing_docs, collapse = ", ")) else "",
+ if (length(extra_docs)) paste0("; extra docs: ", paste(extra_docs, collapse = ", ")) else ""
+ )
+ }
+ }
+}
+
+parse_shared_strings <- function(path) {
+ # The workbook uses shared strings for all populated cells, so these need
+ # to be decoded before the existing sheet can be matched and rebuilt.
+ xml <- read_text(path)
+ string_nodes <- extract_matches(xml, "(?s).*?")
+
+ vapply(string_nodes, function(node) {
+ text_nodes <- extract_matches(node, "(?s).*?")
+ pieces <- vapply(text_nodes, function(text_node) {
+ xml_unescape(extract_first(text_node, "(?s)^(.*?)$"))
+ }, character(1))
+ paste(pieces, collapse = "")
+ }, character(1))
+}
+
+parse_sheet_template <- function(path, shared_strings) {
+ # Read the existing sheet so current argument rows and function columns can
+ # be preserved instead of re-sorted from scratch on each run.
+ sheet_xml <- read_text(path)
+ row_nodes <- extract_matches(sheet_xml, "(?s)]*>.*?
")
+ rows <- list()
+
+ for (row_node in row_nodes) {
+ row_number <- as.integer(extract_first(row_node, 'r="([0-9]+)"'))
+ cell_nodes <- extract_matches(row_node, "(?s)]*>.*?")
+ row_values <- character()
+
+ for (cell_node in cell_nodes) {
+ ref <- extract_first(cell_node, 'r="([A-Z]+[0-9]+)"')
+ cell_type <- extract_first(cell_node, 't="([^"]+)"')
+ raw_value <- extract_first(cell_node, "(?s)(.*?)")
+
+ if (is.na(ref) || is.na(raw_value)) {
+ next
+ }
+
+ value <- if (!is.na(cell_type) && identical(cell_type, "s")) {
+ shared_strings[as.integer(raw_value) + 1L]
+ } else {
+ xml_unescape(raw_value)
+ }
+
+ row_values[ref] <- value
+ }
+
+ rows[[as.character(row_number)]] <- row_values
+ }
+
+ header_values <- rows[["1"]]
+ header_refs <- names(header_values)
+ header_cols <- sub("[0-9]+$", "", header_refs)
+ header_order <- order(vapply(header_cols, col_to_number, integer(1)))
+ header_values <- header_values[header_order]
+ header_cols <- header_cols[header_order]
+
+ data_row_numbers <- sort(setdiff(as.integer(names(rows)), 1L))
+ argument_rows <- lapply(data_row_numbers, function(row_number) {
+ row_values <- rows[[as.character(row_number)]]
+ list(
+ row_number = row_number,
+ display = get_named_value(row_values, paste0("A", row_number)),
+ usage = get_named_value(row_values, paste0("B", row_number))
+ )
+ })
+
+ list(
+ xml = sheet_xml,
+ headers = unname(header_values[!(header_cols %in% c("A", "B"))]),
+ argument_rows = argument_rows
+ )
+}
+
+legacy_function_aliases <- c(
+ "Helper_InputCheck" = "Helper_InputChecker",
+ "Metrics_ETTCDI" = "Metrics_ETCCDI"
+)
+
+argument_description_overrides <- c(
+ extent = paste(
+ "Optional. The extent to subset the data to, in coordinates of the projection",
+ "or decimal degrees of longitude and latitude. A numeric vector of length 4",
+ "with values minimum and maximum X/longitude and minimum and maximum",
+ "Y/latitude, in that order. Valid ranges depend on the dataset being queried.",
+ "Defaults to NULL, returning full spatial range of data."
+ )
+)
+
+order_functions <- function(existing_headers, current_functions) {
+ # Preserve the current workbook column order, apply known legacy header
+ # mappings, expand the old grouped Discovery column, and append anything new.
+ ordered <- character()
+ used <- character()
+ removed_headers <- character()
+ replaced_headers <- character()
+ discovery_functions <- current_functions[grepl("^Discovery_", current_functions)]
+
+ add_function <- function(function_name) {
+ if (function_name %in% current_functions && !(function_name %in% used)) {
+ ordered <<- c(ordered, function_name)
+ used <<- c(used, function_name)
+ }
+ }
+
+ for (header in existing_headers) {
+ if (header %in% current_functions) {
+ add_function(header)
+ next
+ }
+
+ if (header %in% names(legacy_function_aliases)) {
+ mapped_name <- unname(legacy_function_aliases[[header]])
+ if (mapped_name %in% current_functions) {
+ add_function(mapped_name)
+ replaced_headers <- c(replaced_headers, paste0(header, " -> ", mapped_name))
+ } else {
+ removed_headers <- c(removed_headers, header)
+ }
+ next
+ }
+
+ if (identical(header, "Discovery_*")) {
+ remaining_discovery <- discovery_functions[!(discovery_functions %in% used)]
+ if (length(remaining_discovery)) {
+ for (function_name in remaining_discovery) {
+ add_function(function_name)
+ }
+ replaced_headers <- c(
+ replaced_headers,
+ paste0("Discovery_* -> ", paste(remaining_discovery, collapse = ", "))
+ )
+ } else {
+ removed_headers <- c(removed_headers, header)
+ }
+ next
+ }
+
+ removed_headers <- c(removed_headers, header)
+ }
+
+ appended <- current_functions[!(current_functions %in% used)]
+ for (function_name in appended) {
+ add_function(function_name)
+ }
+
+ list(
+ ordered = ordered,
+ removed_headers = removed_headers,
+ replaced_headers = replaced_headers,
+ appended = appended
+ )
+}
+
+build_existing_argument_map <- function(argument_rows) {
+ argument_map <- list()
+
+ for (row in argument_rows) {
+ key <- normalize_arg_key(row$display)
+ if (key %in% names(argument_map)) {
+ stop("Duplicate argument row in workbook template after normalization: ", key)
+ }
+ argument_map[[key]] <- row
+ }
+
+ argument_map
+}
+
+build_argument_order <- function(existing_argument_rows, function_specs, ordered_functions) {
+ # Keep existing argument row order for surviving arguments, then append
+ # newly introduced arguments in first-seen source order.
+ source_order <- unique(unlist(lapply(ordered_functions, function(function_name) {
+ normalize_arg_key(function_specs[[function_name]])
+ }), use.names = FALSE))
+
+ ordered_keys <- character()
+ used <- character()
+
+ for (row in existing_argument_rows) {
+ key <- normalize_arg_key(row$display)
+ if (key %in% source_order && !(key %in% used)) {
+ ordered_keys <- c(ordered_keys, key)
+ used <- c(used, key)
+ }
+ }
+
+ c(ordered_keys, source_order[!(source_order %in% used)])
+}
+
+build_doc_index <- function(ordered_functions, rd_docs) {
+ doc_index <- list()
+
+ for (function_name in ordered_functions) {
+ docs <- rd_docs[[function_name]]
+ for (arg_key in names(docs)) {
+ doc_index[[arg_key]] <- c(doc_index[[arg_key]], docs[[arg_key]])
+ }
+ }
+
+ doc_index
+}
+
+resolve_argument_description <- function(arg_key, existing_argument_map, doc_index) {
+ # Existing workbook text wins to avoid churn. New arguments must either
+ # have a single documented description or an explicit override above.
+ if (!is.null(existing_argument_map[[arg_key]])) {
+ existing_usage <- existing_argument_map[[arg_key]]$usage
+ if (nzchar(normalize_ws(existing_usage))) {
+ return(existing_usage)
+ }
+ }
+
+ if (arg_key %in% names(argument_description_overrides)) {
+ return(unname(argument_description_overrides[[arg_key]]))
+ }
+
+ candidates <- unique(normalize_ws(unlist(doc_index[[arg_key]])))
+ candidates <- candidates[nzchar(candidates)]
+
+ if (!length(candidates)) {
+ stop("No documented description found for argument: ", arg_key)
+ }
+
+ if (length(candidates) > 1) {
+ stop(
+ "Conflicting descriptions found for new argument ", arg_key, ": ",
+ paste(candidates, collapse = " | ")
+ )
+ }
+
+ candidates[[1]]
+}
+
+resolve_argument_display <- function(arg_key, existing_argument_map) {
+ if (!is.null(existing_argument_map[[arg_key]]) && nzchar(existing_argument_map[[arg_key]]$display)) {
+ return(existing_argument_map[[arg_key]]$display)
+ }
+ arg_key
+}
+
+new_string_pool <- function() {
+ values <- character()
+
+ list(
+ index = function(value) {
+ match_index <- match(value, values)
+ if (is.na(match_index)) {
+ values <<- c(values, value)
+ match_index <- length(values)
+ }
+ match_index - 1L
+ },
+ values = function() values
+ )
+}
+
+make_shared_strings_xml <- function(values, total_count) {
+ si_nodes <- vapply(values, function(value) {
+ preserve_space <- grepl("(^\\s)|(\\s$)", value)
+ sprintf(
+ "%s",
+ if (preserve_space) ' xml:space="preserve"' else "",
+ xml_escape(value)
+ )
+ }, character(1))
+
+ paste0(
+ '',
+ '',
+ paste(si_nodes, collapse = ""),
+ ""
+ )
+}
+
+make_sheet_data_xml <- function(argument_rows, ordered_functions, function_specs) {
+ # Rebuild the single worksheet as the same argument/function matrix:
+ # row 1 = headers, column A = argument names, column B = descriptions,
+ # remaining cells = X markers for argument usage.
+ pool <- new_string_pool()
+ total_cells <- 0L
+ total_columns <- length(ordered_functions) + 2L
+
+ make_cell <- function(ref, value, style = NULL) {
+ total_cells <<- total_cells + 1L
+ attributes <- c(sprintf('r="%s"', ref))
+ if (!is.null(style)) {
+ attributes <- c(attributes, sprintf('s="%s"', style))
+ }
+ attributes <- c(attributes, 't="s"')
+ sprintf(
+ "%d",
+ paste(attributes, collapse = " "),
+ pool$index(value)
+ )
+ }
+
+ header_cells <- c(
+ make_cell("A1", "Argument", style = 2),
+ make_cell("B1", "Usage", style = 2),
+ vapply(seq_along(ordered_functions), function(index) {
+ make_cell(paste0(col_from_number(index + 2L), "1"), ordered_functions[[index]], style = 2)
+ }, character(1))
+ )
+
+ row_nodes <- character(length(argument_rows) + 1L)
+ row_nodes[[1]] <- sprintf(
+ '%s
',
+ total_columns,
+ paste(header_cells, collapse = "")
+ )
+
+ function_arguments <- lapply(function_specs[ordered_functions], normalize_arg_key)
+
+ for (row_index in seq_along(argument_rows)) {
+ sheet_row <- row_index + 1L
+ argument_row <- argument_rows[[row_index]]
+ cells <- c(
+ make_cell(paste0("A", sheet_row), argument_row$display, style = 1),
+ make_cell(paste0("B", sheet_row), argument_row$usage)
+ )
+
+ marker_cells <- vapply(seq_along(ordered_functions), function(function_index) {
+ function_name <- ordered_functions[[function_index]]
+ if (!(argument_row$key %in% function_arguments[[function_name]])) {
+ return("")
+ }
+ make_cell(paste0(col_from_number(function_index + 2L), sheet_row), "X")
+ }, character(1))
+ marker_cells <- marker_cells[nzchar(marker_cells)]
+
+ row_nodes[[row_index + 1L]] <- sprintf(
+ '%s
',
+ sheet_row,
+ total_columns,
+ paste(c(cells, marker_cells), collapse = "")
+ )
+ }
+
+ list(
+ shared_strings = pool$values(),
+ shared_string_count = total_cells,
+ sheet_data = paste0("", paste(row_nodes, collapse = ""), ""),
+ total_columns = total_columns,
+ total_rows = length(argument_rows) + 1L
+ )
+}
+
+update_sheet_xml <- function(template_xml, new_dimension, new_cols_xml, new_sheet_data_xml) {
+ updated <- template_xml
+ updated <- sub('', sprintf('', new_dimension), updated)
+ updated <- sub("(?s).*?", new_cols_xml, updated, perl = TRUE)
+ updated <- sub("(?s).*?", new_sheet_data_xml, updated, perl = TRUE)
+ updated
+}
+
+repack_xlsx <- function(tempdir, destination) {
+ # Keep the rest of the xlsx package untouched and only replace the rebuilt
+ # OpenXML payload after the new parts have been written to the temp tree.
+ destination <- normalizePath(destination, winslash = "/", mustWork = FALSE)
+ zip_path <- tempfile(fileext = ".xlsx")
+ files <- list.files(tempdir, recursive = TRUE, all.files = TRUE, no.. = TRUE)
+ old_wd <- setwd(tempdir)
+ on.exit(setwd(old_wd), add = TRUE)
+
+ utils::zip(zipfile = zip_path, files = files, flags = "-q -r9X")
+
+ if (!file.exists(zip_path)) {
+ stop("Failed to rebuild workbook at ", destination)
+ }
+
+ if (!file.copy(zip_path, destination, overwrite = TRUE)) {
+ stop("Failed to copy rebuilt workbook to ", destination)
+ }
+}
+
+if (!file.exists(xlsx_path)) {
+ stop("Workbook does not exist: ", xlsx_path)
+}
+
+# Main flow:
+# 1. unpack the current workbook
+# 2. read existing layout from sheet1/sharedStrings
+# 3. derive current functions and docs from source
+# 4. reconcile old layout with new source-of-truth
+# 5. write the workbook back only if the XML payload changed
+tempdir <- tempfile("argument-usage-")
+dir.create(tempdir)
+on.exit(unlink(tempdir, recursive = TRUE), add = TRUE)
+
+utils::unzip(xlsx_path, exdir = tempdir)
+
+shared_strings_path <- file.path(tempdir, "xl", "sharedStrings.xml")
+sheet_path <- file.path(tempdir, "xl", "worksheets", "sheet1.xml")
+
+if (!file.exists(shared_strings_path) || !file.exists(sheet_path)) {
+ stop("Workbook template is missing required OpenXML parts")
+}
+
+existing_shared_xml <- read_text(shared_strings_path)
+existing_sheet_xml <- read_text(sheet_path)
+shared_strings <- parse_shared_strings(shared_strings_path)
+sheet_template <- parse_sheet_template(sheet_path, shared_strings)
+
+function_specs <- parse_function_specs("R")
+ordered_source_functions <- names(function_specs)
+rd_docs <- parse_rd_arguments(ordered_source_functions, "man")
+validate_documentation(function_specs, rd_docs)
+
+existing_argument_map <- build_existing_argument_map(sheet_template$argument_rows)
+function_order <- order_functions(sheet_template$headers, ordered_source_functions)
+ordered_functions <- function_order$ordered
+ordered_argument_keys <- build_argument_order(sheet_template$argument_rows, function_specs, ordered_functions)
+doc_index <- build_doc_index(ordered_functions, rd_docs)
+
+argument_rows <- lapply(ordered_argument_keys, function(arg_key) {
+ list(
+ key = arg_key,
+ display = resolve_argument_display(arg_key, existing_argument_map),
+ usage = resolve_argument_description(arg_key, existing_argument_map, doc_index)
+ )
+})
+
+sheet_data <- make_sheet_data_xml(argument_rows, ordered_functions, function_specs)
+last_column <- col_from_number(sheet_data$total_columns)
+new_dimension <- sprintf("A1:%s%d", last_column, sheet_data$total_rows)
+new_cols_xml <- sprintf(
+ '',
+ sheet_data$total_columns
+)
+new_sheet_xml <- update_sheet_xml(
+ template_xml = sheet_template$xml,
+ new_dimension = new_dimension,
+ new_cols_xml = new_cols_xml,
+ new_sheet_data_xml = sheet_data$sheet_data
+)
+new_shared_xml <- make_shared_strings_xml(
+ values = sheet_data$shared_strings,
+ total_count = sheet_data$shared_string_count
+)
+
+message("Workbook function columns: ", length(ordered_functions))
+message("Workbook argument rows: ", length(argument_rows))
+
+if (length(function_order$replaced_headers)) {
+ message("Replaced legacy headers: ", paste(function_order$replaced_headers, collapse = "; "))
+}
+if (length(function_order$removed_headers)) {
+ message("Removed stale headers: ", paste(function_order$removed_headers, collapse = ", "))
+}
+if (length(function_order$appended)) {
+ message("Appended new function columns: ", paste(function_order$appended, collapse = ", "))
+}
+
+if (identical(existing_shared_xml, new_shared_xml) && identical(existing_sheet_xml, new_sheet_xml)) {
+ message("ArgumentUsage workbook is already up to date.")
+ quit(status = 0)
+}
+
+write_text(shared_strings_path, new_shared_xml)
+write_text(sheet_path, new_sheet_xml)
+repack_xlsx(tempdir, xlsx_path)
+message("Updated ", xlsx_path)
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 6c71411..fadc58f 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -58,6 +58,15 @@ jobs:
fetch-depth: 0
fetch-tags: true
+ - name: Set up R
+ uses: r-lib/actions/setup-r@v2
+
+ - name: Validate ArgumentUsage workbook
+ run: |
+ set -euo pipefail
+ Rscript .github/scripts/update_argument_usage.R
+ git diff --exit-code -- .github/ArgumentUsage.xlsx
+
- name: Set up environment / dependencies
run: |
# install dependencies, etc.
@@ -71,7 +80,9 @@ jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest
- needs: versioning
+ needs:
+ - versioning
+ - build
# only create a release on pushes and when a version was produced
if: ${{ github.event_name == 'push' && needs.versioning.outputs.version != '' }}
permissions:
@@ -84,6 +95,9 @@ jobs:
fetch-depth: 0
fetch-tags: true
+ - name: Set up R
+ uses: r-lib/actions/setup-r@v2
+
- name: Update DESCRIPTION file with new version
run: |
set -euo pipefail
@@ -96,15 +110,24 @@ jobs:
echo "New DESCRIPTION header:"
head -n 5 DESCRIPTION
- # Commit only if it actually changed
+ - name: Refresh ArgumentUsage workbook
+ run: |
+ set -euo pipefail
+ Rscript .github/scripts/update_argument_usage.R
+
+ - name: Commit release metadata
+ run: |
+ set -euo pipefail
+ VERSION="${{ needs.versioning.outputs.version }}"
+
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git add DESCRIPTION
+ git add DESCRIPTION .github/ArgumentUsage.xlsx
if ! git diff --cached --quiet; then
- git commit -m "[skip ci] chore: bump DESCRIPTION Version to ${VERSION}"
+ git commit -m "[skip ci] chore: bump DESCRIPTION Version to ${VERSION} and refresh ArgumentUsage workbook"
git push
else
- echo "DESCRIPTION already at ${VERSION}; nothing to commit."
+ echo "DESCRIPTION and ArgumentUsage workbook already up to date; nothing to commit."
fi
- name: Create GitHub Release