Skip to main content

What a CI step gets

When a CI step is aspect <task>, it gets these with no extra YAML or shell scripting:
  • Retry on transient Bazel errors. BLAZE_INTERNAL_ERROR (exit code 37) and LOCAL_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 lint and aspect gazelle --scope=changed find the PR’s changed files by diffing against the merge base, and fall back to the GitHub PR Files API when git diff can’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 delivery re-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 build and test, lint, format, gazelle, run, warming and delivery stream their build events, and each one’s status check links to its invocation.
On a CI runner other than Aspect Workflows, --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 — job test running aspect test gives test, and test-linux gives test-linux, not test-test-linux. A job that merely shares a leading substring, like testing, 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 Actions GITHUB_JOB, Buildkite BUILDKITE_STEP_KEY (else BUILDKITE_LABEL), CircleCI CIRCLE_JOB, GitLab CI_JOB_NAME. Steps using native parallelism append the shard index (Buildkite BUILDKITE_PARALLEL_JOB, CircleCI CIRCLE_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).
Set --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=&lt;kind&gt;- 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 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: store it as a Buildkite secret named ASPECT_API_TOKEN and expose it on each step with secrets: [ASPECT_API_TOKEN], as the examples below do.
  • GitLab CI, CircleCI: expose it as the ASPECT_API_TOKEN env var on the job (a masked CI/CD variable / project environment variable).
See Authenticating the Aspect CLI for the one-time account, GitHub App, and token-generation setup.

Aspect Workflows runners

On Aspect Workflows CI runners, the Workflows 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 delivery task 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 with aspect 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/workflows/aspect.yaml
setup-aspect also installs Bazelisk and caches Bazel’s repository cache in the GitHub Actions cache. The disk cache is off by default, since the remote cache covers the same ground; 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 with aspect 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/workflows/aspect-workflows.yaml
setup-aspect skips the launcher install on Workflows CI runners (the runner image already ships 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

The aspect-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.
Per-task links from a recent run. The task name links to its GitHub status check; the Buildkite column links to the matching annotation.