Files
deployment-policies/README.md
T
jdevega 59cacfdfab
ci / verify (push) Failing after 10m57s
ci / publish (push) Skipped
feat: scaffold deployment-policies monorepo
OPA/Rego deployment gateway policies: 5 rules (valid-environment,
require-approvals, no-self-approval, block-weekends, freeze-window),
a deploy-gate combining them, JSON schemas, scenario-driven tests,
Gitea Actions CI (verify + publish to generic registry), changesets
versioning, Makefile and scripts.
2026-09-15 19:05:03 +02:00

172 lines
7.3 KiB
Markdown

# 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.v1` syntax)
- **Hosting:** Gitea (`gitea.devegamoreno.com`), repo `jdevega/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` (or `wget`)
- OPA binary installed via `make install` (pinned release, no Docker)
- `regal` on `PATH` for linting (optional, skipped if absent)
## Quick start
```sh
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
```sh
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 with `write:package`)
- `GITEA_URL` (default `https://gitea.devegamoreno.com`)
- `GITEA_OWNER` (default `jdevega`)
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`](.changeset/README.md).
## CI
`.gitea/workflows/ci.yml`:
1. `verify``npm ci`, install OPA, `scripts/check.sh`, `scripts/build.sh` (every push/PR).
2. `publish` — on `main` only, rebuilds and uploads bundles with `GITEA_TOKEN`.
Create the `GITEA_TOKEN` repository secret on Gitea with at least `write:package` scope.
## Evaluating a bundle
```sh
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:
```sh
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.