feat(train): PipelineReleasePolicy — automatic release intake, disabled (R8) #2041

Merged
pipeline-bot merged 1 commit from fix/r11-propose-only into trunk 2026-08-13 19:36:17 +00:00
Owner

Decision 1 promised an AUTOMATIC intake mode where every green Build starts a release. Nothing implemented it — the enum offered the mode and no code read it. This is that code, and it is inert: no project has a policy.

Modeled field-for-field on PipelineDeliveryPolicy per the design. The reconciler watches Build and Release and creates one integrateAllGreen Release when the project's open changes are all green and none is already running. ReleaseSubmitter still owns the train, the version claim, and execution.

Two decisions the sketch left open (asked and settled before building):

  • Every open change must be green, not merely one. Releasing around a red change makes every automatic release partial and turns the skip record into noise. Reported as ChangesNotGreen — not a fault.
  • Retries bounded per change SET (maxAttempts, default 3). Unbounded, every Build event mints another attempt and burns a version; zdns/zerotea sat on a broken release leg for six days and this would have released into it hundreds of times. The budget is keyed on a hash of the sorted open change numbers, so a NEW green change forms a new set and restores it. Exhaustion reports AttemptsExhausted.

The SubjectAccessReview is the load-bearing part. serviceAccountName is an unverified string; without it, creating a policy would mint release intent attributed to ANY identity — and a release publishes signed artifacts and moves main, so the forged actor lands on the provenance. "May create a release policy" must not silently mean "may release as anyone".

7 envtest specs: disabled is inert · non-release-gated refused · a red change holds the set · all-green mints exactly one with correct attribution · a second pass while running does not stack another (coalescing) · the budget stops a failing set · a new change restores it · an ungranted actor refused. 132/132 controller specs pass, 129/129 guards.

Also in this branch: R11 (previous commit, already reviewed in #2040 — this branch was reused after that merged, so the diff here is R8 only).

The CRD is registered in config/crd/kustomization.yaml — a CRD that exists but is never applied is exactly the pruning trap crd-generated-in-sync guards.

Decision 1 promised an AUTOMATIC intake mode where every green Build starts a release. Nothing implemented it — the enum offered the mode and no code read it. **This is that code, and it is inert: no project has a policy.** Modeled field-for-field on `PipelineDeliveryPolicy` per the design. The reconciler watches Build and Release and creates one `integrateAllGreen` Release when the project's open changes are all green and none is already running. ReleaseSubmitter still owns the train, the version claim, and execution. **Two decisions the sketch left open** (asked and settled before building): - **Every open change must be green**, not merely one. Releasing around a red change makes every automatic release partial and turns the skip record into noise. Reported as `ChangesNotGreen` — not a fault. - **Retries bounded per change SET** (`maxAttempts`, default 3). Unbounded, every Build event mints another attempt and burns a version; zdns/zerotea sat on a broken release leg for six days and this would have released into it hundreds of times. The budget is keyed on a hash of the sorted open change numbers, so a NEW green change forms a new set and restores it. Exhaustion reports `AttemptsExhausted`. **The SubjectAccessReview is the load-bearing part.** `serviceAccountName` is an unverified string; without it, creating a policy would mint release intent attributed to ANY identity — and a release publishes signed artifacts and moves `main`, so the forged actor lands on the provenance. "May create a release policy" must not silently mean "may release as anyone". **7 envtest specs**: disabled is inert · non-release-gated refused · a red change holds the set · all-green mints exactly one with correct attribution · a second pass while running does **not** stack another (coalescing) · the budget stops a failing set · a new change restores it · an ungranted actor refused. **132/132 controller specs pass**, 129/129 guards. Also in this branch: **R11** (previous commit, already reviewed in #2040 — this branch was reused after that merged, so the diff here is R8 only). The CRD is registered in `config/crd/kustomization.yaml` — a CRD that exists but is never applied is exactly the pruning trap `crd-generated-in-sync` guards.
feat(train): PipelineReleasePolicy — automatic release intake, disabled (R8)
All checks were successful
pipeline/ci CI green @ e59f8953d6e4
e59f8953d6
Decision 1 promised an AUTOMATIC intake mode where every green Build starts a
release. Nothing implemented it: the enum offered the mode and no code read
it. This is that code, and it is inert until someone creates a policy.

Modeled field for field on PipelineDeliveryPolicy, per the design: immutable
projectRef and serviceAccountName, enabled defaulting false, a Ready
condition. The reconciler watches Build and Release, and creates one
integrateAllGreen Release when the project's open changes are all green and
no release is already running. ReleaseSubmitter still owns the train, the
version claim, and execution — this only declares intent.

Two decisions the design sketch left open:

EVERY open change must be green, not merely one. Releasing around a red
change would make every automatic release partial and turn the skip record
into noise nobody reads. Waiting is reported as ChangesNotGreen, which is not
a fault and not an error.

Retries are bounded per change SET (maxAttempts, default 3). A release can
fail for a reason automation cannot fix — a compile error, a broken publish
leg. Unbounded, every Build event would mint another attempt and burn a
version each time; zdns and zerotea sat on exactly such a leg for six days,
and an unbounded policy would have released into it hundreds of times. The
budget is spent against a hash of the sorted open change numbers, so a NEW
green change forms a new set and restores it — work that genuinely moved on
is never blocked by an older set. Exhaustion reports AttemptsExhausted.

It also refuses a project that is not release-gated: integrateAllGreen is a
train instruction, so on a continuous project the policy would mint releases
that integrate nothing.

The SubjectAccessReview is the load-bearing part. spec.serviceAccountName is
an unverified string, so without it, creating a policy would be enough to
mint release intent attributed to ANY identity — and a release publishes
signed artifacts and moves main, so the forged actor would sit on the
provenance too. "May create a release policy" must not silently mean "may
release as anyone".

7 envtest specs cover: disabled is inert, non-release-gated is refused, a red
change holds the set, all-green mints exactly one with the right attribution,
a second pass while it runs does NOT stack another, the budget stops a
failing set, a new change restores it, and an ungranted actor is refused.
132/132 controller specs pass. The CRD is registered in the kustomization —
a CRD that exists but is never applied is the pruning trap crd-generated-in-sync
was written for.

No project has a policy. Turning one on is a separate, deliberate act.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sean/pipelines!2041
No description provided.