run stage. Ankra brings what Bazel itself leaves to the CI system: a cluster to run on, caches kept between runs, test results in the run’s check, and an image build that is scanned and gated before it is published. Bazel brings hermetic builds and an action cache, so a run with a warm cache only rebuilds what changed.
This guide assumes the repository is already connected to Ankra Pipelines. If it is not, start with Get started with Ankra CI.
What Bazel needs from a pipeline step
A pipeline step differs from a laptop or a hosted CI runner in a few ways, and each one matters for Bazel. The three rules apply to every step; this is what they mean here:- The Bazel output tree goes under
/workspace.HOMEis/tmp, which is 256 MiB and held in memory. Bazel’s default output root is underHOMEand outgrows that within one build, so pass--output_user_root=/workspace/.ankra-bazel/rooton every Bazel command. - Bazel comes from Bazelisk. The root filesystem is read-only, so nothing can be installed into the image. The step installs Bazelisk into the workspace, and Bazelisk downloads the Bazel version pinned in
.bazelversion. - Downloads need network. A
runstage has no network by default.defaults.network: "egress-https"lets Bazel fetch toolchains and external dependencies from public hosts. - Bazel needs more than the default size. A step gets 500m of CPU and 1Gi of memory unless it asks for more. Give the Bazel stage a few cores and several gigabytes, and a longer timeout for the first cold build.
Add a CI config to .bazelrc
Keep the CI flags in a named config, so the pipeline only says --config=ci and you can run the same build locally:
test inherits every build flag, so the caches apply to tests too. On Bazel 7.4 or later, add build:ci --experimental_disk_cache_gc_max_size=20G to keep the disk cache from growing without limit.
The pipeline
This.ankra/pipeline.yaml tests every target on each pull request and push to main, and reports each test in the run:
golang:1.26is the image because it has the C toolchain Bazel’s default rules expect and letsgo installfetch Bazelisk. Any image your organisation’s image policy allows works the same way, as long as it has a shell and a compiler.- The cache keeps Bazel’s disk cache, repository cache and downloaded Bazel binary between runs, keyed on the module lock file and the Bazel version. A repository still on
WORKSPACEkeys on the files that pin its external dependencies instead. Bazel’s own cache decides what to rebuild, so a run where little changed finishes in a fraction of the cold time. test_resultsreads the JUnittest.xmlBazel writes for every test target. The script copies them out ofbazel-testlogsinto one directory before the step ends, flattening each path into a file name so two targets never overwrite each other’s report.statusholds Bazel’s exit code while the reports are copied, so a failing test still reports its results and still fails the step.
cache, resources and timeout are protected settings. They take effect once an administrator approves the pipeline on the default branch, so the first run before that approval is cold and uses the default size. A large repository can run out of memory or time on that first run - approve the definition and run it again.Use a remote cache
Bazel’s remote cache shares results between runs, branches and developers’ machines. Ankra does not host one, but a step can reach any cache that speaks Bazel’s remote cache protocol over HTTPS. Store the cache’s API key as an organisation variable, which the CLI reads from standard input:/run/agent-secrets:
secrets is a protected section too, so the binding reaches the step once the definition that declares it is approved on the default branch.
Build an image from a Bazel target
For a repository linked to an Ankra application, akind: "build" stage builds an image from a Dockerfile, then a scan and a gate judge it before publish gives it an immutable sha- tag. To ship a Bazel-built binary, build it in a run stage, copy it into the workspace, and let the Dockerfile pick it up from there.
Add these stages after test:
bazel cquery --output=files prints where Bazel put the target’s output, so the copy keeps working when the rule set changes its output layout. //cmd/server is an example; use your own target. The Dockerfile copies the binary in:
main publishes it to the application’s registry, and that tag is what push to deploy rolls out.
Make it faster
- Let Bazel skip unchanged work. With the cache warm,
bazel test //...reruns only the tests whose inputs changed. That is usually faster than narrowing the target list by hand. - Skip runs that touch nothing Bazel builds.
affected:reads Go and pnpm import graphs, not Bazel’s, so use path filters instead:on.pull_request.paths: ["!docs/**", "!*.md"]records a docs-only change as skipped. See Fast CI. - Split slow suites onto a schedule. Tag long tests
slow, leave them out with--test_tag_filters=-slowon pull requests and pushes, and run them from anon.schedulestage at night.
Troubleshooting
Caches explains keys, trust scopes and retention, and the pipeline reference lists every field used here.