Skip to main content
Selective testing runs only the tests a change can affect, instead of every test on every commit. aspect cache diff finds that set by asking the remote cache rather than analyzing the source: it lists the test targets affected by the current working tree and runs no tests. Which tests are affected? Ask the cache, run nothing. explains how it works and how it compares with other approaches. Two Bazel invocations on the same warm output base probe the cache with --experimental_remote_require_cached: every cache hit means “unaffected”, and every miss is attributed to the dependent tests. It’s a remote-cache operation; it needs no remote execution.

When to use it

  • Debug cache misses. Find the exact set of cache-missed actions that explain why a CI run rebuilt or re-ran more than expected.
  • Validate CI cache coverage. Run it against a clean checkout to confirm your CI uploads the baseline you expect. A populated cache reports Affected 0 of N.
  • Pre-flight a change. Before pushing, list the tests affected by your edits and pipe them into bazel test to run only what matters.
  • Drive selective CI. Use the lines or json output to gate downstream jobs on the affected target set.

How it works

  1. Enumerates the test targets in scope with tests(<patterns>).
  2. Runs one Bazel invocation with --experimental_remote_require_cached. Each action’s up-to-date check resolves against the remote cache; on a miss, execution is denied so the probe runs nothing.
  3. Reads the gRPC log of GetActionResult calls. A miss anywhere in a test’s transitive closure trips at that action, the frontier.
  4. Attributes the affected tests from the frontier (overreport) or only from missing test actions (precise).
Affected labels stream to stdout; progress, per-target reasons, and the summary go to stderr, so you can pipe stdout cleanly.

Prerequisites

  • A writable --remote_cache, in .bazelrc or on the command line. The task stops early if none is configured.
  • A populated baseline: a cache hit is what makes a target “unaffected”, so the mainline’s action results must already be uploaded. Normally this happens as a byproduct of regular CI:
    Seed on a clean checkout (or a fresh --output_base): on a warm output base Bazel finds everything locally cached, executes nothing, and uploads nothing. CI checkouts are cold, so this is automatic there.
  • Bazel 7 or later. Test selection follows Bazel’s test patterns and filters. Wildcards such as //... exclude manual-tagged tests; name a manual test explicitly to include it. Pass negative patterns after --.
  • The probe and the baseline must resolve the same Bazel flags, so they compute the same action digests. The probe reuses the same rc/config as aspect build / aspect test, so they line up by default. A flag that changes an action’s environment (for example --action_env=FOO) must be present on both sides or neither.

Modes

Pick a mode with --mode: Execution guarantee: no test ever runs in either mode. overreport executes nothing; precise executes only the missing non-test actions (it builds with bazel build, which cannot execute TestRunner). The lone exception is actions tagged no-remote, local, no-remote-exec or no-cache: they bypass the remote path entirely, so they run locally and read as affected.

Output

By default, affected labels stream to stdout, one per line:
Pipe directly into Bazel:
Or hand the labels to any command with --exec:
For scripting, switch to JSON:
Progress, per-target reasons (including the cache-missed target that each test is “caused by”), and the final Affected X of N test target(s). summary go to stderr.

Configuration flags

Run aspect cache diff --help for the complete list of flags.

Examples

Scope to a sub-tree and run only the affected tests:
Use a CI config so the probe matches your CI baseline’s flags:
Narrow overreport noise to genuine input changes:
Drive a downstream job from JSON:

Soundness and limits

The result is bounded by your baseline’s cache coverage:
  • A hit means the target is unaffected.
  • A miss means the target is either changed or never-uploaded/evicted.
Against a fully-populated baseline this is exact. Against a sparse cache, overreport over-reports and precise narrows the result to genuine input changes. The probe runs in two passes on one warm output base. The first flips a salt in the action’s exec properties, which invalidates the ACTION_EXECUTION nodes without discarding the analysis; the second re-checks them with --nouse_action_cache, forcing a fresh remote GetActionResult per action. Keeping the analysis is the point — it needs no separate or wiped output base, and it replaces the old --nokeep_state_after_build, which re-analyzed on every probe.