Skip to main content
aspect cache diff lists the test targets affected by the current working tree by diffing it against your remote cache. It runs no tests. A single Bazel invocation with --experimental_remote_require_cached probes the cache: every cache hit means “unaffected”, and every miss is attributed to the dependent tests. Strictly a remote-cache operation — no remote build execution (RBE) is required.
aspect cache diff                       # affected tests under //...
aspect cache diff //services/...        # scope to a sub-tree
aspect cache diff --format=json         # scriptable output
aspect cache diff --exec="bazel test"   # pipe affected labels into a command

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 is uploading the baseline you expect — a populated cache should report Affected 0 of N.
  • Pre-flight a change. Before pushing, list the tests that are actually 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 configured in .bazelrc (or via --bazel-flag). The task fails fast if no remote cache 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:
    bazel test //... --remote_upload_local_results
    
    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.
  • 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:
ModeWhat it doesCost
overreport (default)Reverse-deps from each cache-missed target to its dependent tests.Runs nothing. May flag a test even when its dependency would rebuild to an identical output.
preciseFirst runs bazel build to build and upload the missing non-test actions, then flags a test only when its own test action misses.Costs the build of the missing non-test actions. Narrows the result to genuine input changes.
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 — those bypass the remote path entirely, so they run locally and read as affected.

Output

By default, affected labels stream to stdout, one per line:
$ aspect cache diff
//backend/api:integration_test
//backend/api:unit_test
//libs/auth:token_test
Pipe directly into Bazel:
aspect cache diff | xargs bazel test
Or hand the labels to any command with --exec:
aspect cache diff --exec="bazel test"
For scripting, switch to JSON:
$ aspect cache diff --format=json
{
  "mode": "overreport",
  "total_tests": 124,
  "affected_count": 2,
  "affected": [
    {
      "label": "//backend/api:integration_test",
      "caused_by": [
        { "target": "//libs/auth:token", "mnemonic": "GoCompile" }
      ]
    },
    {
      "label": "//libs/auth:token_test",
      "caused_by": [
        { "target": "//libs/auth:token_test", "mnemonic": "TestRunner" }
      ]
    }
  ]
}
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

FlagDefaultDescription
--modeoverreportoverreport or precise. See Modes.
--formatlineslines (one label per line) or json (single document on stdout).
--exec(empty)Run the given command with the affected labels appended as arguments (for example --exec="bazel test"). No-op when nothing is affected. Overrides --format.
--bazel-flagAdditional Bazel flags forwarded to the probe and the precise build (for example --bazel-flag=--config=ci). Repeatable.
--bazel-startup-flagAdditional Bazel startup flags. Repeatable. Note that changing startup flags restarts the Bazel server.
positional//...Target patterns scoping the affected set (intersected with tests(<patterns>)).
Run aspect cache diff --help for the complete list of flags.

Examples

Scope to a sub-tree and run only the affected tests:
aspect cache diff //services/... | xargs bazel test
Use a CI config so the probe matches your CI baseline’s flags:
aspect cache diff --bazel-flag=--config=ci //...
Narrow overreport noise to genuine input changes:
aspect cache diff --mode=precise //...
Drive a downstream job from JSON:
aspect cache diff --format=json > affected.json
jq -r '.affected[].label' affected.json | xargs bazel test

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 with --nokeep_state_after_build --nouse_action_cache, which forces a fresh remote GetActionResult per action — so the command runs on the default output base with no separate or wiped base.