ci(release): show the error body, retry the POST, and sweep for orphan tags #15
Reference in New Issue
Block a user
No description provided.
Delete Branch "ci/release-post-retry-and-error-body"
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?
Same hardening as installer#23 and link#33 — but this repo is where the whole problem was first seen, and it turned out to still be carrying the scar.
The two gaps, shared verbatim
installer#22's release run built every artifact, pushed its tag, then took a
500fromPOST /releasesone second later and exited 22. Re-running published it unchanged, so it was a race with the tag push, not a bad request.1.
curl -sSfprints no response body on an error status, so the cause had to be inferred from timestamps. Every call now captures the body and prints it.2. Nothing retried, so a transient 5xx became a permanent orphan. The POST now retries 5 times with a 5/10/15/20s backoff;
4xxis deliberately not retried.The asset upload gets the same treatment, and it matters more here than anywhere else: this release ships an overlay tarball plus
SHA256SUMS, the installer verifies against that file, and a release whose checksums do not cover the tarball they advertise is worse than no release at all.The third gap — and this repo is the proof
The orphan-tag recovery in the plan step is version-scoped: it computes
VERSIONfrom the newest tag plus the bump, then only ever checksrefs/tags/v${VERSION}. It recovers an orphan on the very next run and is useless afterwards, because once any releasable commit lands the next run computes a new version and never looks back.v0.1.0in this repository had been orphaned since 2026-08-04 — tag present, no release, no assets — while v0.1.1, v0.2.0 and v1.0.0 all published normally.The reason is worth reading twice. The commit that added the recovery was itself:
Typed
fix(...), so it bumped the version to v0.1.1 — and the run that introduced the recovery stepped straight past the tag it was written to rescue. It never looked back, and could not have.So the plan step now sweeps every
v*tag and warns about any without a release. It warns rather than recovers, deliberately: publishing an old version would mean building today's tree and shipping it under a tag whose tree it is not. And it never fails the run — a sweep that can break a good release is a sweep someone will delete.v0.1.0 is deleted
On the org lead's decision. Checked first: it is three releases behind, and no published bundle names it — not even
bundle-2026.08.04.json, because the tag never had a release for a bundle to point at.It was
724262548b8193ff7d31ed92136ad449464d66e6, the merge of #7, recorded in the commit message so it can be recreated if that turns out to be wrong.Verification
Every run block extracted from the YAML and
bash -nchecked, YAML parses, no empty${{ }}token.The asset loop was preserved rather than normalised — this repo ships
for f in "${TARBALL}" SHA256SUMS, not link's three binaries, and a port that flattened that would ship the wrong artifacts.Retry loop against a stubbed
curl, seven cases, all passing (first-try success, the 500-then-success race, two 500s, five 500s giving up, 403 and 404 aborting without retry, and a 000 network failure retried).Sweep against the real repositories, before and after the deletion:
Note on the commit type
Typed
ci(...)rather thanfix(...)on purpose — the plan step bumps onfeat/fix, and this changes no artifact, so afix(...)here would cut an empty release. Which is precisely the rule that stranded v0.1.0 in the first place.AI-assisted: written with Claude Code (Opus 5).
🤖 Generated with Claude Code
installer#22's release run built every artifact, pushed its tag, then took a 500 from POST /releases one second later and exited 22 -- leaving the tag orphaned with no binaries. Re-running published it unchanged, so the 500 was a race with the tag push rather than a bad request. This repo's release step has the same two gaps verbatim, and it is where the whole problem was first seen. `curl -sSf` prints no response body on an error status, so such a failure leaves only "curl: (22) ... error: 500" in the log and the cause has to be inferred from timestamps. Every call now captures the body and prints it on failure. Nothing retried, so a transient 5xx became a permanent orphan. The POST now retries five times with a 5/10/15/20s backoff. 4xx is deliberately not retried: a bad token or a malformed body will not improve by being sent again. The asset upload gets the same treatment. That matters more here than anywhere else: this release ships an overlay tarball and a SHA256SUMS, and a release whose checksums do not cover the tarball they advertise is worse than no release, because that file is the trust anchor and the installer verifies against it. The third gap is the one this repo proves. The orphan-tag recovery in the plan step is VERSION-SCOPED -- it computes VERSION from the newest tag plus the bump, then only checks refs/tags/v${VERSION}. That recovers an orphan on the very next run and is useless afterwards, because once any releasable commit lands the next run computes a NEW version and never looks at the old tag again. v0.1.0 was the proof, and the proof is pointed: the commit that ADDED that recovery was itself typed "fix(release): preflight credentials and recover the orphaned v0.1.0 tag", so it bumped the version to v0.1.1 -- and the run that introduced the recovery stepped straight past the tag it was written to rescue. The tag stayed orphaned from 2026-08-04 until today. So the plan step now sweeps every v* tag and warns about any without a release. It warns rather than recovers, deliberately: publishing an old version would mean building today's tree and shipping it under a tag whose tree it is not, which is worse than the inconsistency it fixes. It never fails the run either -- a sweep that can break a good release is a sweep someone will delete. v0.1.0 itself is deleted, on the org lead's decision. Nothing referenced it: it is three releases behind, and no published bundle names it -- not even bundle-2026.08.04, because the tag never had a release for a bundle to point at. It was724262548b, the merge of #7, recorded here so the tag can be recreated if that turns out to be wrong. Verified by extracting every run block from the YAML: bash -n clean across all of them, the YAML parses, no empty template token, the asset loop still the tarball-and-checksums pair rather than link's three binaries, the retry loop exercised against a stubbed curl across seven cases, and the sweep run against the real repositories -- reporting v0.1.0 before the deletion and clean after. Typed ci(...) rather than fix(...) on purpose: the plan step bumps on feat/fix, and this changes no artifact, so a release here would be an empty one. That is the same rule the fix commit above tripped over. Co-Authored-By: Claude <noreply@anthropic.com>