@@ -113,9 +113,11 @@ jobs:
113113
114114 # Only fires on the first release, where the baseline is 0.0.0. A patch
115115 # from there lands v0.0.1 permanently, so this is enforced rather than
116- # left to the input description.
116+ # left to the input description. Reads `previous_version` rather than
117+ # `previous_tag`, which carries the action's tag prefix and would make
118+ # this a second copy of a default owned elsewhere.
117119 - name : Require the first release to be major
118- if : ${{ steps.version.outputs.previous_tag == 'v0 .0.0' && inputs.bump != 'major' }}
120+ if : ${{ steps.version.outputs.previous_version == '0 .0.0' && inputs.bump != 'major' }}
119121 env :
120122 BUMP : ${{ inputs.bump }}
121123 run : |
@@ -182,28 +184,33 @@ jobs:
182184 # taken, the release call publishes against whatever commit that tag
183185 # points at, and immutability makes that permanent.
184186 #
185- # Claiming first leaves the tag briefly unlocked, and a tag left
187+ # Claiming first leaves the tag briefly unlocked, and one left
186188 # behind makes the next dispatch skip this version number. So the
187- # cleanup is armed before the claim, leaving no window where the tag
188- # is unowned, and it is conditional on the tag being this run's so
189- # that it never deletes one the claim lost to. Anything it cannot
190- # confirm it leaves alone: a stray tag costs a number, a wrong delete
191- # costs a release.
189+ # cleanup is armed before the claim, and acts only on a claim that
190+ # returned. Ownership is recorded rather than read back off the tag,
191+ # because a tag that beat us to the name is almost always at this
192+ # same commit: every run targets the default-branch tip.
193+ #
194+ # What is left is the one statement between the claim landing and
195+ # `claimed=1`, and it fails the safe way. A stray tag costs a version
196+ # number, a wrong delete costs a release.
192197 #
193198 # An EXIT trap covers a cancelled job as well as a failed command,
194199 # though not a SIGKILL.
200+ claimed=0
195201 cleanup() {
196- local ref_sha releases
197- ref_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq .object.sha 2>/dev/null) || return 0
198- if [ "${ref_sha}" != "${COMMIT}" ]; then
199- return 0
200- fi
201- # Listing answers "no release" with an empty result and a success,
202- # which is what tells a confirmed absence apart from a lookup that
203- # failed. Asking for the release by tag reports both the same way,
204- # and deleting on an unconfirmed absence would strip the tag off a
205- # release that did publish.
206- releases=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" \
202+ [ "${claimed}" = 1 ] || return 0
203+ local releases
204+ # The release call can land while this run dies before clearing
205+ # the trap, so a tag of ours can already be locked. Listing
206+ # answers "no release" with an empty result and a success, which
207+ # tells a confirmed absence apart from a lookup that failed.
208+ # Asking for the release by tag reports both the same way, and
209+ # deleting on an unconfirmed absence would strip the tag off a
210+ # release that did publish. One page is enough, newest first, and
211+ # it keeps this to a bounded number of calls inside whatever
212+ # grace period a cancelled job gets.
213+ releases=$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
207214 --jq '.[] | select(.tag_name == env.TAG) | .id') || return 0
208215 if [ -n "${releases}" ]; then
209216 return 0
@@ -214,6 +221,7 @@ jobs:
214221 gh api "repos/${GITHUB_REPOSITORY}/git/refs" \
215222 -f "ref=refs/tags/${TAG}" \
216223 -f "sha=${COMMIT}" > /dev/null
224+ claimed=1
217225 gh release create "${TAG}" \
218226 --repo "${GITHUB_REPOSITORY}" \
219227 --target "${COMMIT}" \
0 commit comments