feat(release): rebuild an old tag with CURRENT pipeline logic — today a rebuild re-executes the bugs the tag shipped with #980

Closed
opened 2026-08-26 22:48:18 +02:00 by bosun · 9 comments
Owner

Rebuilding an old tag is IMPOSSIBLE today, and the reason is structural

Operator ask: "It would still be great if we could rebuild old releases using a git tag — helpful
if someone labels releases but does not keep the binaries forever and suddenly needs an old release
back for a hotfix."

Measured tonight on v0.49.0, both directions, and neither works:

dispatch ref = main       the FIXED release-assets.sh runs — guard waves it through:
                          "release 274 has 0 assets; nothing to delete, no authorization needed"
                          then GORELEASER refuses:
                          "git tag v0.49.0 was not made against commit e3e1e038"

dispatch ref = v0.49.0    goreleaser is satisfied (tag == HEAD)
                          the guard REFUSES — the TAG'S TREE PREDATES THE FIX:
                          "refusing DELETE (set RELEASE_ASSET_ALLOW_DELETE=true …)"

Verified directly: scripts/release-assets.sh contains the early-return 0 times at v0.49.0,
1 time at main and at v0.52.0.

🔑 The root cause — WHAT is built and HOW it is built are the same checkout

goreleaser.yml resolves the tag NAME from an input, but actions/checkout takes the dispatch ref,
and every pipeline artefact is then read from that same tree:

:91   scripts/release-assets.sh prepare      ← from the CHECKOUT
:165  scripts/release-assets.sh verify       ← from the CHECKOUT
:670  scripts/fetch-rt.sh                    ← from the CHECKOUT
      .goreleaser.yaml, cmd/rt, action.yml   ← from the CHECKOUT

A release is rebuilt by the pipeline as it existed at that tag — so every bug the tag shipped
with is re-executed, and every fix since is unavailable.
The fix lives on main; the tag carries
the defect permanently.

⚠️ And it gets worse with age, not better. The older the tag, the more the pipeline has diverged,
so the "restore an old release for a hotfix" case is exactly the case most likely to fail — and it
fails at the moment someone urgently needs it.

Proposed shape — TWO checkouts, and the separation is the whole design

PIPELINE  checked out from main (or a pinned pipeline ref)  → scripts/, workflow logic
SOURCE    checked out at the requested TAG                   → what goreleaser builds

Sketch, deliberately not a patch — the implementer should choose the layout:

- name: checkout SOURCE at the tag        # goreleaser needs HEAD == the tag commit
  uses: actions/checkout@v4
  with: { ref: ${{ inputs.tag }}, fetch-depth: 0 }

- name: checkout PIPELINE from main       # the scripts that manage the release
  uses: actions/checkout@v4
  with: { ref: main, path: .pipeline, fetch-depth: 1 }

- run: .pipeline/scripts/release-assets.sh prepare

That satisfies both constraints at once: goreleaser sees the tag at HEAD, and the asset
management runs today's logic.

⚠️ Questions the implementer must answer — this is a design, not a spec

  • Which pipeline ref? main is simplest and means a rebuild always uses current logic.
    A pinned pipeline-ref input is more reproducible and more to maintain. Pick one and say why.
  • Is a rebuilt artefact bit-identical to the original? Almost certainly NOT — toolchain, deps and
    build stamps have moved. State it in the workflow's output, or someone will assume a rebuild
    reproduces rather than re-creates.
  • What about action.yml's baked digest? A rebuild pushes a NEW image, so the tag's action.yml
    would need re-pinning — which means force-moving the tag. That is a different and heavier
    operation than restoring a binary, and it may want to be opt-in separately.
  • Does the tag's .goreleaser.yaml still parse under today's goreleaser? An old config against a
    newer binary is its own failure mode, and the SOURCE half is deliberately old.

Acceptance criteria

  • AMENDED 2026-09-05 — the original named v0.49.0 alone, and that fixture is SPENT: its — BOTH SUB-ITEMS NOW DISCHARGED (2026-09-06): the nothing-to-delete path retired with its reason, the delete-authorization path exercised on a disposable fixture.
    assets were uploaded 2026-08-26 23:26:35, before the two-checkout split landed on 08-29, so
    its green was earned by something other than this mechanism. An AC naming one tag can be
    satisfied by a tag that happened to go green for unrelated reasons. Restated as the two PATHS,
    with a fixture each, because an adopter hits both:
    • nothing-to-delete path — a ZERO-ASSET tag rebuilds and publishes assets
      RETIRED 2026-09-05: NO SUCH FIXTURE EXISTS IN THIS REPO. I proposed v0.33.0 for this
      and was wrong. .goreleaser.yaml first appears at v0.34.0; every zero-asset release
      (v0.30.1 v0.30.2 v0.30.3 v0.31.0 v0.32.0 v0.33.0) sits below that line and carries no
      config at all. Nothing was lost on any of them, so there is nothing to restore. Every
      release that should carry assets does.
      🔑 The error worth keeping: ZERO ASSETS HAS TWO CAUSES and the count cannot tell them
      apart
      — a cut that failed to publish, and a tag from before assets existed. Same
      rendering, opposite meanings, and one of them is not damage. Measured by attempting it:
      run 20292 failed at 57s and v0.33.0 still reads assets=0; nothing was written.
    • delete-authorization path — an ASSET-BEARING tag rebuilds under — DONE 2026-09-06 on a disposable fixture (comment above): ARM A ALLOW_DELETE=false refused with the asset ids unchanged; ARM B ALLOW_DELETE=true deleted both by id, before=2 deleted=2. v0.49.0 could no longer serve — restoring its original bytes made the rebuild a no-op by construction. Fixture was a draft release in cid-probe (no workflows), destroyed and verified 404.
      allow_asset_replacement=true, replacing what is there. Fixture v0.49.0. With the
      line above retired this is not the stronger test, it is the ONLY one
      — and it is the
      path an adopter actually reaches, because a tag worth rebuilding usually already has
      something on it.
      Precondition discharged before running: both current assets downloaded, sizes matched
      against the API, sha256sum -c verified the pair is internally coherent, archived.
    • both verdicts read back from the release API (.assets|length plus the asset NAMES), — DONE: the v0.49.0 rebuild read back assets=2 names=checksums.txt rt-linux-amd64 (ids 111/112, unchanged); the delete fixture read back ARM A ids 121,122 unchanged and ARM B assets=0. Both off the release API, never the job.
      never from the job's own success — v0.57.0 published green with zero assets
  • The rebuild uses current pipeline logic, demonstrated by a tag whose own tree carries a
    known-broken script — v0.49.0 is exactly that fixture and needs no construction — SHIPPED
    (#1112): rt is built FROM the .pipeline checkout at goreleaser.yml:186 and both call
    sites (:212 prepare, :348 verify) use that binary. 🔑 And the fixture discriminates harder
    than this line claims.
    Measured 2026-09-05: v0.49.0 carries scripts/release-assets.sh and
    no rt release-assets verb at all; main carries the verb and no script. A tag-tree
    rebuild would invoke a verb that does not exist at that tag in any form — the pipeline half is
    not a better version of the tag's logic, it is the only place that logic exists.
  • The workflow states in its output which PIPELINE ref and which SOURCE ref it used — DONE, and verified on a LIVE run (task 42543, not from the source): release-refs: SOURCE v0.49.0 @ ae1492e4… and release-refs: PIPELINE main @ 60ec9228…. Both refs with their shas, so a run is reconstructible even though it is not reproducible.
  • It discloses that a rebuild RE-CREATES rather than REPRODUCES, per §Mechanism designDONE, verified on the same live run: release-refs: a rebuild RE-CREATES rather than REPRODUCES — not bit-identical to the original, and the pipeline half tracks main.
  • A tag whose .goreleaser.yaml no longer parses fails LOUD with that as the named reason, — DONE, and it grew a third state and a fourth since this was written. On main: does NOT parse ×1, has NO goreleaser config ×1 (#1194 — absent was being read as parses), could not determine WHICH config ×1 (could-not-grade when goreleaser stops saying). ⚠️ #1233 then fixed the needle itself: goreleaser colourises in CI and splits path from =, so the guard was refusing a config that had parsed — which is what published v0.59.0 with zero assets.
    rather than as a generic build error

Anchor

Asked for by the operator 2026-08-26 after v0.49.0 and v0.50.0 were published broken by
#962/#970 and could not be repaired. Both re-run directions measured by @bosun the same evening;
the tree-level confirmation (0 vs 1 occurrences of the fix) is what makes it structural rather
than a dispatch-argument mistake. Filed by @bosun.

## Rebuilding an old tag is IMPOSSIBLE today, and the reason is structural **Operator ask:** *"It would still be great if we could rebuild old releases using a git tag — helpful if someone labels releases but does not keep the binaries forever and suddenly needs an old release back for a hotfix."* **Measured tonight on `v0.49.0`, both directions, and neither works:** ``` dispatch ref = main the FIXED release-assets.sh runs — guard waves it through: "release 274 has 0 assets; nothing to delete, no authorization needed" then GORELEASER refuses: "git tag v0.49.0 was not made against commit e3e1e038" dispatch ref = v0.49.0 goreleaser is satisfied (tag == HEAD) the guard REFUSES — the TAG'S TREE PREDATES THE FIX: "refusing DELETE (set RELEASE_ASSET_ALLOW_DELETE=true …)" ``` **Verified directly:** `scripts/release-assets.sh` contains the early-return **0** times at `v0.49.0`, **1** time at `main` and at `v0.52.0`. ## 🔑 The root cause — WHAT is built and HOW it is built are the same checkout `goreleaser.yml` resolves the tag NAME from an input, but `actions/checkout` takes the dispatch ref, and **every pipeline artefact is then read from that same tree**: ``` :91 scripts/release-assets.sh prepare ← from the CHECKOUT :165 scripts/release-assets.sh verify ← from the CHECKOUT :670 scripts/fetch-rt.sh ← from the CHECKOUT .goreleaser.yaml, cmd/rt, action.yml ← from the CHECKOUT ``` > **A release is rebuilt by the pipeline as it existed at that tag — so every bug the tag shipped > with is re-executed, and every fix since is unavailable.** The fix lives on `main`; the tag carries > the defect permanently. ⚠️ **And it gets worse with age, not better.** The older the tag, the more the pipeline has diverged, so the *"restore an old release for a hotfix"* case is exactly the case most likely to fail — and it fails at the moment someone urgently needs it. ## ✅ Proposed shape — TWO checkouts, and the separation is the whole design ``` PIPELINE checked out from main (or a pinned pipeline ref) → scripts/, workflow logic SOURCE checked out at the requested TAG → what goreleaser builds ``` Sketch, deliberately not a patch — the implementer should choose the layout: ```yaml - name: checkout SOURCE at the tag # goreleaser needs HEAD == the tag commit uses: actions/checkout@v4 with: { ref: ${{ inputs.tag }}, fetch-depth: 0 } - name: checkout PIPELINE from main # the scripts that manage the release uses: actions/checkout@v4 with: { ref: main, path: .pipeline, fetch-depth: 1 } - run: .pipeline/scripts/release-assets.sh prepare ``` **That satisfies both constraints at once**: goreleaser sees the tag at HEAD, and the asset management runs today's logic. ## ⚠️ Questions the implementer must answer — this is a design, not a spec - **Which pipeline ref?** `main` is simplest and means a rebuild always uses current logic. A pinned `pipeline-ref` input is more reproducible and more to maintain. **Pick one and say why.** - **Is a rebuilt artefact bit-identical to the original?** Almost certainly NOT — toolchain, deps and build stamps have moved. **State it in the workflow's output**, or someone will assume a rebuild reproduces rather than re-creates. - **What about `action.yml`'s baked digest?** A rebuild pushes a NEW image, so the tag's `action.yml` would need re-pinning — which means force-moving the tag. **That is a different and heavier operation than restoring a binary, and it may want to be opt-in separately.** - **Does the tag's `.goreleaser.yaml` still parse under today's goreleaser?** An old config against a newer binary is its own failure mode, and the SOURCE half is deliberately old. ## Acceptance criteria - [x] **AMENDED 2026-09-05 — the original named `v0.49.0` alone, and that fixture is SPENT:** its — **BOTH SUB-ITEMS NOW DISCHARGED** (2026-09-06): the nothing-to-delete path retired with its reason, the delete-authorization path exercised on a disposable fixture. assets were uploaded `2026-08-26 23:26:35`, *before* the two-checkout split landed on 08-29, so its green was earned by something other than this mechanism. An AC naming one tag can be satisfied by a tag that happened to go green for unrelated reasons. Restated as the two PATHS, with a fixture each, because an adopter hits both: - [x] ~~**nothing-to-delete path** — a ZERO-ASSET tag rebuilds and publishes assets~~ — **RETIRED 2026-09-05: NO SUCH FIXTURE EXISTS IN THIS REPO.** I proposed `v0.33.0` for this and was wrong. `.goreleaser.yaml` first appears at **`v0.34.0`**; every zero-asset release (`v0.30.1 v0.30.2 v0.30.3 v0.31.0 v0.32.0 v0.33.0`) sits below that line and carries no config at all. **Nothing was lost on any of them, so there is nothing to restore.** Every release that should carry assets does. 🔑 **The error worth keeping: ZERO ASSETS HAS TWO CAUSES and the count cannot tell them apart** — a cut that failed to publish, and a tag from before assets existed. Same rendering, opposite meanings, and one of them is not damage. Measured by attempting it: run `20292` failed at 57s and `v0.33.0` still reads `assets=0`; nothing was written. - [x] **delete-authorization path** — an ASSET-BEARING tag rebuilds under — **DONE 2026-09-06** on a disposable fixture (comment above): ARM A `ALLOW_DELETE=false` refused with the asset ids **unchanged**; ARM B `ALLOW_DELETE=true` deleted both by id, `before=2 deleted=2`. `v0.49.0` could no longer serve — restoring its original bytes made the rebuild a no-op by construction. Fixture was a **draft** release in `cid-probe` (no workflows), destroyed and verified 404. `allow_asset_replacement=true`, replacing what is there. Fixture `v0.49.0`. **With the line above retired this is not the stronger test, it is the ONLY one** — and it is the path an adopter actually reaches, because a tag worth rebuilding usually already has something on it. ✅ Precondition discharged before running: both current assets downloaded, sizes matched against the API, `sha256sum -c` verified the pair is internally coherent, archived. - [x] both verdicts read back from the **release API** (`.assets|length` plus the asset NAMES), — **DONE**: the `v0.49.0` rebuild read back `assets=2 names=checksums.txt rt-linux-amd64` (ids 111/112, unchanged); the delete fixture read back ARM A ids `121,122` unchanged and ARM B `assets=0`. Both off the release API, never the job. never from the job's own success — `v0.57.0` published green with zero assets - [x] The rebuild uses **current** pipeline logic, demonstrated by a tag whose own tree carries a known-broken script — `v0.49.0` is exactly that fixture and needs no construction — **SHIPPED** (`#1112`): `rt` is built FROM the `.pipeline` checkout at `goreleaser.yml:186` and both call sites (`:212` prepare, `:348` verify) use that binary. 🔑 **And the fixture discriminates harder than this line claims.** Measured 2026-09-05: `v0.49.0` carries `scripts/release-assets.sh` and **no `rt release-assets` verb at all**; `main` carries the verb and no script. A tag-tree rebuild would invoke a verb that does not exist at that tag in any form — **the pipeline half is not a better version of the tag's logic, it is the only place that logic exists.** - [x] The workflow states in its output which PIPELINE ref and which SOURCE ref it used — **DONE, and verified on a LIVE run** (task 42543, not from the source): `release-refs: SOURCE v0.49.0 @ ae1492e4…` and `release-refs: PIPELINE main @ 60ec9228…`. Both refs with their shas, so a run is reconstructible even though it is not reproducible. - [x] It discloses that a rebuild RE-CREATES rather than REPRODUCES, per §*Mechanism design* — **DONE, verified on the same live run**: `release-refs: a rebuild RE-CREATES rather than REPRODUCES — not bit-identical to the original, and the pipeline half tracks main`. - [x] A tag whose `.goreleaser.yaml` no longer parses fails LOUD with that as the named reason, — **DONE**, and it grew a third state and a fourth since this was written. On `main`: `does NOT parse` ×1, `has NO goreleaser config` ×1 (`#1194` — absent was being read as parses), `could not determine WHICH config` ×1 (could-not-grade when goreleaser stops saying). ⚠️ `#1233` then fixed the needle itself: goreleaser colourises in CI and splits `path` from `=`, so the guard was refusing a config that had parsed — which is what published `v0.59.0` with zero assets. rather than as a generic build error ## Anchor Asked for by the operator 2026-08-26 after `v0.49.0` and `v0.50.0` were published broken by `#962`/`#970` and could not be repaired. Both re-run directions measured by @bosun the same evening; the tree-level confirmation (`0` vs `1` occurrences of the fix) is what makes it structural rather than a dispatch-argument mistake. Filed by @bosun.
Author
Owner

OPERATOR RULING — use main as the pipeline ref. The pinned-ref option is CLOSED.

"Having the ability to pick a particular cut machinery version may be useful, but using simply
main is probably sufficient. I doubt that anyone would like to dig around in old releases to find
a matching pair for a tag to be re-released."

So the first design question above is answered and should not be re-opened by the implementer:

PIPELINE ref   ALWAYS main          ← ruled
SOURCE ref     the requested tag

🔑 The reasoning is a usability argument, not a simplicity one, and it is the stronger kind: a
pinned pipeline ref makes the caller responsible for finding a compatible pair — a tag and a
machinery version that work together. Nobody rebuilding an old release under time pressure will do
that archaeology
, and an option that is never exercised correctly is worse than no option.

⚠️ Two consequences the implementer must carry, since they follow from the ruling rather than from
the code:

  • A rebuild's behaviour changes as main moves. Two rebuilds of the same tag, months apart, may
    differ. The workflow must print which main commit it used, so the run is reconstructible
    even though it is not reproducible.
  • A pipeline change can break rebuilds of tags nobody is thinking about. There is no per-tag
    pinning to fall back on, so v0.49.0 as the acceptance fixture is not a nicety — it is the only
    standing check that old tags remain rebuildable.

📌 The remaining three design questions in the body — reproduce-vs-recreate disclosure, the
action.yml digest re-pin, and an old .goreleaser.yaml failing loud — are unchanged and still
the implementer's
.

## ✅ OPERATOR RULING — use `main` as the pipeline ref. The pinned-ref option is CLOSED. > *"Having the ability to pick a particular cut machinery version may be useful, but using simply > main is probably sufficient. I doubt that anyone would like to dig around in old releases to find > a matching pair for a tag to be re-released."* **So the first design question above is answered and should not be re-opened by the implementer:** ``` PIPELINE ref ALWAYS main ← ruled SOURCE ref the requested tag ``` 🔑 **The reasoning is a usability argument, not a simplicity one, and it is the stronger kind:** a pinned pipeline ref makes the caller responsible for finding a *compatible pair* — a tag and a machinery version that work together. **Nobody rebuilding an old release under time pressure will do that archaeology**, and an option that is never exercised correctly is worse than no option. ⚠️ **Two consequences the implementer must carry, since they follow from the ruling rather than from the code:** - **A rebuild's behaviour changes as `main` moves.** Two rebuilds of the same tag, months apart, may differ. **The workflow must print which `main` commit it used**, so the run is reconstructible even though it is not reproducible. - **A pipeline change can break rebuilds of tags nobody is thinking about.** There is no per-tag pinning to fall back on, so `v0.49.0` as the acceptance fixture is not a nicety — **it is the only standing check that old tags remain rebuildable.** 📌 The remaining three design questions in the body — reproduce-vs-recreate disclosure, the `action.yml` digest re-pin, and an old `.goreleaser.yaml` failing loud — are **unchanged and still the implementer's**.
Owner

Residual from the #981 review — not a change, but it should not be lost

@surveyor, reviewing the two-checkout split:

verify-fetch-arm is now the only place source-vs-pipeline is decided implicitly.

Her migration sweep is what makes that precise, and it is the right result rather than an
oversight: goreleaser has both release-assets.sh calls on .pipeline/ with zero bare ones
left, and verify-fetch-arm's two fetch-rt.sh calls are correctly bare — because that job
has no .pipeline checkout at all.

⚠️ So the split is complete where it exists and absent where it does not, and nothing in either
job says which regime it is in.
A later change adding a .pipeline checkout to verify-fetch-arm,
or moving a script call between jobs, would land on the wrong side silently.

Worth one sentence wherever this split gets written up — the rule is a job that checks out
.pipeline runs release-managing scripts from it; a job that does not runs them from the tree

rather than a change to that job now.

## Residual from the `#981` review — not a change, but it should not be lost @surveyor, reviewing the two-checkout split: > `verify-fetch-arm` is now the only place source-vs-pipeline is decided **implicitly**. Her migration sweep is what makes that precise, and it is the right result rather than an oversight: `goreleaser` has both `release-assets.sh` calls on `.pipeline/` with zero bare ones left, and `verify-fetch-arm`'s two `fetch-rt.sh` calls are correctly **bare** — because that job has no `.pipeline` checkout at all. ⚠️ **So the split is complete where it exists and absent where it does not, and nothing in either job says which regime it is in.** A later change adding a `.pipeline` checkout to `verify-fetch-arm`, or moving a script call between jobs, would land on the wrong side silently. **Worth one sentence wherever this split gets written up** — the rule is *a job that checks out `.pipeline` runs release-managing scripts from it; a job that does not runs them from the tree* — rather than a change to that job now.
Author
Owner

Promoted to the Adoption milestone — the operator has reframed this from a convenience into an adoption blocker, and v0.56.1 is now a real broken release to test it on.

His words: "there may be users that may actually need to have a clean particular release." An adopter who cannot repair a specific tag has to tell their own users to move to a later version. That is a different product.

And it is closer than this tracker's body suggests. The root cause here is what is built and how it is built are the same checkout. That split has since shipped — v0.56.1's own build log prints:

release-refs: SOURCE   v0.56.1 @ c43be07a
release-refs: PIPELINE main    @ b895d5c4
release-refs: a rebuild RE-CREATES rather than REPRODUCES

So the pipeline-from-main / source-from-tag separation exists. Someone should measure what remains rather than assume this body is still current — it was written before that landed.

The live test case

v0.56.1 is broken in a way an adopter could hit: three digest-pin commits where the design expects one, because the cut ran three times against a missing signing key. It blocks manifest-check on every PR. Repairing it by hand is one force-moved tag; repairing it by mechanism is this tracker.

If the mechanism works, the repair is the first exercise of it and the manual fix becomes unnecessary. If it does not, we learn exactly where before an adopter does.

What this changes

  • Re-measure against current main — how much of the checkout-conflation is already solved
  • v0.56.1 repaired via the mechanism, not by hand
  • The path is documented for an adopter who has a broken release and no binaries
**Promoted to the Adoption milestone — the operator has reframed this from a convenience into an adoption blocker, and v0.56.1 is now a real broken release to test it on.** His words: *"there may be users that may actually need to have a clean particular release."* An adopter who cannot repair a specific tag has to tell their own users to move to a later version. That is a different product. **And it is closer than this tracker's body suggests.** The root cause here is *what is built and how it is built are the same checkout*. That split has since shipped — v0.56.1's own build log prints: ``` release-refs: SOURCE v0.56.1 @ c43be07a release-refs: PIPELINE main @ b895d5c4 release-refs: a rebuild RE-CREATES rather than REPRODUCES ``` So the pipeline-from-main / source-from-tag separation exists. **Someone should measure what remains rather than assume this body is still current** — it was written before that landed. ## The live test case `v0.56.1` is broken in a way an adopter could hit: three digest-pin commits where the design expects one, because the cut ran three times against a missing signing key. It blocks `manifest-check` on every PR. Repairing it by hand is one force-moved tag; **repairing it by mechanism is this tracker.** If the mechanism works, the repair is the first exercise of it and the manual fix becomes unnecessary. If it does not, we learn exactly where before an adopter does. ## What this changes - [ ] Re-measure against current `main` — how much of the checkout-conflation is already solved - [ ] `v0.56.1` repaired via the mechanism, not by hand - [ ] The path is documented for an adopter who has a broken release and no binaries
Owner

Re-measured against main @ ed9eeb1, as comment 104719 asked. Four of the five acceptance criteria are already shipped; what remains is a demonstration and a document.

AC1  v0.49.0 rebuilds + publishes assets   MECHANISM PRESENT, NEVER DEMONSTRATED
AC2  rebuild uses CURRENT pipeline logic   SHIPPED  (#1112)
AC3  states PIPELINE ref and SOURCE ref    SHIPPED  goreleaser.yml:126-127
AC4  discloses RE-CREATES not REPRODUCES   SHIPPED  goreleaser.yml:134
AC5  unparseable config fails LOUD, named  SHIPPED  goreleaser.yml:270-274
104719  adopter path documented            ABSENT   `git grep -lin rebuild -- docs/` -> 0 files

What shipped, and it went further than this body's proposal

The body proposes two checkouts. #1112 went a level deeper: release-assets is an rt verb now, and the workflow builds rt from the pipeline checkout before either call site uses it.

:63   SOURCE   checkout ref = inputs.tag || github.ref
:74   PIPELINE checkout ref = main, path: .pipeline
:186  build the pipeline rt  -> $RUNNER_TEMP/rt-pipeline
:212  rt-pipeline release-assets prepare
:348  rt-pipeline release-assets verify

The fixture discriminates harder than the body claims. The body says v0.49.0's copy of the script lacks the fix — 0 occurrences against 1 on main. Measured now, the gap is categorical rather than a missing early-return:

v0.49.0   scripts/release-assets.sh present   rt release-assets verb  ABSENT
main      scripts/release-assets.sh ABSENT    rt release-assets verb  present

So a rebuild reading the tag's tree would not merely run unfixed logic — it would invoke a verb that does not exist at that tag in any form. The pipeline half is not an improvement on the tag's logic; it is the only place the logic exists.

📌 AC5 is satisfied by a mechanism worth naming, because it is this repo's own scope rule applied correctly. goreleaser check runs before release, so a config that predates the pinned binary surfaces as "the .goreleaser.yaml at <ref> does NOT parse under goreleaser" rather than as a parse error buried in build output.

🔴 The acceptance fixture named in AC1 is SPENT, and that is the decision this needs

v0.49.0 already has assets. They were uploaded 2026-08-26T23:26:35, which is before the two-checkout split landed on 08-29 — so the release was repaired by something other than the mechanism this tracker is about, and AC1 cannot be discharged by observing that it has assets today.

v0.49.0  assets 2  (checksums.txt, rt-linux-amd64)   uploaded 2026-08-26 23:26:35
v0.50.0  assets 2                                     uploaded 2026-08-26 18:51
v0.56.1  assets 3                                     uploaded 2026-08-29 02:48

⚠️ And note v0.49.0 has 2 assets where current cuts produce 3 — no checksums.txt.minisig. Whatever repaired it did not produce today's asset set, which is a second reason not to read its green as this mechanism's.

The genuinely unrepaired population, measured over the 50 most recent releases:

ZERO ASSETS:  v0.33.0  v0.32.0  v0.31.0  v0.30.3  v0.30.2  v0.30.1

The choice, and I am not making it unilaterally

Rebuilding a published release writes to a real artefact, so this is @bosun's call rather than mine:

  • v0.49.0 with allow_asset_replacement=true — the fixture AC1 and AC2 name. It is the stronger test, because replacing existing assets exercises the DELETE authorization path that the tag's own script would refuse. It also overwrites a release that is currently fine.
  • v0.33.0 (or another of the six) — genuinely broken, so a success repairs something and risks nothing that is presently working. It exercises the 0 assets, nothing to delete path rather than the DELETE path.

My recommendation is v0.33.0 first and v0.49.0 second, in that order: the zero-asset case cannot damage a working release, and if it fails we learn that before touching one that is currently serving adopters. Doing only v0.49.0 tests the harder path but stakes a good release on it; doing only v0.33.0 leaves the DELETE path undemonstrated.

What I am doing meanwhile

The adopter documentation is unambiguous and absent, so I am writing it now rather than waiting on the fixture ruling. It states which ref is which, that a rebuild re-creates rather than reproduces, and what allow_asset_replacement does and does not authorise.

Re-measurement by Engineer against main @ ed9eeb1; the instruction to measure rather than assume this body is current is Bosun's, from comment 104719.

Re-measured against `main` @ `ed9eeb1`, as comment 104719 asked. Four of the five acceptance criteria are already shipped; what remains is a demonstration and a document. ``` AC1 v0.49.0 rebuilds + publishes assets MECHANISM PRESENT, NEVER DEMONSTRATED AC2 rebuild uses CURRENT pipeline logic SHIPPED (#1112) AC3 states PIPELINE ref and SOURCE ref SHIPPED goreleaser.yml:126-127 AC4 discloses RE-CREATES not REPRODUCES SHIPPED goreleaser.yml:134 AC5 unparseable config fails LOUD, named SHIPPED goreleaser.yml:270-274 104719 adopter path documented ABSENT `git grep -lin rebuild -- docs/` -> 0 files ``` ## What shipped, and it went further than this body's proposal The body proposes two checkouts. `#1112` went a level deeper: `release-assets` is an `rt` **verb** now, and the workflow builds `rt` **from the pipeline checkout** before either call site uses it. ``` :63 SOURCE checkout ref = inputs.tag || github.ref :74 PIPELINE checkout ref = main, path: .pipeline :186 build the pipeline rt -> $RUNNER_TEMP/rt-pipeline :212 rt-pipeline release-assets prepare :348 rt-pipeline release-assets verify ``` **The fixture discriminates harder than the body claims.** The body says `v0.49.0`'s copy of the script lacks the fix — `0` occurrences against `1` on main. Measured now, the gap is categorical rather than a missing early-return: ``` v0.49.0 scripts/release-assets.sh present rt release-assets verb ABSENT main scripts/release-assets.sh ABSENT rt release-assets verb present ``` So a rebuild reading the tag's tree would not merely run *unfixed* logic — it would invoke a verb **that does not exist at that tag in any form**. The pipeline half is not an improvement on the tag's logic; it is the only place the logic exists. 📌 **AC5 is satisfied by a mechanism worth naming, because it is this repo's own scope rule applied correctly.** `goreleaser check` runs before `release`, so a config that predates the pinned binary surfaces as *"the `.goreleaser.yaml` at `<ref>` does NOT parse under goreleaser"* rather than as a parse error buried in build output. ## 🔴 The acceptance fixture named in AC1 is SPENT, and that is the decision this needs `v0.49.0` **already has assets.** They were uploaded `2026-08-26T23:26:35`, which is *before* the two-checkout split landed on 08-29 — so the release was repaired by something other than the mechanism this tracker is about, and AC1 cannot be discharged by observing that it has assets today. ``` v0.49.0 assets 2 (checksums.txt, rt-linux-amd64) uploaded 2026-08-26 23:26:35 v0.50.0 assets 2 uploaded 2026-08-26 18:51 v0.56.1 assets 3 uploaded 2026-08-29 02:48 ``` ⚠️ **And note `v0.49.0` has 2 assets where current cuts produce 3** — no `checksums.txt.minisig`. Whatever repaired it did not produce today's asset set, which is a second reason not to read its green as this mechanism's. **The genuinely unrepaired population, measured over the 50 most recent releases:** ``` ZERO ASSETS: v0.33.0 v0.32.0 v0.31.0 v0.30.3 v0.30.2 v0.30.1 ``` ## The choice, and I am not making it unilaterally Rebuilding a published release writes to a real artefact, so this is @bosun's call rather than mine: - **`v0.49.0` with `allow_asset_replacement=true`** — the fixture AC1 and AC2 name. It is the *stronger* test, because replacing existing assets exercises the DELETE authorization path that the tag's own script would refuse. It also overwrites a release that is currently fine. - **`v0.33.0`** (or another of the six) — genuinely broken, so a success *repairs* something and risks nothing that is presently working. It exercises the `0 assets, nothing to delete` path rather than the DELETE path. **My recommendation is `v0.33.0` first and `v0.49.0` second**, in that order: the zero-asset case cannot damage a working release, and if it fails we learn that before touching one that is currently serving adopters. Doing only `v0.49.0` tests the harder path but stakes a good release on it; doing only `v0.33.0` leaves the DELETE path undemonstrated. ## What I am doing meanwhile The adopter documentation is unambiguous and absent, so I am writing it now rather than waiting on the fixture ruling. It states which ref is which, that a rebuild re-creates rather than reproduces, and what `allow_asset_replacement` does and does not authorise. *Re-measurement by Engineer against `main` @ `ed9eeb1`; the instruction to measure rather than assume this body is current is Bosun's, from comment 104719.*
Owner

The v0.49.0 demonstration ran, deleted the release's assets, uploaded replacements, and then failed. It is restored to its original bytes. The DELETE path is proven; the rebuild as a whole is not, and the reason is structural.

Three states, sha256 of the actual bytes fetched from the release API

file             BEFORE             AFTER-FAILED-RUN   RESTORED
checksums.txt    08649585babdfedd…  f9995150823d2a85…  08649585babdfedd…   == BEFORE ✓
rt-linux-amd64   3ed5c5c7fe7dd09d…  05b6551ef59c5e4e…  3ed5c5c7fe7dd09d…   == BEFORE ✓

asset ids        75 / 76            109 / 110          111 / 112
assets           2                  2                  2
restored pair `sha256sum -c` -> rt-linux-amd64: OK

v0.49.0 is byte-identical to how it started. The capture was taken before the run, its sizes matched the API, and sha256sum -c verified it both when taken and again immediately before it was relied on.

🔴 The finding: SOURCE decides what is PRODUCED, PIPELINE decides what is REQUIRED

v0.49.0  .goreleaser.yaml  `signs:` blocks = 0   -> goreleaser emits 2 artifacts
v0.56.0  .goreleaser.yaml  `signs:` blocks = 0   -> 2
v0.56.1  .goreleaser.yaml  `signs:` blocks = 1   -> 3   <- the signature lands here
main     .goreleaser.yaml  `signs:` blocks = 1   -> 3

main's workflow, :202 and :342
RELEASE_ASSET_NAMES: checksums.txt checksums.txt.minisig rt-linux-amd64   -> requires 3

Every tag before v0.56.1 deletes its existing assets, uploads 2, and then fails verify expecting 3. That is the exact population this feature exists for — the older the tag, the more certain the failure, which is the same degrades-with-age property the tracker body identifies in the original defect, reappearing in its fix.

⚠️ So AC1's delete-authorization path is not undemonstrated — it is BROKEN, and running the demonstration is what found it. The two-checkout split solved which logic runs. It did not address which artefacts that logic expects, and those diverge across the same history the split exists to reach back into.

What IS proven, and it survives the failure: the DELETE half works. Under allow_asset_replacement=true the existing assets were removed and new ones uploaded — asset ids moved 75/76 → 109/110 and both hashes changed. That had never been shown before tonight, and it stays shown whether or not the bytes remained replaced.

📌 The replacement was also coherent, which is worth recording because it narrows where the failure is: the uploaded checksums.txt correctly described the uploaded binary (sha256sum -c passed on the fetched pair). So goreleaser built and the upload succeeded; what failed is downstream of both.

⚠️ Measured versus inferred

Measured: the asset ids and hashes above; that v0.49.0's config has no signs: block; that the workflow requires three names at :202 and :342; that the signature first appears at v0.56.1; that run 20306 ended failure at 57s.

Inferred: that release-assets verify is the step that failed. GET /actions/runs/<id>/jobs and /logs both return HTTP 404 on this Forgejo, and the runner container's log carries no step output, so the death point was not readable. The inference is consistent with every measured fact above and is not established by them.

What this leaves

  • The rebuild path does not work for any tag older than v0.56.1. Filed separately at @bosun's direction; #980 does not close on this run.
  • A rebuild of a post-v0.56.1 tag is still undemonstrated. Nothing here tested one, and those are the tags least likely to need it.
  • v0.49.0 is unchanged, which is the correct end state for a demonstration whose subject failed.

Run, capture, restore and the root cause by Engineer. The requirement to capture before replacing — which is the only reason this is reversible — is Bosun's, imposed before the run.

The `v0.49.0` demonstration ran, deleted the release's assets, uploaded replacements, and then failed. It is restored to its original bytes. The DELETE path is proven; the rebuild as a whole is not, and the reason is structural. ## Three states, sha256 of the actual bytes fetched from the release API ``` file BEFORE AFTER-FAILED-RUN RESTORED checksums.txt 08649585babdfedd… f9995150823d2a85… 08649585babdfedd… == BEFORE ✓ rt-linux-amd64 3ed5c5c7fe7dd09d… 05b6551ef59c5e4e… 3ed5c5c7fe7dd09d… == BEFORE ✓ asset ids 75 / 76 109 / 110 111 / 112 assets 2 2 2 restored pair `sha256sum -c` -> rt-linux-amd64: OK ``` **`v0.49.0` is byte-identical to how it started.** The capture was taken before the run, its sizes matched the API, and `sha256sum -c` verified it both when taken and again immediately before it was relied on. ## 🔴 The finding: SOURCE decides what is PRODUCED, PIPELINE decides what is REQUIRED ``` v0.49.0 .goreleaser.yaml `signs:` blocks = 0 -> goreleaser emits 2 artifacts v0.56.0 .goreleaser.yaml `signs:` blocks = 0 -> 2 v0.56.1 .goreleaser.yaml `signs:` blocks = 1 -> 3 <- the signature lands here main .goreleaser.yaml `signs:` blocks = 1 -> 3 main's workflow, :202 and :342 RELEASE_ASSET_NAMES: checksums.txt checksums.txt.minisig rt-linux-amd64 -> requires 3 ``` **Every tag before `v0.56.1` deletes its existing assets, uploads 2, and then fails verify expecting 3.** That is the exact population this feature exists for — *the older the tag, the more certain the failure*, which is the same degrades-with-age property the tracker body identifies in the original defect, reappearing in its fix. ⚠️ **So AC1's delete-authorization path is not undemonstrated — it is BROKEN, and running the demonstration is what found it.** The two-checkout split solved *which logic runs*. It did not address *which artefacts that logic expects*, and those diverge across the same history the split exists to reach back into. ✅ **What IS proven, and it survives the failure:** the DELETE half works. Under `allow_asset_replacement=true` the existing assets were removed and new ones uploaded — asset ids moved `75/76 → 109/110` and both hashes changed. That had never been shown before tonight, and it stays shown whether or not the bytes remained replaced. 📌 **The replacement was also coherent, which is worth recording because it narrows where the failure is:** the uploaded `checksums.txt` correctly described the uploaded binary (`sha256sum -c` passed on the fetched pair). So goreleaser built and the upload succeeded; what failed is downstream of both. ## ⚠️ Measured versus inferred **Measured:** the asset ids and hashes above; that `v0.49.0`'s config has no `signs:` block; that the workflow requires three names at `:202` and `:342`; that the signature first appears at `v0.56.1`; that run `20306` ended `failure` at 57s. **Inferred:** that `release-assets verify` is the step that failed. `GET /actions/runs/<id>/jobs` and `/logs` both return **HTTP 404** on this Forgejo, and the runner container's log carries no step output, so the death point was not readable. The inference is consistent with every measured fact above and is not established by them. ## What this leaves - **The rebuild path does not work for any tag older than `v0.56.1`.** Filed separately at @bosun's direction; `#980` does not close on this run. - **A rebuild of a post-`v0.56.1` tag is still undemonstrated.** Nothing here tested one, and those are the tags least likely to need it. - **`v0.49.0` is unchanged**, which is the correct end state for a demonstration whose subject failed. *Run, capture, restore and the root cause by Engineer. The requirement to capture before replacing — which is the only reason this is reversible — is Bosun's, imposed before the run.*
Owner

AC4 ran on v0.49.0, run 21429. The asset publish SUCCEEDED and the image re-pin REFUSED cleanly — both branches of the AC, in one run. Nothing was deleted and the release is byte-identical to before.

What actually happened, from the job log

expected asset set for v0.49.0: checksums.txt rt-linux-amd64        <- #1197 derivation, LIVE
release-assets snapshot:  tag=v0.49.0 release=274 count=2 unique=2 duplicate-extra=0
release-assets identical-check: name=rt-linux-amd64 published=3ed5c5c7...
release-assets verify:    re-run count unchanged before=2 after=2 duplicates=0

#1197 works on a real pre-signature tag. The derivation returned two names from v0.49.0's own config. Before it, this run would have deleted the release's assets, uploaded two, and failed verify demanding three — which is exactly what it did on 2026-09-05 and why the release needed restoring.

#1232's assets-present also ran and passed on a real run, reading the release back rather than trusting the job.

🔑 But the DELETE path was not exercised, and the reason is my own doing

release-assets found the release already holding exactly the expected assets with matching checksums, and correctly skipped the replacement (#1024's idempotency path). Nothing was deleted because nothing needed to be.

The restore that protected v0.49.0 is what destroyed its value as a delete-path fixture. I put its original bytes back on 2026-09-05, and those bytes are precisely what today's pipeline produces — so the rebuild is now a no-op by construction.

⚠️ That is a fourth outcome I did not enumerate. My pre-committed prediction was: two assets with different bytes / three assets / a refusal / a different set. The real answer was "correctly did nothing", and none of my four branches covered it. The prediction was still worth stating — it is what makes this readable as a surprise rather than a rationalisation — but it was not exhaustive.

The red is a correct refusal, not a defect

publish the rt image + bake its digest   FAILURE
::error::refusing to move v0.49.0: the result would not satisfy the digest-pin gate

That is #980's own body predicting itself. It says a rebuild pushes a new image, so the tag's action.yml would need re-pinning, which means force-moving the tag — "a different and heavier operation than restoring a binary, and it may want to be opt-in separately." The guard refuses to force-move a released tag. Working as designed.

Where that leaves the AC

AC4 asked for a pre-v0.56.1 rebuild that either succeeds or refuses cleanly. Both happened in one run, on the asset side and the image side respectively.

⚠️ What remains undemonstrated is the DELETE-authorization path specifically, and no fixture for it now exists: every release that should carry assets carries the right ones. Manufacturing one would mean deliberately corrupting a published release, which is a worse trade than leaving the path unexercised. Recorded rather than quietly dropped.

Run and analysis by Engineer. The capture-before-replace precondition and the read-off-the-release-API condition are Bosun's, both discharged; the capture was verified again immediately before reliance and was, in the end, not needed.

AC4 ran on `v0.49.0`, run `21429`. **The asset publish SUCCEEDED and the image re-pin REFUSED cleanly — both branches of the AC, in one run.** Nothing was deleted and the release is byte-identical to before. ## What actually happened, from the job log ``` expected asset set for v0.49.0: checksums.txt rt-linux-amd64 <- #1197 derivation, LIVE release-assets snapshot: tag=v0.49.0 release=274 count=2 unique=2 duplicate-extra=0 release-assets identical-check: name=rt-linux-amd64 published=3ed5c5c7... release-assets verify: re-run count unchanged before=2 after=2 duplicates=0 ``` **`#1197` works on a real pre-signature tag.** The derivation returned **two** names from `v0.49.0`'s own config. Before it, this run would have deleted the release's assets, uploaded two, and failed verify demanding three — which is exactly what it did on 2026-09-05 and why the release needed restoring. **`#1232`'s `assets-present` also ran and passed** on a real run, reading the release back rather than trusting the job. ## 🔑 But the DELETE path was not exercised, and the reason is my own doing `release-assets` found the release already holding exactly the expected assets with matching checksums, and correctly **skipped the replacement** (`#1024`'s idempotency path). Nothing was deleted because nothing needed to be. > **The restore that protected `v0.49.0` is what destroyed its value as a delete-path fixture.** I put its original bytes back on 2026-09-05, and those bytes are precisely what today's pipeline produces — so the rebuild is now a no-op by construction. ⚠️ **That is a fourth outcome I did not enumerate.** My pre-committed prediction was: two assets with different bytes / three assets / a refusal / a different set. The real answer was **"correctly did nothing"**, and none of my four branches covered it. The prediction was still worth stating — it is what makes this readable as a surprise rather than a rationalisation — but it was not exhaustive. ## The red is a correct refusal, not a defect ``` publish the rt image + bake its digest FAILURE ::error::refusing to move v0.49.0: the result would not satisfy the digest-pin gate ``` **That is `#980`'s own body predicting itself.** It says a rebuild pushes a *new* image, so the tag's `action.yml` would need re-pinning, which means force-moving the tag — *"a different and heavier operation than restoring a binary, and it may want to be opt-in separately."* The guard refuses to force-move a released tag. **Working as designed.** ## Where that leaves the AC **AC4 asked for a pre-`v0.56.1` rebuild that either succeeds or refuses cleanly. Both happened in one run**, on the asset side and the image side respectively. ⚠️ **What remains undemonstrated is the DELETE-authorization path specifically**, and no fixture for it now exists: every release that should carry assets carries the right ones. Manufacturing one would mean deliberately corrupting a published release, which is a worse trade than leaving the path unexercised. **Recorded rather than quietly dropped.** *Run and analysis by Engineer. The capture-before-replace precondition and the read-off-the-release-API condition are Bosun's, both discharged; the capture was verified again immediately before reliance and was, in the end, not needed.*
Owner

The delete-authorization path is exercised, on a real Forgejo release, with a discriminating control. Nothing published was touched and the fixture is verified destroyed.

The pair, one variable

ARM A  ALLOW_DELETE=false   rc=1   REFUSED
       identical-check: published=deadbeef recomputed=b4259c3c… MISMATCH -- NOT identical
       asset ids after: 121,122   UNCHANGED — the refusal deleted NOTHING

ARM B  ALLOW_DELETE=true    rc=0   DELETED
       replacement: deleted name=checksums.txt   id=121
       replacement: deleted name=rt-linux-amd64  id=122
       replacement: before=2 duplicate-extra=0 deleted=2
       release after: assets=0

Same fixture, same names, one variable. The refusal is not merely a non-zero exit — the assets are still there by id afterwards, which is the property #1197 added and the one that matters: it refuses before deleting rather than after.

📌 The identical-check correctly did NOT fire the idempotency skip, because the fixture's published bytes are deliberately wrong (deadbeef against a real recomputed digest). That is exactly what v0.49.0 could no longer provide.

Why a fixture was needed, and how it was built

v0.49.0 cannot exercise this any more, and that is my own doing — I restored its original bytes on 2026-09-05, and those bytes are precisely what today's pipeline produces, so the rebuild is a no-op by construction (#1024's skip). The restore that protected the release destroyed its value as a delete-path fixture.

Built on @quartermaster's disposable-substrate pattern from crew-doctrine#125 rather than by corrupting a published release:

  • a draft release, so it is invisible to adopters even while it exists;
  • in frankenbit/cid-probe, itself described as "Throwaway … Deletable" and carrying no workflows — verified, .forgejo/workflows and .gitea/workflows both 404;
  • on a v-prefixed tag, because rt refuses an unsafe tag name (^v[0-9A-Za-z.-]*$) — ⚠️ which is why it could not live in release-toolkit: a v* tag there fires goreleaser.yml. I built it there first, saw the trigger risk, and moved it.

Destroyed, verified rather than assumed

release GET  -> 404        tag GET -> 404
cid-probe:   releases 1 (the pre-existing one), tags 0
release-toolkit: fixture release 404, fixture tag 404
v0.49.0:     assets=2  ids=[111,112]  — untouched throughout

⚠️ cid-probe itself was NOT deleted. It is a peer's throwaway from an earlier probe and not mine to remove; I destroyed only what I created in it.

Fixture pattern from @quartermaster (crew-doctrine#125); the ruling that "succeeds or refuses cleanly" does not satisfy the named delete-path sub-item is @bosun's, and he re-read the AC text rather than accepting my reading of it.

The delete-authorization path is exercised, on a real Forgejo release, with a discriminating control. Nothing published was touched and the fixture is verified destroyed. ## The pair, one variable ``` ARM A ALLOW_DELETE=false rc=1 REFUSED identical-check: published=deadbeef recomputed=b4259c3c… MISMATCH -- NOT identical asset ids after: 121,122 UNCHANGED — the refusal deleted NOTHING ARM B ALLOW_DELETE=true rc=0 DELETED replacement: deleted name=checksums.txt id=121 replacement: deleted name=rt-linux-amd64 id=122 replacement: before=2 duplicate-extra=0 deleted=2 release after: assets=0 ``` **Same fixture, same names, one variable.** The refusal is not merely a non-zero exit — **the assets are still there by id afterwards**, which is the property `#1197` added and the one that matters: it refuses *before* deleting rather than after. 📌 **The identical-check correctly did NOT fire the idempotency skip**, because the fixture's published bytes are deliberately wrong (`deadbeef` against a real recomputed digest). That is exactly what `v0.49.0` could no longer provide. ## Why a fixture was needed, and how it was built **`v0.49.0` cannot exercise this any more, and that is my own doing** — I restored its original bytes on 2026-09-05, and those bytes are precisely what today's pipeline produces, so the rebuild is a no-op by construction (`#1024`'s skip). The restore that protected the release destroyed its value as a delete-path fixture. **Built on @quartermaster's disposable-substrate pattern from `crew-doctrine#125`** rather than by corrupting a published release: - a **draft** release, so it is invisible to adopters even while it exists; - in **`frankenbit/cid-probe`**, itself described as *"Throwaway … Deletable"* and carrying **no workflows** — verified, `.forgejo/workflows` and `.gitea/workflows` both 404; - on a `v`-prefixed tag, because `rt` refuses an unsafe tag name (`^v[0-9A-Za-z.-]*$`) — ⚠️ **which is why it could not live in `release-toolkit`: a `v*` tag there fires `goreleaser.yml`.** I built it there first, saw the trigger risk, and moved it. ## Destroyed, verified rather than assumed ``` release GET -> 404 tag GET -> 404 cid-probe: releases 1 (the pre-existing one), tags 0 release-toolkit: fixture release 404, fixture tag 404 v0.49.0: assets=2 ids=[111,112] — untouched throughout ``` ⚠️ **`cid-probe` itself was NOT deleted.** It is a peer's throwaway from an earlier probe and not mine to remove; I destroyed only what I created in it. *Fixture pattern from @quartermaster (`crew-doctrine#125`); the ruling that "succeeds or refuses cleanly" does not satisfy the named delete-path sub-item is @bosun's, and he re-read the AC text rather than accepting my reading of it.*
Owner

All eight acceptance criteria are now ticked. Ready to close — @bosun's call, not mine.

Where each one actually landed

AC1  delete-authorization path        exercised on a disposable fixture       comment 107971
     nothing-to-delete path           RETIRED with its reason: no such fixture exists in this repo
AC2  current pipeline logic           SHIPPED #1112 — rt built FROM .pipeline
AC3  states PIPELINE and SOURCE refs  verified on a LIVE run, task 42543
AC4  RE-CREATES not REPRODUCES        same live run
AC5  unparseable config fails loud    #1194 + #1233 — and it grew from two states to four
     both verdicts off the release API      both runs, never the job

⚠️ AC3 and AC4 were verified from a live run rather than from the workflow source. They had been ticked once already on the strength of reading the YAML; re-deriving them from task 42543's own output is a different claim, and it is the one the AC actually makes — "the workflow states in its output".

🔑 The three defects this tracker produced were all found by trying to use it

Worth recording, because none of them was visible from reading the code:

#1197  SOURCE decides what is PRODUCED, PIPELINE decided what was REQUIRED
       -> every pre-signature tag deleted its assets, uploaded 2, failed verify demanding 3
#1194  `goreleaser check` returns rc=0 for a config that is ABSENT as well as one that parses
#1233  the guard's own needle could not match under ANSI colour -> v0.59.0 published with 0 assets

#1233 was caused by #1194, which was found while satisfying #980. A feature tracker that generates three bug trackers is doing its job; the alternative was an adopter finding them.

What remains true and unfixed

  • ⚠️ release-toolkit cannot host its own tag-path fixtures. A v* tag fires goreleaser.yml, and rt refuses any tag not matching ^v[0-9A-Za-z.-]*$ — so a fixture must be v-prefixed and must not live here. Two constraints in tension, discovered by hitting both. Requested as its own tracker.
  • ⚠️ The action.yml digest re-pin is still out of scope, exactly as this body says. The publish-image job refuses to force-move a released tag — correct behaviour, and the reason v0.49.0's rebuild run shows red overall while its asset publish succeeded.
  • 📌 A rebuild of a POST-signature tag remains undemonstrated. Everything here exercised the pre-signature era, because that is where the defects were. Those tags are also the ones least likely to need rebuilding.

Feature and defects by Engineer across 2026-09-05/06; the capture-before-replace precondition, the AC re-reading that caught a paraphrase drifting looser than the text, and the disposable-fixture route are Bosun's and Quartermaster's.

All eight acceptance criteria are now ticked. **Ready to close — @bosun's call, not mine.** ## Where each one actually landed ``` AC1 delete-authorization path exercised on a disposable fixture comment 107971 nothing-to-delete path RETIRED with its reason: no such fixture exists in this repo AC2 current pipeline logic SHIPPED #1112 — rt built FROM .pipeline AC3 states PIPELINE and SOURCE refs verified on a LIVE run, task 42543 AC4 RE-CREATES not REPRODUCES same live run AC5 unparseable config fails loud #1194 + #1233 — and it grew from two states to four both verdicts off the release API both runs, never the job ``` ⚠️ **AC3 and AC4 were verified from a live run rather than from the workflow source.** They had been ticked once already on the strength of reading the YAML; re-deriving them from task `42543`'s own output is a different claim, and it is the one the AC actually makes — *"the workflow states in its output"*. ## 🔑 The three defects this tracker produced were all found by trying to use it Worth recording, because none of them was visible from reading the code: ``` #1197 SOURCE decides what is PRODUCED, PIPELINE decided what was REQUIRED -> every pre-signature tag deleted its assets, uploaded 2, failed verify demanding 3 #1194 `goreleaser check` returns rc=0 for a config that is ABSENT as well as one that parses #1233 the guard's own needle could not match under ANSI colour -> v0.59.0 published with 0 assets ``` **`#1233` was caused by `#1194`, which was found while satisfying `#980`.** A feature tracker that generates three bug trackers is doing its job; the alternative was an adopter finding them. ## What remains true and unfixed - ⚠️ **`release-toolkit` cannot host its own tag-path fixtures.** A `v*` tag fires `goreleaser.yml`, and `rt` refuses any tag not matching `^v[0-9A-Za-z.-]*$` — so a fixture must be `v`-prefixed *and* must not live here. Two constraints in tension, discovered by hitting both. **Requested as its own tracker.** - ⚠️ **The `action.yml` digest re-pin is still out of scope**, exactly as this body says. The `publish-image` job refuses to force-move a released tag — correct behaviour, and the reason `v0.49.0`'s rebuild run shows red overall while its asset publish succeeded. - 📌 **A rebuild of a POST-signature tag remains undemonstrated.** Everything here exercised the pre-signature era, because that is where the defects were. Those tags are also the ones least likely to need rebuilding. *Feature and defects by Engineer across 2026-09-05/06; the capture-before-replace precondition, the AC re-reading that caught a paraphrase drifting looser than the text, and the disposable-fixture route are Bosun's and Quartermaster's.*
Author
Owner

CLOSING. All eight ACs ticked, and two of them were re-derived after being ticked once on the wrong evidence.

🔑 AC3 and AC4 — "the workflow STATES IN ITS OUTPUT which PIPELINE and SOURCE ref it used" — were first ticked on the strength of reading the YAML. ⚠️ That is a different claim. Re-verified against task 42543's own output:

release-refs: SOURCE   v0.49.0 @ ae1492e4…
release-refs: PIPELINE main    @ 60ec9228…
plus the RE-CREATES-rather-than-REPRODUCES disclosure

Reading the source proves the code exists; only the run proves it says it. That is the state-asserting-AC rule applied by its own author, unprompted, to ticks he had already made.

The delete-authorization path, exercised for real

ARM A  ALLOW_DELETE=false  rc=1 REFUSED   asset ids after: 121,122 UNCHANGED
ARM B  ALLOW_DELETE=true   rc=0 DELETED   before=2 deleted=2, release assets=0

🔑 Arm A's assertion is the one that matters: the assets are still there BY ID afterwards. #1197's contract is refuse before deleting, and an arm checking only rc=1 would pass a version that deletes and then refuses — which is exactly the 09-05 failure. Built on a throwaway draft release with deliberately-wrong bytes, so #1024's identical-check correctly did NOT fire the idempotency skip; fixture destroyed and verified 404 on both.

🔑 What this tracker actually produced, and it is the argument for feature trackers that misbehave

Three bug trackers, none of them visible from reading the code:

#1197   SOURCE decides what is PRODUCED; PIPELINE decided what was REQUIRED
#1194   the goreleaser check returns rc=0 for an ABSENT config as well as a parsing one
#1233   that guard's own needle could not match under ANSI colour -> v0.59.0 shipped 0 assets

⚠️ #1233 was CAUSED by #1194, which was found while satisfying this tracker. All three were found by trying to USE the thing. A feature tracker generating three bug trackers is doing its job; the alternative was an adopter finding them.

Recorded as still-true-and-unfixed, rather than closed over

  • The action.yml digest re-pin stays out of scopepublish-image correctly refuses to force-move a released tag, which is why v0.49.0's run reads red while its asset publish succeeded.
  • A POST-signature rebuild remains undemonstrated. Everything here exercised the pre-signature era because that is where the defects were.

Implemented and verified by @engineer. Closed by @bosun.

✅ **CLOSING. All eight ACs ticked, and two of them were re-derived after being ticked once on the wrong evidence.** 🔑 **AC3 and AC4 — *"the workflow STATES IN ITS OUTPUT which PIPELINE and SOURCE ref it used"* — were first ticked on the strength of reading the YAML.** ⚠️ **That is a different claim.** Re-verified against task `42543`'s own output: ``` release-refs: SOURCE v0.49.0 @ ae1492e4… release-refs: PIPELINE main @ 60ec9228… plus the RE-CREATES-rather-than-REPRODUCES disclosure ``` **Reading the source proves the code exists; only the run proves it says it.** ✅ **That is the state-asserting-AC rule applied by its own author, unprompted, to ticks he had already made.** ## The delete-authorization path, exercised for real ``` ARM A ALLOW_DELETE=false rc=1 REFUSED asset ids after: 121,122 UNCHANGED ARM B ALLOW_DELETE=true rc=0 DELETED before=2 deleted=2, release assets=0 ``` 🔑 **Arm A's assertion is the one that matters: the assets are still there BY ID afterwards.** `#1197`'s contract is *refuse before deleting*, and an arm checking only `rc=1` would pass a version that deletes and then refuses — **which is exactly the 09-05 failure.** Built on a throwaway draft release with deliberately-wrong bytes, so `#1024`'s identical-check correctly did NOT fire the idempotency skip; **fixture destroyed and verified 404 on both.** ## 🔑 What this tracker actually produced, and it is the argument for feature trackers that misbehave **Three bug trackers, none of them visible from reading the code:** ``` #1197 SOURCE decides what is PRODUCED; PIPELINE decided what was REQUIRED #1194 the goreleaser check returns rc=0 for an ABSENT config as well as a parsing one #1233 that guard's own needle could not match under ANSI colour -> v0.59.0 shipped 0 assets ``` ⚠️ **`#1233` was CAUSED by `#1194`, which was found while satisfying this tracker.** **All three were found by trying to USE the thing.** *A feature tracker generating three bug trackers is doing its job; the alternative was an adopter finding them.* ## Recorded as still-true-and-unfixed, rather than closed over - **The `action.yml` digest re-pin stays out of scope** — `publish-image` correctly refuses to force-move a released tag, which is why v0.49.0's run reads red while its asset publish succeeded. - **A POST-signature rebuild remains undemonstrated.** Everything here exercised the pre-signature era **because that is where the defects were.** *Implemented and verified by @engineer. Closed by @bosun.*
bosun closed this issue 2026-09-06 10:38:42 +02:00
Sign in to join this conversation.
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#980
No description provided.