6.5.4 Selecting Affected Targets Across Configurations
One affected-target set cannot safely stand for every way CI builds a repository. A dependency selected by select() may exist only on Windows, a transition may create another configured instance of a target, and toolchain resolution may add an edge for one execution platform but not another. If the selector analyzes only the default Linux build, an empty result says nothing about those unexamined graphs.
The practical rule is simple: selection coverage is a matrix, not a single graph. Define the supported top-level roots and configurations, compare the relevant graph for every supported cell, and combine only results whose identity and coverage remain explicit.
Start from the CI Work You Promise to Protect
6.5.1 Affected-Target Service Contract defined a selection universe as revisions, candidate roots, configurations, platforms, toolchains, and selector identity. Here the important part is that “configuration” is not merely a friendly name such as release. It is the effective collection of build options and platform/toolchain choices under which Bazel analyzes a top-level request.
A service contract might declare this matrix:
| CI lane | Top-level roots | Configuration inputs |
|---|---|---|
| Linux tests | //app/..., //services/... | Linux target platform, release flags |
| Windows tests | //app/... | Windows target platform, release flags |
| Packaging | //release:bundle | Linux target platform, packaging transition |
The selector need not support every configuration a developer could invent. It must support every cell for which its consumer treats unaffected as permission to skip work. Deciding which cells deserve that protection is an organizational policy developed in H.7.3 Evidence Selection; the selector's technical responsibility is to state and prove the cells it actually covers.
Do not infer roots from labels returned by a query. Top-level roots help construct the configured graph in the first place. Bazel's cquery documentation recommends an explicit --universe_scope for queries more complex than a simple dependency closure because transitions can configure a target differently when it appears beneath another root.1 The same label can therefore appear as multiple configured targets, including target and execution configurations.2
Choose Target or Configured-Target Evidence Deliberately
query and cquery do not offer two interchangeable serializations of one graph:
queryexamines the target graph after loading, before build options resolve configurable attributes. It conservatively includes all possible branches of aselect().cqueryexamines configured targets after analysis. It resolves the branch selected by the supplied options and observes transitions and resolved toolchain dependencies for the analyzed roots.3
That creates an engineering trade-off rather than a universal winner. An unconfigured target graph can be cheaper to produce and can deliberately overapproximate configuration-dependent edges. A configured graph can represent a particular build more precisely, but only for the roots and configuration inputs that were analyzed. The published BazelDiff implementation supports both modes, and its authors report that configured-query mode has a significant performance cost.4
Choose the evidence according to the selector's promised result:
| Evidence mode | Defensible claim | Required guardrail |
|---|---|---|
| Unconfigured target graph | Impact within the declared roots under the selector's documented conservative edge model | Prove that overapproximation retains every configuration-dependent edge relevant to supported lanes |
| Configured-target graph | Impact for the exact roots and configuration inputs represented by that snapshot | Analyze every supported matrix cell; never extrapolate one cell to another |
| Hybrid | Broad target evidence plus configured checks for selected lanes or mechanisms | Version the split and test that no supported lane falls between the two models |
Using query is not automatically unsafe, and using cquery is not automatically complete. Completeness comes from matching evidence to the declared matrix and proving the relationship. A precise configured graph for the wrong root or platform is precise evidence about the wrong build.
Treat Each Matrix Cell as Comparison Evidence
Suppose //app:binary depends on a platform-selected implementation:
filegroup(
name = "root",
srcs = select({
":linux": [":posix_io"],
":windows": [":win32_io"],
}),
)
A change beneath //app:win32_io may have no path to //app:root in the Linux configured graph. If CI protects both Linux and Windows, comparing only Linux snapshots creates a false negative. The selector should compare the Windows cell too and union the affected roots:
affected(Linux, //app:root) = {}
affected(Windows, //app:root) = {//app:root}
union for the declared request = {//app:root}
The union is safe only when all required cells completed successfully. If the Windows head snapshot is missing, incompatible, or produced for different roots, the overall result is unknown, not the Linux result. 6.5.5 Incomplete Selector Evidence develops those freshness and compatibility checks.
A declared selector request protects root //app:root for Linux and Windows. The Linux configured graph does not contain the changed Windows-only dependency. The Windows configured graph contains the path from //app:win32_io to //app:root. With both cells complete, their union selects the root. If the Windows cell is missing, the selector returns unknown instead of the empty Linux result.
//app:win32_io absent
//app:win32_io → //app:root
affected: //app:root
unknown: missing Windows cell
Keep cell identity with the evidence. At minimum record:
- immutable base and head revisions;
- top-level root set and candidate-root boundary;
- target platform and relevant build settings;
- toolchain and execution-platform dimensions represented by the mode;
- selector, snapshot schema, and graph mode versions;
- completion state and selected labels for that cell.
Do not use Bazel's short configuration hash as the service's complete durable identity. cquery prints an opaque hash derived from build option values, and bazel config can display its contents,5 but the selector still needs the surrounding revision, roots, tools, schema, and declared coverage that make a comparison reproducible.
Account for Transitions, Platforms, and Toolchains
A top-level configuration does not imply that every dependency uses that same configuration. Attribute and rule transitions can change options below a root. Tool dependencies commonly move to an execution configuration, and toolchain resolution can introduce configured dependencies selected for an execution platform. A cquery universe built from the real top-level roots exposes these configured instances; a separate cquery invocation whose only top-level root is a dependency label can configure that label differently.6
For selection, separate two responsibilities:
- Enumerate top-level cells. These are the CI entry points and configuration inputs the service promises to protect.
- Preserve configurations reached inside each cell. These arise through transitions and resolution while Bazel analyzes the top-level request.
Do not attempt to enumerate every internal configuration hash as an independent CI lane. Instead, construct each supported top-level request faithfully and retain the configured dependency graph it produces. If the selector enables filters, that filtering becomes part of its completeness claim and needs a test: --notool_deps can remove paths that cross from the target configuration to a non-target configuration, --noimplicit_deps can remove implicit dependencies (including resolved toolchains), and --noinclude_aspects can remove aspect-added dependencies.7
6.5.3 Global Target-Selection Invalidators handles a different problem: a platform declaration, toolchain registration, transition implementation, or rc input may change the graph-producing rules themselves. When such an input changes, the selector must first decide whether its snapshots remain valid or must broaden/return unknown. Only then can it compare the supported configuration cells described here.
Test the Edge That a Single Graph Would Miss
A useful fixture needs at least two supported configurations and one deliberately asymmetric edge:
- Define one candidate root whose dependency differs by
select(), platform/toolchain resolution, or a transition. - Place a changed target behind only the second configuration's edge.
- Produce compatible base and head evidence for both configurations from the same declared roots.
- Verify that the first cell omits the root and the second selects it.
- Verify that the combined answer includes the root and explains which cell supplied the path.
- Remove the second cell's snapshot and require
unknownrather than the first cell's empty result.
The runnable configuration-selection-matrix fixture uses this exact //app:root / //app:win32_io asymmetry. Its matrix verifier runs both configured queries, records the selected Windows cell, and proves that withholding that required cell produces unknown rather than an empty Linux answer.
Also test root dependence. Analyze a tool label alone, then analyze it beneath a root that applies an execution transition. The fixture should demonstrate that label equality does not imply configured-target equality and that the selector never reuses an isolated-root comparison as proof for the transitioned instance.
This test is more valuable than asserting that two configuration runs produce different hashes. It connects the difference to the decision CI actually consumes: a root that must not be skipped.
Decide: A selector analyzes all repository labels with unconfigured query, whose graph includes every branch of each select(). Can the service now claim coverage for every platform without further evidence?
Reveal
No. The broad graph may be a valid conservative implementation, but the service must prove that its representation retains every relevant configuration-dependent edge, including transitions, toolchains, implicit dependencies, and the promised root universe. “All labels” does not by itself establish “all configured builds.” If that proof exists, the unconfigured mode may safely over-select; otherwise the unsupported cells remain unknown.
Affected-target selection is complete only relative to an explicit matrix of top-level roots and supported configurations. Choose unconfigured or configured-target evidence according to the claim it can prove: query may conservatively overapproximate configurable edges, while cquery precisely represents the configurations reached from particular analyzed roots. Neither mode is globally complete by name alone.
Compare every required matrix cell, retain its revision, root, configuration, platform/toolchain, graph-mode, and schema identity, and union affected roots only after all required cells complete. A fixture with an edge visible in just one configuration must still select its consumer; missing evidence for that cell turns the combined result into unknown.
Check your understanding · 4 questions
1.CI protects //app:root on Linux and Windows. A change reaches that root only in the Windows configured graph, while the complete Linux graph has no such path. What result is defensible?
Select one answer
2.Match each part of a configuration cell's evidence identity to the question it answers:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
3.Before a selector unions per-cell results into an answer that may permit CI to skip work, which conditions must it establish?
Select all that apply
4.A cquery invocation analyzes a tool label by itself. Another invocation reaches the same label beneath a root that applies a transition. What may the selector conclude?
Select one answer
Footnotes
-
Configurable Query —
--universe_scopedefines the top-level targets whose configured transitive closure is queried ↩ -
Configurable Query — one label can have target and execution configured-target instances in the same query universe ↩
-
Configurable Query —
queryloads targets before options are evaluated, whilecqueryruns after analysis and resolvesselect()branches ↩ -
Precision CI at Scale: Target-Aware Workflows with Bazel Diff — support for query and cquery modes and the reported configured-query cost trade-off ↩
-
Configurable Query — configuration hashes and inspection with
bazel config↩ -
Configurable Query — top-level universe scope, transitions, and target versus execution configuration examples ↩
-
Configurable Query —
--implicit_deps,--tool_deps, and--include_aspectschange which configured dependencies appear ↩