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

# GitHub Actions

> Deploy rules from GoRules BRMS to your object storage with GitHub Actions.

When a release is deployed to a BRMS virtual (Stage) environment, a webhook dispatches a GitHub Actions workflow. 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 GHA as GitHub Actions
    box transparent Your account - one per environment
        participant S3 as Object storage<br/>(S3 / Blob / GCS)
        participant Agent as GoRules Agent
    end

    BRMS->>GHA: Release deployed - workflow_dispatch with payload
    Note over GHA,BRMS: gorules pull (one command)
    GHA->>BRMS: Resolve target via Rules Sync API
    GHA->>BRMS: Download release artifact
    BRMS-->>GHA: Rules artifact (zip)
    GHA->>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 dispatches your workflow via `workflow_dispatch`, passing the event as the `payload` input. The payload carries the project key and the target (for example `env:production`) in the exact syntax `gorules pull` accepts.
3. The [gorules/cli pull action](https://github.com/gorules/cli) resolves the target and downloads the artifact. Resolution and download are two API calls under the hood, but a single `gorules pull` - the action handles both.
4. Your upload step - in the same job - ships the artifact to your object storage. The Agent reads it from there.

Polling is also supported for setups that cannot accept inbound triggers: a scheduled workflow passes the previous release id as the `current` input and nothing is re-downloaded when the environment has not moved - see [Action outputs](#action-outputs). For a manual run, paste a payload into the **Run workflow** form; without one, the action fails fast on the missing project.

## Prerequisites

* BRMS webhooks (eligible plan): an organisation admin connects the GitHub 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 project access token with read access (**Settings → Tokens**), stored as the `GORULES_TOKEN` repository secret.
* Credentials for your storage destination; the examples show only the upload step.

## 1. Create the workflow

The workflow must declare a `payload` input on `workflow_dispatch` - that is where BRMS delivers the event, and the action picks it up automatically. Every variant follows the same order: pull the release, resolve the destination into a variable, authenticate (keyless OIDC), push. Pick your destination - each tab is the complete file:

<Tabs>
  <Tab title="Amazon S3">
    ```yaml .github/workflows/deploy-rules.yml theme={null}
    name: Deploy rules

    on:
      workflow_dispatch:
        inputs:
          payload:
            description: BRMS webhook payload
            required: false
            type: string

    permissions:
      id-token: write # keyless OIDC auth to the cloud provider
      contents: read

    jobs:
      deploy-rules:
        runs-on: ubuntu-latest
        steps:
          # 1. Pull the release from BRMS
          # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production
          - uses: gorules/cli/actions/pull@main
            id: rules
            with:
              url: https://acme.us1.gorules.io
              token: ${{ secrets.GORULES_TOKEN }}
              # No project/target baked in: a BRMS-triggered run supplies both via
              # the payload; a manual run pastes a payload in the Run workflow form
              out: dist

          # 2. Resolve the destination from the pulled target
          - name: Pick destination
            id: route
            env:
              TARGET: ${{ steps.rules.outputs.target }}
            run: |
              case "$TARGET" in
                env:production) echo "bucket=acme-rules-prod" >> "$GITHUB_OUTPUT" ;;
                env:dev)        echo "bucket=acme-rules-dev"  >> "$GITHUB_OUTPUT" ;;
                # 'main' and anything unmapped is refused: deploys ship releases,
                # not the unreleased branch head
                *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;;
              esac

          # 3. ADD YOUR AUTH HERE - keyless OIDC via aws-actions/configure-aws-credentials
          #    (role-to-assume + aws-region; uses the id-token permission above)

          # 4. Push
          - name: Deploy to S3
            env:
              PROJECT: ${{ steps.rules.outputs.project }}
            run: aws s3 cp "dist/$PROJECT" "s3://${{ steps.route.outputs.bucket }}/rules/$PROJECT"
    ```
  </Tab>

  <Tab title="Azure Blob Storage">
    ```yaml .github/workflows/deploy-rules.yml theme={null}
    name: Deploy rules

    on:
      workflow_dispatch:
        inputs:
          payload:
            description: BRMS webhook payload
            required: false
            type: string

    permissions:
      id-token: write # keyless OIDC auth to the cloud provider
      contents: read

    jobs:
      deploy-rules:
        runs-on: ubuntu-latest
        steps:
          # 1. Pull the release from BRMS
          # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production
          - uses: gorules/cli/actions/pull@main
            id: rules
            with:
              url: https://acme.us1.gorules.io
              token: ${{ secrets.GORULES_TOKEN }}
              # No project/target baked in: a BRMS-triggered run supplies both via
              # the payload; a manual run pastes a payload in the Run workflow form
              out: dist

          # 2. Resolve the destination from the pulled target
          - name: Pick destination
            id: route
            env:
              TARGET: ${{ steps.rules.outputs.target }}
            run: |
              case "$TARGET" in
                env:production) echo "account=acmerulesprod" >> "$GITHUB_OUTPUT" ;;
                env:dev)        echo "account=acmerulesdev"  >> "$GITHUB_OUTPUT" ;;
                # 'main' and anything unmapped is refused: deploys ship releases,
                # not the unreleased branch head
                *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;;
              esac

          # 3. ADD YOUR AUTH HERE - keyless OIDC via azure/login
          #    (client-id / tenant-id / subscription-id; uses the id-token permission above)

          # 4. Push
          - name: Deploy to Blob Storage
            env:
              PROJECT: ${{ steps.rules.outputs.project }}
            run: |
              az storage blob upload --account-name "${{ steps.route.outputs.account }}" \
                --container-name rules --name "rules/$PROJECT" \
                --file "dist/$PROJECT" --auth-mode login --overwrite
    ```
  </Tab>

  <Tab title="Google Cloud Storage">
    ```yaml .github/workflows/deploy-rules.yml theme={null}
    name: Deploy rules

    on:
      workflow_dispatch:
        inputs:
          payload:
            description: BRMS webhook payload
            required: false
            type: string

    permissions:
      id-token: write # keyless OIDC auth to the cloud provider
      contents: read

    jobs:
      deploy-rules:
        runs-on: ubuntu-latest
        steps:
          # 1. Pull the release from BRMS
          # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production
          - uses: gorules/cli/actions/pull@main
            id: rules
            with:
              url: https://acme.us1.gorules.io
              token: ${{ secrets.GORULES_TOKEN }}
              # No project/target baked in: a BRMS-triggered run supplies both via
              # the payload; a manual run pastes a payload in the Run workflow form
              out: dist

          # 2. Resolve the destination from the pulled target
          - name: Pick destination
            id: route
            env:
              TARGET: ${{ steps.rules.outputs.target }}
            run: |
              case "$TARGET" in
                env:production) echo "bucket=acme-rules-prod" >> "$GITHUB_OUTPUT" ;;
                env:dev)        echo "bucket=acme-rules-dev"  >> "$GITHUB_OUTPUT" ;;
                # 'main' and anything unmapped is refused: deploys ship releases,
                # not the unreleased branch head
                *) echo "Refusing to deploy target '$TARGET'" >&2; exit 1 ;;
              esac

          # 3. ADD YOUR AUTH HERE - keyless via google-github-actions/auth
          #    (workload_identity_provider + service_account; uses the id-token permission above)

          # 4. Push
          - name: Deploy to GCS
            env:
              PROJECT: ${{ steps.rules.outputs.project }}
            run: gcloud storage cp "dist/$PROJECT" "gs://${{ steps.route.outputs.bucket }}/rules/$PROJECT"
    ```
  </Tab>
</Tabs>

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

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 GitHub integration** (organisation admin): open **Settings → Integrations & Apps** under the organisation group and connect GitHub. 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 **GitHub**, then select the repository, the branch to dispatch on (for example `main`), and the workflow file (`deploy-rules.yml`).
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>
  **Organization IP allow list.** GitHub Enterprise Cloud organizations can restrict access with an [IP allow list](https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/managing-allowed-ip-addresses-for-your-organization) (Settings → Security → IP allow list). If enabled, the setting "Enable IP allow list configuration for installed GitHub Apps" governs whether app traffic is exempt - otherwise BRMS's traffic must be allowed. A blocked IP makes the dispatch fail as if the repository did not exist, so check this first when a correctly configured webhook cannot reach the repo. 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>

## 3. Test the flow

Deploy a release to the environment in BRMS and watch the workflow run appear under the repository's **Actions** tab. 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 workflow without re-deploying releases.

To test without BRMS, trigger the workflow manually from the Actions tab and paste a payload:

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

## Action outputs

| Output    | Description                                                            |
| --------- | ---------------------------------------------------------------------- |
| `project` | Project that was pulled, payload-aware                                 |
| `target`  | Target that was pulled, payload-aware                                  |
| `changed` | `false` when the target still matches the `current` input, else `true` |
| `release` | Release id, when the target resolved to a release                      |
| `version` | Release version, when the target resolved to a release                 |
| `commit`  | Commit id, when the target resolved to a commit                        |
| `sha256`  | Checksum of the downloaded artifact                                    |
| `files`   | JSON array of the paths written                                        |

`changed` is always `true` on a BRMS-triggered run - the workflow never passes the `current` input, so the action always downloads, which is why the workflow above has no `if:` guards: every trigger means a fresh deploy, and re-running a delivery re-uploads the same artifact, which is idempotent. The output exists for scheduled pulls: persist the last `release` output (for example in a cache), feed it back as the `current` input, and gate the upload steps with `if: steps.rules.outputs.changed == 'true'` - the action then skips the download entirely when the environment has not moved. 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}
permissions:
  contents: write # lets the default GITHUB_TOKEN push

jobs:
  sync-rules:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Pin to the latest release tag (cli-vX.Y.Z) rather than @main in production
      - uses: gorules/cli/actions/pull@main
        id: rules
        with:
          url: https://acme.us1.gorules.io
          token: ${{ secrets.GORULES_TOKEN }}
          out: rules
          unpack: true
          name: '.'
          delete: true

      - name: Commit rules
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add rules
          git diff --cached --quiet && exit 0 # nothing changed
          git commit -m "chore(rules): ${{ steps.rules.outputs.project }} ${{ steps.rules.outputs.target }} ${{ steps.rules.outputs.version }}"
          git push
```

`contents: write` lets the default `GITHUB_TOKEN` push, and since the workflow only runs on `workflow_dispatch`, the push cannot re-trigger it.
