setup-node with cache: npm requires a lock file; the initial run failed with 'Dependencies lock file is not found'. Committing the generated package-lock.json and updating checkout/setup-node to the latest v7 major (node24 runtime; requires act_runner >= 0.2.13).
deployment-policies
Small, composable OPA/Rego policies that act as deployment gateways. Each rule decides one narrow question about a deployment request (environment validity, approvals, weekends, freeze windows…), and gates bundle the rules you want to enforce together.
- Language: Rego (OPA 1.x,
rego.v1syntax) - Hosting: Gitea (
gitea.devegamoreno.com), repojdevega/deployment-policies - CI/CD: Gitea Actions (verify + build on every push/PR, publish on
main) - Distribution: Gitea generic package registry (rules and gates as OPA bundles)
- Versioning: actions/Changesets over npm workspaces
Repository layout
policies/
rules/
<rule>/ # one deployable policy = one bundle
policy.rego # module in package rules.<name>; exposes allow + violations
policy_test.rego # tests driven by scenarios.json
scenarios.json # input/data/expectation fixtures (loaded at data.scenarios)
schema/ # JSON Schemas for input and data (opa check --schema)
package.json # version + identity
gates/
<gate>/ # one deployable gate = bundle of the rules it depends on
gate_test.rego # integration tests over the combined rules
scenarios.json
schema/
package.json # lists rule packages as dependencies
scripts/ # install-opa, check, build, publish
.gitea/workflows/ci.yml # Gitea Actions pipeline
Makefile # convenience targets
Requirements
- Node.js ≥ 22.11 (workspaces + changesets v3)
make,curl(orwget)- OPA binary installed via
make install(pinned release, no Docker) regalonPATHfor linting (optional, skipped if absent)
Quick start
make install # install OPA v1.20.2 to tools/opa
npm ci # install changesets + oxfmt
make check # format check + regal lint + schema check + tests
make build # build rule and gate bundles into dist/
Rego conventions
Every policy module targets separate evaluation and follows the same contract:
package rules.<name>— one package per rule, no cross-file imports between rules.allow(boolean,default false) — this rule alone permits the deployment.violations(set of{"code", "message"}) — the reasons for denial.- Input is read from
input.deployment/input.approvals. - Environment configuration and freeze windows are read from
data.deploygate.*(config.<env>.min_approvals,config.<env>.block_weekends,freeze_windows[]).
Combining policies is done at runtime by the consumer: load one or more bundles, evaluate
each rule under data.rules.<name> separately, and fold the violations/allow results
together. Gates simply ship several rules in one bundle for convenience.
Input contract
| Field | Type | Used by |
|---|---|---|
input.deployment.environment |
string | all rules |
input.deployment.service |
string | (informational) |
input.deployment.version |
string | (informational) |
input.deployment.requested_by |
string | no-self-approval |
input.deployment.created_at |
RFC3339 | block-weekends, freeze-window |
input.approvals[].by |
string | no-self-approval |
input.approvals[].active |
boolean | require-approvals, no-self-approval |
input.approvals[].at |
RFC3339 | (informational) |
Data contract
| Field | Type | Read by |
|---|---|---|
data.deploygate.config.<env>.min_approvals |
int | require-approvals |
data.deploygate.config.<env>.block_weekends |
bool | block-weekends |
data.deploygate.freeze_windows[].environment |
string | freeze-window |
data.deploygate.freeze_windows[].start |
RFC3339 | freeze-window |
data.deploygate.freeze_windows[].end |
RFC3339 | freeze-window |
Config lives in the data document (not in the bundle), so rules never embed environment-specific
settings. Bundles scope their roots to rules, letting callers merge data.deploygate.*.
Available rules
| Rule | Package | Denies when |
|---|---|---|
valid-environment |
rules.valid_environment |
environment is not in data.deploygate.config |
require-approvals |
rules.require_approvals |
fewer active approvals than min_approvals |
no-self-approval |
rules.no_self_approval |
an active approver is the requestor |
block-weekends |
rules.block_weekends |
created_at is Sat/Sun and env has block_weekends |
freeze-window |
rules.freeze_window |
deploy falls inside a matching freeze window |
Available gates
| Gate | Package | Includes |
|---|---|---|
deploy-gate |
gate.deploy_gate_test |
all 5 rules |
A gate's package.json lists its rules under dependencies; changesets bumps the gate when any
of its rules change, and scripts/build.sh bundles exactly those rule modules into the gate.
Testing
Each rule carries scenarios.json with { scenarios: { <name>: { input, data, expect } } }.
opa test loads it at data.scenarios (top-level key, not filename). Tests assert both the
expected violations codes and allow. See scripts/check.sh; run with make test.
Building and publishing
make build # dist/rules/<name>/<version>/bundle.tar.gz and dist/gates/<name>/<version>/bundle.tar.gz
make publish # upload all bundles to the Gitea generic registry
publish.sh needs:
GITEA_TOKEN(API token withwrite:package)GITEA_URL(defaulthttps://gitea.devegamoreno.com)GITEA_OWNER(defaultjdevega)
Artifacts land at:
/api/packages/{owner}/generic/rule-<rule>/<version>/bundle.tar.gz
/api/packages/{owner}/generic/gate-<gate>/<version>/bundle.tar.gz
Bundles are built fresh in CI (publish job on main) and uploaded with curl.
Versioning
On a new commit, run make changeset, select the changed packages and bump types, and commit the
generated changeset. Later run make version to apply them. See
.changeset/README.md.
CI
.gitea/workflows/ci.yml:
verify—npm ci, install OPA,scripts/check.sh,scripts/build.sh(every push/PR).publish— onmainonly, rebuilds and uploads bundles withGITEA_TOKEN.
Create the GITEA_TOKEN repository secret on Gitea with at least write:package scope.
Evaluating a bundle
opa eval \
--bundle dist/rules/valid-environment/0.0.0/bundle.tar.gz \
--data deploygate.json \
--input request.json \
'data.rules.valid_environment'
Or load a gate bundle and fuse the rule results yourself:
opa eval --bundle dist/gates/deploy-gate/<v>/bundle.tar.gz --data deploygate.json \
--input request.json \
'{codes: {c | some r in [data.rules.valid_environment, data.rules.require_approvals]; some v in r.violations; c := v.code}}'
See the input/data contracts above for deploygate.json and request.json shapes.