# 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/ / # one deployable policy = one bundle policy.rego # module in package rules.; 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/ / # 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.` — 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..min_approvals`, `config..block_weekends`, `freeze_windows[]`). Combining policies is done **at runtime** by the consumer: load one or more bundles, evaluate each rule under `data.rules.` 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..min_approvals` | int | require-approvals | | `data.deploygate.config..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: { : { 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///bundle.tar.gz and dist/gates///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-//bundle.tar.gz /api/packages/{owner}/generic/gate-//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//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.