Skip to content

Release-gate fix: split journey-ledger from goreleaser so the 0.19.6/0.20.0 cut can't be blocked#308

Merged
clkao merged 5 commits into
nextfrom
spacedock-ensign/release-gate-job-separation-fix
Jun 6, 2026
Merged

Release-gate fix: split journey-ledger from goreleaser so the 0.19.6/0.20.0 cut can't be blocked#308
clkao merged 5 commits into
nextfrom
spacedock-ensign/release-gate-job-separation-fix

Conversation

@clkao
Copy link
Copy Markdown
Collaborator

@clkao clkao commented Jun 6, 2026

Stops a release cut from failing on a Runtime-Live-E2E gate that never auto-fires on next — the failure that broke 0.19.5.

What changed

  • Split release.yml into sibling jobs: goreleaser (no needs:) and journey-ledger (needs: goreleaser)
  • Make the journey-metrics download a non-fatal skip; gate Build/Publish on found == 'true'
  • Code-gate the separation: a yaml.v3-parsed guard rejects any goreleaser → journey-ledger re-coupling in every YAML shape
  • Verify tag-body OPTION B (already shipped) holds against the separated workflow

Evidence

  • internal/release 54/54 passed (deterministic across -count=10)
  • Offline go test ./... 1196 passed; five detached adversarial audits (block-list, comment/blank-line, alias/quoted-key, map-iteration determinism) → final: no material findings

bq

clkao and others added 5 commits June 5, 2026 13:14
…CY 1)

Splits the single goreleaser job into two siblings so a never-fired
Runtime-Live-E2E run on next can no longer block the cut:

- journey-ledger (authored before goreleaser so the document-order guard
  stays green): Download / Build / Publish the cost ledger. The download
  step degrades to a non-fatal skip (exit 0, found=false) when no producer
  run exists, and the Build/Publish steps are gated on that output so a
  producer-less / empty-dir cut SKIPS them (job green) instead of running
  journey-costs over an empty dir and REDding the job (POLICY 1). Carries
  the one-way needs: goreleaser edge required so gh release upload runs
  after the Release exists.
- goreleaser: cuts the release regardless of journey-ledger.

Hardens assertReleaseWorkflowPublishesJourneyCosts to bind needs: to the
OWNING job: a goreleaser->journey-ledger edge now REDs the guard, while the
safe journey-ledger->goreleaser edge does not false-trip it. Adds the
job-level skip-consequence guard plus adversarial tests for the unsafe edge
and an ungated builder, and a shell-level exercise of the download skip
branch. Anchors the empty-body guard extractor on release-notes.txt so it
selects the right if-condition now that the download step also carries one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard

M1 (route-back from validation cycle 1): parseNeeds only read scalar/flow
needs:, so a future goreleaser re-coupling written as a YAML block-list
(needs: then - journey-ledger) reported goreleaser needs=[] and evaded
assertGoreleaserDoesNotNeedJourneyLedger — the guard would greenlight a real
re-coupling edge. parseWorkflowJobs now consumes the block-list entries that
follow a bare needs: line, and TestReleaseWorkflowGuardRejectsGoreleaserNeeds-
JourneyLedger injects the edge in all three forms (scalar/flow/block-list);
the safe reverse edge on journey-ledger stays tolerated.

P1: the download skip-branch now tolerates a gh error / missing gh as
found=false (|| true on the run_id query) instead of aborting under set -e;
TestReleaseDownloadSkipBranchToleratesGhError exercises both the gh-error and
missing-gh paths.

go test ./internal/release/ 34/34; full offline go test ./... 1176 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ard shape evasions

M1a/M1b (cycle-2 re-audit): the hand-rolled needs-parser still let real
goreleaser->journey-ledger re-couplings stay GREEN — an inline trailing
`# comment` evaded all three shapes (the file's own safe reverse edge is
comment-documented), and a blank line between block-list entries dropped the
later entries. Patching more shapes is whack-a-mole; replace the scanner with a
gopkg.in/yaml.v3 parse (already a direct dep) that reads each job's needs as a
normalized list — comments, blank lines, scalar/flow/block, quoting, and
anchors handled for free. parseJobNeeds replaces parseNeeds entirely;
parseWorkflowJobs now sources needs from it and keeps line-based step
attribution.

assertGoreleaserDoesNotNeedJourneyLedger keeps its contract: REJECT
goreleaser->journey-ledger, TOLERATE the reverse. The rejection test now covers
scalar/flow/block plus all three inline-comment variants plus a blank-line-split
block list (all RED); a new TestReleaseWorkflowGuardToleratesSafeReverseEdge-
InEveryShape proves the safe reverse edge stays GREEN in every shape.
Mutation-confirm: neutering parseJobNeeds reds all 7 rejection forms while
tolerance stays green.

No regression: skip-gating, AC-1 job-level, the AC-2 release-notes.txt anchor,
and the P1 || true gh-error tolerance are untouched. go test ./internal/release/
45/45; full offline go test ./... 1187 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ity shapes can't evade the separation guard

M1/M2 (cycle-3 re-audit): the cycle-3 fix closed the needs-VALUE shapes, but
JOB IDENTITY was still a hand-rolled line-walk and two shapes beat it — an
`&anchor`/`*alias` on needs reached parseJobNeeds as an unhandled node so the
edge vanished, and a quoted job key ("goreleaser":) missed the literal
needsByJob lookup. Patching either is more whack-a-mole.

Finish the conversion: parse the entire job graph — names, alias-resolved needs,
and per-job steps — from ONE yaml.v3 pass and attribute goreleaser/builder from
that structure. The last line-walk for job identity (parseJobNeeds, jobsStart,
jobHeaderAt) is gone. yaml.v3 resolves aliases before decode and normalizes
quoted keys natively, so M1, M2, and any future job-identity shape are handled
by the parser, not a growing line scan. needs decodes through a needsList type
that accepts scalar or sequence.

Adversarial coverage: TestReleaseWorkflowGuardRejectsGoreleaserNeedsJourneyLedger-
ViaJobIdentityShapes REDs both the anchor/alias and quoted-key re-couplings; the
tolerance twin keeps the safe reverse edge green in those shapes; a new
TestReleaseWorkflowJobGraphMatchesGitHubActions pins the real-file parse to GHA
(goreleaser needs empty, journey-ledger needs only goreleaser). Mutation-confirm:
dropping sequence-edge decode reds the alias form; dropping scalar-edge decode
reds the quoted-key form.

No regression: the 7-form value table + its tolerance twin, skip-gating, AC-1
job-level, the AC-2 release-notes.txt anchor, and P1 gh-error tolerance all green.
go test ./internal/release/ 52/52; full offline go test ./... 1194 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ple action carriers

Cycle-5 (4th audit): assertGoreleaserDoesNotNeedJourneyLedger overwrote a single
goreleaserJob/ledgerJob string on each map-iteration match and checked needs on
only the LAST-iterated carrier. Go map order is random, so a workflow with 2+
jobs running the goreleaser action — one declaring needs: journey-ledger —
greenlit on the runs that happened to iterate the safe carrier last (~flaky),
leaving the guard latently unsound on any multi-carrier shape.

Collect ALL goreleaser-action carriers AND ALL ledger-builder carriers in one
pass, then reject if ANY goreleaser carrier needs ANY ledger carrier. No
last-wins overwrite, no map-order dependence — deterministic. A job that is
itself both carriers cannot need itself, so the self-pair is skipped (preserves
the old single-job-layout semantics).

Adversarial coverage: TestReleaseWorkflowGuardRejectsMultiCarrierGoreleaserNeeds-
JourneyLedger inserts a second goreleaser-action job that needs journey-ledger
and asserts the guard REDs on every one of 200 runs (a last-wins regression
greenlights within that loop); TestReleaseWorkflowGuardToleratesMultiCarrier-
SafeShape keeps two carriers that need nothing GREEN over 200 runs.
Mutation-confirm: reverting to a last-wins single-carrier check reds the
rejection test.

No regression: the job-identity rejections + tolerance twins + parse-matches-GHA,
the 7-form value table, skip-gating, AC-1, AC-2 release-notes.txt anchor, and P1
all green; release.yml untouched this cycle. go test ./internal/release/ 54/54
(deterministic across -count=10); full offline go test ./... 1196 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clkao added a commit that referenced this pull request Jun 6, 2026
@clkao clkao temporarily deployed to CI-E2E-OPUS June 6, 2026 02:27 — with GitHub Actions Inactive
@clkao clkao temporarily deployed to CI-E2E-CODEX June 6, 2026 02:27 — with GitHub Actions Inactive
@clkao clkao merged commit 4964a31 into next Jun 6, 2026
5 checks passed
clkao added a commit that referenced this pull request Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant