Skip to main content
On the runners you already have, install the Aspect CLI launcher, authenticate, and point vanilla bazel at Aspect Cloud. A vanilla bazel build //... then reads and writes a cache shared across jobs, branches and developers, and every invocation shows up in the Build Results UI. Your pipeline keeps whatever image and executor it has today. It’s one step on GitHub Actions, Buildkite, CircleCI or GitLab, or a few shell commands anywhere else.

What the setup step does

  1. Installs the Aspect CLI launcher and Bazelisk, each skipped when the binary is already on PATH.
  2. Authenticates, exchanging your Aspect API token for a short-lived session JWT. It’s stored where a developer’s login goes: the OS keyring when the CLI can open one (on Linux, the kernel keyring), otherwise a 0600 file at ~/.aspect/credentials.json. Later steps in the same job reuse it. Set ASPECT_CREDENTIALS_FILE to store it in a file at that path instead.
  3. Writes the machine rc with aspect setup bazelrc --home: ~/.aspect/bazelrc, imported from ~/.bazelrc, naming Aspect Cloud’s cache and build event endpoints.
  4. Leaves your build alone. Your existing bazel steps are unchanged.
--home keeps the rc out of the checkout. Without it the task writes <workspace>/.aspect/bazelrc and a try-import in the repository’s .bazelrc, files meant to be committed, which on a runner would leave the checkout dirty.

Add the setup step

Store your Aspect API token as a CI secret named ASPECT_API_TOKEN. The GitHub Action takes it as the aspect-api-token input; the other integrations read it from the environment. On Buildkite, expose it with the step’s secrets: key (agent v3.106.0 or later) rather than env:, which Buildkite interpolates at upload and so writes the token into the job definition. Either way it reaches aspect auth login on stdin, so it stays out of both the process table and the job log.
On GitLab CI, a job with its own before_script replaces the template’s, and the setup doesn’t run. List the template’s first, with !reference:

Where each integration lives

The versions above were current when this page was written. Each publishes on its own schedule, so take the latest from its own listing: Buildkite’s shorthand resolves aspect-build/setup-aspect to the full repository name, which is why the plugin reference looks shorter than the repository it comes from.
The GitHub and Buildkite examples pin a commit; the CircleCI and GitLab ones pin a version. An Action reference and a Buildkite plugin reference are git refs, so a commit is the tighter pin. A CircleCI orb resolves from the orb registry and a GitLab component from the CI/CD Catalog, where an immutable published version is the pin. The trailing comment gives the version each commit corresponds to.

Label your builds

A vanilla bazel call reaches the build event service with no commit, branch or pull request attached, so it arrives in the UI anonymous. aspect setup workspace-data supplies them as Bazel’s workspace status command:
Bazel takes one --workspace_status_command, so a repository that already has a status script calls the task from inside it. See composing with an existing script.

Tuning the setup step

Every integration takes the same inputs for the rc. Each is passed to aspect setup bazelrc, and an unset input leaves that flag off the command line, so the CLI applies its own default: Each CI host passes them its own way:
Buildkite, CircleCI and GitLab keep credentials out of pipeline YAML, so they read two environment variables as well: The GitHub Action reads no environment variables. It takes the credential as an input, and it’s the only integration that manages caches: disk-cache and repository-cache also take a string, which keys the cache: ${{ github.workflow }} gives each workflow its own. If both bazelrc-generate and disk-cache are off, the Action warns that the job has no cache.

--remote on CI

Aspect CLI tasks don’t need the flag here. A task’s --remote defaults to auto, which means bare --remote on CI, and nothing off it, so aspect build and aspect test in the same job reach the cache and build events on their own. auto won’t wire an endpoint the job can’t authenticate to, because Bazel treats a credential helper that returns no token as fatal, so a job without ASPECT_API_TOKEN builds without the cache. An explicit --remote is honored regardless.

Any other CI

Any CI that can run shell can run the same commands. The CLI recognizes the packaged hosts by their own variables, and treats any other host as CI when CI is set. Jenkins and TeamCity don’t set it, so the snippet does; without it, aspect setup bazelrc would treat the job as a developer’s machine and turn nothing on. Set it in the job’s environment too, so later aspect tasks treat the job as CI.
Anything after step 3 in the same job picks up the cache. The install script moves the binary into place with sudo, so the job needs it. The launcher version is separate from the CLI version, which .aspect/version.axl pins. What the packaged integrations do that this doesn’t:
  • They install Bazelisk too, skipping it when bazel is already on PATH. If your image already has Bazel, you need nothing here.
  • They treat every setup failure as a warning. The snippet above fails the job if the install or the login fails. To keep the job going instead, append || true to steps 2 and 3; the rc goes unwritten and bazel runs as before.
  • The GitHub Action caches the launcher’s downloads between jobs. Without that, each job downloads the CLI again.
If you’d use a packaged integration for another CI host, tell us.

Beyond the cache

Remote execution and CI runners are available on Aspect Enterprise today and are coming soon to Aspect Cloud (early access). See choosing an offering.