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

# GitHub Actions pipelines

> Configure a GitHub Actions pipeline to run on Aspect Workflows CI runners: target a runner group, keep your existing bazel steps, and optionally adopt Aspect CLI tasks.

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>;
};

Your GitHub Actions pipeline runs on Aspect Workflows CI runners by targeting a runner group with `runs-on:`. Your existing `bazel` steps keep working unchanged: the [setup step](/docs/aspect-workflows/enterprise/connect/ci-setup#the-setup-step) wires the runner's remote cache, build event service and NVMe-backed output base into every invocation.

<Note>
  Registering the runners with GitHub Actions is covered in <GatedLink access={gatedAccess(user, "workflows-subscriber")} href="/docs/aspect-workflows/enterprise/self-hosted/runner-registration/github-actions" group="workflows-subscriber">runner registration</GatedLink>.
</Note>

## Configure GitHub Actions for Aspect Workflows

Create or update your workflow file (for example, `.github/workflows/aspect-workflows.yaml`). Point each job's `runs-on:` at your Workflows runner labels and keep your existing `bazel` steps.

Every job uses the [`aspect-build/setup-aspect`](https://github.com/aspect-build/setup-aspect) action before its first `bazel` call. [The setup step](/docs/aspect-workflows/enterprise/connect/ci-setup#the-setup-step) covers what it does and the inputs it takes.

```yaml title=.github/workflows/aspect-workflows.yaml theme={null}
name: Aspect Workflows

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
      - name: Build
        run: bazel build //...

  test:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
      - name: Test
        run: bazel test //...
```

Pin to a full-length SHA per [GitHub's third-party action security guidance](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions). Find the latest SHA and version on the [setup-aspect releases page](https://github.com/aspect-build/setup-aspect/releases).

## Non-Bazel jobs

Jobs that don't call Bazel need no setup step. Target the runner group and run your commands:

```yaml title=.github/workflows/aspect-workflows.yaml theme={null}
jobs:
  custom-job:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - run: npm install && npm test
```

## Aspect CLI tasks (optional)

Swap a `bazel` step for the matching `aspect <task>` from the open-source [Aspect CLI](/docs/cli/overview) to add status checks, inline PR comments with one-click suggested fixes, retries on transient Bazel errors and [selective delivery](/docs/aspect-workflows/platform/features/selective-delivery).

To use it, pass your `ASPECT_API_TOKEN` to `setup-aspect` and call `aspect <task>`. The action exchanges the token for a short-lived JWT and persists only the JWT, so the long-lived token isn't written to `GITHUB_ENV` or visible to other steps.

`id-token: write` is for artifact uploads and PR summary comments. Status checks and PR comments post as the Aspect Workflows GitHub App, so the job token itself needs only read access:

```yaml title=.github/workflows/aspect-workflows.yaml theme={null}
permissions:
  contents: read
  id-token: write

jobs:
  build:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - name: Build
        run: aspect build --task:name build -- //...

  test:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - name: Test
        run: aspect test --task:name test -- //...

  format:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - name: Format
        run: aspect format --task:name format

  lint:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - name: Lint
        run: aspect lint --task:name lint -- //...

  delivery:
    needs: [test]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@ebca96eb49ef58c00d4226de8bd3a0813507dd87 # v2026.38.3
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - name: Delivery
        run: aspect delivery --task:name delivery --ci-host=gh --query='attr(tags, deliverable, //...)'
```

The `delivery` job runs only on pushes to `main`. `--query` selects the targets to deliver; without it or positional targets, `aspect delivery` delivers nothing. You can set the query once in `.aspect/config.axl` instead (`ctx.tasks["delivery"].args.query`). See [`aspect delivery`](/docs/cli/tasks/delivery).

See [Running tasks in CI](/docs/cli/tasks-ci) for the full task reference.

<Tip>
  See [Aspect Bazel Examples](https://github.com/aspect-build/bazel-examples/actions) for a complete working example of a GitHub Actions workflow.
</Tip>

### `GITHUB_TOKEN` fallback (optional)

`format`, `lint`, and `gazelle` read a pull request's changed files from git. When git can't resolve them, the Aspect CLI asks GitHub's pull request files API, using the Aspect Workflows GitHub App's token. If the App isn't installed on the repository, expose the job's `GITHUB_TOKEN` with `pull-requests: read` so that call can use it instead:

```yaml theme={null}
jobs:
  format:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    permissions:
      contents: read
      id-token: write
      pull-requests: read
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

A task that resolves no changed files at all runs across the whole repository, as with `--scope=all`.
