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.
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
This repository uses [Changesets](https://github.com/changesets/changesets) to version the OPA
|
||||||
|
policy packages. Every `policies/rules/*` and `policies/gates/*` package is an independent npm
|
||||||
|
workspace; changesets records which packages changed so versions can be bumped consistently.
|
||||||
|
|
||||||
|
## Adding a changeset
|
||||||
|
|
||||||
|
Run from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make changeset
|
||||||
|
```
|
||||||
|
|
||||||
|
It opens an interactive prompt: select the packages you changed, the bump type (`patch` for
|
||||||
|
fixes/behaviour tweaks, `minor` for new rules, `major` for breaking input/data contract changes)
|
||||||
|
and a summary. This writes a file under `.changeset/`.
|
||||||
|
|
||||||
|
Commit the changeset file together with your policy code changes.
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
When changesets are present on `main`, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make version
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies all changesets, rewrites the `version` field of affected `package.json`s and
|
||||||
|
regenerates `CHANGELOG.md` files (formatted with `oxfmt`). Commit the resulting bump.
|
||||||
|
|
||||||
|
Bundles are built from `package.json#version`, so the registry upload uses the new version.
|
||||||
|
|
||||||
|
## Gitea Actions
|
||||||
|
|
||||||
|
CI verifies (format, lint, schema check, tests) and builds bundles on every push/PR. On `main`
|
||||||
|
the `publish` job uploads the bundles to the Gitea generic package registry using the
|
||||||
|
`GITEA_TOKEN` repository secret (requires `write:package` scope).
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "restricted",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": [],
|
||||||
|
"format": "oxfmt",
|
||||||
|
"privatePackages": {
|
||||||
|
"version": true,
|
||||||
|
"tag": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install npm deps
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install OPA
|
||||||
|
run: ./scripts/install-opa.sh
|
||||||
|
|
||||||
|
- name: Check, lint and test
|
||||||
|
run: ./scripts/check.sh
|
||||||
|
|
||||||
|
- name: Build bundles
|
||||||
|
run: ./scripts/build.sh
|
||||||
|
|
||||||
|
publish:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
needs: verify
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install npm deps
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install OPA
|
||||||
|
run: ./scripts/install-opa.sh
|
||||||
|
|
||||||
|
- name: Build bundles
|
||||||
|
run: ./scripts/build.sh
|
||||||
|
|
||||||
|
- name: Publish bundles to Gitea package registry
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://gitea.devegamoreno.com
|
||||||
|
GITEA_OWNER: jdevega
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: ./scripts/publish.sh
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
tools/opa
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
SHELL := /bin/bash
|
||||||
|
SCRIPT_DIR := scripts
|
||||||
|
OPA := tools/opa
|
||||||
|
|
||||||
|
.PHONY: help install check fmt lint test build publish changeset version
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " install Install pinned OPA binary into tools/"
|
||||||
|
@echo " check Rego fmt + regal lint (if present) + schema-check + tests"
|
||||||
|
@echo " fmt Format all .rego files with opa fmt"
|
||||||
|
@echo " lint Run regal lint (requires regal on PATH)"
|
||||||
|
@echo " test Run opa test for every rule and gate"
|
||||||
|
@echo " build Build OPA bundles into dist/"
|
||||||
|
@echo " publish Publish bundles to Gitea generic registry (needs GITEA_TOKEN)"
|
||||||
|
@echo " changeset Create a changeset for a new version"
|
||||||
|
@echo " version Apply changesets to bump package versions"
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(SCRIPT_DIR)/install-opa.sh
|
||||||
|
|
||||||
|
check:
|
||||||
|
$(SCRIPT_DIR)/check.sh
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
$(OPA) fmt -w policies
|
||||||
|
|
||||||
|
lint:
|
||||||
|
[ -z "$$(command -v regal)" ] || regal lint policies --format github
|
||||||
|
|
||||||
|
test:
|
||||||
|
@for d in policies/rules/*/; do \
|
||||||
|
name="$$(basename "$$d")"; \
|
||||||
|
echo "== test $$name"; \
|
||||||
|
$(OPA) test "$$d/policy.rego" "$$d/policy_test.rego" "$$d/scenarios.json" || exit 1; \
|
||||||
|
done
|
||||||
|
@rules=""; \
|
||||||
|
for d in policies/rules/*/; do rules="$$rules $$d/policy.rego"; done; \
|
||||||
|
echo "== test deploy-gate"; \
|
||||||
|
$(OPA) test policies/gates/deploy-gate/gate_test.rego policies/gates/deploy-gate/scenarios.json $$rules
|
||||||
|
|
||||||
|
build:
|
||||||
|
$(SCRIPT_DIR)/build.sh
|
||||||
|
|
||||||
|
publish:
|
||||||
|
$(SCRIPT_DIR)/publish.sh
|
||||||
|
|
||||||
|
changeset:
|
||||||
|
npx changeset
|
||||||
|
|
||||||
|
version:
|
||||||
|
npx changeset version
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Tech Stack
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
| Layer | Technology | Version | Purpose |
|
||||||
|
| --------------- | ---------- | ------- | ------- |
|
||||||
|
| Policy language | Rego | OPA 1.x (`rego.v1`) | Authoring deployment-gateway policy rules and gates |
|
||||||
|
| Policy engine | OPA | `v1.20.2` (pinned) | Evaluating, testing, type-checking and bundling the policies |
|
||||||
|
| Policy linter | regal | `v0.42.0` | Static linting of Rego best practices (optional in CI) |
|
||||||
|
| Bundles | OPA bundle format | — | Distributable `.tar.gz` bundles (rule modules at `data.rules.*`) |
|
||||||
|
| Schemas | JSON Schema (draft 2020-12) | — | Static validation of `input` and `data` via `opa check --schema` |
|
||||||
|
| Versioning | Changesets | `@changesets/cli@3.0.3` | Tracking per-policy version bumps across npm workspaces |
|
||||||
|
| Changelog formatting | oxfmt | `^0.68.0` | Formatting generated changelogs/changesets via `format: "oxfmt"` |
|
||||||
|
| Package management | npm workspaces | npm 11 / Node 24 | Monorepo layout (`policies/rules/*`, `policies/gates/*`) |
|
||||||
|
| CI/CD | Gitea Actions | Gitea 1.27.0 | Verify, test, build and publish on `main` |
|
||||||
|
| Package registry| Gitea generic registry | — | Storing published rule/gate bundles |
|
||||||
|
| Build tooling | GNU make + bash | — | `Makefile` targets wrapping the scripts |
|
||||||
|
| Hosting | Gitea self-hosted | `1.27.0` | Source control, CI runners, package registry (gitea.devegamoreno.com) |
|
||||||
|
|
||||||
|
## Justification
|
||||||
|
|
||||||
|
- **OPA + Rego**: standard, policy-as-code engine for authorization/gating; bundles are the
|
||||||
|
portable distribution unit; binary CLI avoids container runtime requirements.
|
||||||
|
- **Pinned OPA `v1.20.2`**: reproducible engine version locally and in CI via `scripts/install-opa.sh`
|
||||||
|
(no Docker), so tests and `opa build` see identical behaviour everywhere.
|
||||||
|
- **`rego.v1`**: canonical OPA 1.x syntax (import of `rego.v1`), future-proof and unambiguous.
|
||||||
|
- **Per-rule JSON Schemas**: `opa check --strict --schema` catches input/data type mistakes at
|
||||||
|
authoring time instead of runtime.
|
||||||
|
- **Separate evaluation / small rules**: each rule is an independent package+bundle with a small,
|
||||||
|
testable contract; consumers combine the results at runtime, enabling reuse across gates.
|
||||||
|
- **Changesets over npm workspaces**: each rule/gate is an npm package solely for versioning;
|
||||||
|
changesets links repository-level changes to published registry versions. `oxfmt` is used only
|
||||||
|
to format changelog markdown (there is no `prettier-plugin-rego` on npm), while Rego formatting
|
||||||
|
is enforced by `opa fmt` in CI.
|
||||||
|
- **Gitea generic registry**: stores arbitrary files (`.tar.gz` bundles) under
|
||||||
|
`/api/packages/{owner}/generic/{package}/{version}/{file}` without adding an external registry
|
||||||
|
dependency.
|
||||||
|
- **npm workspaces** (over pnpm/yarn): zero extra tooling; changesets v3 requires Node ≥ 22.11 and
|
||||||
|
npm ≥ 10.9, both met.
|
||||||
|
|
||||||
|
## Tools not used (and why)
|
||||||
|
|
||||||
|
- **Docker/containers**: excluded by design; OPA ships a static binary, so containers add
|
||||||
|
unnecessary runtime complexity for tests, checks and bundle builds.
|
||||||
|
- **prettier-plugin-rego**: does not exist on npm (verified 404); Rego formatting is
|
||||||
|
instead owned by `opa fmt`.
|
||||||
|
- **pnpm/yarn**: changesets supports them, but npm workspaces suffice since these packages are
|
||||||
|
private versioning shims, not published JS packages.
|
||||||
|
|
||||||
|
## Integration points
|
||||||
|
|
||||||
|
- **Gitea Actions** reads `secrets.GITEA_TOKEN` (scope `write:package`) and
|
||||||
|
`GITEA_OWNER`/`GITEA_URL`; publish job runs only on `main`.
|
||||||
|
- **`scripts/publish.sh`** uploads via `curl` to the generic registry; package names are
|
||||||
|
prefixed `rule-*` and `gate-*`.
|
||||||
|
- **Consumer contract**: bundles scope roots to `rules` so operators can supply
|
||||||
|
`data.deploygate.*` configuration separately and still have static schema checks apply.
|
||||||
|
|
||||||
|
## Environment (local dev machine)
|
||||||
|
|
||||||
|
- macOS (darwin/arm64), zsh
|
||||||
|
- Node.js `v24.18.0`, npm `11.16.0`
|
||||||
|
- git `2.50.1`, GNU make
|
||||||
|
- `tools/opa` (OPA `v1.20.2` binary committed to the repo working tree via install script, not git)
|
||||||
|
- regal `v0.42.0` optional on `PATH`
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "deployment-policies",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Small composable OPA/Rego deployment gateway policies, tested, linted and published to the Gitea generic package registry.",
|
||||||
|
"workspaces": [
|
||||||
|
"policies/rules/*",
|
||||||
|
"policies/gates/*"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^3.0.3",
|
||||||
|
"oxfmt": "^0.68.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package gate.deploy_gate_test
|
||||||
|
|
||||||
|
import data.rules.block_weekends as block_weekends
|
||||||
|
import data.rules.freeze_window as freeze_window
|
||||||
|
import data.rules.no_self_approval as no_self_approval
|
||||||
|
import data.rules.require_approvals as require_approvals
|
||||||
|
import data.rules.valid_environment as valid_environment
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
violations_for[scenario_name] contains code if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
some v in valid_environment.violations with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
code := v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
violations_for[scenario_name] contains code if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
some v in require_approvals.violations with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
code := v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
violations_for[scenario_name] contains code if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
some v in no_self_approval.violations with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
code := v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
violations_for[scenario_name] contains code if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
some v in block_weekends.violations with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
code := v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
violations_for[scenario_name] contains code if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
some v in freeze_window.violations with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
code := v.code
|
||||||
|
}
|
||||||
|
|
||||||
|
test_scenario[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
{code | code := violations_for[scenario_name][_]} == expected
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "deploy-gate",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Combined deployment gateway: validates the environment, approval counts, self-approval, weekends and freeze windows.",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"valid-environment": "0.0.0",
|
||||||
|
"require-approvals": "0.0.0",
|
||||||
|
"no-self-approval": "0.0.0",
|
||||||
|
"block-weekends": "0.0.0",
|
||||||
|
"freeze-window": "0.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"all_pass": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2, "block_weekends": true}
|
||||||
|
},
|
||||||
|
"freeze_windows": [
|
||||||
|
{"environment": "production", "start": "2026-12-20T00:00:00Z", "end": "2026-12-31T23:59:59Z"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weekend_plus_no_approvals": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-12T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-12T09:00:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2, "block_weekends": true}
|
||||||
|
},
|
||||||
|
"freeze_windows": []
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["weekend_deploy", "insufficient_approvals"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"freeze_window_blocks": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-12-24T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-12-24T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-12-24T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2, "block_weekends": false}
|
||||||
|
},
|
||||||
|
"freeze_windows": [
|
||||||
|
{"environment": "production", "start": "2026-12-20T00:00:00Z", "end": "2026-12-31T23:59:59Z"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["deploy_in_freeze"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"self_approval": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "alice", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2, "block_weekends": true}
|
||||||
|
},
|
||||||
|
"freeze_windows": []
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["self_approval"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"freeze_windows": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment", "start", "end"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"start": {"type": "string", "format": "date-time"},
|
||||||
|
"end": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"gate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment", "service", "requested_by", "created_at"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"service": {"type": "string"},
|
||||||
|
"requested_by": {"type": "string"},
|
||||||
|
"created_at": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["by", "active"],
|
||||||
|
"properties": {
|
||||||
|
"by": {"type": "string"},
|
||||||
|
"active": {"type": "boolean"},
|
||||||
|
"at": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "block-weekends",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Denies deployments that fall on a weekend when the environment has block_weekends enabled.",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package rules.block_weekends
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow if {
|
||||||
|
count(violations) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "weekend_deploy", "message": sprintf("deployments blocked on %s for environment %q", [weekday, input.deployment.environment])} if {
|
||||||
|
input.deployment
|
||||||
|
weekday := time.weekday(time.parse_rfc3339_ns(input.deployment.created_at))
|
||||||
|
weekday in {"Saturday", "Sunday"}
|
||||||
|
data.deploygate.config[input.deployment.environment].block_weekends
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package rules.block_weekends_test
|
||||||
|
|
||||||
|
import data.rules.block_weekends as rule
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
test_scenarios[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
actual := {code | code := rule.violations[_].code} with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
actual == expected
|
||||||
|
rule.allow == scenario.expect.allow with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"weekday_ok": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"block_weekends": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"saturday_blocked": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-12T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"block_weekends": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["weekend_deploy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sunday_blocked": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-13T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"block_weekends": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["weekend_deploy"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weekend_not_blocked_when_disabled": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "development",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-12T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"development": {"block_weekends": false}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weekend_ignored_when_config_missing": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-12T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"block_weekends": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment", "created_at"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"created_at": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "freeze-window",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Denies deployments that fall inside a scheduled freeze window for the environment.",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package rules.freeze_window
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow if {
|
||||||
|
count(violations) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "deploy_in_freeze", "message": sprintf("deployment within freeze window starting %s ending %s", [window.start, window.end])} if {
|
||||||
|
some window in data.deploygate.freeze_windows
|
||||||
|
window.environment == input.deployment.environment
|
||||||
|
created := time.parse_rfc3339_ns(input.deployment.created_at)
|
||||||
|
created >= time.parse_rfc3339_ns(window.start)
|
||||||
|
created <= time.parse_rfc3339_ns(window.end)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package rules.freeze_window_test
|
||||||
|
|
||||||
|
import data.rules.freeze_window as rule
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
test_scenarios[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
actual := {code | code := rule.violations[_].code} with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
actual == expected
|
||||||
|
rule.allow == scenario.expect.allow with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"outside_freeze": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"freeze_windows": [
|
||||||
|
{"environment": "production", "start": "2026-12-20T00:00:00Z", "end": "2026-12-31T23:59:59Z"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inside_freeze": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-12-24T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"freeze_windows": [
|
||||||
|
{"environment": "production", "start": "2026-12-20T00:00:00Z", "end": "2026-12-31T23:59:59Z"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["deploy_in_freeze"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"window_for_other_environment": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "development",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-12-24T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"freeze_windows": [
|
||||||
|
{"environment": "production", "start": "2026-12-20T00:00:00Z", "end": "2026-12-31T23:59:59Z"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"freeze_windows": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment", "start", "end"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"start": {"type": "string", "format": "date-time"},
|
||||||
|
"end": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment", "created_at"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"created_at": {"type": "string", "format": "date-time"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "no-self-approval",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Denies deployments where the requestor approved their own deployment.",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package rules.no_self_approval
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow if {
|
||||||
|
count(violations) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "self_approval", "message": sprintf("approval by %q is the deployment requestor", [input.deployment.requested_by])} if {
|
||||||
|
some approval in input.approvals
|
||||||
|
approval.active
|
||||||
|
approval.by == input.deployment.requested_by
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "missing_requestor", "message": "deployment has no requestor"} if {
|
||||||
|
input.deployment
|
||||||
|
not input.deployment.requested_by
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package rules.no_self_approval_test
|
||||||
|
|
||||||
|
import data.rules.no_self_approval as rule
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
test_scenarios[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
actual := {code | code := rule.violations[_].code} with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
actual == expected
|
||||||
|
rule.allow == scenario.expect.allow with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"others_approve": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"self_approval": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "alice", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["self_approval"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"revoked_self_approval_ignored": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "alice", "at": "2026-09-15T09:00:00Z", "active": false},
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["requested_by"],
|
||||||
|
"properties": {
|
||||||
|
"requested_by": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["active", "by"],
|
||||||
|
"properties": {
|
||||||
|
"active": {"type": "boolean"},
|
||||||
|
"by": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "require-approvals",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Denies deployments that do not have a sufficient number of active approvals.",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package rules.require_approvals
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow if {
|
||||||
|
count(violations) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "insufficient_approvals", "message": sprintf("need at least %d approval(s), got %d", [min_approvals, count(active_approvals)])} if {
|
||||||
|
cfg := data.deploygate.config[input.deployment.environment]
|
||||||
|
cfg.min_approvals > 0
|
||||||
|
count(active_approvals) < cfg.min_approvals
|
||||||
|
}
|
||||||
|
|
||||||
|
min_approvals := data.deploygate.config[input.deployment.environment].min_approvals if {
|
||||||
|
input.deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
active_approvals contains approval if {
|
||||||
|
some approval in input.approvals
|
||||||
|
approval.active
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "not_configured", "message": sprintf("no min_approvals configured for environment %q", [input.deployment.environment])} if {
|
||||||
|
input.deployment
|
||||||
|
not data.deploygate.config[input.deployment.environment].min_approvals
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package rules.require_approvals_test
|
||||||
|
|
||||||
|
import data.rules.require_approvals as rule
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
test_scenarios[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
actual := {code | code := rule.violations[_].code} with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
actual == expected
|
||||||
|
rule.allow == scenario.expect.allow with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"enough_approvals": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-09-15T09:30:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"insufficient_approvals": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["insufficient_approvals"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"revoked_approval_not_counted": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true},
|
||||||
|
{"by": "carol", "at": "2026-09-15T09:30:00Z", "active": false}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["insufficient_approvals"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"no_approval_required": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "development",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": []
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"development": {"min_approvals": 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"min_approvals": {"type": "integer", "minimum": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["active"],
|
||||||
|
"properties": {
|
||||||
|
"active": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "valid-environment",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Denies deployments to environments that are not configured in data.deploygate.config.",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# METADATA
|
||||||
|
# schemas:
|
||||||
|
# - input: schema["input"]
|
||||||
|
# - data: schema["data"]
|
||||||
|
|
||||||
|
package rules.valid_environment
|
||||||
|
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
default allow := false
|
||||||
|
|
||||||
|
allow if {
|
||||||
|
count(violations) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
violations contains {"code": "unknown_environment", "message": sprintf("environment %q is not configured", [input.deployment.environment])} if {
|
||||||
|
input.deployment
|
||||||
|
not data.deploygate.config[input.deployment.environment]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package rules.valid_environment_test
|
||||||
|
|
||||||
|
import data.rules.valid_environment as rule
|
||||||
|
import rego.v1
|
||||||
|
|
||||||
|
test_scenarios[scenario_name] if {
|
||||||
|
some scenario_name, scenario in data.scenarios
|
||||||
|
actual := {code | code := rule.violations[_].code} with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
expected := {code | code := scenario.expect.violations[_]}
|
||||||
|
actual == expected
|
||||||
|
rule.allow == scenario.expect.allow with input as scenario.input with data.deploygate as scenario.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"scenarios": {
|
||||||
|
"valid_production": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"production": {"min_approvals": 2, "block_weekends": true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": true,
|
||||||
|
"violations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unknown_environment": {
|
||||||
|
"input": {
|
||||||
|
"deployment": {
|
||||||
|
"environment": "production",
|
||||||
|
"service": "payments-api",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"requested_by": "alice",
|
||||||
|
"created_at": "2026-09-15T10:00:00Z"
|
||||||
|
},
|
||||||
|
"approvals": [
|
||||||
|
{"by": "bob", "at": "2026-09-15T09:00:00Z", "active": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"config": {
|
||||||
|
"staging": {"min_approvals": 1}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expect": {
|
||||||
|
"allow": false,
|
||||||
|
"violations": ["unknown_environment"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deploygate": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["config"],
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"deployment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["environment"],
|
||||||
|
"properties": {
|
||||||
|
"environment": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds deployable OPA bundles for every rule and gate.
|
||||||
|
#
|
||||||
|
# Composition model: SEPARATE EVALUATION.
|
||||||
|
# - Each rule is built as its own bundle where the module lives at data.rules.<name>.
|
||||||
|
# - Each gate bundles the policy.rego of all its dependency rules; consumers evaluate
|
||||||
|
# data.rules.<name> per rule and combine results at runtime.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
||||||
|
|
||||||
|
rm -rf "$ARTIFACTS_DIR"
|
||||||
|
STAGE_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGE_DIR"' EXIT
|
||||||
|
|
||||||
|
MANIFEST='{"revision":"","roots":["rules"],"rego_version":1}'
|
||||||
|
|
||||||
|
write_manifest() {
|
||||||
|
printf '%s' "$MANIFEST" > "$1/.manifest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- rules ---
|
||||||
|
for d in "$POLICIES_DIR"/rules/*/; do
|
||||||
|
name="$(repo_name "$d")"
|
||||||
|
version="$(repo_version "$d")"
|
||||||
|
out="$ARTIFACTS_DIR/rules/$name/$version"
|
||||||
|
mkdir -p "$out"
|
||||||
|
echo "==> build rule $name@$version"
|
||||||
|
mkdir -p "$STAGE_DIR/rule/$name"
|
||||||
|
cp "$d/policy.rego" "$STAGE_DIR/rule/$name/policy.rego"
|
||||||
|
write_manifest "$STAGE_DIR/rule/$name"
|
||||||
|
(cd "$STAGE_DIR/rule/$name" && "$OPA" build -b . -o "$out/bundle.tar.gz")
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- gates ---
|
||||||
|
for g in "$POLICIES_DIR"/gates/*/; do
|
||||||
|
name="$(repo_name "$g")"
|
||||||
|
version="$(repo_version "$g")"
|
||||||
|
out="$ARTIFACTS_DIR/gates/$name/$version"
|
||||||
|
mkdir -p "$out"
|
||||||
|
echo "==> build gate $name@$version"
|
||||||
|
stage="$STAGE_DIR/gate/$name"
|
||||||
|
for dep in $(node -p "Object.keys(require('$g/package.json').dependencies ?? {}).join(' ')"); do
|
||||||
|
for r in "$POLICIES_DIR"/rules/*/; do
|
||||||
|
rname="$(repo_name "$r")"
|
||||||
|
if [ "$rname" = "$dep" ]; then
|
||||||
|
mkdir -p "$stage/$rname"
|
||||||
|
cp "$r/policy.rego" "$stage/$rname/policy.rego"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
[ -d "$stage" ] || { echo "ERROR: gate $name has no buildable dependencies" >&2; exit 1; }
|
||||||
|
write_manifest "$stage"
|
||||||
|
(cd "$stage" && "$OPA" build -b . -o "$out/bundle.tar.gz")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Artifacts written under $ARTIFACTS_DIR"
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lints, schema-checks and tests every rule and gate.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rego formatting.
|
||||||
|
echo "==> opa fmt --fail"
|
||||||
|
"$OPA" fmt --fail --diff "$POLICIES_DIR" || fail "rego files are not formatted (run 'make fmt' or 'opa fmt -w')"
|
||||||
|
|
||||||
|
# Lint with regal if available.
|
||||||
|
if command -v regal >/dev/null 2>&1; then
|
||||||
|
echo "==> regal lint"
|
||||||
|
regal lint "$POLICIES_DIR" --format github || fail "regal lint found issues"
|
||||||
|
else
|
||||||
|
echo "==> regal not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RULES_ARG=""
|
||||||
|
rule_dirs=( "$POLICIES_DIR"/rules/*/ )
|
||||||
|
[ "${#rule_dirs[@]}" -gt 0 ] && [ -d "${rule_dirs[0]}" ] || fail "no rules found"
|
||||||
|
|
||||||
|
# Schema-check + unit tests per rule (runs are isolated so each rule's scenarios.json loads at data.scenarios).
|
||||||
|
for d in "${rule_dirs[@]}"; do
|
||||||
|
name="$(basename "$d")"
|
||||||
|
echo "==> check ${name}"
|
||||||
|
"$OPA" check --strict --schema "$d/schema" "$d/policy.rego" "$d/policy_test.rego" || fail "check ${name}"
|
||||||
|
echo "==> test ${name}"
|
||||||
|
"$OPA" test "$d/policy.rego" "$d/policy_test.rego" "$d/scenarios.json" || fail "test ${name}"
|
||||||
|
RULES_ARG="$RULES_ARG $d/policy.rego"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Gates: check + tests with all rule policy.rego files loaded so data.rules.* resolves.
|
||||||
|
gate_dirs=( "$POLICIES_DIR"/gates/*/ )
|
||||||
|
for g in "${gate_dirs[@]}"; do
|
||||||
|
[ -d "$g" ] || continue
|
||||||
|
name="$(basename "$g")"
|
||||||
|
echo "==> check ${name}"
|
||||||
|
"$OPA" check --strict --schema "$g/schema" "$g/gate_test.rego" || fail "check ${name}"
|
||||||
|
echo "==> test ${name}"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"$OPA" test "$g/gate_test.rego" "$g/scenarios.json" $RULES_ARG || fail "test ${name}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All checks passed."
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared helpers for policy management scripts.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
POLICIES_DIR="$ROOT_DIR/policies"
|
||||||
|
TOOLS_DIR="$ROOT_DIR/tools"
|
||||||
|
OPA="$TOOLS_DIR/opa"
|
||||||
|
ARTIFACTS_DIR="$ROOT_DIR/dist"
|
||||||
|
|
||||||
|
repo_name() {
|
||||||
|
node -p "require('$1/package.json').name"
|
||||||
|
}
|
||||||
|
|
||||||
|
repo_version() {
|
||||||
|
node -p "require('$1/package.json').version"
|
||||||
|
}
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Installs a pinned OPA release binary into tools/opa.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
TOOLS_DIR="$ROOT_DIR/tools"
|
||||||
|
OPA_VERSION="${OPA_VERSION:-1.20.2}"
|
||||||
|
|
||||||
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
arm64) ARCH="arm64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
URL="https://openpolicyagent.org/downloads/v${OPA_VERSION}/opa_${OS}_${ARCH}_static"
|
||||||
|
|
||||||
|
mkdir -p "$TOOLS_DIR"
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$URL" -o "$TOOLS_DIR/opa"
|
||||||
|
else
|
||||||
|
wget -qO "$TOOLS_DIR/opa" "$URL"
|
||||||
|
fi
|
||||||
|
chmod +x "$TOOLS_DIR/opa"
|
||||||
|
|
||||||
|
echo "Installed OPA v${OPA_VERSION} (${OS}/${ARCH}) to $TOOLS_DIR/opa"
|
||||||
|
"$TOOLS_DIR/opa" version | head -1
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Publishes every built rule/gate bundle to the Gitea generic package registry.
|
||||||
|
#
|
||||||
|
# Requires:
|
||||||
|
# - GITEA_TOKEN API token with write:package scope
|
||||||
|
# - GITEA_URL e.g. https://gitea.devegamoreno.com (default: env or https://gitea.devegamoreno.com)
|
||||||
|
# - GITEA_OWNER repo/package owner (default: git remote owner or jdevega)
|
||||||
|
#
|
||||||
|
# Registry path: /api/packages/{owner}/generic/{package}/{version}/{file}
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
||||||
|
|
||||||
|
GITEA_URL="${GITEA_URL:-https://gitea.devegamoreno.com}"
|
||||||
|
GITEA_OWNER="${GITEA_OWNER:-jdevega}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
[ -n "$GITEA_TOKEN" ] || { echo "ERROR: GITEA_TOKEN is not set" >&2; exit 1; }
|
||||||
|
|
||||||
|
publish_file() {
|
||||||
|
local pkg="$1" version="$2" file="$3"
|
||||||
|
local url="$GITEA_URL/api/packages/$GITEA_OWNER/generic/$pkg/$version/$(basename "$file")"
|
||||||
|
echo "==> publish $pkg@$version $(basename "$file")"
|
||||||
|
curl -fsSL \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--upload-file "$file" \
|
||||||
|
"$url"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -d "$ARTIFACTS_DIR" ]; then
|
||||||
|
echo "No artifacts found. Run 'make build' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for bundle in "$ARTIFACTS_DIR"/rules/*/*/bundle.tar.gz; do
|
||||||
|
path="${bundle#"$ARTIFACTS_DIR/rules/"}"
|
||||||
|
name="${path%%/*}"
|
||||||
|
version="$(basename "$(dirname "$bundle")")"
|
||||||
|
publish_file "rule-$name" "$version" "$bundle"
|
||||||
|
done
|
||||||
|
|
||||||
|
for bundle in "$ARTIFACTS_DIR"/gates/*/*/bundle.tar.gz; do
|
||||||
|
path="${bundle#"$ARTIFACTS_DIR/gates/"}"
|
||||||
|
name="${path%%/*}"
|
||||||
|
version="$(basename "$(dirname "$bundle")")"
|
||||||
|
publish_file "gate-$name" "$version" "$bundle"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Publish complete."
|
||||||
Reference in New Issue
Block a user