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

# GitLab CI/CD pipelines

> Configure a GitLab CI/CD 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 GitLab CI/CD pipeline runs on Aspect Workflows CI runners by targeting a runner group with `tags:`. 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 GitLab CI/CD is covered in <GatedLink access={gatedAccess(user, "workflows-subscriber")} href="/docs/aspect-workflows/enterprise/self-hosted/runner-registration/gitlab" group="workflows-subscriber">runner registration</GatedLink>.
</Note>

## Configure a pipeline

1. In your GitLab repository, click the **Set up CI/CD** link in the sidebar of your project.
2. On the **Pipeline editor** page, click the **Configure pipeline** button.
3. Replace the contents of the editor with the following. Each job runs your existing `bazel` commands unchanged.

Every job pulls in the [`aspect-build/setup-aspect-gitlab-component`](https://gitlab.com/aspect-build/setup-aspect-gitlab-component) CI/CD component through the `.ci` template, which extends `.setup-aspect`. [The setup step](/docs/aspect-workflows/enterprise/connect/ci-setup#the-setup-step) covers what it does and the inputs it takes.

```yaml title=.gitlab-ci.yml theme={null}
include:
  - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.38.3

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

stages:
  - CI

.ci:
  extends: .setup-aspect
  stage: CI
  tags: [aspect-workflows, aspect-default]
  rules:
    # Scheduled pipelines run only the warming job.
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - when: on_success

build:
  extends: .ci
  script:
    - bazel build //...

test:
  extends: .ci
  script:
    - bazel test //...
```

The `workflow: rules` run a merge request pipeline while a merge request is open and a branch pipeline otherwise, so a push creates one pipeline, not two. The job `rules` keep build and test off scheduled pipelines, which run the [warming](/docs/aspect-workflows/enterprise/ci-runners/warming) job instead.

<Note>
  Keep <code>.setup-aspect</code> in the <code>extends</code> chain of every job that calls <code>bazel</code>.
</Note>

A job that defines its own `before_script` replaces the one `.setup-aspect` supplies, and the setup step doesn't run. Pull it in with `!reference` ahead of your own commands:

```yaml title=.gitlab-ci.yml theme={null}
build:
  extends: .ci
  before_script:
    - !reference [.setup-aspect, before_script]
    - ./tools/prepare.sh
  script:
    - bazel build //...
```

<Note>
  **Self-hosted GitLab.** The <code>\$CI\_SERVER\_FQDN</code> in the <code>include</code> resolves to the GitLab instance running your pipeline, and GitLab only resolves components from that same instance, so a self-hosted pipeline can't pull the component from <code>gitlab.com</code> directly. To use it on a self-hosted instance, [mirror the component project](https://docs.gitlab.com/ci/components/#use-a-gitlabcom-component-on-gitlab-self-managed) into a group on your instance and mark the mirror as a CI/CD Catalog resource. The pipeline config above then works unchanged.
</Note>

## Non-Bazel jobs

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

```yaml title=.gitlab-ci.yml theme={null}
custom-job:
  tags: [aspect-workflows, aspect-default]
  script:
    - npm install && npm test
```

## Aspect CLI tasks (optional)

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

To use it, add your `ASPECT_API_TOKEN` as a CI/CD variable (Settings > CI/CD > Variables) that is **masked, not protected**: protected variables don't reach merge request pipelines from unprotected branches, and merge request comments and status checks need those pipelines. Then call `aspect <task>` in each job:

```yaml title=.gitlab-ci.yml theme={null}
include:
  - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.38.3

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

stages:
  - CI

.ci:
  extends: .setup-aspect
  stage: CI
  tags: [aspect-workflows, aspect-default]
  rules:
    # Scheduled pipelines run only the warming job.
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - when: on_success

build:
  extends: .ci
  script:
    - aspect build --task:name build -- //...

test:
  extends: .ci
  script:
    - aspect test --task:name test -- //...

format:
  extends: .ci
  script:
    - aspect format --task:name format

lint:
  extends: .ci
  script:
    - aspect lint --task:name lint -- //...

delivery:
  extends: .ci
  needs: [test]
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - aspect delivery --task:name delivery --ci-host=gl --query='attr(tags, deliverable, //...)'
```

The `delivery` job runs only on the default branch; its `rules` replace the ones it would inherit from `.ci`. `--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://gitlab.com/aspect-build/bazel-examples/-/pipelines) for a complete working example of a GitLab CI pipeline.
</Tip>
