diff --git a/.github/.gitignore b/.github/.gitignore index 2d19fc7..0845489 100644 --- a/.github/.gitignore +++ b/.github/.gitignore @@ -1 +1,2 @@ *.html +/pkg.lock diff --git a/.github/workflows/R-CMD-check-dev.yaml b/.github/workflows/R-CMD-check-dev.yaml new file mode 100644 index 0000000..bb87451 --- /dev/null +++ b/.github/workflows/R-CMD-check-dev.yaml @@ -0,0 +1,146 @@ +# This workflow calls the GitHub API very frequently. +# Can't be run as part of commits +on: + schedule: + - cron: "5 0 * * *" # 05:00 UTC every day only run on main branch + push: + branches: + - "cran-*" + tags: + - "v*" + +name: rcc dev + +jobs: + matrix: + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + name: Collect deps + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/rate-limit + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: r-lib/actions/setup-r@v2 + + - id: set-matrix + uses: ./.github/workflows/dep-matrix + + check-matrix: + runs-on: ubuntu-22.04 + needs: matrix + + name: Check deps + + steps: + - name: Install json2yaml + run: | + sudo npm install -g json2yaml + + - name: Check matrix definition + run: | + matrix='${{ needs.matrix.outputs.matrix }}' + echo $matrix + echo $matrix | jq . + echo $matrix | json2yaml + + R-CMD-check-base: + runs-on: ubuntu-22.04 + + name: base + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install + with: + cache-version: rcc-dev-base-1 + needs: check + extra-packages: "any::rcmdcheck any::remotes ." + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Session info + run: | + options(width = 100) + if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") + pkgs <- installed.packages()[, "Package"] + sessioninfo::session_info(pkgs, include_base = TRUE) + shell: Rscript {0} + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - uses: ./.github/workflows/update-snapshots + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + + - uses: ./.github/workflows/check + with: + results: ${{ matrix.package }} + + R-CMD-check-dev: + needs: + - matrix + - R-CMD-check-base + + runs-on: ubuntu-22.04 + + name: 'rcc-dev: ${{ matrix.package }}' + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + matrix: ${{fromJson(needs.matrix.outputs.matrix)}} + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install + with: + cache-version: rcc-dev-${{ matrix.package }}-1 + needs: check + extra-packages: "any::rcmdcheck any::remotes ." + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dev version of ${{ matrix.package }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: | + remotes::install_dev("${{ matrix.package }}", "https://cloud.r-project.org", upgrade = "always") + shell: Rscript {0} + + - name: Session info + run: | + options(width = 100) + if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") + pkgs <- installed.packages()[, "Package"] + sessioninfo::session_info(pkgs, include_base = TRUE) + shell: Rscript {0} + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - uses: ./.github/workflows/update-snapshots + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + + - uses: ./.github/workflows/check + with: + results: ${{ matrix.package }} diff --git a/.github/workflows/R-CMD-check-status.yaml b/.github/workflows/R-CMD-check-status.yaml new file mode 100644 index 0000000..7a4ea62 --- /dev/null +++ b/.github/workflows/R-CMD-check-status.yaml @@ -0,0 +1,75 @@ +# Workflow to update the status of a commit for the R-CMD-check workflow +# Necessary because remote PRs cannot update the status of the commit +on: + workflow_run: + workflows: + - rcc + types: + - requested + - completed + +name: rcc-status + +jobs: + rcc-status: + runs-on: ubuntu-24.04 + + name: "Update commit status" + + steps: + - name: "Update commit status" + # Only run if triggered by rcc workflow + if: github.event.workflow_run.name == 'rcc' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -x + + if [ "${{ github.event.workflow_run.status }}" == "completed" ]; then + if [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then + state="success" + else + state="failure" + fi + + # Read artifact ID + artifact_id=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts | jq -r '.artifacts[] | select(.name == "rcc-smoke-sha") | .id') + + if [ -n "${artifact_id}" ]; then + # Download artifact + curl -L -o rcc-smoke-sha.zip \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip + + # Unzip artifact + unzip rcc-smoke-sha.zip + + # Read artifact + sha=$(cat rcc-smoke-sha.txt) + + # Clean up + rm rcc-smoke-sha.zip rcc-smoke-sha.txt + fi + else + state="pending" + fi + + if [ -z "${sha}" ]; then + sha=${{ github.event.workflow_run.head_sha }} + fi + + html_url=${{ github.event.workflow_run.html_url }} + description=${{ github.event.workflow_run.name }} + + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/statuses/${sha} \ + -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" + shell: bash diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index d8aeb54..41ae8a5 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -6,58 +6,340 @@ # usethis::use_github_action("check-standard") will install it. on: push: - branches: [main, master] + branches: + - main + - master + - release + - cran-* pull_request: - branches: [main, master] + branches: + - main + - master + workflow_dispatch: + inputs: + ref: + description: "Branch, tag, or commit to check out" + required: false + default: "main" + versions-matrix: + description: "Create a matrix of R versions" + type: boolean + default: false + dep-suggests-matrix: + description: "Create a matrix of suggested dependencies" + type: boolean + default: false + merge_group: + types: + - checks_requested + schedule: + - cron: "10 0 * * *" -name: R-CMD-check +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.ref || github.head_ref || github.sha }}-${{ github.base_ref || '' }} + cancel-in-progress: true + +name: rcc jobs: - R-CMD-check: - runs-on: ${{ matrix.config.os }} + rcc-smoke: + runs-on: ubuntu-24.04 + outputs: + sha: ${{ steps.commit.outputs.sha }} + versions-matrix: ${{ steps.versions-matrix.outputs.matrix }} + dep-suggests-matrix: ${{ steps.dep-suggests-matrix.outputs.matrix }} + + name: "Smoke test: stock R" + + # Begin custom: services + # End custom: services + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Update status for rcc + # FIXME: Wrap into action + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check status of this workflow + state="pending" + sha=${{ inputs.ref }} + if [ -z "${sha}" ]; then + sha=${{ github.head_ref }} + fi + if [ -z "${sha}" ]; then + sha=${{ github.sha }} + fi + sha=$(git rev-parse ${sha}) + + html_url=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) + + description="${{ github.workflow }} / ${{ github.job }}" + + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/statuses/${sha} \ + -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" + shell: bash + + - uses: ./.github/workflows/rate-limit + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: ./.github/workflows/git-identity + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install + with: + token: ${{ secrets.GITHUB_TOKEN }} + cache-version: rcc-smoke-2 + needs: check, website + # Beware of using dev pkgdown here, has brought in dev dependencies in the past + extra-packages: any::rcmdcheck r-lib/roxygen2 any::decor r-lib/styler r-lib/pkgdown deps::. + + - name: Install package + run: | + _R_SHLIB_STRIP_=true R CMD INSTALL . + shell: bash + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - id: versions-matrix + # Only run for pull requests if the base repo is different from the head repo, not for workflow_dispatch if not requested, always run for other events + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) && (github.event_name != 'workflow_dispatch' || inputs.versions-matrix) + uses: ./.github/workflows/versions-matrix + + - id: dep-suggests-matrix + # Not for workflow_dispatch if not requested, always run for other events + if: github.event_name != 'workflow_dispatch' || inputs.dep-suggests-matrix + uses: ./.github/workflows/dep-suggests-matrix + + - uses: ./.github/workflows/update-snapshots + with: + base: ${{ inputs.ref || github.head_ref }} + + - uses: ./.github/workflows/style + + - uses: ./.github/workflows/roxygenize + + - name: Remove config files from previous iteration + run: | + rm -f .github/dep-suggests-matrix.json .github/versions-matrix.json + shell: bash + + - id: commit + uses: ./.github/workflows/commit + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: ./.github/workflows/check + with: + results: ${{ runner.os }}-smoke-test + + - uses: ./.github/workflows/pkgdown-build + if: github.event_name != 'push' + + - uses: ./.github/workflows/pkgdown-deploy + if: github.event_name == 'push' + + # Upload sha as artifact + - run: | + echo -n "${{ steps.commit.outputs.sha }}" > rcc-smoke-sha.txt + shell: bash + + - uses: actions/upload-artifact@v4 + with: + name: rcc-smoke-sha + path: rcc-smoke-sha.txt + + - name: Update status for rcc + # FIXME: Wrap into action + if: always() && github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check status of this workflow + if [ "${{ job.status }}" == "success" ]; then + state="success" + else + state="failure" + fi + + sha=${{ steps.commit.outputs.sha }} + if [ -z "${sha}" ]; then + sha=${{ inputs.ref }} + fi + if [ -z "${sha}" ]; then + sha=${{ github.head_ref }} + fi + if [ -z "${sha}" ]; then + sha=${{ github.sha }} + fi + sha=$(git rev-parse ${sha}) + + html_url=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) + + description="${{ github.workflow }} / ${{ github.job }}" + + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + repos/${{ github.repository }}/statuses/${sha} \ + -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" + shell: bash + + rcc-smoke-check-matrix: + runs-on: ubuntu-24.04 - name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + name: "Check matrix" + + needs: + - rcc-smoke + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.rcc-smoke.outputs.sha }} + + - uses: ./.github/workflows/matrix-check + with: + matrix: ${{ needs.rcc-smoke.outputs.versions-matrix }} + + - uses: ./.github/workflows/matrix-check + with: + matrix: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix }} + + rcc-full: + needs: + - rcc-smoke + + runs-on: ${{ matrix.os }} + + if: ${{ needs.rcc-smoke.outputs.versions-matrix != '' }} + + name: 'rcc: ${{ matrix.os }} (${{ matrix.r }}) ${{ matrix.desc }}' + + # Begin custom: services + # End custom: services strategy: fail-fast: false - matrix: - config: - - {os: macos-latest, r: 'release'} - - - {os: windows-latest, r: 'release'} - # Use 3.6 to trigger usage of RTools35 - - {os: windows-latest, r: '3.6'} - # use 4.1 to check with rtools40's older compiler - - {os: windows-latest, r: '4.1'} - - - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - - {os: ubuntu-latest, r: 'release'} - - {os: ubuntu-latest, r: 'oldrel-1'} - - {os: ubuntu-latest, r: 'oldrel-2'} - - {os: ubuntu-latest, r: 'oldrel-3'} - - {os: ubuntu-latest, r: 'oldrel-4'} - - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - R_KEEP_PKG_SOURCE: yes + matrix: ${{fromJson(needs.rcc-smoke.outputs.versions-matrix)}} steps: - uses: actions/checkout@v4 + with: + ref: ${{ needs.rcc-smoke.outputs.sha }} + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install + with: + r-version: ${{ matrix.r }} + cache-version: rcc-full-1 + token: ${{ secrets.GITHUB_TOKEN }} + needs: check + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' - - uses: r-lib/actions/setup-pandoc@v2 + - uses: ./.github/workflows/update-snapshots + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - - uses: r-lib/actions/setup-r@v2 + - uses: ./.github/workflows/check with: - r-version: ${{ matrix.config.r }} - http-user-agent: ${{ matrix.config.http-user-agent }} - use-public-rspm: true + results: ${{ runner.os }}-r${{ matrix.r }} - - uses: r-lib/actions/setup-r-dependencies@v2 +# The status update is taken care of by R-CMD-check-status.yaml + + rcc-suggests: + needs: + - rcc-smoke + + runs-on: ubuntu-22.04 + + if: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix != '' }} + + name: Without ${{ matrix.package }} + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + matrix: ${{fromJson(needs.rcc-smoke.outputs.dep-suggests-matrix)}} + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install with: - extra-packages: any::rcmdcheck + cache-version: rcc-dev-${{ matrix.package }}-1 needs: check + extra-packages: "any::rcmdcheck any::remotes ." + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove ${{ matrix.package }} and all strong dependencies + run: | + pkg <- "${{ matrix.package }}" + pkgs <- tools::package_dependencies(pkg, reverse = TRUE)[[1]] + installed <- rownames(utils::installed.packages()) + to_remove <- c(pkg, intersect(pkgs, installed)) + print(to_remove) + remove.packages(to_remove) + shell: Rscript {0} + + - name: Session info + run: | + options(width = 100) + if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") + pkgs <- installed.packages()[, "Package"] + sessioninfo::session_info(pkgs, include_base = TRUE) + shell: Rscript {0} - - uses: r-lib/actions/check-r-package@v2 + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - name: Define _R_CHECK_FORCE_SUGGESTS_ + run: | + cat('_R_CHECK_FORCE_SUGGESTS_=false\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) + shell: Rscript {0} + + - name: Must allow NOTEs, even with _R_CHECK_FORCE_SUGGESTS_ + run: | + if (Sys.getenv("RCMDCHECK_ERROR_ON") %in% c("", "note")) { + cat('RCMDCHECK_ERROR_ON="warning"\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) + } + shell: Rscript {0} + + - name: Check env vars + run: | + print(Sys.getenv('_R_CHECK_FORCE_SUGGESTS_')) + print(Sys.getenv('RCMDCHECK_ERROR_ON')) + shell: Rscript {0} + + - uses: ./.github/workflows/check with: - upload-snapshots: true - build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' + results: ${{ matrix.package }} + +# The status update is taken care of by R-CMD-check-status.yaml diff --git a/.github/workflows/check/action.yml b/.github/workflows/check/action.yml new file mode 100644 index 0000000..afb3819 --- /dev/null +++ b/.github/workflows/check/action.yml @@ -0,0 +1,40 @@ +name: "Actions to check an R package" +inputs: + results: + description: Slug for check results + required: true + +runs: + using: "composite" + steps: + - uses: r-lib/actions/check-r-package@v2 + with: + # Fails on R 3.6 on Windows, remove when this job is removed? + args: 'c("--no-manual", "--as-cran", "--no-multiarch")' + error-on: ${{ env.RCMDCHECK_ERROR_ON || '"note"' }} + + - name: Run coverage check + if: ${{ matrix.config.covr }} + run: | + if (dir.exists("tests/testthat")) { + covr::codecov() + } else { + message("No tests found, coverage not tested.") + } + shell: Rscript {0} + + - name: Show test output + if: always() + run: | + ## -- Show test output -- + echo "::group::Test output" + find check -name '*.Rout*' -exec head -n 1000000 '{}' \; || true + echo "::endgroup::" + shell: bash + + - name: Upload check results + if: failure() + uses: actions/upload-artifact@main + with: + name: ${{ inputs.results }}-results + path: check diff --git a/.github/workflows/commit/action.yml b/.github/workflows/commit/action.yml new file mode 100644 index 0000000..ed6f9b8 --- /dev/null +++ b/.github/workflows/commit/action.yml @@ -0,0 +1,63 @@ +name: "Action to commit changes to the repository" +inputs: + token: + description: "GitHub token" + required: true +outputs: + sha: + description: "SHA of generated commit" + value: ${{ steps.commit.outputs.sha }} + +runs: + using: "composite" + steps: + - name: Commit if changed, create a PR if protected + id: commit + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: | + set -x + if [ -n "$(git status --porcelain)" ]; then + echo "Changed" + protected=${{ github.ref_protected }} + foreign=${{ github.event.pull_request.head.repo.full_name != github.repository }} + if [ "${foreign}" = "true" ]; then + # https://github.com/krlmlr/actions-sync/issues/44 + echo "Can't push to foreign branch" + elif [ "${protected}" = "true" ]; then + current_branch=$(git branch --show-current) + new_branch=gha-commit-$(git rev-parse --short HEAD) + git checkout -b ${new_branch} + git add . + git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + # Force-push, used in only one place + # Alternative: separate branch names for each usage + git push -u origin HEAD -f + + existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') + if [ -n "${existing_pr}" ]; then + echo "Existing PR: ${existing_pr}" + else + gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + fi + + gh workflow run rcc -f ref=$(git rev-parse HEAD) + gh pr merge --merge --auto + else + git fetch + if [ -n "${GITHUB_HEAD_REF}" ]; then + git add . + git stash save + git switch ${GITHUB_HEAD_REF} + git merge origin/${GITHUB_BASE_REF} --no-edit + git stash pop + fi + git add . + git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + git push -u origin HEAD + + # Only set output if changed + echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + fi + fi + shell: bash diff --git a/.github/workflows/dep-matrix/action.yml b/.github/workflows/dep-matrix/action.yml new file mode 100644 index 0000000..35dcb3c --- /dev/null +++ b/.github/workflows/dep-matrix/action.yml @@ -0,0 +1,134 @@ +name: "Actions to compute a matrix with all dependent packages" +outputs: + matrix: + description: "Generated matrix" + value: ${{ steps.set-matrix.outputs.matrix }} + +runs: + using: "composite" + steps: + - id: set-matrix + run: | + # Determine package dependencies + # From remotes + read_dcf <- function(path) { + fields <- colnames(read.dcf(path)) + as.list(read.dcf(path, keep.white = fields)[1, ]) + } + + re_match <- function(text, pattern, perl = TRUE, ...) { + + stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) + text <- as.character(text) + + match <- regexpr(pattern, text, perl = perl, ...) + + start <- as.vector(match) + length <- attr(match, "match.length") + end <- start + length - 1L + + matchstr <- substring(text, start, end) + matchstr[ start == -1 ] <- NA_character_ + + res <- data.frame( + stringsAsFactors = FALSE, + .text = text, + .match = matchstr + ) + + if (!is.null(attr(match, "capture.start"))) { + + gstart <- attr(match, "capture.start") + glength <- attr(match, "capture.length") + gend <- gstart + glength - 1L + + groupstr <- substring(text, gstart, gend) + groupstr[ gstart == -1 ] <- NA_character_ + dim(groupstr) <- dim(gstart) + + res <- cbind(groupstr, res, stringsAsFactors = FALSE) + } + + names(res) <- c(attr(match, "capture.names"), ".text", ".match") + class(res) <- c("tbl_df", "tbl", class(res)) + res + } + + dev_split_ref <- function(x) { + re_match(x, "^(?[^@#]+)(?[@#].*)?$") + } + + has_dev_dep <- function(package) { + cran_url <- "https://cloud.r-project.org" + + refs <- dev_split_ref(package) + url <- file.path(cran_url, "web", "packages", refs[["pkg"]], "DESCRIPTION") + + f <- tempfile() + on.exit(unlink(f)) + + utils::download.file(url, f) + desc <- read_dcf(f) + + url_fields <- c(desc$URL, desc$BugReports) + + if (length(url_fields) == 0) { + return(FALSE) + } + + pkg_urls <- unlist(strsplit(url_fields, "[[:space:]]*,[[:space:]]*")) + + # Remove trailing "/issues" from the BugReports URL + pkg_urls <- sub("/issues$", "", pkg_urls) + + valid_domains <- c("github[.]com", "gitlab[.]com", "bitbucket[.]org") + + parts <- + re_match(pkg_urls, + sprintf("^https?://(?%s)/(?%s)/(?%s)(?:/(?%s))?", + domain = paste0(valid_domains, collapse = "|"), + username = "[^/]+", + repo = "[^/@#]+", + subdir = "[^/@$ ]+" + ) + )[c("domain", "username", "repo", "subdir")] + + # Remove cases which don't match and duplicates + + parts <- unique(stats::na.omit(parts)) + + nrow(parts) == 1 + } + + if (!requireNamespace("desc", quietly = TRUE)) { + install.packages("desc") + } + + deps_df <- desc::desc_get_deps() + deps_df <- deps_df[deps_df$type %in% c("Depends", "Imports", "LinkingTo", "Suggests"), ] + + packages <- sort(deps_df$package) + packages <- intersect(packages, rownames(available.packages())) + + valid_dev_dep <- vapply(packages, has_dev_dep, logical(1)) + + # https://github.com/r-lib/remotes/issues/576 + valid_dev_dep[packages %in% c("igraph", "duckdb", "logging")] <- FALSE + + deps <- packages[valid_dev_dep] + if (any(!valid_dev_dep)) { + msg <- paste0( + "Could not determine development repository for packages: ", + paste(packages[!valid_dev_dep], collapse = ", ") + ) + writeLines(paste0("::warning::", msg)) + } + + json <- paste0( + '{"package":[', + paste0('"', deps, '"', collapse = ","), + ']}' + ) + writeLines(json) + writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) + shell: Rscript {0} diff --git a/.github/workflows/dep-suggests-matrix/action.R b/.github/workflows/dep-suggests-matrix/action.R new file mode 100644 index 0000000..eff3a50 --- /dev/null +++ b/.github/workflows/dep-suggests-matrix/action.R @@ -0,0 +1,49 @@ +# FIXME: Dynamic lookup by parsing https://svn.r-project.org/R/tags/ +get_deps <- function() { + # Determine package dependencies + if (!requireNamespace("desc", quietly = TRUE)) { + install.packages("desc") + } + + deps_df <- desc::desc_get_deps() + deps_df_optional <- deps_df$package[deps_df$type %in% c("Suggests", "Enhances")] + deps_df_hard <- deps_df$package[deps_df$type %in% c("Depends", "Imports", "LinkingTo")] + deps_df_base <- unlist(tools::standard_package_names(), use.names = FALSE) + + packages <- sort(deps_df_optional) + packages <- intersect(packages, rownames(available.packages())) + + # Too big to fail, or can't be avoided: + off_limits <- c("testthat", "rmarkdown", "rcmdcheck", deps_df_hard, deps_df_base) + off_limits_dep <- unlist(tools::package_dependencies(off_limits, recursive = TRUE, which = "strong")) + setdiff(packages, c(off_limits, off_limits_dep)) +} + +if (Sys.getenv("GITHUB_BASE_REF") != "") { + print(Sys.getenv("GITHUB_BASE_REF")) + system("git fetch origin ${GITHUB_BASE_REF}") + # Use .. to avoid having to fetch the entire history + # https://github.com/krlmlr/actions-sync/issues/45 + has_diff <- (system("git diff origin/${GITHUB_BASE_REF}.. | egrep '^[+][^+]' | grep -q ::") == 0) + if (has_diff) { + system("git diff origin/${GITHUB_BASE_REF}.. | egrep '^[+][^+]' | grep -q ::") + packages <- get_deps() + } else { + writeLines("No changes using :: found, not checking without suggested packages") + packages <- character() + } +} else { + packages <- get_deps() +} + +if (length(packages) > 0) { + json <- paste0( + '{"package":[', + paste0('"', packages, '"', collapse = ","), + ']}' + ) + writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) + writeLines(json) +} else { + writeLines("No suggested packages found.") +} diff --git a/.github/workflows/dep-suggests-matrix/action.yml b/.github/workflows/dep-suggests-matrix/action.yml new file mode 100644 index 0000000..0f5e649 --- /dev/null +++ b/.github/workflows/dep-suggests-matrix/action.yml @@ -0,0 +1,13 @@ +name: "Actions to compute a matrix with all suggested packages" +outputs: + matrix: + description: "Generated matrix" + value: ${{ steps.set-matrix.outputs.matrix }} + +runs: + using: "composite" + steps: + - id: set-matrix + run: | + Rscript ./.github/workflows/dep-suggests-matrix/action.R + shell: bash diff --git a/.github/workflows/fledge.yaml b/.github/workflows/fledge.yaml new file mode 100644 index 0000000..7785ad9 --- /dev/null +++ b/.github/workflows/fledge.yaml @@ -0,0 +1,117 @@ +name: fledge + +on: + # for manual triggers + workflow_dispatch: + inputs: + pr: + description: "Create PR" + required: false + type: boolean + default: false + # daily run + schedule: + - cron: "30 0 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }} + cancel-in-progress: true + +jobs: + check_fork: + runs-on: ubuntu-24.04 + outputs: + is_forked: ${{ steps.check.outputs.is_forked }} + steps: + - name: Check if the repo is forked + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + is_forked=$(gh api repos/${{ github.repository }} | jq .fork) + echo "is_forked=${is_forked}" >> $GITHUB_OUTPUT + shell: bash + + fledge: + runs-on: ubuntu-24.04 + needs: check_fork + if: needs.check_fork.outputs.is_forked == 'false' + permissions: + contents: write + pull-requests: write + actions: write + env: + FLEDGE_GHA_CI: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git identity + run: | + env | sort + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + shell: bash + + - name: Update apt + run: | + sudo apt-get update + shell: bash + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + with: + pak-version: devel + packages: cynkra/fledge + cache-version: fledge-1 + + - name: Count rulesets + # Assume that branch is protected if ruleset exists + id: rulesets + env: + GH_TOKEN: ${{ github.token }} + run: | + n_rulesets=$(gh api repos/${{ github.repository }}/rulesets -q length) + echo "count=${n_rulesets}" >> $GITHUB_OUTPUT + shell: bash + + - name: Switch to branch if branch protection is enabled + if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 + run: | + git checkout -b fledge + git push -f -u origin HEAD + shell: bash + + - name: Bump version + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: | + check_default_branch <- ("${{ github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 }}" != "true") + if (fledge::bump_version(which = "dev", no_change_behavior = "noop", check_default_branch = check_default_branch)) { + fledge::finalize_version(push = TRUE) + } + shell: Rscript {0} + + - name: Create and merge PR if branch protection is enabled + if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -x + gh pr create --base main --head fledge --fill-first + gh workflow run rcc -f ref=$(git rev-parse HEAD) + gh pr merge --squash --auto + shell: bash + + - name: Check release + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + run: | + fledge:::release_after_cran_built_binaries() + shell: Rscript {0} diff --git a/.github/workflows/get-extra/action.yml b/.github/workflows/get-extra/action.yml new file mode 100644 index 0000000..84c56d9 --- /dev/null +++ b/.github/workflows/get-extra/action.yml @@ -0,0 +1,16 @@ +name: "Action to determine extra packages to be installed" +outputs: + packages: + description: "List of extra packages" + value: ${{ steps.get-extra.outputs.packages }} + +runs: + using: "composite" + steps: + - name: Get extra packages + id: get-extra + run: | + set -x + packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) + echo packages=$packages >> $GITHUB_OUTPUT + shell: bash diff --git a/.github/workflows/git-identity/action.yml b/.github/workflows/git-identity/action.yml new file mode 100644 index 0000000..7234dab --- /dev/null +++ b/.github/workflows/git-identity/action.yml @@ -0,0 +1,11 @@ +name: "Actions to set up a Git identity" + +runs: + using: "composite" + steps: + - name: Configure Git identity + run: | + env | sort + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + shell: bash diff --git a/.github/workflows/install/action.yml b/.github/workflows/install/action.yml new file mode 100644 index 0000000..03bcb1e --- /dev/null +++ b/.github/workflows/install/action.yml @@ -0,0 +1,160 @@ +name: "Actions to run for installing R packages" +inputs: + token: + description: GitHub token, set to secrets.GITHUB_TOKEN + required: true + r-version: + description: Passed on to r-lib/actions/setup-r@v2 + required: false + default: release + install-r: + description: Passed on to r-lib/actions/setup-r@v2 + required: false + default: true + needs: + description: Passed on to r-lib/actions/setup-r-dependencies@v2 + required: false + default: "" + packages: + description: Passed on to r-lib/actions/setup-r-dependencies@v2 + required: false + default: deps::., any::sessioninfo + extra-packages: + description: Passed on to r-lib/actions/setup-r-dependencies@v2 + required: false + default: any::rcmdcheck + cache-version: + description: Passed on to r-lib/actions/setup-r-dependencies@v2 + required: false + default: 1 + +runs: + using: "composite" + steps: + - name: Set environment variables + run: | + echo "R_REMOTES_NO_ERRORS_FROM_WARNINGS=true" | tee -a $GITHUB_ENV + echo "R_KEEP_PKG_SOURCE=yes" | tee -a $GITHUB_ENV + echo "_R_CHECK_SYSTEM_CLOCK_=false" | tee -a $GITHUB_ENV + echo "_R_CHECK_FUTURE_FILE_TIMESTAMPS_=false" | tee -a $GITHUB_ENV + # prevent rgl issues because no X11 display is available + echo "RGL_USE_NULL=true" | tee -a $GITHUB_ENV + # from https://github.com/r-devel/r-dev-web/blob/main/CRAN/QA/Kurt/lib/R/Scripts/check_CRAN_incoming.R + echo "_R_CHECK_CRAN_INCOMING_CHECK_FILE_URIS_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_CRAN_INCOMING_NOTE_GNU_MAKE_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_PACKAGE_DEPENDS_IGNORE_MISSING_ENHANCES_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_CODE_CLASS_IS_STRING_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_CODOC_VARIABLES_IN_USAGES_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_CONNECTIONS_LEFT_OPEN_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_DATALIST_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_NEWS_IN_PLAIN_TEXT_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_PACKAGES_USED_CRAN_INCOMING_NOTES_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_RD_CONTENTS_KEYWORDS_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_R_DEPENDS_=warn" | tee -a $GITHUB_ENV + echo "_R_CHECK_S3_METHODS_SHOW_POSSIBLE_ISSUES_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_THINGS_IN_TEMP_DIR_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_UNDOC_USE_ALL_NAMES_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_URLS_SHOW_301_STATUS_=true" | tee -a $GITHUB_ENV + echo "_R_CXX_USE_NO_REMAP_=true" | tee -a $GITHUB_ENV + # There is no way to disable recency and frequency checks when the incoming checks are run + # echo "_R_CHECK_CRAN_INCOMING_=true" | tee -a $GITHUB_ENV + echo "_R_CHECK_CRAN_INCOMING_SKIP_LARGE_VERSION_=true" | tee -a $GITHUB_ENV + shell: bash + + - name: Set environment variables (non-Windows only) + if: runner.os != 'Windows' + run: | + echo "_R_CHECK_BASHISMS_=true" | tee -a $GITHUB_ENV + shell: bash + + - name: Update apt + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y aspell + echo "_R_CHECK_CRAN_INCOMING_USE_ASPELL_=true" | tee -a $GITHUB_ENV + shell: bash + + - name: Remove pkg-config@0.29.2 + if: runner.os == 'macOS' + run: | + brew uninstall pkg-config@0.29.2 || true + shell: bash + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ inputs.r-version }} + install-r: ${{ inputs.install-r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - id: get-extra + run: | + set -x + packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) + echo packages=$packages >> $GITHUB_OUTPUT + shell: bash + + - uses: r-lib/actions/setup-r-dependencies@v2 + env: + GITHUB_PAT: ${{ inputs.token }} + with: + pak-version: stable + needs: ${{ inputs.needs }} + packages: ${{ inputs.packages }} + extra-packages: ${{ inputs.extra-packages }} ${{ ( matrix.config.covr && 'any::covr' ) || '' }} ${{ steps.get-extra.outputs.packages }} + cache-version: ${{ inputs.cache-version }} + + - name: Add pkg.lock to .gitignore + run: | + set -x + if ! [ -f .github/.gitignore ] || [ -z "$(grep '^/pkg.lock$' .github/.gitignore)" ]; then + echo /pkg.lock >> .github/.gitignore + fi + shell: bash + + - name: Add fake qpdf and checkbashisms + if: runner.os == 'Linux' + run: | + sudo ln -s $(which true) /usr/local/bin/qpdf + sudo ln -s $(which true) /usr/local/bin/checkbashisms + shell: bash + + - name: Install ccache + uses: krlmlr/ccache-action@parallel-dir + with: + max-size: 10G + verbose: 1 + save: false + restore: false + + - name: Use ccache for compiling R code, and parallelize + run: | + mkdir -p ~/.R + echo 'CC := ccache $(CC)' >> ~/.R/Makevars + echo 'CXX := ccache $(CXX)' >> ~/.R/Makevars + echo 'CXX11 := ccache $(CXX11)' >> ~/.R/Makevars + echo 'CXX14 := ccache $(CXX14)' >> ~/.R/Makevars + echo 'CXX17 := ccache $(CXX17)' >> ~/.R/Makevars + echo 'MAKEFLAGS = -j2' >> ~/.R/Makevars + cat ~/.R/Makevars + + echo 'CCACHE_SLOPPINESS=locale,time_macros' | tee -a $GITHUB_ENV + + # echo 'CCACHE_DEBUG=true' | tee -a $GITHUB_ENV + # echo "CCACHE_DEBUGDIR=$(dirname $(pwd))/ccache-debug" | tee -a $GITHUB_ENV + # mkdir -p $(dirname $(pwd))/.ccache-debug + + echo 'PKG_BUILD_EXTRA_FLAGS=false' | tee -a $GITHUB_ENV + + # Repair + git rm -rf .ccache || true + rm -rf .ccache + shell: bash + + - name: Show R CMD config --all + run: | + R CMD config --all + shell: bash diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml new file mode 100644 index 0000000..6c52f68 --- /dev/null +++ b/.github/workflows/lock.yaml @@ -0,0 +1,23 @@ +name: "Lock threads" +permissions: + issues: write + pull-requests: write + discussions: write +on: + workflow_dispatch: + schedule: + - cron: "37 2 * * *" + +jobs: + lock: + runs-on: ubuntu-24.04 + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-inactive-days: "365" + issue-lock-reason: "" + issue-comment: > + This old thread has been automatically locked. If you think you have + found something related to this, please open a new issue and link to this + old issue if necessary. diff --git a/.github/workflows/matrix-check/action.yml b/.github/workflows/matrix-check/action.yml new file mode 100644 index 0000000..b943048 --- /dev/null +++ b/.github/workflows/matrix-check/action.yml @@ -0,0 +1,23 @@ +name: "Actions to check a matrix with all R and OS versions, computed with the versions-matrix action" +inputs: + matrix: + description: "Generated matrix" + required: true + +runs: + using: "composite" + steps: + - name: Install json2yaml + run: | + sudo npm install -g json2yaml + shell: bash + + - run: | + matrix='${{ inputs.matrix }}' + if [ -n "${matrix}" ]; then + echo $matrix | jq . + echo $matrix | json2yaml + else + echo "No matrix found" + fi + shell: bash diff --git a/.github/workflows/pkgdown-build/action.yml b/.github/workflows/pkgdown-build/action.yml new file mode 100644 index 0000000..381d067 --- /dev/null +++ b/.github/workflows/pkgdown-build/action.yml @@ -0,0 +1,9 @@ +name: "Action to build a pkgdown website" + +runs: + using: "composite" + steps: + - name: Build site + run: | + pkgdown::build_site() + shell: Rscript {0} diff --git a/.github/workflows/pkgdown-deploy/action.yml b/.github/workflows/pkgdown-deploy/action.yml new file mode 100644 index 0000000..b7c3ff4 --- /dev/null +++ b/.github/workflows/pkgdown-deploy/action.yml @@ -0,0 +1,12 @@ +name: "Action to deploy a pkgdown website" + +runs: + using: "composite" + steps: + - name: Deploy site + uses: nick-fields/retry@v3 + with: + timeout_minutes: 15 + max_attempts: 10 + command: | + R -q -e 'pkgdown::deploy_to_branch(new_process = FALSE)' diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index a7276e8..60d1849 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -1,48 +1,53 @@ # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +# Also included in R-CMD-check.yaml, this workflow only listens to pushes to branches +# that start with "docs*" on: push: - branches: [main, master] - pull_request: - branches: [main, master] - release: - types: [published] + branches: + - "docs*" + - "cran-*" workflow_dispatch: name: pkgdown +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.sha }}-${{ github.base_ref || '' }} + cancel-in-progress: true + jobs: pkgdown: - runs-on: ubuntu-latest - # Only restrict concurrency for non-PR jobs - concurrency: - group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - permissions: - contents: write + runs-on: ubuntu-24.04 + + name: "pkgdown" + + # Begin custom: services + # End custom: services + steps: - uses: actions/checkout@v4 - - uses: r-lib/actions/setup-pandoc@v2 - - - uses: r-lib/actions/setup-r@v2 + - uses: ./.github/workflows/rate-limit with: - use-public-rspm: true + token: ${{ secrets.GITHUB_TOKEN }} - - uses: r-lib/actions/setup-r-dependencies@v2 + - uses: ./.github/workflows/git-identity + if: github.event_name == 'push' + + - uses: ./.github/workflows/custom/before-install + if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' + + - uses: ./.github/workflows/install with: - extra-packages: any::pkgdown, local::. + token: ${{ secrets.GITHUB_TOKEN }} + cache-version: pkgdown-2 needs: website + extra-packages: r-lib/pkgdown local::. - - name: Build site - run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) - shell: Rscript {0} + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' - - name: Deploy to GitHub pages 🚀 - if: github.event_name != 'pull_request' - uses: JamesIves/github-pages-deploy-action@v4.5.0 - with: - clean: false - branch: gh-pages - folder: docs + - uses: ./.github/workflows/pkgdown-build + if: github.event_name != 'push' + + - uses: ./.github/workflows/pkgdown-deploy + if: github.event_name == 'push' diff --git a/.github/workflows/pr-commands.yaml b/.github/workflows/pr-commands.yaml new file mode 100644 index 0000000..b07a6f3 --- /dev/null +++ b/.github/workflows/pr-commands.yaml @@ -0,0 +1,99 @@ +on: + issue_comment: + types: [created] +name: Commands +jobs: + document: + if: startsWith(github.event.comment.body, '/document') + name: document + # macos is actually better here due to native binary packages + runs-on: macos-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: r-lib/actions/setup-r@v2 + - name: Configure Git identity + run: | + env | sort + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + shell: bash + - name: Install dependencies + run: | + install.packages(c("remotes", "roxygen2"), type = "binary") + remotes::install_deps(dependencies = TRUE) + shell: Rscript {0} + - name: Document + run: | + roxygen2::roxygenise() + shell: Rscript {0} + - name: commit + run: | + if [ -n "$(git status --porcelain man/ NAMESPACE)" ]; then + git add man/ NAMESPACE + git commit -m 'Document' + fi + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + style: + if: startsWith(github.event.comment.body, '/style') + name: style + # macos is actually better here due to native binary packages + runs-on: macos-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + - uses: r-lib/actions/pr-fetch@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: r-lib/actions/setup-r@v2 + - name: Configure Git identity + run: | + env | sort + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + shell: bash + - name: Install dependencies + run: | + install.packages(c("styler", "roxygen2"), type = "binary") + shell: Rscript {0} + - name: Style + run: | + styler::style_pkg(strict = FALSE) + shell: Rscript {0} + - name: commit + run: | + if [ -n "$(git status --porcelain '*.R' '*.Rmd')" ]; then + git add '*.R' '*.Rmd' + git commit -m 'Style' + fi + - uses: r-lib/actions/pr-push@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + merge: + if: startsWith(github.event.comment.body, '/merge') + name: merge + runs-on: ubuntu-22.04 + steps: + - name: Create and merge pull request + run: | + set -exo pipefail + PR_DETAILS=$( curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} ) + echo "$PR_DETAILS" | jq . + PR_BASE=$(echo "$PR_DETAILS" | jq -r .base.ref) + PR_HEAD=$(echo "$PR_DETAILS" | jq -r .head.ref) + PR_URL=$(curl -s -X POST --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --data '{ "head": "'$PR_BASE'", "base": "'$PR_HEAD'", "title": "Merge back PR target branch", "body": "Target: #${{ github.event.issue.number }}" }' https://api.github.com/repos/${{ github.repository }}/pulls | jq -r .url ) + echo $PR_URL + # Merging here won't run CI/CD + # curl -s -X PUT --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL/merge + # A mock job just to ensure we have a successful build status + finish: + runs-on: ubuntu-22.04 + steps: + - run: true diff --git a/.github/workflows/rate-limit/action.yml b/.github/workflows/rate-limit/action.yml new file mode 100644 index 0000000..8180fe4 --- /dev/null +++ b/.github/workflows/rate-limit/action.yml @@ -0,0 +1,13 @@ +name: "Check GitHub rate limits" +inputs: + token: # id of input + description: GitHub token, pass secrets.GITHUB_TOKEN + required: true + +runs: + using: "composite" + steps: + - name: Check rate limits + run: | + curl -s --header "authorization: Bearer ${{ inputs.token }}" https://api.github.com/rate_limit + shell: bash diff --git a/.github/workflows/revdep.yaml b/.github/workflows/revdep.yaml new file mode 100644 index 0000000..0fa67dc --- /dev/null +++ b/.github/workflows/revdep.yaml @@ -0,0 +1,213 @@ +# This workflow creates many jobs, run only when a branch is created +on: + push: + branches: + - "revdep*" # never run automatically on main branch + +name: revdep + +jobs: + matrix: + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + name: Collect revdeps + + env: + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + # prevent rgl issues because no X11 display is available + RGL_USE_NULL: true + # Begin custom: env vars + # End custom: env vars + + steps: + - name: Check rate limits + run: | + curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit + shell: bash + + - uses: actions/checkout@v4 + + # FIXME: Avoid reissuing succesful jobs + # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-jobs-for-a-workflow-run + # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#workflow-runs + - id: set-matrix + run: | + package <- read.dcf("DESCRIPTION")[, "Package"][[1]] + deps <- tools:::package_dependencies(package, reverse = TRUE, which = c("Depends", "Imports", "LinkingTo", "Suggests"))[[1]] + json <- paste0( + '{"package":[', + paste0('"', deps, '"', collapse = ","), + ']}' + ) + writeLines(json) + writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) + shell: Rscript {0} + + check-matrix: + runs-on: ubuntu-22.04 + needs: matrix + steps: + - name: Install json2yaml + run: | + sudo npm install -g json2yaml + + - name: Check matrix definition + run: | + matrix='${{ needs.matrix.outputs.matrix }}' + echo $matrix + echo $matrix | jq . + echo $matrix | json2yaml + + R-CMD-check: + needs: matrix + + runs-on: ubuntu-22.04 + + name: 'revdep: ${{ matrix.package }}' + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + matrix: ${{fromJson(needs.matrix.outputs.matrix)}} + + env: + R_REMOTES_NO_ERRORS_FROM_WARNINGS: true + RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + # prevent rgl issues because no X11 display is available + RGL_USE_NULL: true + # Begin custom: env vars + # End custom: env vars + + steps: + - name: Check rate limits + run: | + curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit + shell: bash + + - uses: actions/checkout@v4 + + # Begin custom: before install + # End custom: before install + + - name: Use RSPM + run: | + mkdir -p /home/runner/work/_temp/Library + echo 'local({release <- system2("lsb_release", "-sc", stdout = TRUE); options(repos=c(CRAN = paste0("https://packagemanager.rstudio.com/all/__linux__/", release, "/latest")), HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os)))}); .libPaths("/home/runner/work/_temp/Library")' | sudo tee /etc/R/Rprofile.site + + - name: Install remotes + run: | + if (!requireNamespace("curl", quietly = TRUE)) install.packages("curl") + if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes") + shell: Rscript {0} + + - uses: r-lib/actions/setup-pandoc@v2 + + - name: Install system dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "22.04")); package <- "${{ matrix.package }}"; deps <- tools::package_dependencies(package, which = "Suggests")[[1]]; lapply(c(package, deps), function(x) { writeLines(remotes::system_requirements("ubuntu", "22.04", package = x)) })' | sort | uniq > .github/deps.sh + cat .github/deps.sh + sudo sh < .github/deps.sh + + - name: Install package + run: | + package <- "${{ matrix.package }}" + install.packages(package, dependencies = TRUE) + remotes::install_cran("rcmdcheck") + shell: Rscript {0} + + - name: Session info old + run: | + options(width = 100) + if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") + pkgs <- installed.packages()[, "Package"] + sessioninfo::session_info(pkgs, include_base = TRUE) + shell: Rscript {0} + + # Begin custom: after install + # End custom: after install + + - name: Check old + env: + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_SYSTEM_CLOCK_: false + _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false + # Avoid downloading binary package from RSPM + run: | + package <- "${{ matrix.package }}" + options(HTTPUserAgent = "gha") + path <- download.packages(package, destdir = ".github")[, 2] + print(path) + + dir <- file.path("revdep", package) + dir.create(dir, showWarnings = FALSE, recursive = TRUE) + check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) + file.rename(file.path(dir, "check"), file.path(dir, "old")) + saveRDS(check, file.path(dir, "old.rds")) + shell: Rscript {0} + + - name: Install local package + run: | + remotes::install_local(".", force = TRUE) + shell: Rscript {0} + + - name: Session info new + run: | + options(width = 100) + pkgs <- installed.packages()[, "Package"] + sessioninfo::session_info(pkgs, include_base = TRUE) + shell: Rscript {0} + + - name: Check new + env: + _R_CHECK_CRAN_INCOMING_: false + _R_CHECK_SYSTEM_CLOCK_: false + _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false + run: | + package <- "${{ matrix.package }}" + path <- dir(".github", pattern = paste0("^", package), full.names = TRUE)[[1]] + print(path) + + dir <- file.path("revdep", package) + check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) + file.rename(file.path(dir, "check"), file.path(dir, "new")) + saveRDS(check, file.path(dir, "new.rds")) + shell: Rscript {0} + + - name: Compare + run: | + package <- "${{ matrix.package }}" + dir <- file.path("revdep", package) + old <- readRDS(file.path(dir, "old.rds")) + new <- readRDS(file.path(dir, "new.rds")) + compare <- rcmdcheck::compare_checks(old, new) + compare + cmp <- compare$cmp + if (!identical(cmp[cmp$which == "old", "output"], cmp[cmp$which == "new", "output"])) { + if (!requireNamespace("waldo", quietly = TRUE)) install.packages("waldo") + print(waldo::compare(old, new)) + + stop("Check output differs.") + } + shell: Rscript {0} + + - name: Upload check results + if: failure() + uses: actions/upload-artifact@main + with: + name: ${{ matrix.package }}-results + path: revdep/${{ matrix.package }} + + - name: Check rate limits + if: always() + run: | + curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit + shell: bash diff --git a/.github/workflows/roxygenize/action.yml b/.github/workflows/roxygenize/action.yml new file mode 100644 index 0000000..ee1d652 --- /dev/null +++ b/.github/workflows/roxygenize/action.yml @@ -0,0 +1,9 @@ +name: "Action to create documentation with roxygen2" + +runs: + using: "composite" + steps: + - name: Roxygenize + run: | + try(roxygen2::roxygenize()) + shell: Rscript {0} diff --git a/.github/workflows/style/action.yml b/.github/workflows/style/action.yml new file mode 100644 index 0000000..6b40c0a --- /dev/null +++ b/.github/workflows/style/action.yml @@ -0,0 +1,71 @@ +name: "Action to auto-style a package" + +runs: + using: "composite" + steps: + - name: Check styler options + id: check + run: | + set -x + scope=$( ( grep Config/autostyle/scope DESCRIPTION || true ) | cut -d " " -f 2) + strict=$( ( grep Config/autostyle/strict DESCRIPTION || true ) | cut -d " " -f 2) + rmd=$( ( grep Config/autostyle/rmd DESCRIPTION || true ) | cut -d " " -f 2) + echo scope=$scope >> $GITHUB_OUTPUT + echo strict=$strict >> $GITHUB_OUTPUT + echo rmd=$rmd >> $GITHUB_OUTPUT + shell: bash + + - uses: actions/cache@v4 + if: ${{ steps.check.outputs.scope }} + with: + path: | + ~/.cache/R/R.cache + key: ${{ runner.os }}-2-${{ github.run_id }}- + restore-keys: | + ${{ runner.os }}-2- + + - name: Imprint run ID + if: ${{ steps.check.outputs.scope }} + run: | + mkdir -p ~/.cache/R/R.cache/styler + touch ~/.cache/R/R.cache/${{ github.run_id }} + shell: bash + + - name: Show cache + if: ${{ steps.check.outputs.scope }} + run: | + ls -l ~/.cache/R/R.cache + ls -l ~/.cache/R/R.cache/styler + shell: bash + + - name: Enable styler cache + if: ${{ steps.check.outputs.scope }} + run: | + styler::cache_activate(verbose = TRUE) + shell: Rscript {0} + + - name: Run styler + if: ${{ steps.check.outputs.scope }} + run: | + strict <- as.logical("${{ steps.check.outputs.strict }}") + if (is.na(strict)) { + strict <- FALSE + } + rmd <- as.logical("${{ steps.check.outputs.rmd }}") + if (is.na(rmd)) { + rmd <- TRUE + } + styler::style_pkg( + scope = "${{ steps.check.outputs.scope }}", + strict = strict, + filetype = c("R", "Rprofile", if (rmd) c("Rmd", "Rmarkdown", "Rnw", "Qmd")) + ) + shell: Rscript {0} + + - name: Show cache again + if: ${{ steps.check.outputs.scope }} + run: | + ls -l ~/.cache/R/R.cache + ls -l ~/.cache/R/R.cache/styler + gdu -s --inodes ~/.cache/R/R.cache/styler/* || du -s --inodes ~/.cache/R/R.cache/styler/* + shell: bash diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml deleted file mode 100644 index 21b8a93..0000000 --- a/.github/workflows/test-coverage.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help -on: - push: - branches: [main, master] - pull_request: - branches: [main, master] - -name: test-coverage - -jobs: - test-coverage: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v4 - - - uses: r-lib/actions/setup-r@v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::covr - needs: coverage - - - name: Test coverage - run: | - covr::codecov( - quiet = FALSE, - clean = FALSE, - install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") - ) - shell: Rscript {0} - - - name: Show testthat output - if: always() - run: | - ## -------------------------------------------------------------------- - find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash - - - name: Upload test results - if: failure() - uses: actions/upload-artifact@v4 - with: - name: coverage-test-failures - path: ${{ runner.temp }}/package diff --git a/.github/workflows/update-snapshots/action.yml b/.github/workflows/update-snapshots/action.yml new file mode 100644 index 0000000..5ee7e3e --- /dev/null +++ b/.github/workflows/update-snapshots/action.yml @@ -0,0 +1,87 @@ +name: "Action to create pull requests for updated testthat snapshots" +description: > + This action will run `testthat::test_local()` for tests that seem to use snapshots, + this is determined by reading and grepping the test files. + If the tests are failing, snapshots are updated, and a pull request is opened. +inputs: + base: + description: "The base branch to create the pull request against." + required: false + default: "main" + +runs: + using: "composite" + steps: + - name: Run tests on test files that use snapshots + id: run-tests + run: | + ## -- Run tests on test files that use snapshots -- + rx <- "^test-(.*)[.][rR]$" + files <- dir("tests/testthat", pattern = rx) + has_snapshot <- vapply(files, function(.x) any(grepl("snapshot", readLines(file.path("tests/testthat", .x)), fixed = TRUE)), logical(1)) + if (any(has_snapshot)) { + patterns <- gsub(rx, "^\\1$", files[has_snapshot]) + pattern <- paste0(patterns, collapse = "|") + tryCatch( + { + result <- as.data.frame(testthat::test_local(pattern = pattern, reporter = "silent", stop_on_failure = FALSE)) + failures <- result[result$failed + result$warning > 0, ] + if (nrow(failures) > 0) { + writeLines("Snapshot tests failed/warned.") + print(failures[names(failures) != "result"]) + print(failures$result) + testthat::snapshot_accept() + writeLines("changed=true", Sys.getenv("GITHUB_OUTPUT")) + } else { + writeLines("Snapshot tests ran successfully.") + } + }, + error = print + ) + } else { + writeLines("No snapshots found.") + } + shell: Rscript {0} + + - name: Add snapshots to Git + if: ${{ steps.run-tests.outputs.changed }} + run: | + ## -- Add snapshots to Git -- + mkdir -p tests/testthat/_snaps + git add -- tests/testthat/_snaps + shell: bash + + - name: Check changed files + if: ${{ steps.run-tests.outputs.changed }} + id: check-changed + run: | + echo "changed=$(git status --porcelain -- tests/testthat/_snaps | head -n 1)" >> $GITHUB_OUTPUT + shell: bash + + - name: Derive branch name + if: ${{ steps.check-changed.outputs.changed }} + id: matrix-desc + run: | + config=$(echo '${{ toJSON(matrix) }}' | jq -c .) + echo "text=$(echo ${config})" >> $GITHUB_OUTPUT + echo "branch=$(echo ${config} | sed -r 's/[^0-9a-zA-Z]+/-/g;s/^-//;s/-$//')" >> $GITHUB_OUTPUT + shell: bash + + - name: Create pull request + if: ${{ steps.check-changed.outputs.changed }} + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + base: ${{ inputs.base }} + branch: snapshot-${{ inputs.base }}-${{ github.job }}-${{ steps.matrix-desc.outputs.branch }} + delete-branch: true + title: "test: Snapshot updates for ${{ github.job }} (${{ steps.matrix-desc.outputs.text }})" + body: "Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action${{ github.event.number && format(' for #{0}', github.event.number) || '' }}." + add-paths: | + tests/testthat/_snaps + + - name: Fail if pull request created + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + false + shell: bash diff --git a/.github/workflows/versions-matrix/action.R b/.github/workflows/versions-matrix/action.R new file mode 100644 index 0000000..b8e2dcf --- /dev/null +++ b/.github/workflows/versions-matrix/action.R @@ -0,0 +1,60 @@ +# Determine active versions of R to test against +tags <- xml2::read_html("https://svn.r-project.org/R/tags/") + +bullets <- + tags |> + xml2::xml_find_all("//li") |> + xml2::xml_text() + +version_bullets <- grep("^R-([0-9]+-[0-9]+-[0-9]+)/$", bullets, value = TRUE) +versions <- unique(gsub("^R-([0-9]+)-([0-9]+)-[0-9]+/$", "\\1.\\2", version_bullets)) + +r_release <- head(sort(as.package_version(versions), decreasing = TRUE), 5) + +deps <- desc::desc_get_deps() +r_crit <- deps$version[deps$package == "R"] +if (length(r_crit) == 1) { + min_r <- as.package_version(gsub("^>= ([0-9]+[.][0-9]+)(?:.*)$", "\\1", r_crit)) + r_release <- r_release[r_release >= min_r] +} + +r_versions <- c("devel", as.character(r_release)) + +macos <- data.frame(os = "macos-latest", r = r_versions[2:3]) +windows <- data.frame(os = "windows-latest", r = r_versions[1:3]) +linux_devel <- data.frame(os = "ubuntu-22.04", r = r_versions[1], `http-user-agent` = "release", check.names = FALSE) +linux <- data.frame(os = "ubuntu-22.04", r = r_versions[-1]) +covr <- data.frame(os = "ubuntu-22.04", r = r_versions[2], covr = "true", desc = "with covr") + +include_list <- list(macos, windows, linux_devel, linux, covr) + +if (file.exists(".github/versions-matrix.R")) { + custom <- source(".github/versions-matrix.R")$value + include_list <- c(include_list, list(custom)) +} + +print(include_list) + +filter <- read.dcf("DESCRIPTION")[1,]["Config/gha/filter"] +if (!is.na(filter)) { + filter_expr <- parse(text = filter)[[1]] + subset_fun_expr <- bquote(function(x) subset(x, .(filter_expr))) + subset_fun <- eval(subset_fun_expr) + include_list <- lapply(include_list, subset_fun) + print(include_list) +} + +to_json <- function(x) { + if (nrow(x) == 0) return(character()) + parallel <- vector("list", length(x)) + for (i in seq_along(x)) { + parallel[[i]] <- paste0('"', names(x)[[i]], '":"', x[[i]], '"') + } + paste0("{", do.call(paste, c(parallel, sep = ",")), "}") +} + +configs <- unlist(lapply(include_list, to_json)) +json <- paste0('{"include":[', paste(configs, collapse = ","), ']}') + +writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) +writeLines(json) diff --git a/.github/workflows/versions-matrix/action.yml b/.github/workflows/versions-matrix/action.yml new file mode 100644 index 0000000..af7378a --- /dev/null +++ b/.github/workflows/versions-matrix/action.yml @@ -0,0 +1,19 @@ +name: "Actions to compute a matrix with all R and OS versions" + +outputs: + matrix: + description: "Generated matrix" + value: ${{ steps.set-matrix.outputs.matrix }} + +runs: + using: "composite" + steps: + - name: Install json2yaml + run: | + sudo npm install -g json2yaml + shell: bash + + - id: set-matrix + run: | + Rscript ./.github/workflows/versions-matrix/action.R + shell: bash diff --git a/DESCRIPTION b/DESCRIPTION index 838faaa..2b7e540 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -56,5 +56,5 @@ Encoding: UTF-8 Language: en-US LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2.9000 Config/testthat/edition: 3