diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 000000000..6917e2d76 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +/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 000000000..aa127c7f4 --- /dev/null +++ b/.github/workflows/R-CMD-check-dev.yaml @@ -0,0 +1,150 @@ +# 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-20.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 + with: + install-r: false + + - 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: + install-r: false + 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: ${{ 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: + install-r: false + 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.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 000000000..c3a7fefe1 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,229 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/master/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +# +# NOTE: This workflow is overkill for most R packages and +# check-standard.yaml is likely a better choice. +# usethis::use_github_action("check-standard") will install it. +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + schedule: + - cron: "10 0 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.sha }}-${{ github.base_ref || '' }} + cancel-in-progress: true + +name: rcc + +jobs: + rcc-smoke: + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.commit.outputs.sha }} + + name: "Smoke test: stock R" + + # Begin custom: services + # End custom: services + + steps: + - uses: actions/checkout@v4 + + - 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 }} + install-r: false + cache-version: rcc-smoke-1 + needs: check + extra-packages: any::rcmdcheck any::roxygen2 r-lib/styler + + - 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/style + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + + - uses: ./.github/workflows/roxygenize + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + + - id: commit + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/workflows/commit + + - uses: ./.github/workflows/check + with: + results: ${{ runner.os }}-smoke-test + + # Runs in a separate workflow, because it's using dev pkgdown + # which might bring in other dev dependencies + pkgdown: + needs: rcc-smoke + + runs-on: ubuntu-latest + + name: "pkgdown" + + # Begin custom: services + # End custom: services + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.rcc-smoke.outputs.sha }} + + - uses: ./.github/workflows/rate-limit + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - 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: + token: ${{ secrets.GITHUB_TOKEN }} + install-r: false + cache-version: pkgdown-1 + needs: website + extra-packages: r-lib/pkgdown local::. + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - uses: ./.github/workflows/pkgdown-build + if: github.event_name != 'push' + + - uses: ./.github/workflows/pkgdown-deploy + if: github.event_name == 'push' + + # Windows checks can be run in parallel and independently + # when they alone take as long as the smoke and full tests combined. + # To achieve this, remove the "needs:" element below. + rcc-windows: + # Begin custom: early run + needs: rcc-smoke + # End custom: early run + + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) ${{ matrix.config.desc }} + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + matrix: + config: + - {os: windows-latest, r: 'release'} + # Use 3.6 to trigger usage of RTools35 + - {os: windows-latest, r: '3.6'} + + 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.config.r }} + cache-version: rcc-windows-1 + token: ${{ secrets.GITHUB_TOKEN }} + needs: check + + - 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: ${{ runner.os }}-r${{ matrix.config.r }} + + rcc-full: + needs: rcc-smoke + + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) ${{ matrix.config.desc }} + + # Begin custom: services + # End custom: services + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + + - {os: ubuntu-20.04, r: 'release'} + + # Use older ubuntu to maximise backward compatibility + - {os: ubuntu-22.04, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-22.04, r: 'release', covr: true, desc: 'with covr'} + - {os: ubuntu-22.04, r: 'oldrel-1'} + + # Begin custom: R 3.6 + - {os: ubuntu-22.04, r: 'oldrel-2'} + # End custom: R 3.6 + + # Begin custom: R 3.5 + - {os: ubuntu-22.04, r: 'oldrel-3'} + # End custom: R 3.5 + + # Begin custom: R 3.4 + - {os: ubuntu-22.04, r: 'oldrel-4'} + # End custom: R 3.4 + + # Begin custom: matrix elements + # End custom: matrix elements + + 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.config.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: ./.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: ${{ runner.os }}-r${{ matrix.config.r }} diff --git a/.github/workflows/check/action.yml b/.github/workflows/check/action.yml new file mode 100644 index 000000000..afb38194c --- /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 000000000..89be42228 --- /dev/null +++ b/.github/workflows/commit/action.yml @@ -0,0 +1,29 @@ +name: "Action to commit changes to the repository" +outputs: + sha: + description: "SHA of generated commit" + value: ${{ steps.commit.outputs.sha }} + +runs: + using: "composite" + steps: + - name: Commit if changed + id: commit + run: | + set -x + if [ -n "$(git status --porcelain)" ]; then + echo "Changed" + 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 "Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + git push -u origin HEAD + echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT + fi + shell: bash diff --git a/.github/workflows/dep-matrix/action.yml b/.github/workflows/dep-matrix/action.yml new file mode 100644 index 000000000..35dcb3c29 --- /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/get-extra/action.yml b/.github/workflows/get-extra/action.yml new file mode 100644 index 000000000..84c56d9b7 --- /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 000000000..7234dab4f --- /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 000000000..7cd912caa --- /dev/null +++ b/.github/workflows/install/action.yml @@ -0,0 +1,99 @@ +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 + shell: bash + + - name: Update apt + if: runner.os == 'Linux' + run: | + sudo apt-get update + 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 + uses: ./.github/workflows/get-extra + + - uses: r-lib/actions/setup-r-dependencies@v2 + env: + GITHUB_PAT: ${{ inputs.token }} + with: + pak-version: devel + 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: hendrikmuhs/ccache-action@v1.2 + + - 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 + shell: bash diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml new file mode 100644 index 000000000..9e33a1f52 --- /dev/null +++ b/.github/workflows/lock.yaml @@ -0,0 +1,19 @@ +name: "Lock threads" + +on: + schedule: + - cron: "0 0 * * *" + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v2 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: "365" + issue-lock-reason: "" + issue-lock-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/pkgdown-build/action.yml b/.github/workflows/pkgdown-build/action.yml new file mode 100644 index 000000000..381d067ee --- /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 000000000..18d0206df --- /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@v2 + 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 new file mode 100644 index 000000000..e906ef935 --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,54 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/master/examples +# Also included in R-CMD-check.yaml, this workflow only listens to pushes to branches +# that start with "docs*" +on: + push: + 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 + + name: "pkgdown" + + # Begin custom: services + # End custom: services + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/rate-limit + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - 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: + token: ${{ secrets.GITHUB_TOKEN }} + install-r: false + cache-version: pkgdown-1 + needs: website + extra-packages: r-lib/pkgdown local::. + + - uses: ./.github/workflows/custom/after-install + if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' + + - 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 000000000..21cf0cad3 --- /dev/null +++ b/.github/workflows/pr-commands.yaml @@ -0,0 +1,103 @@ +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@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: r-lib/actions/setup-r@master + with: + install-r: false + - 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@master + 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@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: r-lib/actions/setup-r@master + with: + install-r: false + - 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@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + merge: + if: startsWith(github.event.comment.body, '/merge') + name: merge + runs-on: ubuntu-20.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-20.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 000000000..8180fe460 --- /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 000000000..4a0d511ae --- /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: ${{ 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 000000000..ee1d652af --- /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 000000000..b209d9efb --- /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@v3 + 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/update-snapshots/action.yml b/.github/workflows/update-snapshots/action.yml new file mode 100644 index 000000000..dd7bc90e1 --- /dev/null +++ b/.github/workflows/update-snapshots/action.yml @@ -0,0 +1,82 @@ +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. + +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)" >> $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@v4 + with: + base: ${{ github.head_ref }} + branch: snapshot-${{ github.ref_name }}-${{ github.job }}-${{ steps.matrix-desc.outputs.branch }} + delete-branch: true + title: 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