For the complete documentation index, see llms.txt. This page is also available as Markdown.

Cached CI pipelines (Turborepo, Nx)

Run Argos reliably in pipelines where a task cache like Turborepo or Nx decides which test suites actually run.

Monorepo build systems like Turborepo and Nx cache test tasks: when a package and its dependencies haven't changed, the task is replayed from cache instead of executing. This is great for CI speed, but it breaks a naive visual testing setup, because a cached test task never uploads screenshots.

Three problems follow:

  1. You can't predict how many uploads a run will produce. Any fixed expectation (a parallel total, a required upload step) will hang or fail on runs where suites are cached.

  2. Missing screenshots look like removed screenshots. If a build only contains the suites that ran, Argos would report every cached suite's screenshots as removed.

  3. Baselines on your main branch can go stale. A baseline must be a complete build. If the suite that produces a screenshot doesn't run on main, no build contains its up-to-date version.

There are two ways to handle this. The examples use Turborepo and GitHub Actions; the same patterns apply to Nx or any cache that skips tasks.

Preferred: cache the snapshot files as task outputs

The key insight: Argos doesn't need your tests to run — it needs their snapshot files. Build system caches can restore task outputs without executing the task. Declare the snapshot directory as a cached output, and upload the full set in a single step after the build-system run:

  • Suites that ran contribute fresh files.

  • Suites replayed from cache contribute their restored files — identical to the run that populated the cache, which is exactly right: a cache hit means the suite's inputs are unchanged.

Every build is complete, on pull requests and on main. A suite executes at most once per content change, on any branch, and baselines are always fresh.

1. Write snapshots to a directory inside the package

Capture snapshots to a directory owned by the task's package — Argos SDKs default to ./screenshots. Don't upload from inside the test task (skip the SDK reporter / uploadToArgos option): the upload moves to a later step, outside the cache boundary.

2. Declare the directory as a cached task output

packages/my-package/turbo.json
{
  "extends": ["//"],
  "tasks": {
    "test": {
      "outputs": ["screenshots/**"]
    }
  }
}

With Nx, add the directory to the target's outputs.

3. Upload once after the run

That's the whole setup: no parallel mode, no subset builds, no finalize step, and nothing special on your main branch.

Caveats

  • Snapshots must be deterministic and machine-independent. Cached files are reused across runners and branches; anything environment-dependent in them (timestamps, absolute paths, font rendering differences) will produce diffs that depend on which machine populated the cache. Text snapshots and stabilized screenshots are fine.

  • You extend the cache's trust model to baselines. With a remote cache, files produced on a pull-request runner can end up in a main-branch build. That's the same trust you already place in the cache for test results — but it's worth stating.

  • Screenshot names become upload-root-relative. Uploading from the repository root names snapshots by their full path (packages/app/screenshots/home.png), which also prevents name collisions between packages sharing a build.

Alternative: subset builds + finalize

When the snapshot files can't be treated as cacheable outputs — uploads happen deep inside the test process, or suites are skipped by change detection rather than an artifact-restoring cache — use parallel finalize mode with subset builds:

  1. Aggregate uploads into one build with finalize mode. Each suite uploads as a shard (ARGOS_PARALLEL=true, ARGOS_PARALLEL_TOTAL=-1, one shared ARGOS_BUILD_NAME); the number of uploads may vary per run.

  2. Mark pull request builds as subset (ARGOS_SUBSET=true on non-main runs), so screenshots missing from skipped suites are ignored instead of reported as removed.

  3. Guarantee complete builds on your main branch. Subset builds are not eligible as baselines, so on main the Argos-enabled suites must actually run. Declare a cache-busting variable in the task's hashed env (not the passthrough list — passthrough doesn't invalidate the cache) and give it a fresh value on main only:

  1. Finalize, and keep a required check green. After the run, argos finalize --skip-if-empty closes the build — and creates a skipped build when every upload was skipped, so a required Argos check still reports success. Run it only when the tests succeeded, so a partial run can never become a baseline.

This costs a re-run of the Argos-enabled suites on every main commit, and uploads happen inside cached tasks — see the FAQ below for a logging gotcha that comes with that.

Troubleshooting / FAQ

How do PR builds compare against the right screenshots when suites are cached?

Argos resolves the baseline by walking the ancestor commits of the pull request's merge base until it finds an eligible build. As long as main builds are complete — automatic with the outputs-caching pattern, forced in the subset + finalize pattern — the nearest ancestor build contains an up-to-date version of every screenshot.

Turborepo strips my ARGOS_* variables inside tasks

Turborepo runs tasks in strict environment mode by default: undeclared variables never reach the task. This only matters when uploading from inside tasks (the subset + finalize pattern) — pass the variables through without hashing them:

With the outputs-caching pattern the upload runs outside the build system, so no passthrough is needed.

The logs show "Argos build created" but the build isn't in my finalized build

When uploading from inside cached tasks, a cache hit replays the cached task's logs, including the "Argos build created" line from the run that populated the cache — but no upload actually happens, and the replayed URL points to the old run's build. Trust the output of the argos finalize step: it lists the builds that were really uploaded and finalized in the current run. (The outputs-caching pattern doesn't have this problem: the only upload log line is the real one.)

What about re-runs of a failed workflow?

With the outputs-caching pattern, the upload step simply runs again with the same files. In finalize mode, the parallel nonce includes the run attempt on GitHub Actions, so a re-run starts a fresh build; Argos detects partial re-runs of parallel builds and fills the missing shards from the previous attempt automatically.

Last updated

Was this helpful?