aspect test --coverage collects Bazel code coverage as part of a normal test run and hands the merged LCOV report to a tool of your choice — an HTML generator locally, a service like Codecov in CI.
How this relates to bazel coverage
bazel coverage is Bazel’s own command for this: it “builds and runs the specified test targets using the specified options while collecting code coverage statistics,” and it accepts everything bazel test accepts.
aspect test --coverage arrives at the same place from the test side. It runs bazel test and sets the coverage flags for you, so Bazel does the same work and writes the same merged LCOV report to the same path. You don’t invoke bazel coverage yourself, and you don’t add a second Bazel pass to get coverage out of a run that was already testing.
Going through the task rather than calling Bazel directly gets you two things:
- It’s still the test task. BES streaming, status checks and MR signal, test-log upload, automatic retries on transient Bazel failures, and
--task-keyall keep working. A rawbazel coverageinvocation has none of that wrapped around it. - The report gets handled.
--coverage-reportand--coverage-toolresolve the merged report’s real path and hand it off. Doing that by hand is the fiddly part, because Bazel’s convenience symlinks make the same report easy to discover twice.
aspect test --coverage a more natural and convenient way to get coverage than a traditional bazel coverage run. And because it is bazel test underneath, your normal test flags and tag filters apply unchanged.
Collecting coverage
--coverage adds --collect_code_coverage and --combined_report=lcov to the Bazel invocation. Bazel merges per-test coverage into a single LCOV report at $(bazel info output_path)/_coverage/_coverage_report.dat.
--coverage-report=PATH to copy that report somewhere predictable for a later step, creating parent directories as needed:
--instrumentation_filter. If files you expected are missing from the report, widen it with --bazel-flag=--instrumentation_filter=^//.
Publishing the report
--coverage-tool runs a binary against the report once testing finishes. --coverage-tool-arg supplies its arguments — repeat the flag once per argument — and {report} (or {lcov}) is substituted with the report’s absolute path. If no argument contains the placeholder, the path is appended as the last argument. Both flags need --coverage; on their own they do nothing.
Three behaviors are worth knowing before you put this in a pipeline:
- The tool runs whether or not the tests passed, so an uploader still publishes partial coverage from a failing run — without a separate always-run step to arrange.
- A non-zero exit from the tool is a warning, not a failure. A rejected upload leaves the build green, so watch for
--coverage-tool: ... exited with code Non stderr rather than trusting a passing job. - If Bazel produced no report, the tool is skipped with a warning. That usually means nothing matched, nothing was instrumented, or the rules in play don’t support coverage.
Codecov
There are two routes. Option 1 runs the Codecov binary from the test task and behaves identically on every CI platform. Option 2 writes the report to a file and lets your platform’s own Codecov integration upload it, which fits repos that already have Codecov wired up that way. Whichever you pick, turn Codecov’s file search off. Left on, it scans the working directory for coverage files and follows Bazel’s convenience symlinks, so it reports the same merged file more than once — once underbazel-out and once under the workspace path that links to it — alongside the per-test coverage files Bazel left behind. Naming a report is not enough on its own: in both the action and the orb, the named files are added to whatever the search turns up. You need the explicit name and search disabled.
Option 1: run the Codecov binary from the test task
The Codecov CLI is a downloaded binary, so install it first:CODECOV_TOKEN environment variable, so set it in the job environment rather than passing it as an argument. Both work, but the CLI echoes the coverage tool’s command line to the log before spawning it — a token passed as --coverage-tool-arg=-t ... ends up in your build output, where it’s only as protected as your provider’s log masking. The environment variable keeps it out entirely.
- Buildkite
- GitLab CI
- CircleCI
- GitHub Actions
.buildkite/pipeline.yaml
Option 2: use your platform’s Codecov integration
Write the report to a known path with--coverage-report, then let the platform’s own Codecov integration upload it. What that integration is — and whether one exists — varies by platform.
- Buildkite
- GitLab CI
- CircleCI
- GitHub Actions
Codecov publishes no official Buildkite plugin; their Buildkite guidance is to run an uploader in a command step, which is Option 1 above. Community plugins such as Codecov does not support tokenless uploads from Buildkite, so
joscha/codecov wrap the same CLI if you’d rather express it as a plugin:.buildkite/pipeline.yaml
CODECOV_TOKEN must be in the step environment either way.aspect and bazel are pre-installed — drop the launcher install from these examples and keep the Codecov download.
