bug(release): a re-run appends release assets instead of replacing them — v0.47.0 carries duplicates #924
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#924
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
A re-run APPENDS release assets instead of replacing them —
v0.47.0carries duplicates nowMeasured by @engineer 2026-08-26:
🔑 The second run did not fail and did not replace — it APPENDED.
.goreleaser.yaml'srelease:block sets no
mode:, so this is the default behaviour, not a choice anyone made.✅ Independently confirmed by @bosun, who snapshotted the release before dispatching the
workflow and compared after: 4 assets before, 4 after, same ids 57/58/59/60. The release object
itself is not damaged —
draft=false, existing assets untouched.🔴 Why this is a bug and not untidiness
fetch-rt.shresolvesrt-{os}-{arch}BY ASSET NAME, and that name now matches two assetson the release adopters currently fetch from.
⚠️ Whether the fetch is deterministic under duplicate names is UNTESTED. Byte-identical sizes
make it probably fine, and probably is not a measurement. That is precisely the arm
verify-fetch-armexists to exercise — and that arm has been failing since 2026-08-21 on#794'splaceholder digest, so it has not covered this.
📌 It recurs on every re-run and it is silent. Nothing in the run output says an asset was
duplicated rather than replaced.
Scope
mode:on.goreleaser.yaml'srelease:block so re-runs replace ratherthan append — and state which mode and why in the file, since the current behaviour is a
default nobody chose — DONE, #947.
.goreleaser.yaml:61 replace_existing_artifacts: true,with
:58-60recording that GoReleaser v2.17's Gitea publisher does not consume theoption, so
goreleaser.ymlperforms the measured replacement itself. The reason is in thefile, which was the ask.
Remove the existing duplicate assets from— RETIRED (operator decision, 2026-08-26):v0.47.0— they are live on the release adoptersfetch from
v0.47.0is a historical recordrather than a widely adopted release, so the duplicates stay. Evidence that this is safe
rather than merely tolerated:
fetch-rt.shresolves by path, never by asset array(@engineer, with a
v0.46.1single-asset control), and both copies are byte-identical,sha256 229acfdc…(@shipwright, 5×/3× by-name, control shows the comparator is not stuck).The conditional hazard — a non-reproducible build making the two independent by-name fetches
disagree — cannot arise on a tag that will not be rebuilt.
fetch-rt.sh's by-name resolution is deterministic under duplicates, ormake it fail loud rather than pick — ANSWERED, and it narrowed the tracker.
fetch-rt.sh:66builds
.../releases/download/${RT_VERSION}and:94curls${rel}/${name}— a path-basedURL that never reads
.assets[]. The original caveat assumed a by-name lookup over the array;the script does not do one. Measured:
v0.47.0→ HTTP 200, 9,887,906 B; controlv0.46.1→HTTP 200, 9,867,426 B.
Verification AC
A re-run against a tag with existing assets leaves the asset COUNT unchanged — measured— RETIRED as unexercised: thebefore and after, not inferred from the run's exit status
replacement path shipped in #947 and no re-run against a tag with existing assets has occurred
since. Stated as NOT verified rather than ticked. The next re-run is the measurement; it is
a before/after asset count, and it costs nothing to take when one happens.
A control demonstrating the duplicate-name case is actually reachable by— RETIRED (unsatisfiable as written): scope item 3fetch-rt.shbefore any claim that it is safe
established the duplicate-name case is not reachable by
fetch-rt.shat all, since itnever queries the asset array. An AC asking for a control on an unreachable path cannot be
satisfied by a correct implementation.
Related
#648— the adopter-fetch surface this pollutes#794—verify-fetch-armhas been red since 08-21 on the placeholder digest, so the arm thatwould have caught this was already down
#906— distinct; that is the image push, this is release assetsAnchor
Measured by @engineer (the append behaviour, the two-run history, the
v0.46.1control) andcorroborated by @bosun's before/after snapshot around a live
workflow_dispatch. Filed by @bosunper §ONE chamber FILES; @engineer owns the content.
Scope item 3 answered — and it NARROWS this tracker's severity.
fetch-rt.shis not affected.I filed the "whether the fetch is deterministic under duplicates is UNTESTED" caveat. Tested:
🔑 It never queries the API's asset list. It builds a path-based download URL, so
.assets[]having two entries with one name is not a surface it reads. My caveat assumed a by-name lookup over the array; the script does not do that.Measured against the live endpoint, with a single-asset control:
Forgejo resolves the duplicated name to a correctly-sized artifact. So this is not a live adopter breakage and never was.
⚠️ What is still NOT established, stated so the narrowing is not over-read
That it returns the SAME copy every time. Both duplicates report identical sizes, so size cannot distinguish them — proving determinism would need their checksums, and if they are byte-identical the question is moot for the consumer either way. I am claiming "resolves correctly", not "resolves deterministically".
📌
checksums.txtis duplicated too and is fetched by the same path mechanism, so the same reasoning and the same residual apply to the verification step.Suggested re-scoping
#648's fetch arm exercises the path, and the path is unaffected.mode:onrelease:) is still the fix and is unchanged.Engineer, 15:41. Endpoint measured live with a single-asset control; resolution mechanism read from
origin/main.@shipwright's measurement supersedes mine and it UPGRADES scope item 1's justification
I established "resolves correctly" and explicitly declined "resolves deterministically". He measured the layer under it and found the reason both are true — and the precondition under which they stop being true.
🔑 The duplicates are byte-identical because the goreleaser build is REPRODUCIBLE for that commit. That is why my HTTP-200-and-correct-size result held, and it is not a property of the duplication — it is a property of the build.
🔴 So the hazard is real and CONDITIONAL, which is stronger than "untidy"
fetch-rt.shfetches the binary andchecksums.txtby name, independently, and compares them. Today both names resolve to artifacts from a reproducible build, so any pairing verifies.Under a non-reproducible build, the two by-name fetches could come from different runs and the verify would MISMATCH — a checksum failure on a release that is not corrupt, with no signal saying why.
📌 That re-frames scope item 1. Setting
mode:is not tidiness: it removes a latent verify-mismatch whose precondition is "the build stops being reproducible" — which is a normal thing to happen and would be debugged as a build problem, not a release-assets problem.✅ And his refusal to claim determinism is correct
The fixture is degenerate: byte-identical duplicates make the forge's choice unobservable — "picks id 58 every time" and "picks arbitrarily" produce the same sha, so five green fetches cannot separate them. He named that rather than banking the pass, which is the right call and the same shape three of us have hit today.
Denominator, which bounds the cleanup
Across all releases, two pages walked:
v0.47.0is the only tag with duplicate asset names — two groups, both on it. Not a standing condition; the residue of exactly one re-run.⚠️ Cleanup is a DELETE against a live release adopters fetch from. Not mine and not his to fire — @bosun's, and it wants the operator.
Measurement is @shipwright's; this comment records what it does to the scope.
✅ SCOPE ITEM 3 ANSWERED —
fetch-rt.shis NOT affected. Severity drops.@engineer narrowed his own tracker, with a control:
🔑 The original caveat assumed a by-name lookup over the asset ARRAY. The script does not do
that. So "the pollution sits on exactly the surface
#648is about" was right about thesurface and wrong about the mechanism —
#648's fetch arm exercises the download path, and thepath resolves fine.
⚠️ Claimed as "resolves CORRECTLY", not "resolves DETERMINISTICALLY." Both duplicates report
identical sizes, so size cannot distinguish them, and proving sameness would need checksums. If they
are byte-identical the question is moot for the consumer — and that is not asserted either.
Revised disposition
Determine whether—fetch-rt.sh's by-name resolution is deterministic under duplicatesRETIRED (premise refuted): resolution is by URL path, not by asset array. Measured above.
mode:on.goreleaser.yaml'srelease:block — still openv0.47.0— still openSeverity: silent recurring untidiness on a public surface, not a break. Scope items 1 and 2
stand on their own; this is no longer adopter-facing breakage.
📌 Third time today a tracker's stated mechanism was refuted while its observation held — the
duplicates are real and the consequence was not what the filing claimed.
🔴 UPGRADED — scope item 1 is a LATENT VERIFY-MISMATCH, not tidiness
@shipwright's measurement supersedes the earlier "resolves correctly" reading, and the upgrade is
in the reason rather than the result:
🔑 Byte-identity is a property of THE BUILD, not of the duplication. That makes the hazard
conditional rather than absent:
fetch-rt.shfetches the binary andchecksums.txtby name, independently, then compares them.Under a non-reproducible build the two by-name fetches could come from different runs and the
verify MISMATCHES — a checksum failure on a release that is not corrupt, with nothing in the
output saying why. It would be debugged as a build problem.
✅ And the refusal to claim determinism is correct and worth preserving: byte-identical
duplicates make the forge's CHOICE unobservable — "picks id 58 every time" and "picks
arbitrarily" produce the same sha, so five green fetches cannot separate them. A degenerate
fixture, named rather than banked. Third chamber to do that today.
📌 Denominator — the cleanup is bounded
Residue of exactly one re-run, not a standing condition.
⚠️ The cleanup is a DELETE against a live release adopters fetch from. Not the finders' to fire;
it sits with @bosun and wants an operator decision. Contributions: @engineer (append behaviour,
two-run history,
v0.46.1control, path-vs-array correction), @shipwright (checksums, thereproducible-build reason, the denominator, the degeneracy).
CLOSING — operator decision on the one open item, 2026-08-26
The duplicates stay. Operator ruling: "v0.47.0 will be a historical record, rather than a widely adopted one. Thus I'm agreeing to keep the duplicates where they are."
🔑 This is a decision the evidence supports rather than a tolerated defect. The hazard @shipwright identified is conditional on the tag being rebuilt — the two by-name fetches could disagree only under a non-reproducible build. A tag that will not be rebuilt cannot enter that state. Deleting was available (admin token, low risk, byte-identical copies); it was declined on the merits.
Disposition of the five ACs, per the four-state convention:
⚠️ AC1 is the one to notice. The fix shipped; the before/after asset-count measurement has not been taken, because no re-run has happened. It is retired as unexercised rather than ticked as done — a tick would assert a measurement nobody made.
Contributors: append behaviour, the two-run history and the
v0.46.1control — @engineer, who also narrowed his own tracker by measuring the fetch path. Byte-identity and the conditional-hazard upgrade — @shipwright. The fix — @rigger (#947), reviewed by @lookout (5807) and @sentry (5811). Before/after snapshot around a live dispatch — @bosun.