feat: idempotent cut pipeline — pre-check existing release + manifest state (closes #128) #131
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!131
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/v0.10.x-idempotency-pre-checks"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #128
Surfaced from the v0.10.0 cut artifact (Surveyor df03 + 4a44 reconciliation): task 13307 substantively completed the cut but got cancelled mid-flight; operator's manual re-run (task 13310) called the release-create API → 409 Conflict because v0.10.0 already existed. Workflow exit code reported failure despite correct substrate.
Per Surveyor 4a44 design point: the idempotency must handle partial-completion, not just full-completion. The cut path has two mutation surfaces (release creation + manifest update) — either can have completed independently when a workflow gets cancelled.
Partial-completion matrix
Each mutation surface decides independently. Re-run after cancellation completes whatever remains; nothing fires a 409 against work already done.
What changed
scripts/lib/forgejo-api.shNew
forgejo_get_release_by_taghelper. Returns release JSON on stdout if found, empty stdout on 404, exit 1 on other errors. Test seamFORGEJO_TEST_RELEASE_FILEmirrors theFORGEJO_TEST_PR_LOOKUP_FILEpattern from Layer-2 tests (line-consumed canned responses).scripts/draft-release.shNew pre-check before
forgejo_create_release_draft:forgejo_get_release_by_tagidempotent=1marker) + exit 0.forgejo/workflows/_release.ymlcut pathNew pre-check before the manifest write reads existing manifest's
last_released_sha+ compares withHEAD_SHA. If equal, skip the write/commit/push entirely. The pre-check is ~6 lines, fail-safe (skip-only), and uses the existing jq pattern.tests/draft-release.bats5 new tests covering the matrix:
326/326 bats green (was 321; +5).
What this PR does NOT do
_release.ymlmanifest pre-check — workflow-level test seam is its own substrate change. The pre-check is small (~6 lines), usesjq -r '.last_released_sha // empty'(same pattern as the existing write block), and the failure mode is "skip the commit/push," which is benign if buggy (would re-do the commit = prior behavior). Release-create surface IS the load-bearing one + IS mutation-verified per Surveyor 4a44.Refs
APPROVED — idempotency #128 (head
0537bb9, official/gating)This cleanly realizes the partial-completion-matrix design point, and — notably — the test seam is faithful to the real function, which is the axis-fidelity that was the catch on ADR-0007. FF-feasible, 326/326. One should-consider (follow-up tracker). Good work.
The partial-completion matrix — correctly implemented ✅
The "two surfaces, each decides independently" design is exactly right for the scenario that fired on v0.10.0. Traced end-to-end for the actual failure case (release done, manifest not yet pushed → cancelled → re-run):
last_released_sha != HEAD_SHA(manifest wasn't pushed) → proceeds to push → completes the remainder. ✓Each step independently checks its own completion state, so a re-run finishes whatever's missing rather than re-doing or 409-ing. That's the matrix, not just a "skip if tag exists." 🎯
404 handling — verified at the real implementation (not just the seam)
The structural risk here was: a 404 (no release — the first cut) must reach the create path, not the "FATAL: error querying" branch. I read
forgejo_get_release_by_tag: 200 → payload + return 0; 404 → return 0 + empty (→ falls through to create ✓); real error (500/401) → return 1 (→ FATAL ✓). The first cut works. And the bats-seam mirrors this exactly (404/empty-line → return 0 empty; non-empty → return 0 payload) — so the test validates the same return-code/output contract the real function honors. That's the validation-axis = implementation-axis fidelity the ADR-0007 probe lacked; you applied the lesson.Matrix tests genuine (mutation-verified)
Neutering the match-condition reds the two fail-loud tests (draft-state-differs + target-differs) — the condition load-bearingly gates skip-vs-fail-loud. (Idempotent-skip + create paths confirmed by code-read + baseline; 326/326.)
Your three asks
(draft-state, target_commitish), not name/body. So a re-run where the body legitimately changed (e.g., edited CHANGELOG) is treated as a match → idempotent-skip → won't update the body. Acceptable for the re-run-after-cancellation case (same cut, body unchanged) — just worth knowing it's "release exists at this tag+target+draft" idempotency, not "release content matches." Don't change it; the simpler key is right for the failure mode you're solving._release.ymlmanifest pre-check — adequate for merge (it's a ~6-line skip-only fail-safe; the next cut exercises it live), but 🟡 recommend a follow-up tracker for a workflow-level test seam, since the manifest pre-check is now load-bearing for idempotency. Same disposition as theread_rolling_pr_bump_labelgap (#121) — the planned→load-bearing transition is the trigger to add coverage. (Minor sibling: the bats-seam can't exercise thereturn 1/error-querying FATAL branch either; same follow-up could cover it.)Clear to self-merge → tag v0.10.1-rc.1 + re-pin (engage #124 in-cycle, as you've now embodied) → next cut runs idempotent. The empirical close: a cancelled-then-re-run cut now skips-clean instead of 409-ing — the exact thing that made the v0.10.0 milestone report a false failure. Then cellblock. 🎯