Why run aspect in CI?
CI for a Bazel monorepo grows the same scaffolding everywhere: install bazel and pin its version, wire up the remote cache, route failed test logs into artifact storage, post a per-job status check, comment on the PR with lint findings, retry the occasional transient error, time the build phases for telemetry, gate delivery so you don’t push identical binaries on every commit. Each piece is small but the pile is fragile, copy-pasted across teams, and breaks differently on every CI provider.
The Aspect CLI replaces that scaffolding. Every aspect <task> invocation is self-contained: it handles flag configuration, artifact upload, status checks, lint comments, retry, and selective delivery internally — and it does all of it the same way on GitHub Actions, Buildkite, GitLab CI, and CircleCI.
What you get out of the box
When a CI step is justaspect <task>, you get all of the following with no extra YAML or shell glue:
- Retry on transient Bazel errors.
BLAZE_INTERNAL_ERROR,LOCAL_ENVIRONMENTAL_ERROR, and similar transient codes trigger a bounded automatic retry — instead of the copy-pasted||retry loop that wakes someone up at 3 a.m. when it gets the wrong errors. - Native CI status checks. Each task posts a per-step status (named by
--task-key) to GitHub Status Checks, Buildkite Annotations, GitLab job annotations, and the equivalent on CircleCI — so reviewers see the result of the lint job, the test job, and the format job individually, not a single opaque “CI passed” line. - Live result streaming. Lint findings stream to PR review comments as the job runs, with one-click suggested fixes when the linter offers them. Reviewers see the first failure within seconds of it happening, not after the entire pipeline finishes.
- Smart changed-file detection.
aspect formatandaspect lintknow which files changed in the PR — they diff against the merge base by default and fall back to the GitHub PR Files API whengit diffcan’t answer (shallow clones, fetch-depth-restricted runners) — so they only act on what the PR touched, with no hand-rolled “diff againstorigin/main” script. - Hold-the-line lint. Pre-existing violations don’t fail the build; only new ones added by the PR do. You enable the strictest lint rules without forcing a flag-day refactor.
- Artifact upload to your CI’s native storage. Test logs, the Bazel execution log, build profile (chrome trace), and the BEP are uploaded via the CI provider’s native artifact API — GitHub Actions artifacts, Buildkite artifacts, GitLab job artifacts, CircleCI
store_artifacts. Drop the bespoke “upload on failure” step. - Selective delivery.
aspect deliveryre-deploys only the services whose Bazel-built outputs actually changed since the last release — driven by the build graph, not git diffs or timestamps. - Same command, same contract, every provider.
aspect linton a laptop does the same thing asaspect linton GitHub Actions. Cuts an entire class of “works on my machine but not in CI” bugs.
aspect <task> also detects the runner environment and applies the remote cache, RBE, and BES flags automatically — no .bazelrc plumbing.
Standard tasks
The built-in tasks, each invoked the same way in every CI step:| Command | What it does | Full reference |
|---|---|---|
aspect build --task-key build -- //... | Build Bazel targets | aspect build / aspect test |
aspect test --task-key test -- //... | Run Bazel tests | aspect build / aspect test |
aspect format --task-key format | Format source files (changed files by default) | aspect format |
aspect lint --task-key lint -- //... | Run linters with hold-the-line strategy | aspect lint |
aspect gazelle --task-key gazelle | Generate and sync BUILD files | aspect gazelle |
aspect buildifier --task-key buildifier | Format Starlark files (opt-in via format.alias()) | aspect buildifier |
aspect delivery --task-key delivery | Deliver only targets whose outputs actually changed | aspect delivery |
.aspect/*.axl files invoke the same way: aspect <name> --task-key <name>.
Task key
--task-key assigns a short identifier to the CI step. The key appears in GitHub Status Checks, Buildkite Annotations, and similar CI-platform integrations, so pick a name that’s meaningful in a status list. Plain keys like build or test are the right default. Only suffix with a CI name (e.g. build-gha) if you run the same task on multiple CI providers simultaneously and need distinct status check names per provider.
Authentication
ASPECT_API_TOKEN is optional, but recommended. Without it, aspect tasks still build, test, format, and lint normally. With it, you unlock the CI-platform integrations that make the tasks worth running in CI in the first place: status checks, inline PR comments, suggested fixes, and the more accurate changed-file detection above.
Store the token as a CI secret on each provider you use, then pass it to aspect:
- GitHub Actions — pass it via
with: aspect-api-token:on theaspect-build/setup-aspectaction. setup-aspect exchanges it for a short-lived JWT and persists only the JWT — the long-lived secret never lands inGITHUB_ENVand isn’t visible to other steps in the job. - Buildkite, GitLab CI, CircleCI — expose it as the
ASPECT_API_TOKENenv var on the job.
Complete pipeline examples
Two sets of CI pipeline examples — one for provider-hosted runners (GitHub Actions’ubuntu-latest, Buildkite’s hosted agents, GitLab.com runners, CircleCI cloud runners, your own self-hosted VMs) and one for Aspect Workflows self-hosted runners (which ship with aspect pre-installed and route Bazel through the deployment’s caching infrastructure automatically).
On GitHub Actions, the same aspect-build/setup-aspect action covers both cases — it installs the launcher on provider-hosted runners, no-ops on Workflows CI runners, and exchanges ASPECT_API_TOKEN for a session JWT either way. On Buildkite, GitLab CI, and CircleCI there’s no equivalent action yet; ephemeral examples install the launcher inline with curl -fsSL https://install.aspect.build | bash, while Workflows-CI-runner examples skip that step.
On provider-hosted runners
Cloud VMs and containers don’t ship withaspect or bazel, so each example installs them at the start of every job. The launcher then reads .aspect/version.axl to pin the CLI version, so local and CI stay in sync.
- GitHub Actions
- Buildkite
- GitLab CI
- CircleCI
.github/workflows/aspect.yaml
--disk_cache / --repository_cache to the GHA cache. Pin to a full-length SHA with the version annotated in a trailing comment per GitHub’s third-party action security guidance; find the latest SHA on the setup-aspect releases page.On Aspect Workflows CI runners
Aspect Workflows CI runners ship withaspect and bazel pre-installed and warm. The runs-on: / agents: / tags: / resource_class: value targets your Workflows CI runner queue (aspect-default here is the example queue name from your Terraform runner group).
- GitHub Actions
- Buildkite
- GitLab CI
- CircleCI
.github/workflows/aspect-workflows.yaml
aspect) and runs rosetta bazelrc > /etc/bazel.bazelrc so raw bazel calls in subsequent steps also pick up the runner’s cache, BES backend, and NVMe disk cache.Live examples
Theaspect-build/bazel-examples repo runs these pipelines on all four supported CI providers — on GitHub Actions it runs both the provider-hosted-runner and Workflows-CI-runner versions side by side; Buildkite, GitLab CI, and CircleCI each run the Workflows-CI-runner version. Click through to inspect a real, current build. Source lives in two places: GitHub (used by the GitHub Actions, Buildkite, and CircleCI pipelines) and GitLab (used by the GitLab CI pipeline).
| CI provider | Live pipeline |
|---|---|
| GitHub Actions | Actions tab |
| Buildkite | Recent builds |
| GitLab CI/CD | Pipelines |
| CircleCI | Pipeline runs |
What aspect <task> reports back
aspect <task> posts task results to three surfaces — examples below are from real runs of aspect-build/aspect-cli’s own CI:
- PR task summary comment — a single comment Marvin posts to the PR thread summarising every task in the pipeline. See example →
- GitHub Status Checks — one check per
aspect <task>invocation, named by--task-key, surfaced on the PR’s Checks tab and on the commit itself. - Buildkite annotations (when running on Buildkite) — one annotation per
aspect <task>invocation, rendered at the top of the build page.
| Task (GitHub Status Check) | Buildkite annotation |
|---|---|
aspect build | build #3019 → |
aspect test | build #3019 → |
aspect format | build #3019 → |
aspect buildifier | build #3019 → |
aspect lint | build #3019 → |
aspect gazelle | build #3019 → |
aspect delivery | build #3019 → |

