From 65998692aeed9251bdcce049f091bb85baca9a27 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 24 Aug 2026 13:57:08 -0500 Subject: [PATCH] ci(release): sweep every tag for a missing release, not just this run's The first commit on this branch said the plan step "CAN recover an orphan, but only on a run that reaches it". Checking link and servuo-plugins for the same gaps showed that understated it. The recovery is VERSION-SCOPED. It computes VERSION from the newest tag plus the conventional-commit bump, then only checks refs/tags/v${VERSION}. So it recovers an orphan on the very next run and is useless afterwards: once any releasable commit lands, the next run computes a NEW version and never looks at the old tag again. The orphan becomes permanent and silent. servuo-plugins proved it, and the proof is pointed. Its v0.1.0 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 commit that ADDED the recovery to that repo was itself typed "fix(release): preflight credentials and recover the orphaned v0.1.0 tag", so it bumped to v0.1.1, and the run that introduced the recovery stepped straight past the tag it was written to rescue. The retry added in the previous commit makes an orphan much less likely, but it does not make one impossible -- a cancelled job or a dying runner produces the same state with no 500 anywhere -- and until now nothing would ever have mentioned it again. So the plan step now sweeps every v* tag and warns about any without a release. It WARNS rather than recovers, on the org lead's decision. 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; and a routine push silently republishing ancient history is not a thing this pipeline should be able to do. Recovery stays limited to the version the run computed. It also never fails the run. A sweep that can break a good release is a sweep someone will delete. Verified by running the loop against the real repositories rather than a stub, since the only thing worth proving is that it tells a clean repo from a dirty one: link (9 tags): clean servuo-plugins (4 tags): ::warning::Tags with no release: v0.1.0 installer (2 tags): clean and again after servuo-plugins#15 deleted that tag, where all three report clean. Every run block bash -n clean, the YAML parses, and no empty template token. Companion PRs: link#33 and servuo-plugins#15. Co-Authored-By: Claude --- .gitea/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index d041440..4fcd44d 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -165,6 +165,39 @@ jobs: fi fi + # ── Orphan sweep ──────────────────────────────────────────────── + # + # The check above is VERSION-SCOPED: it only ever asks about the one + # version this run computed. That is enough to recover an orphan on + # the very next run, and useless afterwards — once any releasable + # commit lands, the next run computes a NEW version, never looks at + # the old tag again, and the orphan becomes permanent and silent. + # + # servuo-plugins v0.1.0 is the proof, and the proof is pointed: the + # commit that ADDED the recovery above was itself typed + # `fix(release): ... recover the orphaned v0.1.0 tag`, so it bumped to + # v0.1.1 — and the run that introduced the recovery stepped straight + # past the tag it was written to rescue. That tag is still orphaned. + # + # So every v* tag is checked, and anything missing a release is + # WARNED about. Deliberately not recovered: 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. + # A human decides whether to recover or drop it. + # + # Never fails the run. A sweep that can break a good release is a + # sweep someone will delete. + ORPHANS="" + for T in $(git tag -l 'v*' --sort=-v:refname); do + T_HTTP="$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: token $(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/releases/tags/${T}" || echo 000)" + [ "$T_HTTP" = "404" ] && ORPHANS="${ORPHANS} ${T}" + done + if [ -n "${ORPHANS}" ]; then + echo "::warning::Tags with no release:${ORPHANS} — a run failed after tagging. Publish or delete them; this job will not do either." + fi + # Changelog range. A recovery run has nothing after the tag, so # summarize what the tag itself contains rather than emitting an empty # list: the range that produced it, i.e. previous-tag..this-tag. -- 2.49.1