> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Aspect Cloud CI setup

> Give the CI runners you already have a shared Bazel remote cache and build history, with a packaged setup integration for GitHub Actions, Buildkite, CircleCI and GitLab, or a few shell commands on any other CI.

On the runners you already have, install the Aspect CLI launcher, authenticate, and point vanilla `bazel` at [Aspect Cloud](/docs/aspect-workflows/cloud/overview).

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](/docs/aspect-workflows/platform/features/webui). Your pipeline keeps whatever image and executor it has today.

It's [one step](#add-the-setup-step) on GitHub Actions, Buildkite, CircleCI or GitLab, or [a few shell commands](#any-other-ci) 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`](/docs/cli/tasks/setup_bazelrc): `~/.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](/docs/cli/authentication#generate-an-aspect-api-token-for-ci) 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.

<Tabs>
  <Tab title="GitHub Actions">
    ```yaml theme={null}
    permissions:
      contents: read
      id-token: write    # artifact uploads and the PR summary comment

    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v6
          - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
            with:
              aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
          - run: bazel test //...
    ```
  </Tab>

  <Tab title="Buildkite">
    ```yaml theme={null}
    steps:
      - label: "test"
        plugins:
          - aspect-build/setup-aspect#8de9aed254d0699baf201f66e2076fc8c09f42b9: ~ # v2026.38.2
        command: bazel test //...
        secrets:
          - ASPECT_API_TOKEN
    ```
  </Tab>

  <Tab title="CircleCI">
    ```yaml theme={null}
    version: 2.1

    orbs:
      setup-aspect: aspect-build/setup-aspect@2026.38.2

    jobs:
      test:
        docker:
          - image: cimg/base:current
        steps:
          - checkout
          - setup-aspect/setup
          - run: bazel test //...
    ```
  </Tab>

  <Tab title="GitLab CI">
    ```yaml theme={null}
    include:
      - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.38.3

    test:
      extends: .setup-aspect
      script:
        - bazel test //...
    ```
  </Tab>
</Tabs>

**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`:

```yaml theme={null}
test:
  extends: .setup-aspect
  before_script:
    - !reference [.setup-aspect, before_script]
    - ./tools/my-setup.sh
  script:
    - bazel test //...
```

### 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:

| CI host        | Source                                                                                         | Latest version                                                                       |
| -------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| GitHub Actions | [aspect-build/setup-aspect](https://github.com/aspect-build/setup-aspect)                      | [Releases](https://github.com/aspect-build/setup-aspect/releases)                    |
| Buildkite      | [setup-aspect-buildkite-plugin](https://github.com/aspect-build/setup-aspect-buildkite-plugin) | [Releases](https://github.com/aspect-build/setup-aspect-buildkite-plugin/releases)   |
| CircleCI       | [setup-aspect orb](https://circleci.com/developer/orbs/orb/aspect-build/setup-aspect)          | The orb registry page                                                                |
| GitLab CI      | [setup-aspect-gitlab-component](https://gitlab.com/aspect-build/setup-aspect-gitlab-component) | [Releases](https://gitlab.com/aspect-build/setup-aspect-gitlab-component/-/releases) |

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.

<Note>
  **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.
</Note>

## 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`](/docs/cli/tasks/setup_workspace_data) supplies them as Bazel's workspace status command:

```shell theme={null}
build --workspace_status_command="aspect setup workspace-data"
```

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](/docs/cli/tasks/setup_workspace_data#if-you-already-have-a-workspace-status-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:

| Input              | Default | Effect                                                                                                                                                                       |
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bazelrc-generate` | `true`  | Run `aspect setup bazelrc` at all. `false` leaves Bazel's configuration to the repository; the installs and the login still happen, and `aspect <task>` steps are unaffected |
| `bazelrc-remote`   | unset   | Passed as `--remote=`, with the same grammar as `aspect build --remote`. Unset leaves the CLI's `auto`: the cache and build event service on CI, nothing off it              |
| `bazelrc-home`     | unset   | Passed as `--home=`. Unset leaves the CLI's `auto`: the machine rc on CI, the checkout rc off it. `false` writes the committed checkout rc, for a job that regenerates it    |
| `bazelrc-force`    | `false` | Passed as `--force`, to regenerate over an rc that's already there. Turn it on for a persistent self-hosted runner, whose home directory outlives the job                    |

Each CI host passes them its own way:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
    with:
      bazelrc-force: true
  ```

  ```yaml Buildkite theme={null}
  plugins:
    - aspect-build/setup-aspect#8de9aed254d0699baf201f66e2076fc8c09f42b9: # v2026.38.2
        bazelrc-force: true
  ```

  ```yaml CircleCI theme={null}
  steps:
    - checkout
    - setup-aspect/setup:
        bazelrc-force: true
  ```

  ```yaml GitLab CI theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.38.3
      inputs:
        bazelrc-force: true
  ```
</CodeGroup>

**Buildkite, CircleCI and GitLab** keep credentials out of pipeline YAML, so they read two environment variables as well:

| Variable               | Effect                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `ASPECT_API_TOKEN`     | The credential. Without it the login is skipped, and an endpoint the job can't authenticate to is left off   |
| `ASPECT_SETUP_BIN_DIR` | Where the launcher and Bazelisk go when the integration has to fetch them. Defaults to `~/.aspect/setup-bin` |

**The GitHub Action** reads no environment variables. It takes the credential as an input, and it's the only integration that manages caches:

| Input              | Default  | Effect                                                                                                                                                                                                          |
| ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aspect-api-token` | unset    | The credential, normally `${{ secrets.ASPECT_API_TOKEN }}`. Piped into `aspect auth login --with-api-token`; only the resulting JWT is left on disk, and the long-lived token is never exported to `GITHUB_ENV` |
| `launcher-version` | latest   | Which launcher release to install. The CLI version stays pinned by `.aspect/version.axl`                                                                                                                        |
| `launcher-install` | `true`   | `false` when you provide the `aspect` binary yourself, earlier in the job or built from source                                                                                                                  |
| `bazelisk-version` | `latest` | Skipped when `bazel` is already on `PATH`                                                                                                                                                                       |
| `bazelisk-cache`   | `true`   | Cache the Bazelisk binary, keyed on `.bazelversion`                                                                                                                                                             |
| `disk-cache`       | `false`  | The GitHub Actions-backed disk cache, per runner and inside a 10 GB budget. Off because the generated rc's remote cache covers the same ground. Turn it on when `bazelrc-generate` is off                       |
| `repository-cache` | `true`   | Bazel's `--repository_cache`, holding external-repository downloads                                                                                                                                             |
| `bazelrc`          | unset    | Extra lines appended to `~/.bazelrc` after the rc is generated, so they override it                                                                                                                             |

`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.

```shell theme={null}
# Mark the job as CI, for hosts that don't set it themselves.
export CI=true

# 1. Install the Aspect CLI launcher into /usr/local/bin, with sudo. Pinning the
#    version skips a GitHub API lookup that shared CI egress IPs can hit the
#    rate limit on.
curl -fsSL https://install.aspect.build | bash -s -- 2026.39.10

# 2. Authenticate. The token is read from the environment and passed on stdin,
#    so it never reaches the process table or the job log.
echo "$ASPECT_API_TOKEN" | aspect auth login --with-api-token

# 3. Point vanilla `bazel` at Aspect Cloud.
aspect setup bazelrc --home

# 4. Optional: label each build with its commit, branch and PR. The leading
#    newline matters: an rc whose last line has none would swallow this one.
printf '\nbuild --workspace_status_command="aspect setup workspace-data"\n' >> ~/.bazelrc
```

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](/contact).

## Beyond the cache

Remote execution and [CI runners](/docs/aspect-workflows/platform/features/ci-runners) are available on [Aspect Enterprise](/docs/aspect-workflows/enterprise/overview) today and are coming soon to Aspect Cloud ([early access](/contact?topic=early-access)). See [choosing an offering](/docs/aspect-workflows/platform/choosing).

## Related

* [Local setup](/docs/aspect-workflows/cloud/local-setup): the same cache from a developer's machine.
* [`aspect setup bazelrc`](/docs/cli/tasks/setup_bazelrc): what the rc contains and how to adjust it.
* [Build metadata](/docs/cli/tasks/setup_workspace_data): commit, branch and pull request attribution.
* [Generating an API token](/docs/cli/authentication#generate-an-aspect-api-token-for-ci): where `ASPECT_API_TOKEN` comes from.
