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

# GitLab CI

> Deploy rules from GoRules BRMS to your object storage with GitLab CI.

When a release is deployed to a BRMS virtual (Stage) environment, a webhook triggers a GitLab pipeline. The run pulls the rules artifact from BRMS and uploads it to your object storage, where the [Agent](/developers/deployment/agent/overview) or an SDK loader reads it. The webhook is push-based: runs happen only on deploys, and the payload identifies the project and target.

The examples cover Amazon S3, Azure Blob Storage, and Google Cloud Storage.

## How it works

```mermaid theme={null}
sequenceDiagram
    box transparent GoRules - cloud or self-hosted
        participant BRMS as GoRules BRMS
    end
    participant GL as GitLab CI
    box transparent Your account - one per environment
        participant S3 as Object storage<br/>(S3 / Blob / GCS)
        participant Agent as GoRules Agent
    end

    BRMS->>GL: Release deployed - create pipeline with GRL_PAYLOAD
    Note over GL,BRMS: gorules pull (one command)
    GL->>BRMS: Resolve target via Rules Sync API
    GL->>BRMS: Download release artifact
    BRMS-->>GL: Rules artifact (zip)
    GL->>S3: Upload artifact
    Agent->>S3: Load rules
```

One environment is shown; each environment has its own storage location and Agent, and the pipeline routes by the payload's target.

1. A release is deployed to a virtual (Stage) environment in BRMS, directly or through a change request.
2. The BRMS webhook creates a pipeline through the GitLab API, passing the event as the `GRL_PAYLOAD` variable. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts.
3. The pull job from the [official template](https://github.com/gorules/cli/blob/main/templates/gitlab-ci-pull.yml) resolves the target and downloads the artifact. Resolution and download are two API calls under the hood, but a single `gorules pull` - the template handles both.
4. Your publish job ships the artifact to your object storage. The Agent reads it from there.

Polling is also supported: run the pipeline on a schedule and pass the previous release id as `GORULES_CURRENT` - see [Job results](#job-results). Without `GRL_PAYLOAD`, a run must state project and target explicitly (**Run pipeline** form variables or a pasted payload); a missing project fails fast, and the deploy job refuses the silent `main` default as a backstop.

## Prerequisites

* BRMS webhooks (eligible plan): an organisation admin connects the GitLab integration; a Project Admin (Manage Project) creates the webhook.
* A Stage [environment](/brms/deploy/environments) in the project (**Settings → Environments**): deploying a release to it fires the webhook and defines `env:<key>`.
* Two CI/CD variables (**Settings → CI/CD → Variables**): `GORULES_URL` (org URL, e.g. `https://acme.us1.gorules.io`) and `GORULES_TOKEN` (project access token, **Masked**; **Protected** only if the pipeline runs on a protected branch).
* Credentials for your storage destination; the examples show only the upload command.

## 1. Create the pipeline

`templates/gitlab-ci-pull.yml` in the [gorules/cli](https://github.com/gorules/cli) repository defines a hidden job, `.gorules-pull`, that you `extends:`. The pull job is identical in every variant; only the deploy job differs. Pick your destination - each tab is the complete file:

<Tabs>
  <Tab title="Amazon S3">
    ```yaml .gitlab-ci.yml theme={null}
    include:
      # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production
      - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml'

    workflow:
      rules:
        - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered
        - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing

    pull:rules:
      extends: .gorules-pull
      # No project/target baked in: a BRMS-triggered run takes both from the
      # webhook payload, and a manual run states them explicitly in the Run
      # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD)

    deploy:rules:
      needs: ['pull:rules']
      image:
        name: amazon/aws-cli:latest
        entrypoint: ['']
      resource_group: rules-deploy # serialize concurrent deploys
      script:
        # 1. resolve the destination from the pulled target
        - |
          case "$RULES_TARGET" in
            env:production) BUCKET=acme-rules-prod ;;
            env:dev)        BUCKET=acme-rules-dev ;;
            # anything else - including 'main', the unreleased branch head - is
            # refused: this pipeline deploys releases, not working state
            *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;;
          esac
        # 2. ADD YOUR AUTH HERE - keyless via GitLab OIDC (id_tokens +
        #    assume-role-with-web-identity, see https://docs.gitlab.com/ci/cloud_services/aws/),
        #    or masked AWS_* CI/CD variables (injected automatically - nothing to add)
        # 3. push
        - aws s3 cp "dist/$RULES_PROJECT" "s3://$BUCKET/rules/$RULES_PROJECT"
        - echo "Deployed $RULES_PROJECT@$RULES_RELEASE to $BUCKET"
    ```

    **Agent setup** - each environment's Agent reads the same location:

    ```shell theme={null}
    PROVIDER__TYPE=S3
    PROVIDER__BUCKET=acme-rules-prod # this environment's bucket
    PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional
    # AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY - optional with IAM roles
    # AWS_REGION - automatic on AWS compute; set it elsewhere (SDK falls back to us-east-1)
    ```
  </Tab>

  <Tab title="Azure Blob Storage">
    ```yaml .gitlab-ci.yml theme={null}
    include:
      # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production
      - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml'

    workflow:
      rules:
        - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered
        - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing

    pull:rules:
      extends: .gorules-pull
      # No project/target baked in: a BRMS-triggered run takes both from the
      # webhook payload, and a manual run states them explicitly in the Run
      # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD)

    deploy:rules:
      needs: ['pull:rules']
      image: mcr.microsoft.com/azure-cli:latest
      resource_group: rules-deploy
      script:
        # ADD YOUR AUTH HERE - OIDC federation or a service principal
        - |
          case "$RULES_TARGET" in
            env:production) ACCOUNT=acmerulesprod ;;
            env:dev)        ACCOUNT=acmerulesdev ;;
            *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;;
          esac
        - az storage blob upload --account-name "$ACCOUNT"
            --container-name rules --name "rules/$RULES_PROJECT"
            --file "dist/$RULES_PROJECT" --auth-mode login --overwrite
    ```

    **Agent setup** - each environment's Agent reads the same location:

    ```shell theme={null}
    PROVIDER__TYPE=AzureStorage
    PROVIDER__ACCOUNT_NAME=acmerulesprod # Entra ID (managed identity) auth
    # PROVIDER__CONNECTION_STRING=<connection-string> # alternative to account name
    PROVIDER__CONTAINER=rules
    PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional
    ```
  </Tab>

  <Tab title="Google Cloud Storage">
    ```yaml .gitlab-ci.yml theme={null}
    include:
      # Pin to the latest release tag (cli-vX.Y.Z) rather than main in production
      - remote: 'https://raw.githubusercontent.com/gorules/cli/main/templates/gitlab-ci-pull.yml'

    workflow:
      rules:
        - if: $CI_PIPELINE_SOURCE == "api" # BRMS-triggered
        - if: $CI_PIPELINE_SOURCE == "web" # manual runs for testing

    pull:rules:
      extends: .gorules-pull
      # No project/target baked in: a BRMS-triggered run takes both from the
      # webhook payload, and a manual run states them explicitly in the Run
      # pipeline form (GORULES_PROJECT / GORULES_TARGET, or a GRL_PAYLOAD)

    deploy:rules:
      needs: ['pull:rules']
      image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
      resource_group: rules-deploy
      script:
        # ADD YOUR AUTH HERE - workload identity federation or a service account key
        - |
          case "$RULES_TARGET" in
            env:production) BUCKET=acme-rules-prod ;;
            env:dev)        BUCKET=acme-rules-dev ;;
            *) echo "Refusing to deploy target '$RULES_TARGET'"; exit 1 ;;
          esac
        - gcloud storage cp "dist/$RULES_PROJECT" "gs://$BUCKET/rules/$RULES_PROJECT"
    ```

    **Agent setup** - each environment's Agent reads the same location:

    ```shell theme={null}
    PROVIDER__TYPE=GCS
    PROVIDER__BUCKET=acme-rules-prod # this environment's bucket
    PROVIDER__PREFIX=rules # must match the upload prefix; trailing slash optional
    # PROVIDER__BASE64_CONTENTS=<base64-credential-json> # omit to use workload identity / ADC
    ```
  </Tab>
</Tabs>

BRMS sends the same event for every environment - `deploy to dev` and `deploy to production` differ only in the payload's `target`, which the pull job re-exports as `RULES_TARGET`. The `case` maps the target to a destination; unmapped targets fail the job so nothing is uploaded to an unintended destination.

The pull job publishes its results as a [dotenv report](#job-results), so `deploy:rules` reads `RULES_TARGET` and `RULES_PROJECT` as ordinary variables in its script - no artifact parsing. There is no changed-gate: a BRMS-triggered run never passes `GORULES_CURRENT`, so the CLI always downloads; re-running a delivery re-uploads the same artifact. `RULES_CHANGED` only matters for [scheduled pulls](#job-results). The pulled files arrive via `needs:` as a job artifact in `dist/`.

The artifact is written as `dist/<project-key>` with no `.zip` suffix, because the Agent's storage providers use the object name verbatim as the project key - upload it as-is and the Agent picks it up. See [naming the output](/developers/cli#naming-the-output) for the other layouts, and [Agent configuration](/developers/deployment/agent/overview#environment-variables) for the full provider settings - applications that evaluate through an SDK loader read the same object and need no Agent.

## 2. Connect BRMS

Integrations are connected once per organisation; webhooks are configured per project.

1. **Connect the GitLab integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect GitLab. The webhook form also deep-links here via **Configure Integrations** if the connection is missing.
2. **Create the webhook** (Project Admin - the Manage Project permission): in the project, go to **Settings → Webhooks → Create webhook**, choose type **GitLab**, then select the repository and the branch to run on (for example `main`).
3. Subscribe it to the **Release Deployed** event - then every run means an environment moved, and the payload always carries an `env:<key>` target.
4. Optionally use the **Test Webhook** section of the form to send a sample event before relying on it.

See [Webhooks](/brms/setup/webhooks) for the full reference, including delivery logs and retries.

BRMS creates the pipeline through the authenticated GitLab API, so no pipeline trigger token is needed and no queue-time variable has to be declared - `GRL_PAYLOAD` just arrives.

<Note>
  **`404 Project Not Found` on delivery.** GitLab hides anything the caller cannot access behind a 404, so this one error has four possible causes:

  1. **IP restrictions.** If the GitLab group restricts access by IP, it blocks BRMS. GoRules cloud has no fixed IP addresses - traffic originates from the AWS IP ranges of us-east-1 or eu-central-1 (your region). Fixes: allow those ranges (filter [ip-ranges.json](https://ip-ranges.amazonaws.com/ip-ranges.json) by region), switch to scheduled polling, or self-host BRMS with a static egress IP.
  2. **Repository role.** The identity connected in the integration needs Developer or higher on the repository.
  3. **CI/CD disabled.** The project's CI/CD feature must be enabled (project Settings → General → Visibility).
  4. **Stale repository selection.** Re-select the repository in the webhook after reconnecting the integration.

  The delivery log (**Settings → Webhooks → View Logs**) shows each request and response; **Retry** re-sends it.
</Note>

## 3. Test the flow

Deploy a release to the environment in BRMS and watch the pipeline appear under **Build → Pipelines**. The webhook log in BRMS (**Settings → Webhooks → logs**) shows the exact payload delivered and lets you replay a delivery, so you can iterate on the pipeline without re-deploying releases.

To test without BRMS, use **Build → Pipelines → New pipeline** and either set `GORULES_PROJECT` and `GORULES_TARGET` directly, or add a `GRL_PAYLOAD` variable to exercise the exact webhook path:

```json theme={null}
{ "project": { "key": "pricing" }, "target": "env:production" }
```

## Job results

The pull job writes a dotenv report, so any job with `needs: ['pull:rules']` reads these as ordinary variables in `script:` and `environment:` (not in `rules:`, which GitLab evaluates before the pipeline runs):

| Variable        | Description                                            |
| --------------- | ------------------------------------------------------ |
| `RULES_CHANGED` | `'true'` / `'false'`                                   |
| `RULES_PROJECT` | Project that was pulled, payload-aware                 |
| `RULES_TARGET`  | Target that was pulled, payload-aware                  |
| `RULES_VERSION` | Release version, when the target resolved to a release |
| `RULES_RELEASE` | Release id                                             |
| `RULES_SHA256`  | Checksum of the downloaded artifact                    |

Job configuration (set under the extending job's `variables:`): `GORULES_PROJECT`, `GORULES_TARGET`, `GORULES_OUT` (default `dist`), `GORULES_NAME`, `GORULES_UNPACK` and `GORULES_DELETE` (quote the booleans: `'true'`), `GORULES_CURRENT`, and `GORULES_CLI_VERSION`.

`RULES_CHANGED` is always `'true'` on a BRMS-triggered run - the pipeline never passes `GORULES_CURRENT`, so the CLI always downloads, which is why the deploy job above has no gate. The flag exists for scheduled pulls: run the pipeline on a schedule, pass the last `RULES_RELEASE` back as `GORULES_CURRENT`, and when the environment has not moved the CLI exits with code `3`, the job reports `RULES_CHANGED=false`, and downstream jobs skip their upload with a script guard:

```yaml theme={null}
  script:
    - if [ "$RULES_CHANGED" != "true" ]; then echo "Unchanged, skipping."; exit 0; fi
```

The guard must live in `script:`, not in `rules:` - GitLab evaluates job `rules:` at pipeline creation, before any job has run, so dotenv variables from an earlier job are always unset there and a `rules: - if: $RULES_CHANGED == "true"` gate would silently never match. See the [CLI exit codes](/developers/cli#exit-codes) for the underlying contract.

## Committing rules to Git

The same pull can also commit the rules as plain files to your repository. Three optional settings: `unpack` extracts the archive instead of writing a zip, `name: '.'` extracts straight into the output directory, and `delete` removes files that no longer exist in BRMS so the directory mirrors the target exactly. Point the output inside the checkout and commit what changed:

```yaml theme={null}
pull:rules:
  extends: .gorules-pull
  variables:
    GORULES_OUT: rules
    GORULES_UNPACK: 'true'
    GORULES_NAME: '.'
    GORULES_DELETE: 'true'

commit:rules:
  needs: ['pull:rules']
  image: alpine/git:latest
  script:
    - git config user.name "gorules-bot"
    - git config user.email "gorules-bot@acme.example"
    - git add rules
    - git diff --cached --quiet && exit 0 # nothing changed
    - git commit -m "chore(rules): $RULES_PROJECT $RULES_TARGET ${RULES_VERSION:-$RULES_RELEASE}"
    - git push "https://gitlab-ci-token:${GIT_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" "HEAD:$CI_COMMIT_REF_NAME"
```

`GIT_PUSH_TOKEN` is a GitLab project access token with `write_repository` scope (the default CI job token cannot push). The extracted files travel from `pull:rules` to `commit:rules` as a job artifact, and the `workflow: rules:` gate from the main example already prevents the push from re-triggering the pipeline (`push` is not an allowed source).
