Skip to content

Commit 4cf3bea

Browse files
johnscaclaude
andcommitted
arm the tag cleanup before the ref write, and verify ownership before deleting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent d1fae41 commit 4cf3bea

1 file changed

Lines changed: 29 additions & 5 deletions

File tree

‎.github/workflows/cut-release.yml‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,38 @@ jobs:
209209
# there and ignoring --target, which publishes an immutable release
210210
# against a commit nobody chose. Immutability makes the name
211211
# unusable afterwards even if the release is deleted.
212+
# Between the ref write and the release there is a tag with nothing
213+
# locking it, and the version job reads the highest tag, so a tag
214+
# left behind makes the next dispatch work out the version after
215+
# this one and skip this number silently.
216+
#
217+
# The cleanup is armed before the write rather than after it. Arming
218+
# it after leaves a window, short but real, where the tag exists and
219+
# nothing would take it back out. That ordering is only safe because
220+
# the cleanup checks that the tag is this run's before removing it,
221+
# so the 422 path below, where the tag belongs to someone else, does
222+
# not delete their tag. Every check that fails to confirm leaves the
223+
# tag alone, including an API error, since a tag left behind costs a
224+
# version number and a wrong delete costs a release.
225+
#
226+
# bash runs an EXIT trap when a signal terminates it, so this covers
227+
# a cancelled job as well as a failed command. SIGKILL defeats it,
228+
# and nothing here can change that.
229+
cleanup() {
230+
local ref_sha
231+
ref_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq .object.sha 2>/dev/null) || return 0
232+
if [ "${ref_sha}" != "${COMMIT}" ]; then
233+
return 0
234+
fi
235+
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > /dev/null 2>&1; then
236+
return 0
237+
fi
238+
gh api -X DELETE "repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}" > /dev/null 2>&1 || true
239+
}
240+
trap cleanup EXIT
212241
gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
213242
-f "ref=refs/tags/${TAG}" \
214243
-f "sha=${COMMIT}" > /dev/null
215-
# The tag now exists with no release locking it. Anything that stops
216-
# this step from here has to take it back out: the version job reads
217-
# the highest tag, so a tag left behind makes the next dispatch work
218-
# out the version after this one and skip this number silently.
219-
trap 'gh api -X DELETE "repos/${GITHUB_REPOSITORY}/git/refs/tags/${TAG}" > /dev/null 2>&1 || true' EXIT
220244
# --target is redundant now that the tag exists at the right commit,
221245
# and `gh` ignores it for an existing tag. It stays as the statement
222246
# of which commit this release is for.

0 commit comments

Comments
 (0)