The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
This vignette shows how to combine resultcheck,
testthat, renv, and a GitHub Actions workflow
into a fully automated reproducibility pipeline. The goal is to make
every push to your repository trigger a test run that fails loudly
whenever any analysis result drifts from its committed snapshot — across
all three major operating systems.
For a real-world example of this pattern in action, see the IMF replication repository.
| Tool | Role |
|---|---|
resultcheck |
Captures named snapshots of R objects; errors in CI when a snapshot changes |
testthat |
Test harness that runs the snapshots and reports failures |
renv |
Locks every package to an exact version so the environment is reproducible |
| GitHub Actions | Runs the test suite automatically on push/PR across Windows, macOS, and Linux |
Without renv, a routine package update could silently
change a coefficient or table. Without snapshots, tests would not catch
numerical drift. Without multi-OS CI, platform-specific floating-point
differences would go unnoticed.
A minimal project that uses this workflow looks like:
myproject/
├── .Rprofile # auto-activates renv
├── renv.lock # locked package versions (committed)
├── renv/ # renv internals (mostly gitignored)
├── _resultcheck.yml # marks the project root
├── data/
│ └── panel_data.rds
├── code/
│ └── analysis.R # your analysis script
├── tests/_resultcheck_snaps/ # committed snapshot files
│ └── analysis/
│ └── main_model.md
└── tests/
└── testthat/
└── test-analysis.R
The _resultcheck.yml file at the root can be empty — its
presence is enough for find_root() to locate the
project:
Inside R, with your project open:
Install the packages your project needs, then snapshot the environment:
renv::install(c("resultcheck", "testthat"))
# ... install any other packages your analysis uses ...
renv::snapshot()Commit both .Rprofile and renv.lock. The
renv/ folder should be partially ignored according to
renv’s own .gitignore (created automatically by
renv::init()).
Call resultcheck::snapshot() on every object whose value
matters for reproducibility. The first time you run the script
interactively the snapshot is saved; on all subsequent runs it is
compared against the saved version.
# code/analysis.R
data <- readRDS("data/panel_data.rds")
model <- lm(y ~ x1 + x2, data = data)
resultcheck::snapshot(model, "main_model")
resultcheck::snapshot(data, "panel_data")
# ... continue writing outputs ...Run the script interactively once to generate the .md
snapshot files, review them, then commit them to version control.
# tests/testthat/test-analysis.R
library(testthat)
library(resultcheck)
test_that("analysis produces stable results", {
sandbox <- setup_sandbox("data")
on.exit(cleanup_sandbox(sandbox), add = TRUE)
# snapshot() inside run_in_sandbox() errors on any mismatch
expect_true(run_in_sandbox("code/analysis.R", sandbox))
})Run locally to confirm everything passes before pushing:
For package examples and quick demos, you can avoid writing into your
current project by wrapping code in
resultcheck::with_example({...}), which creates a temporary
project in tempdir() and cleans it up automatically.
Create .github/workflows/run-tests.yml. The key
ingredients are:
ragg, xml2, or curl need native
libraries on Linux and macOS that are not required on Windows.r-lib/actions/setup-renv@v2 restores the renv
cache between runs, avoiding re-installing hundreds of packages on every
push.name: R Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
test:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- {os: windows-latest}
- {os: ubuntu-latest}
- {os: macos-latest}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup R
uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
# ── Linux system libraries ──────────────────────────────────────────
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libfontconfig1-dev \
libharfbuzz-dev \
libfribidi-dev \
libfreetype6-dev \
libpng-dev \
libtiff5-dev \
libjpeg-dev
# ── macOS system libraries ───────────────────────────────────────────
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: |
set -euxo pipefail
brew update
brew install pkg-config libpng cairo freetype harfbuzz fribidi
BREW_PREFIX="$(brew --prefix)"
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> $GITHUB_ENV
echo "PATH=${BREW_PREFIX}/bin:${PATH}" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH=${BREW_PREFIX}/lib/pkgconfig:$(brew --prefix libpng)/lib/pkgconfig" >> $GITHUB_ENV
mkdir -p ~/.R
PNG_CFLAGS="$(pkg-config --cflags libpng)"
PNG_LIBS="$(pkg-config --libs libpng)"
{
echo "CPPFLAGS += -I${BREW_PREFIX}/include"
echo "LDFLAGS += -L${BREW_PREFIX}/lib -Wl,-rpath,${BREW_PREFIX}/lib"
echo "PKG_CPPFLAGS += ${PNG_CFLAGS}"
echo "PKG_LIBS += ${PNG_LIBS}"
} >> ~/.R/Makevars
# ── Restore renv cache (fast!) ───────────────────────────────────────
- name: Restore renv packages
uses: r-lib/actions/setup-renv@v2
with:
cache-version: 2
# ── Run tests ────────────────────────────────────────────────────────
- name: Run tests
run: Rscript -e "testthat::test_dir('tests/testthat')"
- name: Upload test artefacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.config.os }}
path: tests/testthat/*.Rout*setup-renv caching worksr-lib/actions/setup-renv@v2 reads your
renv.lock file, computes a cache key from its hash, and
restores a previously saved cache of installed packages before calling
renv::restore(). When the lock file has not changed, all
packages are served from the cache and renv::restore()
completes in seconds rather than minutes.
Increment cache-version (e.g. from 2 to
3) whenever you want to force a full re-install — for
example after an OS upgrade or when debugging a strange linking
error.
Developer CI runner
───────── ─────────
1. Edit analysis.R
2. Run interactively → snapshots
generated / updated
3. Review diffs, accept changes
4. git add tests/_resultcheck_snaps/
git commit && git push
5. Workflow triggered
6. renv::restore() (from cache)
7. testthat::test_dir()
└─ run_in_sandbox("code/analysis.R")
└─ snapshot() in *testing mode*
✓ matches committed file → pass
✗ differs → FAIL
CI never updates snapshots; it only enforces them. To accept a
legitimate result change, always re-run the script interactively,
confirm the diff, and commit the updated .md files.
When the same computation yields slightly different floating-point
values on different operating systems, use the mechanisms described in
vignette("snapshot-tolerance"):
[ignored] markers — replace a volatile
line in the snapshot file with the literal text [ignored].
That line position is skipped on every platform.snapshot.precision — add a
precision key to _resultcheck.yml to round all
floating-point numbers before comparison:Either option lets CI pass on all platforms without losing the safety net on the lines that do matter.
renv.lock but not
renv/library/. The library is rebuilt from the
lock file on each runner; only the lock file needs to be in version
control..md
files produced by snapshot() are plain text and diff well
in pull requests — reviewers can see at a glance whether a coefficient
changed.r-version: '4.4.2') if minor R releases have ever
changed your numerical results.fail-fast: false lets all three OS
jobs run to completion even when one fails, giving you a full picture of
where the discrepancy occurs.workflow_dispatch allows you to
trigger the workflow manually from the GitHub Actions UI — useful for
debugging without having to push a commit.These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.