Skip to main content
aspect changed told you which files you touched. That’s a step on the way to the question you actually have: which tests could my change break?

Predict before you run

lib/log is near the bottom of the stack and nearly everything is above it. There are 55 tests in this repository. How many do you think a one-line change to lib/log affects?
33 of 55. That is the whole merge-base SHA on the base: line, not an abbreviation — the task prints what git merge-base handed it. Yours will be a different commit. Now a package nothing much depends on:
4 of 55. Same command, same repository, same size of edit — 60% of the suite versus 7%. That difference is the entire argument for asking the question.

How it works

.aspect/impact.axl is 294 lines, and you do not need to read them in order. The shape is _impl near the bottom — nineteen lines that do nothing but call the others, and you have the whole algorithm. Then read the two functions that exist because the obvious version is wrong: _classify, which sorts changed paths into the three things they can be, and _declared, which decides which of them Bazel actually knows about. Skim the rest: _emit_human and _emit_json are formatting, and the module docstring at the top is this page in prose. Three steps, and all three are in _impl:
  1. Diff against the merge base, including uncommitted and untracked files.
  2. Label each path by walking up to its nearest BUILD.bazel — lib/log/log.go becomes //lib/log:log.go. Source files are real nodes in Bazel’s target graph, which is why this works at all.
  3. Reverse-dep: one tests(rdeps(//..., <labels>)).
Note what the code doesn’t do:
ctx.bazel.query returns objects, not text. No --output=label, no splitting lines, no regex. t.name is the label.

Where it breaks

Two things in that file exist because the obvious version is wrong. Not every changed path is a target. Edit a README, a go.mod, or add a source file Gazelle hasn’t indexed, and rdeps fails outright with no such target — taking the whole query down. So _declared runs a cheap query of its own first, //<pkg>:* over the touched packages, which lists every target Bazel actually declares there; the candidate labels are then kept or dropped by dict membership in Starlark. Note the division of labour: Bazel answers “what exists”, and the filtering happens in AXL. (The one real Bazel intersect in this course is in the next section, in policy.axl.) Anything dropped is reported as skipped rather than silently discarded, because “my file isn’t in any srcs” usually means somebody forgot to run Gazelle. A changed BUILD file reaches nothing. Package loading is not a target-graph edge, so rdeps on a BUILD.bazel returns empty. The case that bites is adding a test over a source file you didn’t otherwise touch: the only changed path is the BUILD file, so the new test is invisible. Build exactly that:
A brand-new test target, and one changed path — the BUILD file. Now the miss:
Zero labels, zero tests, exit 0. You added a test and the tool told you nothing could break. Now the default, which escalates a changed BUILD file to //<pkg>:all:
//lib/clock:all names the new rule directly, and //lib/clock:clock_smoke_test is in the 35. Now look at the cost, because the page owes you that too. 35 of 55 — 64% of the suite — from a BUILD-file edit that added one test to a leaf package. :all is package-granular: it names //lib/clock:clock as well, so you inherit everything above lib/clock, which is almost everything. A source edit to lib/clock/clock.go selects 34 on its own, so escalation bought you exactly one test here and charged you nothing extra — but only because lib/clock holds nothing unrelated. In a package with several independent targets, escalating one BUILD edit pulls in the dependents of all of them. That is the trade, and it is why escalation is the default: a selection that is too wide wastes CPU, and one that is too narrow tells you a test can’t break when it can. --escalate=false exists only to show you the narrow version failing.

When it won’t answer

Read the bottom half, because it is the confusing part and it is deliberate. You were just told the real affected set is wider than what follows, and what follows is zero — under a green ✅, exit 0. MODULE.bazel is not a label, so it contributes no labels; no labels means no rdeps query; no query means no tests. The floor is zero, and zero is an honest floor. A .bzl or MODULE.bazel change can rewrite any rule in the repository, and standard bazel query has no reverse lookup for them. The task could have widened to //... and printed 55. It doesn’t, because a number you invented is worse than a warning you have to read: --format=json carries "complete": false alongside "affected_count": 0, and a caller that cares can branch on it.
A tool that knows when it doesn’t know is fine for advice and useless as a gate — which is exactly the line being drawn.
A skipped file is the other kind of incomplete answer, and the terminal barely mentions it. Add a source file Gazelle hasn’t indexed yet:
affected tests: 0, green ✅, exit 0 — on the terminal that single SKIPPED line is the whole warning, so read it. The JSON is blunter, because complete covers every way the selection can be a floor and not only unresolvable build logic:
A caller that branches on complete is therefore covered for all three cases — unresolvable build logic, a changed path no rule lists, and one outside every package. skipped_not_a_target and skipped_no_package are still worth reading, because they say which file to hand to Gazelle.

This is not a CI gate

Be clear about what this is for. bazel-diff hashes the target graph at two revisions and diffs the hashes; target-determinator configures the graph at both commits and compares. Both are more correct than this. Both also need two revisions, which means two checkouts and two full analyses. You cannot run either against uncommitted work. That’s the gap this fills: what does my dirty tree affect, right now, in one query — the question you have while you’re still typing, which no amount of CI tooling answers.

Your turn

--run hands the selection to bazel test and returns its exit code. Three tests, and you never named them.
Next: a question about your repository that no off-the-shelf tool answers.