What a CI step gets
When a CI step isaspect <task>, it gets these with no extra YAML or shell scripting:
- Retry on transient Bazel errors.
BLAZE_INTERNAL_ERROR(exit code 37) andLOCAL_ENVIRONMENTAL_ERROR(36) trigger a bounded retry, 3 attempts by default (--bazel-retry-attempts), in place of a hand-written||retry loop. - Native CI status checks. Each task posts a per-step status (named by
--task:name) as a GitHub check run, a Buildkite annotation or a GitLab commit status, whichever applies, updated live while the job runs. - Lint findings on the diff. Lint findings post as PR review comments, with one-click suggested fixes when the linter offers them.
- Changed-file detection.
aspect format,aspect lintandaspect gazelle --scope=changedfind the PR’s changed files by diffing against the merge base, and fall back to the GitHub PR Files API whengit diffcan’t answer (shallow clones, fetch-depth-restricted runners). They act only on what the PR touched. - Hold-the-line lint. Pre-existing violations don’t fail the build; only new ones added by the PR do, so strict lint rules don’t need a flag-day cleanup.
- Artifact upload to your CI’s native storage. Test logs, the Bazel execution log, build profile (chrome trace), and the BEP upload through the CI provider’s artifact API: GitHub Actions artifacts, Buildkite artifacts, the GitLab generic package registry (enable the project’s package registry) or CircleCI
store_artifacts. Each kind is opt-in. - Selective delivery.
aspect deliveryre-deploys only the services whose Bazel-built outputs changed since the last release, decided by output content hashes rather than git diffs or timestamps. It needs Aspect Workflows runners, which hold the delivery state. - Every task in the Build Results UI. Besides
buildandtest,lint,format,gazelle,run,warminganddeliverystream their build events, and each one’s status check links to its invocation.
--remote defaults to auto, which configures Bazel from the default deployment. Aspect Workflows runners configure themselves; see Aspect Workflows runners.
Standard tasks
Each invoked the same way in every CI step; Aspect CLI tasks lists them all:
Custom tasks defined in your repo’s
.aspect/*.axl files invoke the same way: aspect <name> --task:name <name>.
Task name
--task:name assigns a short name uniquely identifying the task invocation. The name appears in GitHub Status Checks, Buildkite Annotations, and similar CI-platform integrations, so pick something meaningful in a status list. Plain names matching the task kind like build or test are the right default. 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.
If you don’t pass --task:name, it’s auto-generated:
- On CI, from the CI job name:
<kind>-<job>(e.g.test-ci-linux), so status checks read meaningfully and stay stable across runs. A job whose name already is the kind, or already starts with<kind>-, keeps its own name rather than being prefixed twice — jobtestrunningaspect testgivestest, andtest-linuxgivestest-linux, nottest-test-linux. A job that merely shares a leading substring, liketesting, is still prefixed. If you set required status checks, check their names after upgrading to 2026.41.50 — they change for jobs in that carve-out. The job name comes from the host: GitHub ActionsGITHUB_JOB, BuildkiteBUILDKITE_STEP_KEY(elseBUILDKITE_LABEL), CircleCICIRCLE_JOB, GitLabCI_JOB_NAME. Steps using native parallelism append the shard index (BuildkiteBUILDKITE_PARALLEL_JOB, CircleCICIRCLE_NODE_INDEX) →<kind>-<job>-<shard>. If the same name is still generated again on the same run and machine (e.g. one job script invoking the same task kind twice), a-2,-3, … suffix is appended. - Off CI, from a random friendly suffix:
<kind>-<suffix>(e.g.test-fluffy-parakeet).
--task:name explicitly anyway. Renaming a CI job renames every check and artifact derived from it.
GitHub Actions matrix jobs: unlike Buildkite/CircleCI parallelism, GitHub exposes no per-shard environment variable (matrix values live only in
{'{'}{'{'} matrix.* {'}'}{'}'}</code>, and <code>GITHUB_JOB</code> is the same for every shard), so all shards of a matrix auto-generate the same name and collapse into one PR-summary row. For per-shard status, set an explicit <code>--task:name=<kind>- matrix.os .The
—task-key flag is a deprecated alias for —task:name. It still works but prints a warning and will be removed in a future release. Switch to —task:name.Authentication
ASPECT_API_TOKEN is optional, but recommended. It enables the Aspect Cloud remote cache and the CI-platform integrations: status checks, inline PR comments, suggested fixes, and the PR Files API fallback for changed-file detection.
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: store it as a Buildkite secret named
ASPECT_API_TOKENand expose it on each step withsecrets: [ASPECT_API_TOKEN], as the examples below do. - GitLab CI, CircleCI: expose it as the
ASPECT_API_TOKENenv var on the job (a masked CI/CD variable / project environment variable).
Aspect Workflows runners
On Aspect Workflows CI runners, theWorkflows feature configures Bazel for the runner, with no .bazelrc changes or per-provider scripting; that page lists the flags it injects. Remote execution is opt-in through --workflows:remote-exec.
aspect setup bazelrc, which the setup integrations run, writes the same flags to ~/.aspect/bazelrc, imported from ~/.bazelrc, so vanilla bazel invocations in later steps pick them up too.
Keep incompatible tasks off each other’s analysis cache
All tasks on a warm runner share one--output_base, which means one Bazel server and one analysis cache. When two tasks run with flag sets Bazel considers incompatible, such as different optimization settings (--compilation_mode), a build-affecting --config, or stamping (--stamp), the second task’s flags invalidate the first’s analysis cache, and Bazel logs Build options … have changed, discarding analysis cache. The next run pays the loading/analysis cost again even though nothing else changed.
Give such a task its own Bazel server by suffixing its --output_base. Only --output_base is made unique; --output_user_root stays shared, so the install base and external-repository cache are reused rather than duplicated across servers. It’s a no-op off a Workflows runner.
-
Any Bazel task: the Workflows feature flag
--workflows:runner-output-base-suffix=<suffix>, or in.aspect/config.axl:.aspect/config.axl -
Delivery: the
deliverytask has its own flag for its stamped release build. See Isolate the delivery Bazel server.
Complete pipeline examples
Pipeline examples for provider-hosted runners (GitHub Actions’ubuntu-latest, Buildkite’s hosted agents, GitLab.com runners, CircleCI cloud runners, your own self-hosted VMs) and for Aspect Workflows CI runners, which ship with aspect pre-installed.
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, the plugin, component and orb do the same job; see Aspect Cloud CI setup.
On provider-hosted runners
Cloud VMs and containers don’t ship withaspect or bazel, so every job starts with the setup integration, which installs the launcher and Bazelisk and logs in with ASPECT_API_TOKEN. The launcher reads .aspect/version.axl to pin the CLI version, so local and CI stay in sync.
Delivery state lives on Aspect Workflows runners, so the delivery jobs here run --mode=always --track-state=false and deliver every resolved target. Every delivery job runs only on the default branch and selects its targets with --query; tag the targets you ship deliverable. See aspect delivery.
- GitHub Actions
- Buildkite
- GitLab CI
- CircleCI
.github/workflows/aspect.yaml
disk-cache: true turns it on. 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 delivery jobs run only on the default branch, as above. The runs-on: / agents: / tags: / resource_class: value targets your Workflows CI runner queue (aspect-default here is an example; use the queue of one of your ).
- GitHub Actions
- Buildkite
- GitLab CI
- CircleCI
.github/workflows/aspect-workflows.yaml
aspect) and runs aspect setup bazelrc to write ~/.aspect/bazelrc, imported from ~/.bazelrc, so vanilla bazel calls in later steps pick up the same runner configuration.Live examples
Theaspect-build/bazel-examples repo runs these pipelines on GitHub Actions, Buildkite, GitLab CI and CircleCI. 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. Source lives in two places: GitHub (used by the GitHub Actions, Buildkite, and CircleCI pipelines) and GitLab (used by the GitLab CI pipeline).
What aspect <task> reports back
aspect <task> posts task results to these surfaces. The examples below are from real runs of aspect-build/aspect-cli’s own CI:
- PR task summary comment: a single comment the Aspect Workflows GitHub App posts to the PR thread summarizing every task in the pipeline. See example →
- GitHub status checks: one check per
aspect <task>invocation, named by--task:name, 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.

