Documentation Index
Fetch the complete documentation index at: https://docs.aspect.build/llms.txt
Use this file to discover all available pages before exploring further.
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 just aspect <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 format and aspect lint know which files changed in the PR — they diff against the merge base by default and fall back to the GitHub PR Files API when git diff can’t answer (shallow clones, fetch-depth-restricted runners) — so they only act on what the PR touched, with no hand-rolled “diff against origin/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 delivery re-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 lint on a laptop does the same thing as aspect lint on GitHub Actions. Cuts an entire class of “works on my machine but not in CI” bugs.
On Aspect Workflows self-hosted runners, 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 |
Custom tasks defined in your repo’s .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 the aspect-build/setup-aspect action. setup-aspect exchanges it for a short-lived JWT and persists only the JWT — the long-lived secret never lands in GITHUB_ENV and isn’t visible to other steps in the job.
- Buildkite, GitLab CI, CircleCI — expose it as the
ASPECT_API_TOKEN env var on the job.
See Authenticating the Aspect CLI for the one-time account, GitHub App, and token-generation setup.
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 with aspect 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
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect build --task-key build -- //...
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect test --task-key test -- //...
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect format --task-key format
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect lint --task-key lint -- //...
delivery:
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect delivery --task-key delivery
setup-aspect also installs Bazelisk and wires --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.env:
ASPECT_API_TOKEN: your-buildkite-secret-ref
steps:
- key: build
label: ":bazel: Build"
command: |
curl -fsSL https://install.aspect.build | bash
aspect build --task-key build -- //...
- key: test
label: ":bazel: Test"
command: |
curl -fsSL https://install.aspect.build | bash
aspect test --task-key test -- //...
- key: format
label: ":hammer_and_wrench: Format"
command: |
curl -fsSL https://install.aspect.build | bash
aspect format --task-key format
- key: lint
label: ":lint-roller: Lint"
command: |
curl -fsSL https://install.aspect.build | bash
aspect lint --task-key lint -- //...
- key: delivery
label: ":package: Delivery"
depends_on: [test]
command: |
curl -fsSL https://install.aspect.build | bash
aspect delivery --task-key delivery
To avoid repeating the install in every step, factor the curl line into a Buildkite agent hook once and drop it from each command:.variables:
ASPECT_API_TOKEN: $ASPECT_API_TOKEN # set as a masked CI/CD variable
default:
before_script:
- curl -fsSL https://install.aspect.build | bash
- export PATH="$HOME/.local/bin:$PATH"
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE != "schedule"'
stages:
- CI
build:
stage: CI
script:
- aspect build --task-key build -- //...
test:
stage: CI
script:
- aspect test --task-key test -- //...
format:
stage: CI
script:
- aspect format --task-key format
lint:
stage: CI
script:
- aspect lint --task-key lint -- //...
delivery:
stage: CI
needs: [test]
script:
- aspect delivery --task-key delivery
The default.before_script block runs the launcher install once per job, so individual jobs stay clean.version: 2.1
parameters:
force_targets:
type: string
default: ""
commands:
install-aspect:
description: Install the Aspect CLI launcher
steps:
- run:
name: Install Aspect CLI
command: curl -fsSL https://install.aspect.build | bash
workflows:
ci:
jobs:
- build
- test
- format
- lint
- delivery:
requires: [test]
when:
not:
equal:
- scheduled_pipeline
- << pipeline.trigger_source >>
jobs:
build:
docker:
- image: cimg/base:current
steps:
- checkout
- install-aspect
- run: aspect build --task-key build -- //...
test:
docker:
- image: cimg/base:current
steps:
- checkout
- install-aspect
- run: aspect test --task-key test -- //...
format:
docker:
- image: cimg/base:current
steps:
- checkout
- install-aspect
- run: aspect format --task-key format
lint:
docker:
- image: cimg/base:current
steps:
- checkout
- install-aspect
- run: aspect lint --task-key lint -- //...
delivery:
docker:
- image: cimg/base:current
environment:
ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS: << pipeline.parameters.force_targets >>
steps:
- checkout
- install-aspect
- run: aspect delivery --task-key delivery
The reusable install-aspect command keeps each job to a single install step.
On Aspect Workflows CI runners
Aspect Workflows CI runners ship with aspect 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
name: Aspect Workflows
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
runs-on: [self-hosted, aspect-workflows, aspect-default]
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect build --task-key build -- //...
test:
runs-on: [self-hosted, aspect-workflows, aspect-default]
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect test --task-key test -- //...
format:
runs-on: [self-hosted, aspect-workflows, aspect-default]
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect format --task-key format
lint:
runs-on: [self-hosted, aspect-workflows, aspect-default]
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect lint --task-key lint -- //...
delivery:
needs: [test]
runs-on: [self-hosted, aspect-workflows, aspect-default]
steps:
- uses: actions/checkout@v6
- uses: aspect-build/setup-aspect@c22a8f64fb38f82f59ce809cd7eb9f8ae096da44 # v2026.23.2
with:
aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
- run: aspect delivery --task-key delivery
setup-aspect skips the launcher install on Workflows CI runners (the runner image already ships 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.env:
ASPECT_API_TOKEN: your-buildkite-secret-ref
steps:
- key: build
label: ":bazel: Build"
command: aspect build --task-key build -- //...
agents:
queue: aspect-default
- key: test
label: ":bazel: Test"
command: aspect test --task-key test -- //...
agents:
queue: aspect-default
- key: format
label: ":hammer_and_wrench: Format"
command: aspect format --task-key format
agents:
queue: aspect-default
- key: lint
label: ":lint-roller: Lint"
command: aspect lint --task-key lint -- //...
agents:
queue: aspect-default
- key: delivery
label: ":package: Delivery"
depends_on: [test]
command: aspect delivery --task-key delivery
agents:
queue: aspect-default
The queue: aspect-default value must match the queue field in your Workflows Terraform runner group.variables:
ASPECT_API_TOKEN: $ASPECT_API_TOKEN # set as a masked CI/CD variable
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE != "schedule"'
stages:
- CI
build:
stage: CI
tags: [aspect-workflows, aspect-default]
script:
- aspect build --task-key build -- //...
test:
stage: CI
tags: [aspect-workflows, aspect-default]
script:
- aspect test --task-key test -- //...
format:
stage: CI
tags: [aspect-workflows, aspect-default]
script:
- aspect format --task-key format
lint:
stage: CI
tags: [aspect-workflows, aspect-default]
script:
- aspect lint --task-key lint -- //...
delivery:
stage: CI
needs: [test]
tags: [aspect-workflows, aspect-default]
script:
- aspect delivery --task-key delivery
The aspect-default tag must match the queue field in your Workflows Terraform runner group.version: 2.1
parameters:
force_targets:
type: string
default: ""
workflows:
aspect-workflows:
jobs:
- build
- test
- format
- lint
- delivery:
requires: [test]
when:
not:
equal:
- scheduled_pipeline
- << pipeline.trigger_source >>
jobs:
build:
machine: true
resource_class: YOUR-ORG/aspect-default
working_directory: /mnt/ephemeral/workdir
steps:
- checkout
- run:
name: Build
command: aspect build --task-key build -- //...
test:
machine: true
resource_class: YOUR-ORG/aspect-default
working_directory: /mnt/ephemeral/workdir
steps:
- checkout
- run:
name: Test
command: aspect test --task-key test -- //...
format:
machine: true
resource_class: YOUR-ORG/aspect-default
working_directory: /mnt/ephemeral/workdir
steps:
- checkout
- run:
name: Format
command: aspect format --task-key format
lint:
machine: true
resource_class: YOUR-ORG/aspect-default
working_directory: /mnt/ephemeral/workdir
steps:
- checkout
- run:
name: Lint
command: aspect lint --task-key lint -- //...
delivery:
machine: true
resource_class: YOUR-ORG/aspect-default
working_directory: /mnt/ephemeral/workdir
environment:
ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS: << pipeline.parameters.force_targets >>
steps:
- checkout
- run:
name: Delivery
command: aspect delivery --task-key delivery
Replace YOUR-ORG/aspect-default with the resource class you created in CircleCI.
Live examples
The aspect-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.
Per-task links from a recent run:
The task name links to its GitHub Status Check; the Buildkite column links to the matching annotation.