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

# Azure DevOps

> Deploy rules from GoRules BRMS to your object storage with Azure Pipelines.

When a release is deployed to a BRMS virtual (Stage) environment, a webhook queues an Azure Pipelines run. 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 template only pulls; the publish step is yours. 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 ADO as Azure Pipelines
    box transparent Your account - one per environment
        participant S3 as Object storage<br/>(S3 / Blob / GCS)
        participant Agent as GoRules Agent
    end

    BRMS->>ADO: Release deployed - queue run with GRL_PAYLOAD
    Note over ADO,BRMS: gorules pull (one command)
    ADO->>BRMS: Resolve target via Rules Sync API
    ADO->>BRMS: Download release artifact
    BRMS-->>ADO: Rules artifact (zip)
    ADO->>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 queues a run through the Azure DevOps Pipelines API, passing the event as the `GRL_PAYLOAD` run variable. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts.
3. The [official steps template](https://github.com/gorules/cli/blob/main/templates/azure-pipelines-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 step - appended after the template in the same job - uploads the artifact wherever your runtime reads it from.

Without `GRL_PAYLOAD`, project and target come from whatever you pass explicitly - queue-time values or schedule configuration. Nothing environment-specific is baked into the pipeline file.

Polling is also supported: a scheduled run passes the previous release id as the `current` parameter - see [Pipeline variables](#pipeline-variables). Without `GRL_PAYLOAD`, a run must state project and target explicitly (queue-time values or schedule configuration); a missing project fails fast.

## Prerequisites

* BRMS webhooks (eligible plan): an organisation admin connects the Azure DevOps 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>`.
* A secret pipeline variable `GORULES_TOKEN` (project access token with read access, **Settings → Tokens**).
* A GitHub service connection to reference the template repository, plus credentials for your storage destination; the examples show only the upload step.

## 1. Create the pipeline

`templates/azure-pipelines-pull.yml` in the [gorules/cli](https://github.com/gorules/cli) repository is a **steps template**: it only pulls - downloading the artifact and setting result variables - and your publish step follows it in the same job. The template steps are identical in every variant; only the publish step differs. Pick your destination - each tab is the complete file:

<Tabs>
  <Tab title="Amazon S3">
    ```yaml azure-pipelines.yml theme={null}
    trigger: none # runs are queued by BRMS, manually, or on a schedule

    resources:
      repositories:
        - repository: gorules
          type: github
          name: gorules/cli
          ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production
          endpoint: <your GitHub service connection>

    jobs:
      - job: deploy_rules
        pool:
          vmImage: ubuntu-latest
        steps:
          - template: templates/azure-pipelines-pull.yml@gorules
            parameters:
              url: https://acme.us1.gorules.io
              # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD

          # AUTH: the AWS service connection carries credentials - role
          # assumption, or OIDC with AWS Toolkit >= 1.15. No keys in the pipeline
          - task: AWSShellScript@1
            displayName: Deploy to S3
            inputs:
              awsCredentials: <your AWS service connection>
              regionName: eu-central-1
              scriptType: inline
              inlineScript: |
                set -euo pipefail
                case "$(rulesTarget)" 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 '$(rulesTarget)'"; exit 1 ;;
                esac
                aws s3 cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" "s3://$BUCKET/rules/$(rulesProject)"
          # Without the extension: run the same script in a plain script step and
          # set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION as
          # secret pipeline variables under env:
    ```

    **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 azure-pipelines.yml theme={null}
    trigger: none # runs are queued by BRMS, manually, or on a schedule

    resources:
      repositories:
        - repository: gorules
          type: github
          name: gorules/cli
          ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production
          endpoint: <your GitHub service connection>

    jobs:
      - job: deploy_rules
        pool:
          vmImage: ubuntu-latest
        steps:
          - template: templates/azure-pipelines-pull.yml@gorules
            parameters:
              url: https://acme.us1.gorules.io
              # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD

          # AUTH: the ARM service connection carries credentials (workload
          # identity federation)
          - task: AzureCLI@2
            displayName: Deploy to Blob Storage
            inputs:
              azureSubscription: <your ARM service connection>
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                set -euo pipefail
                case "$(rulesTarget)" in
                  env:production) ACCOUNT=acmerulesprod ;;
                  env:dev)        ACCOUNT=acmerulesdev ;;
                  *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;;
                esac
                az storage blob upload --account-name "$ACCOUNT" \
                  --container-name rules --name "rules/$(rulesProject)" \
                  --file "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" \
                  --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 azure-pipelines.yml theme={null}
    trigger: none # runs are queued by BRMS, manually, or on a schedule

    resources:
      repositories:
        - repository: gorules
          type: github
          name: gorules/cli
          ref: refs/heads/main # pin to the latest release tag (cli-vX.Y.Z) in production
          endpoint: <your GitHub service connection>

    jobs:
      - job: deploy_rules
        pool:
          vmImage: ubuntu-latest
        steps:
          - template: templates/azure-pipelines-pull.yml@gorules
            parameters:
              url: https://acme.us1.gorules.io
              # no project/target: a BRMS-triggered run supplies both via GRL_PAYLOAD

          - script: |
              set -euo pipefail
              # ADD YOUR AUTH HERE - workload identity federation or a
              # service account key
              case "$(rulesTarget)" in
                env:production) BUCKET=acme-rules-prod ;;
                env:dev)        BUCKET=acme-rules-dev ;;
                *) echo "Refusing to deploy target '$(rulesTarget)'"; exit 1 ;;
              esac
              gcloud storage cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" \
                "gs://$BUCKET/rules/$(rulesProject)"
            displayName: Deploy to GCS
    ```

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

The `case` maps the target to a destination; unmapped targets fail the step so nothing is uploaded to an unintended destination.

The artifact is written as `<project-key>` with no `.zip` suffix, because the Agent's storage providers use the object name verbatim as the project key. 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. Declare the queue-time variable

Unlike GitLab, Azure DevOps only accepts variables at queue time when the pipeline declares them. In the pipeline's **Edit → Variables**, add a variable named `GRL_PAYLOAD` (any value) with **"Let users override this value when running this pipeline"** checked.

<Note>
  Organizations with **Limit variables that can be set at queue time** enabled - the default on newer organizations - reject the BRMS queue request entirely if this variable is not declared. If webhook deliveries fail with a queue error, this is the first thing to check.
</Note>

## 3. Connect BRMS

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

1. **Connect the Azure DevOps integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect Azure DevOps. 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 **Azure DevOps**, then select the repository (`project/repository`), the branch, and the pipeline to queue.
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.

<Note>
  **IP-restricted organizations.** Azure DevOps enforces IP restrictions through Microsoft Entra [Conditional Access policies](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/change-application-access-policies) with location conditions, plus the organization policy "Enable IP Conditional Access policy validation". If your organization uses these, BRMS's traffic must be permitted by the allowed named locations - otherwise token sign-in or the queue request is rejected even though the same configuration works from your office network. 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.
</Note>

## 4. Test the flow

Deploy a release to the environment in BRMS and watch the run appear under **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, run the pipeline manually and set the `GRL_PAYLOAD` variable at queue time:

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

## Pipeline variables

The template sets these for the steps that follow it in the job:

| Variable       | Description                                            |
| -------------- | ------------------------------------------------------ |
| `rulesChanged` | `'true'` / `'false'`                                   |
| `rulesProject` | Project that was pulled, payload-aware                 |
| `rulesTarget`  | Target that was pulled, payload-aware                  |
| `rulesVersion` | Release version, when the target resolved to a release |
| `rulesRelease` | Release id                                             |
| `rulesSha256`  | Checksum of the downloaded artifact                    |

`rulesChanged` is always `'true'` on a BRMS-triggered run - the pipeline never passes `current`, so the CLI always downloads. It exists for scheduled runs: pass the previous release id as the `current` parameter and gate your publish step on `rulesChanged` when the environment has not moved.

Template parameters: `url`, `project`, `target`, `out` (default `$(Build.ArtifactStagingDirectory)/rules`), `name`, `unpack`, `delete`, `current`, and `cliVersion`.

## 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}
    steps:
      - checkout: self
        persistCredentials: true # keep credentials for the push below

      - template: templates/azure-pipelines-pull.yml@gorules
        parameters:
          url: https://acme.us1.gorules.io
          out: $(Build.SourcesDirectory)/rules
          unpack: true
          name: '.'
          delete: true

      - script: |
          set -euo pipefail
          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): $(rulesProject) $(rulesTarget) $(rulesVersion)"
          git push origin "HEAD:$(Build.SourceBranchName)"
        displayName: Commit rules
```

The build service identity needs the **Contribute** permission on the repository (Project Settings → Repositories → Security), and `persistCredentials: true` keeps the job's token available for the push. `trigger: none` already prevents the push from re-queuing the pipeline.
