r-cmd-check.yml
Run R CMD check across an OS x R-version matrix, wrapping r-lib/actions setup-r, setup-r-dependencies, and check-r-package. A second caller job with hard: true runs a Depends/Imports/LinkingTo-only check that is gated to pull_request.
Two designs come from IndrajeetPatil/workflows R-CMD-check.yaml (MIT): the hard input, and cache: false on that job so a restored pak cache cannot silently contain Suggests. Do not copy that file’s concurrency.group of ${{ github.workflow }}-${{ github.head_ref }} with no github.ref fallback: head_ref is empty on push, so every push across every branch collapsed into one group. This reusable workflow sets no concurrency; the example stub keys on github.ref.
error-on defaults to '"note"' to match rpt’s bespoke workflow, not r-lib’s '"warning"'. The _R_CHECK_* knobs, the matrix, the timeout, and rpt’s Julia / Quarto / pandoc / rocker/verse steps are inputs rather than hard-coded. See Migrating from rpt before treating this as a drop-in for that file.
This capability is workflow-only (no root composite): the matrix, the two mutually exclusive jobs, and the optional Linux container are job-level, so splitting the steps into a composite would add a nested @v2 bootstrap without reuse value, the same shape as update-snapshots.yml.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
hard |
boolean | false |
Run the hard-dependencies-only job instead of the full matrix. Gated to pull_request. Call the workflow twice from the stub if you want both. |
extra-packages |
string | '' |
Extra r-lib/actions package specs appended after the check’s own extras (any::rcmdcheck on the full matrix; knitr, rcmdcheck, rmarkdown, curl, and testthat on the hard job). |
error-on |
string | '"note"' |
R expression for check-r-package error-on on the full matrix. Default matches rpt (NOTEs fail). The hard job omits this so r-lib’s '"warning"' default applies. |
cran-incoming-remote |
boolean | false |
Value for _R_CHECK_CRAN_INCOMING_REMOTE_ and _R_CHECK_CRAN_INCOMING_. r-lib’s check-r-package sets INCOMING false when unset, so REMOTE alone cannot turn incoming checks on. |
force-suggests |
boolean | false |
Value for _R_CHECK_FORCE_SUGGESTS_ on the full matrix. The hard job always sets this false. |
stop-on-invalid-numeric-version-inputs |
boolean | true |
Value for _R_CHECK_STOP_ON_INVALID_NUMERIC_VERSION_INPUTS_. |
path |
string | '.' |
Path to the R package root (the directory holding DESCRIPTION). |
config |
string | (r-lib 5-way JSON) | JSON array of {os, r, http-user-agent?} objects for the full matrix. Ignored when hard is true. |
setup-pandoc |
boolean | true |
Install Pandoc before the check. |
install-quarto |
boolean | false |
Install Quarto via quarto-dev/quarto-actions/setup before the check. On the full matrix, skipped on ubuntu-latest only when linux-container contains verse. Separate from r-lib setup-r-dependencies’ own install-quarto (default auto when the tree has .qmd files). |
setup-julia |
boolean | false |
Install Julia before the check (for JuliaCall packages). |
julia-version |
string | '1' |
Julia version to install. Only used when setup-julia is true. |
julia-project |
string | 'inst/julia' |
Path to the Julia project to instantiate when setup-julia is true. Pass empty string to skip. |
apt-packages |
string | (common build deps) | Space-separated apt packages to install before R setup on ubuntu-latest runner cells without a container, and on the hard job. Pass empty string to skip. |
brew-packages |
string | '' |
Space-separated Homebrew packages to install on macos-* runner cells before R setup. When gettext is included, it is force-linked and added to ~/.R/Makevars. |
brew-casks |
string | '' |
Space-separated Homebrew casks to install on macos-* runner cells before R setup (e.g. xquartz). |
linux-container |
string | '' |
Container image for ubuntu-latest cells of the full matrix (e.g. rocker/verse:latest). Ignored on the hard job. Quarto install is skipped on ubuntu only when this image name contains verse. |
checkout-submodules |
boolean | false |
Initialize the caller’s git submodules before the check. |
timeout-minutes |
number | 90 |
Job timeout. rpt’s bespoke workflow used 50. |
build-args |
string | 'c("--no-manual","--compact-vignettes=gs+qpdf")' |
R expression forwarded to check-r-package build_args. |
upload-snapshots |
boolean | true |
Upload all testthat snapshots as an artifact (r-lib’s wording). Default uploads on success too. |
upload-results |
boolean | true |
Upload the check results directory as a workflow artifact. |
Secrets
| Secret | Required | Description |
|---|---|---|
SUBMODULES_TOKEN |
No | Token with read access to private submodules. Only needed when checkout-submodules is true. |
Permissions
Only contents: read (the default), so no permissions: block is needed.
Hard-dependencies check
The standard matrix installs Suggests, so it cannot see a package that works for the author and breaks for a user who did not install them. hard: true installs only Depends, Imports, and LinkingTo, then adds back knitr, rcmdcheck, rmarkdown, curl, and testthat so the check itself can run.
cache: false on that job is load-bearing, not a speed knob: a restored pak cache may already contain a Suggested package, which would make a green hard-deps run meaningless. It is hard-coded, not an input.
The hard job also never uses linux-container. rocker/verse ships a large set of R packages, which is the same silent-Suggests failure cache: false exists to prevent. It omits error-on so r-lib’s '"warning"' default applies: missing Suggests are NOTEs, and error-on: note would fail the job for the condition it exists to tolerate.
Migrating from rpt
rpt’s bespoke .github/workflows/R-CMD-check.yaml is not a drop-in for this workflow’s defaults. Each of its steps has an equivalent or an input; none is silently dropped. This table is the feature-by-feature diff CLAUDE.md asks for before calling a reusable capability drop-in. Do not migrate rpt until a follow-up actually passes these through.
| rpt step | This workflow |
|---|---|
5-way OS x R-version matrix, fail-fast: false, devel http-user-agent: release |
Default config JSON. |
container: rocker/verse:latest on ubuntu-latest |
linux-container: rocker/verse:latest on the full job. The hard job never uses it. |
timeout-minutes: 50 |
timeout-minutes: 50 (default here is 90). |
actions/checkout with submodules: recursive |
checkout-submodules: true. |
r-lib/actions/setup-pandoc |
setup-pandoc: true (the default). |
julia-actions/setup-julia@v3 |
setup-julia: true. |
quarto-dev/quarto-actions/setup@v2 when os != ubuntu-latest |
install-quarto: true. Skipped on ubuntu only when linux-container contains verse. |
setup-r with r-version, http-user-agent, use-public-rspm: true |
Always. |
extra-packages: any::rcmdcheck and local::. |
any::rcmdcheck is always installed; pass local::. in extra-packages if you still want it. |
error-on: '"note"' |
Default on the full matrix. The hard job omits error-on so r-lib’s '"warning"' default applies (missing-Suggests NOTEs must not fail that job). |
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' |
Default build-args. |
(unset) _R_CHECK_CRAN_INCOMING_REMOTE_, _R_CHECK_FORCE_SUGGESTS_, _R_CHECK_STOP_ON_INVALID_NUMERIC_VERSION_INPUTS_ |
Inputs. IndrajeetPatil/workflows currently sets only _R_CHECK_CRAN_INCOMING_REMOTE_: false on the full job (fetched 2026-08-26); it does not set the other two. This workflow’s cran-incoming-remote also sets _R_CHECK_CRAN_INCOMING_, because check-r-package forces INCOMING false when unset. force-suggests: false matches r-lib’s unset override (and therefore rpt, which also leaves it unset). stop-on-invalid-numeric-version-inputs: true restates R’s default (stop unless that env is false). |
upload-snapshots: true |
Default true. |
(unset) upload-results (r-lib default false) |
Default true. Pass upload-results: false to match rpt. |
| No hard-deps job | Add the R-CMD-check-hard caller job from the example stub. |
Migrating from bcs
ucdavis/bcs’s bespoke .github/workflows/R-CMD-check.yaml and its .github/actions/install-system-deps composite can migrate directly to this reusable workflow with setup-julia: true, brew-packages: gettext, brew-casks: xquartz, and default apt-packages.
| bcs step | This workflow |
|---|---|
.github/actions/install-system-deps (apt packages on Linux) |
Default apt-packages matches bcs’s package list exactly. |
brew install gettext and brew link gettext --force on macOS |
brew-packages: gettext. Force-link and ~/.R/Makevars configuration run automatically. |
brew install --cask xquartz on macOS |
brew-casks: xquartz. |
julia-actions/setup-julia@v2 |
setup-julia: true. |
julia-actions/cache@v2 |
Automatic when setup-julia: true and julia-project != '' (pinned to v3.3.0). |
Pkg.instantiate() / Pkg.precompile() on inst/julia |
julia-project: inst/julia (the default when setup-julia: true). |
PKG_INCLUDE_LINKINGTO: "true" in job env |
Always set in both jobs’ env. |
Example
# Copy to .github/workflows/r-cmd-check.yml in your repo.
name: R-CMD-check
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
concurrency:
group: r-cmd-check-${{ github.ref }}
cancel-in-progress: true
jobs:
R-CMD-check:
uses: Morrison-Lab/gha/.github/workflows/r-cmd-check.yml@v2
# with:
# error-on: '"note"'
# cran-incoming-remote: false
# force-suggests: false # matches r-lib/check-r-package when unset
# setup-julia: true
# julia-project: inst/julia
# apt-packages: '' # empty to skip; default installs build deps
# brew-packages: gettext # bcs macOS deps
# brew-casks: xquartz
# install-quarto: true
# linux-container: rocker/verse:latest
# checkout-submodules: true
# timeout-minutes: 50
R-CMD-check-hard:
uses: Morrison-Lab/gha/.github/workflows/r-cmd-check.yml@v2
with:
hard: trueSee the examples/ directory for the full caller stub.
A brand-new capability is not usable at @v2 until that tag slides past this merge; pin the example as-is and wait for the slide, or point a template repo at this branch to try it.