Build and promote with deploy-action over GitHub OIDC
Objective
Build an OPA bundle with opa build, explain how deploy-action ships that signed bundle to the EnforceAuth API with a short-lived GitHub OIDC token instead of a stored API key, and read a GitHub Actions workflow that builds, tests, and promotes a bundle dev to staging to prod.
Concept
deploy-action is the EnforceAuth GitHub Action that deploys an OPA bundle to the EnforceAuth API. The important design choice is how it authenticates: it holds no API key and no long-lived secret. Instead it uses GitHub OIDC workload identity.
Here is the chain, and why each piece exists.
id-token: write. A workflow can only request a GitHub OIDC token if you grant this permission in the job. Without it, GitHub will not mint the token and the deploy cannot authenticate. Grant it on the job that deploys, not repo-wide.- The OIDC token. GitHub mints a short-lived JSON Web Token that describes the workflow run: the repository, the branch or tag, the environment, and the workflow. It is scoped to this run and expires quickly, so there is nothing durable to steal.
- Trust policy. In EnforceAuth you configure a trust policy that says which repository, branch, and environment claims are allowed to deploy which bundle to which environment. The EnforceAuth API verifies the token's claims against this policy. A token from the wrong repo or branch is rejected.
- RFC 8693 token exchange. The Action exchanges the GitHub OIDC token for short-lived EnforceAuth API credentials (OAuth 2.0 Token Exchange, RFC 8693). The GitHub token is the "subject token"; the API returns a scoped, short-lived access token used only to make this one deploy call.
The payoff: no API keys live in GitHub secrets, credentials are minted per run and expire, and the trust policy is the single place that decides which pipeline may ship to prod.
Build the bundle with opa build
Promotion moves an artifact, and opa build is what produces that artifact. It compiles a directory of .rego files into a single, self-contained bundle tarball that a PDP can load directly. Three flags matter for the pipeline:
-b <dir>builds in bundle mode from a directory (here, the lab bundle).-r <revision>stamps a revision into the bundle manifest. Set it to thecommit_sha, and the artifact carries the exact version it was built from, the same identifier promotion and rollback are expressed in.--ignore '*_test.rego'keeps the test files out of the shipped artifact. Tests gate the build; they are not shipped to production.
Build the lab bundle and inspect it (from the cloned university-labs repo):
opa build -b courses/policy-lifecycle-build-promote-rollback \
-r 9f8e7d6 --ignore '*_test.rego' -o bundle.tar.gz
opa inspect bundle.tar.gz
opa inspect reads the manifest of the artifact you just built:
MANIFEST:
┌──────────────┬─────────┐
│ FIELD │ VALUE │
├──────────────┼─────────┤
│ Rego Version │ 1 │
│ Revision │ 9f8e7d6 │
└──────────────┴─────────┘
NAMESPACES:
┌─────────────────────────────────────┬──────────────────┐
│ NAMESPACE │ FILE │
├─────────────────────────────────────┼──────────────────┤
│ data │ /data.json │
│ data.university.lifecycle.promotion │ /lab/policy.rego │
└─────────────────────────────────────┴──────────────────┘
bundle.tar.gz is the deployable artifact: policy.rego under its package, no test files, and a manifest whose Revision is the commit_sha it was built from. The starter compiles, so this build succeeds even before you finish the lesson-1 exercise. That is deliberate: opa build produces the artifact, and opa test is the gate that decides whether the artifact may ship, not opa build.
From artifact to production, three tools with three jobs:
opa buildproduces the OPA bundle (the tarball above). This is standard OPA, nothing EnforceAuth-specific.- Writ signs that bundle into a signed bundle and records which signed version is live in each environment. The control plane automates the build-and-sign step and keeps the decision logs; it is not a PDP on the request path.
deploy-actionships the signed bundle to the EnforceAuth API over GitHub OIDC (RFC 8693 token exchange, no API keys), verified against the trust policy.
So "build, promote, roll back" is: opa build makes the artifact, Writ signs and governs it, and deploy-action delivers it, with opa test as the gate between build and promote.
A workflow that builds, tests, and promotes
The workflow below is illustrative of the shape you write. It runs the opa test gate from lesson 1, promotes to staging automatically, then promotes to prod behind a GitHub environment protection rule (which is where an approver or wait timer lives).
The with: block below uses placeholder input names (bundle, environment) to show the shape of a deploy step. They are teaching placeholders, not a documented deploy-action API; substitute the real input names from the version of the Action you adopt. The grounded, non-negotiable part of this lesson is the authentication mechanism, not the input keys: deploy-action authenticates over GitHub OIDC with RFC 8693 token exchange and holds no API keys.
name: promote-authz-bundle
on:
push:
branches: [main]
permissions:
contents: read # checkout
id-token: write # required to mint the GitHub OIDC token (RFC 8693 subject token)
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install opa
run: |
curl -sSL -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x opa && sudo mv opa /usr/local/bin/opa
- name: Test the bundle (promotion gate)
run: opa test policy -v
- name: Build the bundle
# Stamp the commit_sha as the bundle revision; keep tests out of the artifact.
run: opa build -b policy -r "${{ github.sha }}" --ignore '*_test.rego' -o bundle.tar.gz
promote-staging:
needs: build-and-test
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy bundle to staging
uses: EnforceAuth/deploy-action@v1
with:
# Placeholder input names (illustrative, not a documented API).
bundle: policy # path to the bundle in the repo
environment: staging
# No API keys. The Action mints a short-lived GitHub OIDC token and
# exchanges it (RFC 8693) for EnforceAuth API credentials, verified
# against the trust policy configured for this repo and environment.
promote-prod:
needs: promote-staging
runs-on: ubuntu-latest
environment: production # GitHub environment protection: approver / wait timer
steps:
- uses: actions/checkout@v4
- name: Deploy bundle to prod
uses: EnforceAuth/deploy-action@v1
with:
# Placeholder input names (illustrative, not a documented API).
bundle: policy
environment: prod
Two things to notice. First, promote-prod needs promote-staging, which needs build-and-test: prod cannot ship unless staging shipped and the bundle built and tested. That is dev-to-staging-to-prod promotion encoded as job dependencies, with opa build and opa test as the gate before any promotion runs. Second, the human gate is the GitHub environment: production protection rule, not a secret in the pipeline. Writ records which signed bundle version is live in each environment and keeps the decision logs for what that version decided.
Hands-on lab
This step assumes you completed the lab exercise in lesson 1, so policy.rego now implements the rules and the suite is green (the starter ships at FAIL: 3/7 until you do). You cannot run a real GitHub Actions deploy from this lab, and you should not fake one. What you can do is run the two steps the build-and-test job runs, against the bundle the workflow would promote: build the artifact, then run the gate.
First clone the public labs repo (if you have not already), then build the deployable artifact and inspect its manifest, exactly as the workflow's build step does:
git clone https://github.com/EnforceAuth/university-labs.git
cd university-labs
opa build -b courses/policy-lifecycle-build-promote-rollback \
-r 9f8e7d6 --ignore '*_test.rego' -o bundle.tar.gz
opa inspect bundle.tar.gz
opa inspect shows a manifest whose Revision is 9f8e7d6 and a single namespace data.university.lifecycle.promotion from policy.rego, with the *_test.rego file left out of the artifact. That tarball is what Writ signs and deploy-action ships.
Then run the promotion gate that decides whether the artifact may ship:
opa test courses/policy-lifecycle-build-promote-rollback -v
PASS: 7/7 is the green result that, in the workflow above, unblocks promote-staging. Then read the lab's policy.rego promotion rules next to the workflow: the policy says a release manager may promote to prod only when the change is approved, and the workflow encodes an approval as the GitHub production environment protection rule. The two express the same control at two layers: one in policy, one in the pipeline.
Reflection tasks (no invented commands):
- In the YAML, point to the one line that lets GitHub mint the OIDC token. Explain what breaks if you delete it.
- Explain, in one sentence each, what the trust policy checks and what RFC 8693 token exchange produces.
Check for understanding
- What does
opa buildproduce, and what does the-rflag stamp into the bundle manifest? - Where does
deploy-actionget its credentials to call the EnforceAuth API, given there is no API key in the repo? - What is the
id-token: writepermission for, and what fails without it? - In the workflow, what makes prod deploy only after staging, and where does the human approval for prod actually live?
- Which tool builds the bundle, which signs it, and which ships it to the EnforceAuth API?