- Status: Accepted
- Date: 2026-07-13
- Deciders: thulasi.ram (architect)
Refines 0024 (which modeled the concept) and wires it to 0008 gates, 0014 secret scopes, and 0034 gate release. Closes the stored-but-not-enforced gaps left after Slice 4.
Context
Section titled “Context”[0024] made Environment a first-class entity carrying protection rules, but the
implementation left real gaps: ProtectionRules.approvers/wait_timer/secret_scope/
oidc_subject are stored and never read at runtime; deploy admission trusts a
caller-supplied approvals list (DeployRequest.approvals) rather than any
authenticated state; secret lookup is exact-scope only (no org→repo→env
inheritance despite [0014]); and env-scoped secret resolution is never wired into
the live run path (resolve_step_secrets is test-only). A manual gate, meanwhile,
is a genuine durable suspend point but records no approver identity.
The value of Environment is not any single rule. It is that one reference —
environment: prod — pulls in a governed bundle (approval + secrets + allowed-refs +
OIDC subject), defined once by an admin and reused across pipelines. That removes the
per-pipeline wiring taxation, and — because rules live in an admin-owned entity, not the
pipeline YAML — it gives separation of duties: a pipeline author requests a deploy; the
environment governs it, regardless of what the YAML says (the GHA model).
Decision
Section titled “Decision”A. Environment is the governed bundle; protection is optional
Section titled “A. Environment is the governed bundle; protection is optional”- Environment stays a first-class entity in
scarab-projects, edited only withAdministerRBAC. A pipeline references it by name (PipelineIr.environment, pipeline-level: one run targets one environment — step-level multi-env deferred). - Protection rules are optional per environment. An environment with no rules
(typical
staging) is a frictionless label: secrets are scoped to it, but no approval, no ref restriction. An environment with reviewers (prod) is gated. Same entity, opt-in governance — low-stakes targets pay nothing.
B. Approval is a gate; admission runs at the point of use
Section titled “B. Approval is a gate; admission runs at the point of use”- Approval = gate release, recorded on the run’s event log. No separate approvals
table. A new event
GateApproved { step, by }records each approver’s authenticatedPrincipal.subject(fixes the dropped identity inapprove_gate). - A
manualgate accumulates distinct approver subjects. The run staysSuspendeduntil every name in the environment’sapprovershas approved; thenGateReleasedfires and the run resumes (exactly-once, as today).min_approvals/N-of-M is a later additive threshold. - Admission moves into the engine.
allowed_refsis checked at run creation (reject a disallowed ref before any pod).approversis satisfied by accumulated gate approvals — not by a request body. The standalonePOST …/deployadmission path is retired;deploy_environmentis demoted to read-only deployment history. admits(git_ref, approvals)keeps its signature;approvalsis now sourced fromGateApprovedevents on the run, never from the caller.
C. Secrets: scoped, inherited, resolved at the point of use
Section titled “C. Secrets: scoped, inherited, resolved at the point of use”- Lookup walks the scope chain
env → repo → org, most-specific wins (implements [0014]’s inheritance; today’s store is exact-match only). A value shared across environments lives once at repo/org scope; only values that genuinely differ are stored per-environment. - Pipelines reference secrets by key, environment-agnostically (
${{ secrets.X }}). Resolution picks the value by the run’s target environment — no_staging/_prodnaming convention baked into the pipeline. - Env-scoped secrets are resolved on the live run path and injected only under an
admitted deployment (wires
resolve_step_secrets): registered with the log redactor before any step output streams, and subject to the fork-PR lockout (fork_policy). - A referenced key that resolves to nothing at every scope is a submit/deploy-time error (see D), not a silent empty.
D. Secret completeness is usage-driven, not parity-driven
Section titled “D. Secret completeness is usage-driven, not parity-driven”- The actionable check: at submit/deploy, for the target environment, resolve every key the pipeline references through the scope chain; flag any that resolve to nothing. Bounded by pipeline usage — independent of environment count, zero false positives from intentional divergence.
- Cross-environment parity (keys × environments) is advisory UI only: it shows effective status per cell — set here / inherited / unset (post-inheritance, so inherited keys never read as missing) — with an explicit “intentionally unset” marker to silence a cell. It never blocks a deploy.
E. Management API surface (Administer for writes)
Section titled “E. Management API surface (Administer for writes)”PUT /v1/environments/{project}/{name} upsert env + protection rulesGET /v1/environments/{project} list environmentsGET /v1/environments/{project}/{name} get one (rules + effective secret status)DELETE /v1/environments/{project}/{name} removeGET /v1/environments/{project}/{name}/deployments deployment history (was the deploy handler)GET /v1/projects/{project}/secrets/matrix keys × envs effective-status read model (UI)Secret CRUD stays the existing scoped endpoints ([0014]). Gate approval stays
POST /v1/runs/{id}/gates/{step}/approve (RBAC Write and membership in the
environment’s approvers); external release stays token-authed ([0034]).
Consequences
Section titled “Consequences”- Deploy approval becomes durable, auditable, and unforgeable: it is authenticated gate state on the event log, not a caller assertion. Restart-safe by construction.
- Prod credentials exist only under the gated environment and cannot be relocated by editing pipeline YAML — the separation-of-duties boundary is real.
- Pipelines become portable across environments; the target, not the pipeline, selects secret values.
- New surface:
GateApprovedevent + gate accumulation; a scope-chain resolver; the live-path secret wiring; four env-management endpoints + one read model. Retires one handler’s admission role. - Pipeline-level environment means no staging→prod promotion within one run; that requires step-level environment, deferred to a future ADR.
Alternatives considered
Section titled “Alternatives considered”- Dedicated approvals table — easier to read, but a run must still durably pause for approval, which only the gate does; a table beside the gate is a dual-write consistency hazard. Rejected in favor of gate-as-record.
- Keep caller-supplied approvals (authenticated per entry) — smallest change, but never unifies with the gate and leaves approval off the durable log.
- Flat secret store with naming conventions (
key_staging/key_prod) — pushes environment selection into pipeline string names, breaking portability. - Cross-environment parity as a blocking check — explodes at N environments and fires on intentional divergence; demoted to advisory.
- Step-level environment (GHA-style, many envs per run) — enables single-run
promotion but needs a transitive-
needsancestry walk to scope approvals per step; deferred.
Resolved
Section titled “Resolved”- Naming.
env:(step OS-env vars) andenvironment:(deploy target) collide in the DSL. GHA ships the identical collision and it is tolerated in practice; we keepenvironmentfor the deploy target. Revisit only if the collision proves confusing.