3.6.3 CI-Specific Flags & Exit Codes

CI needs Bazel to do two things well: leave behind a log humans can scan after the job finishes, and return a status a script can classify without regexes. The practical pattern is small: keep CI-only defaults behind the named config from 3.2.1 .bazelrc Hierarchy, use that same config on every CI command, and treat the numeric exit code as the first result, not as an afterthought.1,2 That extends 3.6.1 Basic CI Recipe without changing the pipeline shape, and it avoids the flag churn that makes warm-runner reuse less effective in 3.6.2 Bazel Server Lifecycle in CI.

Put log behavior behind --config=ci

A good starter block in .bazelrc looks like this:

build:ci --keep_going
build:ci --noshow_progress
build:ci --color=no --curses=no
test:ci --test_output=errors

The ci-cache-baseline .bazelrc wires the same ideas behind real common:ci / build:ci / test:ci blocks in a runnable workspace, plus CI-cache paths and a few extra log-shaping flags. Note that --color / --curses sit on common:ci there so they apply to non-build commands too. For a smaller reproduction stripped to the named-config plus the diagnostic flags discussed below, the bazelrc-layering snippet's .bazelrc keeps the same set of build:ci and test:ci flags as the starter block above (with --color=no and --curses=no split onto two build:ci lines for readability), alongside meta commands that exercise bazel build --announce_rc --config=ci //app:app and the startup-flag form bazel --ignore_all_rc_files build //app:app. Then the pipeline just runs:

bazel test --config=ci //...

--keep_going is the feedback flag: Bazel still exits non-zero, but it keeps building and testing as far as it can so one CI run shows the full independent breakage set instead of only the first failure.1,3,4 --noshow_progress removes the constantly updating progress chatter that is useful in an interactive terminal and mostly noise in archived logs.2 --test_output=errors is a good default when the CI UI makes it annoying to fetch per-test log files, because it prints failed test stdout/stderr immediately after the failing test completes without interleaving simultaneous tests.1

--color and --curses deserve a more precise rule than "always disable them". The manual defines them as output modes: auto follows terminal detection, no forces plain text, and yes forces terminal-style rendering.1 On plain log collectors, --color=no --curses=no avoids ANSI noise and cursor-control churn. On CI systems that emulate a terminal well, some teams intentionally force them on for readability.5 The important part is to choose the mode deliberately and keep it stable across the job.

If you need fail-fast test execution, that is a separate knob. --test_keep_going is enabled by default, and disabling it makes Bazel abort on the first non-passing test. --notest_keep_going should not be combined with --keep_going.1 For most broad CI sweeps, --keep_going is the flag that matters.

Exit codes are the script API

Once the command returns, the exit code is Bazel's machine-readable contract. For bazel test, the maintainer-level distinctions that matter most are these:2

  • 0 - success
  • 1 - the build failed
  • 3 - the build succeeded, but some tests failed or timed out
  • 4 - testing was requested, but the target pattern matched no tests

There are also cross-command failures worth recognizing: 2 for bad flags or command combinations, 8 for interruption, 9 for server-lock contention when --noblock_for_lock is in play, and several higher-numbered codes for Bazel or environment trouble such as 32, 33, 36, 37, 38, 39, and 45.2 That is the CI extension of 1.1.9 Exit Codes: capture $? immediately, special-case only the codes your automation truly understands, and treat every other non-zero as failure. The scripting docs explicitly warn that future Bazel versions may replace some generic failures with more specific non-zero codes.2 The fuller taxonomy and phase-aware debugging workflow come later in 5.6 Debugging.

bazel test --config=ci //...
status=$?

case "$status" in
  0) ;;
  3) echo "tests failed" ;;
  4) echo "no tests matched the pattern" ;;
  2|8|9) echo "command or interruption problem" ;;
  32|33|36|37|38|39|45) echo "Bazel or environment problem" ;;
  *) echo "build failed; inspect Bazel output" ;;
esac

exit "$status"

Flags for red-build diagnosis

--announce_rc is the first flag to add when CI behaves differently from a laptop: it prints which .bazelrc files contributed startup and command options, which quickly exposes a hidden project or user default.1,3,5 The bazelrc-layering snippet ships an assert_announce_rc.sh script that captures the output of bazel build --announce_rc --config=ci //app:app and asserts the workspace .bazelrc path is in it, which is a useful template for a CI-side check that the rc files you expect are actually being read. --verbose_failures prints the full failing command line, and --show_timestamps adds a timestamp to each Bazel log line so long gaps stop being guesswork.1,5

For broken tests, --test_output=streamed is better than errors when you need real-time output while the test is still running.1,3 --flaky_test_attempts is a different kind of knob: it retries individual tests, and a test that eventually passes is reported as FLAKY but still contributes to an overall success exit code.1,4 That is useful for triage, but it is not the same as retrying the whole Bazel invocation. That policy belongs later in 6.6.5 Classifying Failures Before Retrying.

Use one unambiguous split: each pipeline task chooses its platform, target patterns, environment, and steps, while stable Bazel flags remain in a named, checked-in .bazelrc config. BazelCI's public schema also exposes per-task build_flags and test_flags. Treat those as an escape hatch only for genuinely task-specific exceptions, not as the normal home for shared Bazel behavior.6 This is a BazelCI/Buildkite schema, not a Bazel command-line API, but the separation is portable.

key takeaway

Keep CI flags boring and explicit: one stable --config=ci, --keep_going for breadth of feedback, a deliberate log mode (--noshow_progress, --color, --curses, --test_output), and exit-code handling before log parsing. If the script cannot tell the difference between test failure, build failure, and Bazel or environment failure, the pipeline is still too naive.

Check your understanding · 3 questions

1.bazel test --config=ci //... exits with code 3. What does this mean and how should a CI script handle it?

Select one answer

2.Which flags belong in a sensible --config=ci block in .bazelrc for most projects? Select all that apply.

Select all that apply

3.True or false about CI diagnostic flags:

Choose True or False for each sentence

--announce_rc prints which .bazelrc files contributed options, making it useful when CI behaves differently from a laptop.
--flaky_test_attempts causes flaky tests to be reported as FAILED, not FLAKY.
0 of 3 answered

Footnotes

  1. Commands and Options--keep_going, --test_keep_going, --color, --curses, --announce_rc, --verbose_failures, --show_timestamps, and --test_output 1 2 3 4 5 6 7 8 9

  2. Calling Bazel from scriptsbazel test exit codes, cross-command exit codes, --noshow_progress, and the warning that future Bazel versions may add more specific non-zero codes 1 2 3 4 5

  3. Bazel flag cheat sheet — fail-fast default, --keep_going, --announce_rc, and --test_output=streamed 1 2 3

  4. How to set up a Bazel testing configuration: comprehensive guide for Scala and Java — CI motivation for --keep_going and practical use of --flaky_test_attempts 1 2

  5. BazelCon 2019 Day 2: Building a great CI w/ Bazel — CI-oriented flag set, including terminal-output choices, --announce_rc, --verbose_failures, and --show_timestamps 1 2 3

  6. Bazel continuous-integration — BazelCI infrastructure and pipelines — the upstream pipeline configuration guide separates task platforms, targets, environment variables, and flag sets, and scopes shell or batch commands to presubmit configuration