6.5.3 Global Target-Selection Invalidators
An affected-target selector is easiest to trust when a source file changes beneath a target already present in both graphs. The difficult cases are files that can change the graph itself: a BUILD file can add an edge, a .bzl file can redefine many targets, a .bazelrc can change the configuration, and Bzlmod or toolchain metadata can change what repositories or implementations exist. If the selector treats all of those as ordinary source edits, its precise-looking result can be dangerously narrow.
The safe model is not “these files always rebuild everything.” It is: every input that can change the selector's graph or configuration must have an explicit invalidation rule. Some changes can still be modeled precisely. Some require broadening the result to a known scope. Others leave too little evidence for a trustworthy comparison and must turn the result into unknown.
Selection Depends on More Than Source Ownership
6.5.2 Comparing Bazel Graphs Across Revisions compares base and head evidence for a declared universe. That comparison is sound only when both snapshots represent every input that can affect the modeled graph.
Consider a selector that maps changed source files to owning targets and then walks reverse dependencies. It may handle lib/parser.cc correctly while missing all of these changes:
- a
BUILDorBUILD.bazelfile adds a dependency that did not exist in the base graph; - a loaded
.bzlfile changes a macro or rule used by packages beyond the changed file's package; - a workspace
.bazelrcor an RC file it imports changes flags used to construct the configured graph; MODULE.bazel, a lockfile, or module-extension inputs change the external repositories visible to the build;- platform or toolchain declarations change which configured dependency or implementation is selected.
These are not equivalent mechanisms. What they share is an operational consequence: ordinary source ownership is not sufficient evidence for bounding their impact. The Tinder BazelDiff case study handles rarely changed Starlark and bazelrc inputs through “seed file paths” that reset the seed for all targets. Its authors also say that deeper configuration-dependent cases require cquery, and that Bzlmod support was still work to be completed in their deployment.1 Those are useful implementation facts, but not a universal completeness guarantee.
Give Every Metadata Class a Deliberate Outcome
Maintain an invalidator registry alongside the selector version. For each supported file or change class, record:
- how the selector recognizes it;
- which comparison evidence models its effect;
- the largest scope the evidence proves safe;
- what happens when that evidence is missing or incompatible.
The resulting decision has three useful shapes:
| Classification | Evidence available | Selector response |
|---|---|---|
| Model precisely | Both snapshots contain compatible evidence that traces the changed metadata to affected roots. | Return the affected roots with the trace and scope. |
| Broaden a known scope | The selector cannot retain fine precision, but a declared boundary is known to contain every possible effect. | Select that entire boundary and explain which invalidator caused the expansion. |
| Invalidate the comparison | Required inputs, snapshots, or semantics are absent, stale, unsupported, or incompatible. | Return unknown; do not manufacture a narrow set from the remaining evidence. |
For example, an implementation may know exactly which packages load a changed .bzl file and have compatible base and head loading evidence. That permits a bounded result. Another implementation may only know that the file is part of a repository-wide seed set; selecting every candidate root in the declared repository is then conservative but explainable. If neither fact is available, the comparison is incomplete and the only defensible technical answer is unknown.
This classification prevents two opposite mistakes. Treating every metadata edit as global throws away useful precision. Treating every one as locally traceable invents precision the evidence does not support.
Classify by Effect, Not Filename Alone
A filename is a useful trigger, not a proof of impact. The same category can require different handling depending on the selector's model and declared universe.
Package and Starlark definitions
BUILD, BUILD.bazel, and loaded .bzl files can alter target declarations and graph edges. Reloading only the package containing a changed .bzl file is not enough when other packages load that file. A precise selector needs loading relationships for both revisions; otherwise it must widen to a boundary known to include every loading package or invalidate the comparison.
Invocation configuration
The workspace .bazelrc and RC files it imports can change the flags with which a configured graph is built.2 The selector request and snapshot identity therefore need the effective configuration inputs they claim to cover. Merely observing that the target labels are unchanged does not show that the configured targets are unchanged. 3.2.1 .bazelrc Hierarchy explains where rc settings come from; 6.5.4 Selecting Affected Targets Across Configurations continues with coverage across configurations.
An optimized changed-file list is evidence too. bazel-diff documents that an omitted changed path in its experimental modified-file scope can be skipped on both revisions; incomplete query evidence likewise cannot justify a narrow result. Reject either state as unknown or rerun evidence collection without that unsafe optimization.3
Modules, lockfiles, and repository creation
MODULE.bazel, the Bzlmod lockfile, module-extension source, and extension inputs can change repository resolution or generated repositories before ordinary target analysis. Module extensions read module-graph inputs and create repositories, while MODULE.bazel.lock records module-resolution and extension-evaluation results.4,5 A selector must not claim exact effects merely because it recognizes one of these filenames. It needs evidence that its snapshots include the relevant external state and extension behavior. The BazelDiff case study is especially instructive here: at the time described, the team expected to over-select while finishing its Bzlmod migration rather than claiming support it had not implemented.6
Platforms and toolchains
Platform and toolchain changes can alter configured dependencies or the implementation chosen for an execution context. Toolchain resolution selects concrete toolchains from the target platform, available execution platforms, and available toolchains; it runs independently for each configured version of a target.7 A static target graph may not contain enough evidence to bound those effects. cquery evaluates the configured target graph after analysis, so it can expose configuration-selected dependencies; the selector still needs to state which configurations and inputs its snapshots cover.8 The case study's authors used broad seed invalidation for rare toolchain changes and pointed to configured queries for deeper cases.9 4.6.3 Toolchain Resolution provides the mechanism; this article's concern is whether the selector has represented that mechanism completely enough to skip CI work.
Host-provided inputs deserve the same treatment when the selector claims they are part of its comparison. If an imported rc file, generated repository input, or selector-side environment dimension is omitted from snapshot identity, reusing the snapshot after that input changes is not justified.
Make Broadening Observable
A global invalidator should not look like an unusually large ordinary result. Preserve why precision was lost:
{
"outcome": "affected",
"scope": "declared candidate roots",
"targets": ["<all roots in scope>"],
"explanation": {
"classification": "broadened",
"invalidator": "workspace bazelrc changed",
"rule_version": "<selector-version>"
}
}
This is an interface sketch, not a required schema. The important distinction is between a broad, completed answer and unknown. A broad result says the selector knows a conservative boundary containing every possible effect. unknown says it cannot establish such a boundary from the required evidence. Both differ from silently returning only the source-derived labels.
The open-source BazelDiff interface demonstrates one concrete broadening mechanism: seed files participate in graph hash generation so selected configuration files can affect all target hashes.10 A seed list is useful only to the extent that it is complete and versioned for the repository it protects. Copying another repository's list does not transfer its guarantee.
Decide: A shared .bzl file changes. Your selector can identify packages that load it in the head revision, but the base snapshot lacks loading relationships. Should it select only the head-side consumers?
Reveal
No. The missing base-side evidence can hide consumers removed or reshaped by the change itself. Broaden to a boundary that is independently known to contain all possible consumers, if such a boundary exists; otherwise return unknown. A head-only list is evidence, but it is not a complete cross-revision bound.
Prove the Registry with Mutations
Treat the invalidator registry as executable compatibility policy. Build a mutation suite around a small base/head fixture and change one metadata class at a time:
The selector-invalidators mutation suite exercises this contract, including incompatible selector evidence, incomplete changed-file scope, and incomplete query evidence. Its checked-in base/head cases model a selector service, not a Bazel wire format. For BUILD and shared-Starlark mutations, the fixture also compares small base/head target graphs and reverse-walks their changed nodes to derive the candidate roots that must remain selected. The verifier then injects a lost root and false invalidator classifications, so success cannot come only from matching a checked-in expected verdict.
- add or remove a dependency in a
BUILDfile; - change a
.bzldefinition loaded from more than one package; - change a workspace or imported bazelrc input represented by the supported CI configuration;
- change module, lockfile, or module-extension input state;
- change a platform or toolchain declaration used by a supported configuration;
- remove one required snapshot input or substitute an incompatible selector version.
For each mutation, assert one of two properties: every affected candidate root is selected, or the result is explicitly unknown. If the implementation deliberately broadens a scope, assert both the boundary and the recorded invalidator reason. This is stronger than checking that the output is non-empty: an arbitrary label does not prove that all affected roots were retained.
Run the suite whenever the selector, graph schema, supported Bazel configuration, repository dependency model, or invalidator registry changes. A new metadata mechanism starts unsupported; it does not become safe merely because no false negative has yet been observed. 6.5.5 Incomplete Selector Evidence examines stale, missing, delete, and rename evidence in more detail, while 6.5.6 Fail-Closed Target Selection defines how CI consumes unknown and validates a selector in shadow or fallback lanes.
Source ownership cannot bound changes that alter package loading, rule definitions, effective Bazel configuration, external repository state, platforms, or toolchain selection. Put those inputs behind a versioned invalidator registry: model an effect precisely when compatible evidence proves the bound, broaden to a known complete scope when precision is unavailable, and return unknown when no such bound can be established.
Prove that contract with one mutation case per supported metadata class. The success condition is not merely a non-empty result; it is that every affected root is selected or the comparison explicitly refuses to make an incomplete claim.
Check your understanding · 3 questions
1.Which selector responses are justified when a changed metadata input cannot be traced precisely?
Select all that apply
2.A selector receives a changed-file optimization list that omits a file that actually changed. What should it do before returning a narrow result?
Select one answer
3.Classify each statement about invalidator evidence.
Choose True or False for each sentence
Footnotes
-
Precision CI at Scale: Target-Aware Workflows with Bazel Diff — Q&A on seed file paths, bazelrc and toolchain changes,
queryversuscquery, and incomplete Bzlmod support ↩ -
Write bazelrc configuration files — workspace
.bazelrc, imports, and option defaults ↩ -
bazel-diff — hash-based affected-target selection — changed-file scope and incomplete-query limits ↩
-
Module extensions — extensions read module-graph data and create repositories through repository rules ↩
-
Bazel Lockfile —
MODULE.bazel.lockrecords module resolution and module-extension evaluation ↩ -
Precision CI at Scale: Target-Aware Workflows with Bazel Diff — Bzlmod migration answer and conservative handling of module-file changes ↩
-
Toolchains — resolution inputs, selected implementations, and per-configuration behavior ↩
-
Configurable Query (cquery) — analysis-phase configured graph and option-selected dependencies ↩
-
Precision CI at Scale: Target-Aware Workflows with Bazel Diff — toolchain invalidation through seed files and configured-query guidance ↩
-
bazel-diff — hash-based affected-target selection — seed-file support in the two-revision graph-hashing workflow ↩