fix(release): recover a tag whose release never published
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m15s

An existing tag was treated as "nothing to release", unconditionally.
That is wrong in the one case it matters: a tag with no release behind
it means an earlier run tagged and then died before publishing -- which
is exactly what happened on servuo-plugins' first release, where absent
REGISTRY_* secrets took the release API call to 401 after the tag had
been pushed. Standing down on the tag alone makes that permanent. Every
later run sees the tag, sets RELEASE=false, and the release never
appears; the version is unpublishable forever.

The tag now decides nothing on its own. The API does:

  200 -> a release exists, stand down
  404 -> tag without release, reuse the tag and publish what is missing
  else -> refuse and exit 1

The last arm matters as much as the others. A 000 from a network failure
or a 401 from a bad token is not evidence of absence, and guessing "no
release" would republish over a good one.

Note the 404 arm deliberately overrides the RELEASE=false decided just
above it: with the tag in place there are no releasable commits after
it, so the normal path always stands down -- which is why this could
never self-heal on its own.

Two consequences handled with it. The changelog range is now
previous-tag..this-tag on a recovery run, since a run finishing an
earlier one has nothing after the tag and would otherwise publish an
empty change list. And the tagging step tolerates the tag already
existing, because `git tag` on an existing name fails under `set -e`
while pushing an identical tag is a harmless no-op -- a push that does
fail there means the remote tag points somewhere else, which should
stop the run.

This is the same handling installer/release.yml already carries; link
was the copy that still had the trap.

Verified by extracting this step and driving it through four scenarios
against a real clone with the live tags, with curl stubbed to return
each status: a normal patch bump (v1.1.1, release=true), a tag with no
release (recovers, release=true), a tag with a release (stands down),
and an unreachable API (exit 1, publishes nothing).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-05 13:05:31 -05:00
parent 2724c292a0
commit 07021d38c9

View File

@@ -80,6 +80,8 @@ jobs:
# ── RELEASE ENGINE: decide the next version + changelog ──────────────
- name: Plan the release (version + changelog)
id: plan
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
mkdir -p dist
@@ -117,21 +119,59 @@ jobs:
VERSION="$(bump "${LAST_TAG#v}" "$BUMP")"
fi
# An existing tag is NOT automatically "nothing to do". A tag with no
# release behind it means a previous run tagged and then died before
# publishing — which is exactly what happened on servuo-plugins' first
# release, where absent REGISTRY_* secrets took the release API call to
# 401 after the tag had already been pushed. Standing down on the tag
# alone makes that state permanent: every later run sees the tag, sets
# RELEASE=false, and the release never appears. Note this deliberately
# OVERRIDES the RELEASE=false decided just above — with the tag in
# place there are no releasable commits after it, so the normal path
# would stand down, which is exactly why it could never self-heal.
REUSE_TAG=false
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
echo "Tag v${VERSION} already exists — nothing to release."
RELEASE=false
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')"
REL_HTTP="$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token ${CI_TOKEN}" "https://${GITEA_HOST}/api/v1/repos/${REPO}/releases/tags/v${VERSION}" || echo 000)"
if [ "$REL_HTTP" = "200" ]; then
echo "Tag v${VERSION} already has a release — nothing to do."
RELEASE=false
elif [ "$REL_HTTP" = "404" ]; then
echo "::warning::Tag v${VERSION} exists but has no release — a previous run failed after tagging. Reusing the tag and publishing the release it is missing."
REUSE_TAG=true
RELEASE=true
else
# Anything else (000 from a network failure, 401/403 from a bad
# token) is not evidence of absence. Guessing "no release" would
# re-publish over a good one, so refuse instead.
echo "::error::Could not determine whether a release exists for v${VERSION} (HTTP ${REL_HTTP}). Refusing to guess."
exit 1
fi
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.
if [ "$REUSE_TAG" = true ]; then
PREV_TAG="$(git describe --tags --match 'v*' --abbrev=0 "v${VERSION}^" 2>/dev/null || true)"
if [ -n "$PREV_TAG" ]; then CL_RANGE="${PREV_TAG}..v${VERSION}"; else CL_RANGE="v${VERSION}"; fi
SINCE="$PREV_TAG"
else
CL_RANGE="$RANGE"
SINCE="$LAST_TAG"
fi
CL_SUBJECTS="$(git log --no-merges --format='%s' $CL_RANGE || true)"
{
echo "## ${BIN} v${VERSION}"
echo
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)"
FEATS="$(echo "$CL_SUBJECTS" | grep -E '^feat' || true)"
FIXES="$(echo "$CL_SUBJECTS" | grep -E '^(fix|perf)' || true)"
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
echo "### All changes"
if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi
echo "$SUBJECTS" | sed 's/^/- /'
if [ -n "$SINCE" ]; then echo "Since ${SINCE}:"; fi
echo "$CL_SUBJECTS" | sed 's/^/- /'
} > dist/CHANGELOG.md
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
@@ -266,7 +306,16 @@ jobs:
else
echo "Version unchanged (first release) — no bump commit needed."
fi
git tag "${TAG}"
# The tag may already exist when finishing a run that died after tagging
# (see the plan step). `git tag` on an existing name fails under
# `set -e`; pushing an identical existing tag is a harmless no-op. A
# push that fails here means the remote tag points somewhere else,
# which SHOULD stop the run.
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag ${TAG} already exists — reusing it."
else
git tag "${TAG}"
fi
git push origin "${TAG}"
# ── RELEASE ENGINE: create the Gitea release + upload assets ─────────