An engineering standard written down as a wiki page or a Slack pin gets followed exactly as consistently as reviewers remember it exists. "Every workflow file needs an explicit permissions block" is easy to state once in an onboarding doc and easy to forget three months later when someone's copy-pasting a workflow from a different repo that predates the standard. The permissions-block post made the case for the standard itself; this post is about the layer most teams skip - making the standard something a machine checks on every PR, not something a human is trusted to remember.
Open Policy Agent (OPA) is a general-purpose policy engine, and Rego is its policy language - rules expressed as code, versioned in the repo, evaluated deterministically against structured input. The pitch isn't "replace code review with a policy engine." It's narrower and more useful: take the subset of review comments that are actually deterministic yes/no checks against a written standard, and stop spending a human reviewer's attention on them.
What Belongs in a Policy vs. What Doesn't
The same boundary the agentic-code-review post drew for AI-enforced architecture rules applies here, for a more mechanical enforcement mechanism: a policy needs a clear, structured yes/no answer computable from the PR's actual content. "Does this Dockerfile's FROM line reference an approved base image" is a policy. "Is this a well-designed API" is not - it has no single correct answer to check against, and forcing it into Rego just produces a rigid rule that's wrong for cases nobody anticipated.
Three real standards make good OPA policies because they're already unambiguous rules, just currently enforced by nothing:
- Every PR must carry at least one label from an approved set (
bug,feature,chore,security) - currently "enforced" by a reviewer noticing an unlabeled PR. - Every
Dockerfilemust reference a base image from an approved list, notlatesttags or arbitrary public images. - Every workflow file under
.github/workflows/must declare an explicit top-levelpermissionsblock - the standard from the permissions-block post, now actually checked.
Writing Rego Policies
Rego evaluates structured input (JSON) against rules and produces a set of violations. Each of the three standards above is its own small policy file:
# policies/required_labels.rego
package pr.labels
import rego.v1
approved_labels := {"bug", "feature", "chore", "security"}
violation contains msg if {
count(input.pull_request.labels & approved_labels) == 0
msg := sprintf(
"PR has no label from the approved set %v - add one before merge.",
[approved_labels],
)
}
# policies/allowed_base_images.rego
package docker.base_images
import rego.v1
approved_images := {"node", "python", "mcr.microsoft.com/devcontainers"}
violation contains msg if {
some file in input.changed_dockerfiles
some line in file.lines
startswith(line.text, "FROM ")
image := trim_space(substring(line.text, 5, -1))
not startswith_any(image, approved_images)
msg := sprintf(
"%s:%d - base image '%s' is not in the approved list %v.",
[file.path, line.number, image, approved_images],
)
}
startswith_any(s, prefixes) if {
some prefix in prefixes
startswith(s, prefix)
}
# policies/workflow_permissions.rego
package workflows.permissions
import rego.v1
violation contains msg if {
some file in input.changed_workflows
not file.has_top_level_permissions
msg := sprintf(
"%s has no top-level 'permissions:' block - add one scoped to least privilege.",
[file.path],
)
}
Each violation rule produces its own message, with the file and line where relevant - the same discipline as a good code review comment, because that message is what a developer sees on a failed check, not a Rego stack trace.
Running OPA in a GitHub Actions Workflow
name: Policy Check
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
opa-policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install OPA
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.68.0/opa_linux_amd64_static
chmod +x opa
sudo mv opa /usr/local/bin/
- name: Build policy input
run: python scripts/build_policy_input.py > input.json
- name: Evaluate policies
run: |
opa eval --format pretty \
--data policies/ \
--input input.json \
"data.pr.labels.violation | data.docker.base_images.violation | data.workflows.permissions.violation" \
> violations.json
- name: Fail on violations
run: |
if [ "$(jq -e '. != [] and . != null' violations.json)" = "true" ]; then
echo "Policy violations found:"
jq -r '.[]' violations.json
exit 1
fi
echo "No policy violations."
build_policy_input.py is the piece that's specific to your repo - it pulls the PR's labels via the GitHub API, diffs Dockerfiles in the PR against main, and parses changed workflow YAML files into the structured shape the Rego policies expect. OPA itself never talks to GitHub directly; it only evaluates whatever structured input it's handed, which keeps the policies themselves testable in isolation without a live PR to test against.
Testing Policies Without a Live PR
Because Rego policies are pure functions over structured input, they're testable the same way any code is - with fixture data, in CI, before they ever run against a real PR:
# policies/required_labels_test.rego
package pr.labels_test
import rego.v1
import data.pr.labels
test_no_labels_violates if {
count(labels.violation) > 0 with input as {"pull_request": {"labels": []}}
}
test_approved_label_passes if {
count(labels.violation) == 0 with input as {"pull_request": {"labels": ["bug"]}}
}
opa test policies/ runs these as part of the same CI pipeline that evaluates real PRs - a policy change that breaks its own test suite fails before it ever gets a chance to incorrectly block (or incorrectly pass) someone else's PR.
Making Violations Actionable
The single biggest determinant of whether a policy-as-code rollout survives contact with a real engineering org is whether a blocked developer knows exactly what to do next. A failed check that says policy violation: PR-4471 sends someone to search Slack for who owns OPA. A failed check that says Dockerfile:3 - base image 'ubuntu:latest' is not in the approved list {node, python, mcr.microsoft.com/devcontainers} tells them the fix in the same line as the failure. Every violation rule in the policies above was written with that message as the actual deliverable - the Rego logic exists to produce it, not the other way around.
Rolling Out Without Blocking Everyone on Day One
Same non-blocking-first sequencing as the agentic code review post and the Ruleset evaluation mode from earlier posts: run the policy check as a required-in-name-only status for a few weeks, let it post results without gating the merge button, and look at the actual violation rate before flipping it to a hard block. A policy that's correct in theory but has a bug in the input-building script will otherwise block every PR in the org simultaneously on rollout day - a failure mode this soft-launch period is specifically designed to catch before it's expensive.
Closing
Policy as code doesn't replace human judgment in review - it removes the subset of review that was never judgment to begin with, just a rule a reviewer had to remember and a developer had to guess they'd violated. Rego makes that rule executable, testable, and versioned like the rest of the codebase; the GitHub Actions integration makes it run automatically on every PR instead of whenever a reviewer happens to notice. What's left for the human reviewer is what should have had their attention the whole time - the genuinely ambiguous calls a deterministic rule was never going to make correctly anyway.
Questions or corrections? Reach out.