3.7.2 Workflows Outside Bazel

recommended

Bazel 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

Bazel core vs outer workflow layer
Bazel handles one graph snapshot and its declared outputs. The outer layer coordinates cases that need broader selection, another revision, side effects, or extra reporting.
Inside the BUILD graph
Bazel core
One invocation sees one checkout state
Best at tree-shaped requests
Inspect
Answer graph questions from declared labels and edges.
Produce
Build or test declared outputs with explicit inputs.
Think query, aquery, build, or test for a known set of labels.
Outside the BUILD graph: workflow layer
A task runner, CI job, or AXL program can call Bazel, Git, storage, or publishing tools around the core.
Whole-repo fan-in
Select repo-wide labels first
no single owning target
query -> target file -> build/test
Two snapshots
Compare current vs base outputs
second snapshot required
current artifact + base artifact
Side effects
Write to source tree or external systems
not declared outputs
gazelle / publish / accept
Post-processing
Merge and present extra results
after Bazel finishes
LCOV / HTML / PR comments
Keep graph inspection and declared outputs in Bazel. Put repo-wide selection, cross-revision checks, side effects, and report shaping in the outer layer.

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

key takeaway

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

Gazelle should be invoked as a Bazel action so its BUILD file updates are tracked by the build graph.
Coverage post-processing (generating HTML reports with genhtml) is an example of a task that belongs in the outer workflow layer, not inside bazel coverage.
A 'bazel query ... | xargs bazel build' pipeline is an anti-pattern that should always be replaced with a single wildcard //... build.

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

0 of 3 answered

Footnotes

  1. 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

  2. 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

  3. Releasing Bazel rulesets that publish tools — concrete git archive + .gitattributes release flow and why it is simpler than a giant Bazel archive target

  4. Using Macros to Create Custom Verbs — keep side-effect work under bazel run. .publish and .accept target pattern 1 2

  5. 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

  6. rules_multirun — composed Bazel-run workflows — ordered and parallel command composition, public rule surface, and focused failure/argument tests.

  7. Code coverage with Bazelbazel coverage, instrumentation flags, merged LCOV report location, and genhtml post-processing 1 2