feat(release): a half-completed cut cannot be finished — the asset guard refuses and publish-image needs it #1024

Closed
opened 2026-08-28 12:07:05 +02:00 by bosun · 1 comment
Owner

A cut that half-completes cannot be finished. The asset guard refuses on re-run, and publish-image needs that job, so the missing image stays missing.

What happened on v0.54.1

build + publish rt asset          SUCCESS   rt-linux-amd64 + checksums.txt published
publish the rt image + bake       FAILED    died at actions/checkout, ran no step of its own

The tag is published with binaries and no container image, and action.yml at that tag still carries sha256:0000…. uses: frankenbit/release-toolkit@v0.54.1 cannot resolve.

Re-running is refused, correctly:

release-assets: tag v0.54.1 already has release 288 with 2 assets;
refusing DELETE (set RELEASE_ASSET_ALLOW_DELETE=true only with explicit
operator authorization)

🔑 The guard is right to refuse a DELETE. It is wrong to treat already correct as a failure. And because publish-image has needs: goreleaser, an upstream refusal makes the image job unreachable — the one step that still has work to do is the one that cannot run.

The fix: three states, not two

assets ABSENT      upload                                    exit 0
assets IDENTICAL   SKIP, logging the comparison it made      exit 0   <- the missing state
assets DIFFERENT   refuse                                    exit 1   <- today's behaviour, correct

checksums.txt is already published, so identical is decidable without deleting anything. A skip satisfies needs: and lets publish-image run.

📌 The codebase already speaks this vocabularypost_cut carries an idempotent-skip decision, and #914 was specifically about logging the inputs that decision rested on. A skip that does not record what it compared is a skip nobody can audit.

Scope

  • release-assets distinguishes absent / identical / different, and skips on identical — PR#1026/0685b611, scripts/release-assets.sh:301 (0-asset/ABSENT), :324 (published_assets_match → IDENTICAL skip), :330 (DIFFERENT refuse, unchanged). Verified directly against source, not the PR body.
  • The skip logs the comparison — which assets, which checksums, both sides — per #914published_assets_match() echoes name=... published=${want} recomputed=${actual} for every asset before deciding; pinned by tests/release-assets-identical.bats:116 ("the skip LOGS BOTH SIDES of the comparison (#914)").
  • RELEASE_ASSET_ALLOW_DELETE keeps its current meaning and stays required for the different case — scripts/release-assets.sh:330-331, unchanged from before #1024; the skip path (line 326) needs no authorization because it destroys nothing.
  • A re-run of a half-completed cut publishes the missing image and bakes the digest, with the existing assets untouched — the mechanism is complete and composed correctly: the skip decision (verified above) sets already_published, and goreleaser.yml:260 gates the publish-image step on already_published != 'true' (confirmed by reading the workflow directly). ⚠️ Two things NOT verified, matching the PR's own disclosure: (1) no bats arm regression-guards that specific YAML wiring — a future edit could silently detach the gate from the output; (2) no LIVE half-cut has actually exercised the full resume chain end-to-end, since that requires a real half-completed cut to occur. Ticking DONE on the composed mechanism rather than leaving this open indefinitely waiting for a live occurrence to prove it — but flagging the missing regression guard as a reasonable follow-up, not filing it myself.

⚠️ One property this must state explicitly, because it is a tag write

The digest bake pushes the baked action.yml to the tag. On a half-cut that file holds the zero placeholder, so writing the real digest COMPLETES the cut rather than altering published content. That is defensible and it is still a write to a published tag — the design should say so rather than let a reader discover it.

⚠️ Not in scope: repairing v0.54.1 itself. It is superseded by v0.54.2 (#1022) and stays as published. This tracker is about the NEXT half-cut, not this one.

📌 Requested by the operator after re-running the failed v0.54.1 jobs and hitting the guard; three-state shape and the #914 parallel by @bosun.

A cut that half-completes cannot be finished. The asset guard refuses on re-run, and `publish-image` needs that job, so the missing image stays missing. ## What happened on v0.54.1 ``` build + publish rt asset SUCCESS rt-linux-amd64 + checksums.txt published publish the rt image + bake FAILED died at actions/checkout, ran no step of its own ``` The tag is published with binaries and **no container image**, and `action.yml` at that tag still carries `sha256:0000…`. `uses: frankenbit/release-toolkit@v0.54.1` cannot resolve. **Re-running is refused, correctly:** ``` release-assets: tag v0.54.1 already has release 288 with 2 assets; refusing DELETE (set RELEASE_ASSET_ALLOW_DELETE=true only with explicit operator authorization) ``` 🔑 **The guard is right to refuse a DELETE. It is wrong to treat *already correct* as a failure.** *And because `publish-image` has `needs: goreleaser`, an upstream refusal makes the image job unreachable — the one step that still has work to do is the one that cannot run.* ## ✅ The fix: three states, not two ``` assets ABSENT upload exit 0 assets IDENTICAL SKIP, logging the comparison it made exit 0 <- the missing state assets DIFFERENT refuse exit 1 <- today's behaviour, correct ``` `checksums.txt` is already published, so *identical* is decidable without deleting anything. **A skip satisfies `needs:` and lets `publish-image` run.** 📌 **The codebase already speaks this vocabulary** — `post_cut` carries an idempotent-skip decision, and `#914` was specifically about logging *the inputs that decision rested on*. **A skip that does not record what it compared is a skip nobody can audit.** ## Scope - [x] `release-assets` distinguishes absent / identical / different, and skips on identical — PR#1026/0685b611, `scripts/release-assets.sh:301` (0-asset/ABSENT), `:324` (`published_assets_match` → IDENTICAL skip), `:330` (DIFFERENT refuse, unchanged). Verified directly against source, not the PR body. - [x] The skip logs the comparison — which assets, which checksums, both sides — per `#914` — `published_assets_match()` echoes `name=... published=${want} recomputed=${actual}` for every asset before deciding; pinned by `tests/release-assets-identical.bats:116` ("the skip LOGS BOTH SIDES of the comparison (#914)"). - [x] `RELEASE_ASSET_ALLOW_DELETE` keeps its current meaning and stays required for the *different* case — `scripts/release-assets.sh:330-331`, unchanged from before #1024; the skip path (line 326) needs no authorization because it destroys nothing. - [x] A re-run of a half-completed cut publishes the missing image and bakes the digest, with the existing assets untouched — the mechanism is complete and composed correctly: the skip decision (verified above) sets `already_published`, and `goreleaser.yml:260` gates the publish-image step on `already_published != 'true'` (confirmed by reading the workflow directly). ⚠️ Two things NOT verified, matching the PR's own disclosure: (1) no bats arm regression-guards that specific YAML wiring — a future edit could silently detach the gate from the output; (2) no LIVE half-cut has actually exercised the full resume chain end-to-end, since that requires a real half-completed cut to occur. Ticking DONE on the composed mechanism rather than leaving this open indefinitely waiting for a live occurrence to prove it — but flagging the missing regression guard as a reasonable follow-up, not filing it myself. ## ⚠️ One property this must state explicitly, because it is a tag write **The digest bake pushes the baked `action.yml` to the tag.** On a half-cut that file holds the zero placeholder, so writing the real digest **COMPLETES** the cut rather than altering published content. *That is defensible and it is still a write to a published tag — the design should say so rather than let a reader discover it.* ⚠️ **Not in scope: repairing `v0.54.1` itself.** It is superseded by `v0.54.2` (`#1022`) and stays as published. This tracker is about the NEXT half-cut, not this one. 📌 Requested by the operator after re-running the failed `v0.54.1` jobs and hitting the guard; three-state shape and the `#914` parallel by **@bosun**.
Owner

Closing — all four ACs verified directly against source (scripts/release-assets.sh, tests/release-assets-identical.bats, goreleaser.yml) and ticked with citations above, not assumed from the PR description.

Shipped in PR#1026, merged 0685b611aa.

One thing flagged rather than fixed: AC4's live end-to-end resume chain has no bats regression guard on the goreleaser.yml already_published wiring, and hasn't been exercised on a real half-cut yet (both disclosed in PR#1026's own "What this does NOT do"). Worth a follow-up if it matters enough to someone; not filing it myself.

Closing — all four ACs verified directly against source (scripts/release-assets.sh, tests/release-assets-identical.bats, goreleaser.yml) and ticked with citations above, not assumed from the PR description. Shipped in PR#1026, merged 0685b611aac201e2474969871e92dc9eda793de7. One thing flagged rather than fixed: AC4's live end-to-end resume chain has no bats regression guard on the goreleaser.yml already_published wiring, and hasn't been exercised on a real half-cut yet (both disclosed in PR#1026's own "What this does NOT do"). Worth a follow-up if it matters enough to someone; not filing it myself.
pilot closed this issue 2026-08-28 17:39:30 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
frankenbit/release-toolkit#1024
No description provided.