> ## 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 Enterprise CI setup

> Point CI jobs at an Aspect Enterprise deployment, whether they run on Aspect Workflows runners or on runners you manage yourself, persistent or ephemeral.

export const gatedAccess = (user, group) => {
  const loggedIn = !!(user && user.loggedIn);
  const groups = user && user.tenantMetadata && user.tenantMetadata.docsGroups || [];
  if (loggedIn && (!group || groups.indexOf(group) >= 0)) {
    return "entitled";
  }
  return loggedIn ? "signed-in" : "anonymous";
};

export const GatedLink = ({access, href, group, children}) => {
  const note = group ? "Aspect Enterprise customers" : "free Aspect account";
  const muted = {
    fontSize: "0.85em",
    opacity: 0.7,
    whiteSpace: "nowrap"
  };
  if (access === "entitled") {
    return <a href={href}>{children}</a>;
  }
  if (access !== "signed-in") {
    return <span>
        <a href={"/login?redirect=" + encodeURIComponent(href)}>{children}</a>
        <span style={muted}> (sign in: {note})</span>
      </span>;
  }
  return <span>
      {children}
      <span style={muted}> ({note})</span>
    </span>;
};

An Aspect Enterprise deployment runs on its own hostnames, so something in the job has to name them. What that is depends on where the job runs.

| Your jobs run on                                                          | What names the endpoints                                                                              |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **[Aspect Workflows runners](#on-aspect-workflows-runners)**              | The runner environment, for Aspect CLI tasks. Vanilla `bazel` still needs the rc generated in the job |
| **[Runners you manage](#on-runners-you-manage)**, persistent or ephemeral | Your pipeline                                                                                         |

## On Aspect Workflows runners

The runner is inside the deployment. Its environment already carries the cache, build event service and results URL, and it holds its own credential, so nothing in your pipeline names an endpoint or handles a token for the build itself.

**Aspect CLI tasks need nothing.** `aspect build` and `aspect test` build against the runner's services without being asked. They refuse `--remote`: the flag names a configured deployment, which would route the build off the runner's own services.

**Vanilla `bazel` needs an rc**, which the setup step writes.

**Commands that don't call Bazel need nothing.** Scripts, Docker builds and other tools run on a Workflows runner as on any other; target the runner group and skip the setup step.

The runner image puts `aspect` and `bazel` on `PATH`: `aspect` is the Aspect CLI launcher, and `bazel` is the legacy Aspect CLI unless the deployment is configured to install upstream Bazelisk as `bazel` instead. A job can tell the two apart by `ASPECT_WORKFLOWS_RUNNER_NO_LEGACY_CLI=1`, which is set only in the Bazelisk case.

### The setup step

Recommended for every job that calls `bazel` directly. Add your CI host's setup integration before the first `bazel` call. On a Workflows runner it:

1. **Runs [`aspect ci runner-health-check`](/docs/cli/tasks/ci_runner_health_check)**, which confirms the runner is ready for the job.
2. **Signs in**, when given an Aspect API token. Tasks that post to pull requests (status checks, lint comments, suggested fixes) reach the GitHub or GitLab App with it; the build itself doesn't need it. Pass it as `aspect-api-token` on GitHub Actions, or as `ASPECT_API_TOKEN` in the environment on the others.
3. **Runs [`aspect setup bazelrc`](/docs/cli/tasks/setup_bazelrc)**, which writes `~/.aspect/bazelrc`, imported from `~/.bazelrc`. It names the runner's remote cache and build event service and puts Bazel's output base on the runner's NVMe, and it's what a vanilla `bazel` call picks up.

The launcher and `bazel` come with the runner image, so there's nothing to install. Your CI host's [pipeline page](#where-each-integration-lives) has a complete pipeline.

<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: [self-hosted, aspect-workflows, aspect-default]
        steps:
          - uses: actions/checkout@v6
          - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
          - run: bazel test //...
    ```
  </Tab>

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

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

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

    jobs:
      test:
        machine: true
        resource_class: YOUR-ORG/aspect-default
        working_directory: /mnt/ephemeral/workdir
        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
      tags: [aspect-workflows, aspect-default]
      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
  tags: [aspect-workflows, aspect-default]
  before_script:
    - !reference [.setup-aspect, before_script]
    - ./tools/my-setup.sh
  script:
    - bazel test //...
```

**Without an integration**, run the same steps in the job yourself:

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

jobs:
  test:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - name: Set up Aspect
        env:
          ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
        run: |
          aspect ci runner-health-check
          echo "$ASPECT_API_TOKEN" | aspect auth login --with-api-token
          aspect setup bazelrc --home
      - run: bazel test //...
```

Drop the `aspect auth login` line if the job doesn't post to pull requests.

[Runner groups](/docs/aspect-workflows/enterprise/ci-runners/runner-groups) covers which group a job lands on.

### What the rc sets

The rc points `bazel` at the runner's remote cache and build event service and puts the output base on the runner's NVMe. Where the runner's cache doesn't accept compressed blobs, it sets `--noremote_cache_compression`, because Bazel errors when a repository asks for compression from a cache that lacks it. [Remote execution](#remote-execution) is offered but not switched on.

Each flag the rc sets can be dropped with `--omit-bazel-flag`, except endpoints, credentials, the `--config` chains and the runner's paths. `--bes_backend` and `--bes_results_url` can be dropped, so a repository that streams build events somewhere else keeps doing that. See [`aspect setup bazelrc`](/docs/cli/tasks/setup_bazelrc).

### 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](/docs/aspect-workflows/enterprise/connect/ci-pipelines/github-actions) | [aspect-build/setup-aspect](https://github.com/aspect-build/setup-aspect)                      | [Releases](https://github.com/aspect-build/setup-aspect/releases)                    |
| [Buildkite](/docs/aspect-workflows/enterprise/connect/ci-pipelines/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](/docs/aspect-workflows/enterprise/connect/ci-pipelines/circleci)             | [setup-aspect orb](https://circleci.com/developer/orbs/orb/aspect-build/setup-aspect)          | The orb registry page                                                                |
| [GitLab CI](/docs/aspect-workflows/enterprise/connect/ci-pipelines/gitlab)              | [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>

### Tuning the setup step

The integrations take 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 login still happens, 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's 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, for a 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>

On a Workflows runner the GitHub Action also ignores its install, cache and `bazelrc` inputs, since the runner image and its rc supply those.

## On runners you manage

A runner you manage needs the launcher, a credential for the deployment, and an rc naming the deployment's endpoints. The setup integrations don't cover it: they take no deployment host and read only `ASPECT_API_TOKEN`, so run the steps below in the job.

**Which hostname** depends on where the runner sits. Inside the deployment's VPC, or a network peered with it, it reaches the private endpoint; anywhere else, the [external one](/docs/aspect-workflows/enterprise/connect/local-setup#the-external-endpoint). The setup is the same either way, and your Aspect contact or your Terraform outputs say which host applies.

<Note>
  **On a persistent runner, pass <code>--force</code> to <code>aspect setup bazelrc</code>.** The home directory
  outlives the job, so an rc written by the first job on that machine is still there for the
  next, and the command keeps an existing rc rather than overwriting it. An ephemeral runner
  starts clean, so this changes nothing there.
</Note>

### Record the deployment, then log in

The same path a developer takes, so the CI job and a laptop agree on what the deployment is called. The CLI recognizes GitHub Actions, Buildkite, CircleCI and GitLab CI 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. Record the deployment and authenticate. The token is passed on stdin, so it
#    never reaches the process table or the job log.
aspect auth configure remote.acme.aspect.build --name acme --default --login=false
echo "$ASPECT_API_TOKEN_ACME" | aspect auth login --with-api-token --deployment acme

# 3. Point vanilla `bazel` at the deployment.
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

bazel test //...
```

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.

The API token is per deployment: one minted for one deployment isn't accepted by any other, or by Aspect Cloud. Your Aspect contact or the deployment's admin issues it. Store it as a CI secret named `ASPECT_API_TOKEN_<NAME>`, the deployment name uppercased with every non-alphanumeric character replaced by `_`, which is the variable the CLI reads for that deployment. A deployment named `gcp.acme` reads `ASPECT_API_TOKEN_GCP_ACME`.

`--with-api-token` exchanges the token for a short-lived session and stores it 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.

### Or name the endpoints outright

Set the endpoints in the job's environment and the CLI takes them from there, with no `auth configure` step and nothing written to `~/.aspect/config.json`:

| Variable                                        | Names                                                                                            |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `ASPECT_WORKFLOWS_REMOTE_CACHE`                 | The remote cache, as a `grpcs://` URL                                                            |
| `ASPECT_WORKFLOWS_BES_BACKEND`                  | The build event service                                                                          |
| `ASPECT_WORKFLOWS_BES_RESULTS_URL`              | Where a build links to in the [Build Results UI](/docs/aspect-workflows/platform/features/webui) |
| `ASPECT_WORKFLOWS_REMOTE_EXECUTOR`              | The remote executor, where the deployment serves one                                             |
| `ASPECT_WORKFLOWS_REMOTE_CACHE_COMPRESSION`     | Set when the cache accepts compressed blobs                                                      |
| `ASPECT_WORKFLOWS_REMOTE_BYTESTREAM_URI_PREFIX` | The bytestream URI prefix, where it differs from the cache host                                  |

Your Aspect contact gives you these; on a self-hosted deployment they're Terraform outputs.

```shell theme={null}
export ASPECT_WORKFLOWS_REMOTE_CACHE="grpcs://remote.acme.aspect.build:443"
export ASPECT_WORKFLOWS_BES_BACKEND="grpcs://remote.acme.aspect.build:443"
export ASPECT_WORKFLOWS_BES_RESULTS_URL="https://app.acme.aspect.build/i/"

aspect setup bazelrc --home
```

This path writes no credential helper. Use it for endpoints the job reaches without an Aspect credential, or pair it with a [credential helper](#if-the-deployment-uses-your-own-identity-provider) you supply.

<Warning>
  **Setting <code>ASPECT\_WORKFLOWS\_REMOTE\_CACHE</code>, <code>ASPECT\_WORKFLOWS\_BES\_BACKEND</code>
  or <code>ASPECT\_WORKFLOWS\_REMOTE\_EXECUTOR</code> tells the CLI which deployment this machine
  is on**, as <code>ASPECT\_WORKFLOWS\_RUNNER</code> does on a Workflows runner.
  <code>aspect setup bazelrc</code> then takes its endpoints from the environment and writes
  no per-deployment sections, and an Aspect CLI task refuses <code>--remote</code>.
</Warning>

**Which to use:**

* **`auth configure`** when the job also runs Aspect CLI tasks, or you want the deployment named the same way everywhere.
* **Environment variables** when the endpoints need no Aspect credential from the job, or authenticate through a credential helper you supply, and you'd rather the pipeline say what it points at than depend on state in a home directory.

### If the deployment uses your own identity provider

There's no Aspect API token to issue, because Aspect doesn't operate the directory that would issue it. CI authenticates with a JWT from your identity provider, fetched by a [Bazel credential helper](https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md) you supply: a client-credentials grant, a workload identity federation exchange, or whatever your organization already uses for machine-to-machine auth.

Point Bazel at it with `--credential_helper`. One helper serves the cache, build event and remote execution endpoints.

## 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).

## `--remote` on CI

You rarely pass it. A task's `--remote` defaults to `auto`, which means **bare `--remote` on CI, and nothing off it**. On a runner you manage, a task reaches the default deployment's cache and build events without the flag.

`auto` steps aside wherever an explicit flag would fail:

| Situation                                                            | `--remote` named explicitly               | `auto`                                           |
| -------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------ |
| The environment already names a deployment, as on a Workflows runner | Refused                                   | Steps aside; the environment's services are used |
| The default deployment serves no Bazel endpoint                      | Fails, naming the deployment              | Nothing wired                                    |
| Nothing here can authenticate to it                                  | Wired, then Bazel fails on the credential | Nothing wired                                    |

Bazel treats a credential helper that can't produce a token as fatal, which is why `auto` won't wire an endpoint it can't authenticate to.

## Remote execution

Never turned on for you: a deployment may run an executor while most jobs still want their actions local. How a job asks for it depends on how the job names the deployment:

| The job names the deployment with                                                    | An Aspect CLI task                                                                                        | A `bazel` call         |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------- |
| The runner environment, on a Workflows runner, or the `ASPECT_WORKFLOWS_*` variables | `--workflows:remote-exec`, or once in `config.axl` with `ctx.features[Workflows].args.remote_exec = True` | `--config=aspect-exec` |
| `aspect auth configure`                                                              | `--remote=exec`                                                                                           | `--config=aspect-exec` |

Which worker pool an action lands on is decided by its Bazel platform; see [Targeting remote execution worker pools](/docs/aspect-workflows/platform/guides/remote-execution-worker-pools).

## Related

* [Local setup](/docs/aspect-workflows/enterprise/connect/local-setup): the same deployment from a developer's machine.
* <GatedLink access={gatedAccess(user, "workflows-subscriber")} href="/docs/aspect-workflows/enterprise/self-hosted/runner-registration/overview" group="workflows-subscriber">Runner registration</GatedLink>: connect Workflows runners to your CI provider.
* [Build metadata](/docs/cli/tasks/setup_workspace_data): label vanilla `bazel` builds with their commit and branch.
