3.7.2 Workflows Outside Bazel
recommendedBazel works best when the request is tree-shaped: inspect this graph, then build or test the declared outputs. Workflows that need the whole checkout, another revision of the repository, a write back into the source tree, or a reporting pass after tests still use Bazel, but they do not fit cleanly inside a BUILD rule. That is the boundary introduced in 3.7.1 Bazel's Output Boundary: Bazel is the core, and a task runner around it handles the rest.1,2
Whole-repo fan-in is orchestration, not a target
A useful test is the shape of the request. If you are tempted to invent a synthetic target whose only purpose is "gather everything", or your mental model starts as bazel query ... | xargs bazel ..., you are no longer describing one artifact and its transitive inputs. You are describing orchestration around multiple Bazel invocations.1
Release archives, documentation bundles, and "run every test matching X" all have the same fan-in shape: they pull together packages that do not naturally depend on one another. Modeling that as a collector target hurts incrementality because even loading or querying the collector pulls Bazel toward whole-repo analysis. The lighter pattern is: discover the labels with bazel query, then feed those labels back to bazel build or bazel test as explicit inputs. The query overview itself belongs in 2.1.5 Inspecting the Graph — Query Preview. The important habit here is to keep the selection step and the execution step separate.1
targets="$(mktemp)"
bazel query --output=label --output_file="$targets" 'kind(test, //...)'
bazel test --target_pattern_file="$targets"
The workflow-orchestration run_release_targets.sh wires this exact shape against a small release-tagged set: it writes labels from attr("tags", "\brelease\b", ...) into a temp file, then hands that file to bazel build --target_pattern_file=.... Adding a new release artifact stays a one-line change — tag the new target release — without growing a synthetic //:all_release collector.
Source archives are the same story. A Bazel rule that tries to gather the whole repository ends up fighting package boundaries and subpackage glob() behavior. For release artifacts, git archive plus .gitattributes is usually the right tool: export-ignore drops files you do not ship, and placeholder substitution lets you stamp the archive without maintaining a giant tree of filegroup glue.1,3
Two-snapshot checks belong above the build
Bazel evaluates one checkout state per invocation. If correctness depends on comparing today's output with a different revision, the second snapshot has to come from outside the build graph. A buf_breaking check is the archetype: build the current output, fetch the base-commit artifact from CI, then run the comparison step afterward.1
That is different from 1.2.9 Golden File Testing (Test-Accept Pattern). Golden files are a checked-in expectation for one target, updated intentionally through a separate side-effect path. A comparison against "whatever main produced yesterday" is not a golden-file problem. It belongs in CI storage and orchestration instead.4
At scale, affected-target CI is the same idea with better target selection. 6.5.1 Affected-Target Service Contract formalizes the outer layer: run a lightweight diff step first, decide which targets matter for this changeset, then generate the downstream pipeline around those results instead of hard-coding one giant static job.5
Keep side effects explicit
Gazelle is a clean example. It is useful precisely because it rewrites BUILD files before Bazel loads them. If it ran as a normal action, it would violate the expectation that build actions only populate declared outputs. So the maintainer workflow is not "hide Gazelle inside Bazel". It is "run Gazelle as an explicit pre-build step", then let Bazel consume the updated files. The implementation details of Gazelle itself live in 3.4.2 Gazelle (BUILD File Generation). The key point here is architectural placement.1 The build-maintenance Gazelle plan helper keeps that placement reviewable: it prints the gazelle ./... command and the BUILD-file review steps a maintainer would run, without invoking Gazelle itself.
The same split applies to deploy, publish, and accept flows. bazel build and bazel test stay read-only and cache-friendly. The step that intentionally mutates the world becomes a separate runnable target or outer task. The official custom-verb tutorial shows this with .publish and .accept targets generated by macros: verify with a normal test, then run the side-effecting update only when a human or CI step asks for it.4 The workflow-orchestration notes.publish launcher keeps that contract concrete: it pulls the build artifacts in through runfiles and writes them under out/published/ (idempotently, by scrubbing the directory first), so the side effect is explicit and re-runnable rather than hidden inside a build action.
When several runnable steps need one entry point, rules_multirun makes the
same boundary explicit: Bazel builds the declared executables, then a launcher
coordinates them in order or in parallel after the build.6 Its public
defs.bzl and
focused tests/
show argument, environment, failure, and ordering contracts. Use that pattern
for process composition. Do not mistake the launcher for one cacheable action
that magically owns all of the child commands.
Coverage is a pipeline, not just a subcommand
bazel coverage --combined_report=lcov is convenient when you want one merged LCOV output, but it is still a workflow with phases: instrument the relevant code, run the tests, merge per-test tracefiles, then render or publish the result separately.7
That separation is visible even in the official docs: with --combined_report=lcov, the merged report lands under $(bazel info output_path)/_coverage/_coverage_report.dat, while HTML rendering happens later through genhtml from the repository root.7 For operator-level usage, 1.2.8 Coverage covers the normal flags.
Once you need incremental coverage, pull-request annotations, or a report format tailored to your organization, you are back in task-runner territory. Bazel can produce the raw coverage artifacts, but the outer layer decides how to merge, filter, and present them. That is exactly the kind of orchestration 3.7.3 Aspect Extension Language (AXL) is meant to make less ad hoc when shell scripts stop being enough.1,2
If the workflow needs one of four things — the whole repo, a second revision, a source-tree or external side effect, or a post-processing/reporting pass — keep it out of the BUILD graph. Let Bazel answer graph questions and produce declared outputs. Let the outer layer select targets, fetch comparison artifacts, update checked-in files, or publish results. When those scripts start turning into a platform of their own, the next step is 3.7.3 Aspect Extension Language (AXL).1,2
Check your understanding · 3 questions
1.A team wants to build a release archive containing outputs from many unrelated packages. What is the recommended approach?
Select one answer
2.True or false about side effects and workflow placement:
Choose True or False for each sentence
3.A team uses bazel query 'kind(java_library, //...)' | xargs bazel build in CI to assemble a per-language batch. A reviewer suggests replacing it with a single 'collector' target whose deps list every Java library. Why does the original query | xargs pattern usually scale better?
Select one answer
Footnotes
-
The 'outside of Bazel' pattern — Bazel's core boundary, collector-target anti-pattern,
git archive, version-comparison workflows, Gazelle placement, and coverage as an outer-layer task ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Sponsored Lightning Talk: Beyond Make Serve: Starlarkification for Tasks - Alex Eagle, Aspect Build — task programs as the layer around Bazel, including coverage/reporting and multi-step orchestration ↩1 ↩2 ↩3
-
Releasing Bazel rulesets that publish tools — concrete
git archive+.gitattributesrelease flow and why it is simpler than a giant Bazel archive target ↩ -
Using Macros to Create Custom Verbs — keep side-effect work under
bazel run..publishand.accepttarget pattern ↩1 ↩2 -
Precision CI at Scale: Target-Aware Workflows with Bazel Diff - Maxwell Elliott & Connor Wybranowski — affected-target selection and dynamic CI pipeline orchestration outside the build graph ↩
-
rules_multirun — composed Bazel-run workflows — ordered and parallel command composition, public rule surface, and focused failure/argument tests. ↩
-
Code coverage with Bazel —
bazel coverage, instrumentation flags, merged LCOV report location, andgenhtmlpost-processing ↩1 ↩2