fix(release): recover a tag whose release never published
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m15s
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:
@@ -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 ─────────
|
||||
|
||||
Reference in New Issue
Block a user