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,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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user