workflows/release: tag after artifact checks#21073
Merged
yperbasis merged 13 commits intoMay 12, 2026
Merged
Conversation
- publish-release checkout uses fetch-depth: 1 (only the build commit is needed to create and push the tag). - Drop the App-token generation from build-release. With the tag push moved out of this job, the implicit GITHUB_TOKEN (contents: read) is enough for the read-only checkout and ls-remote check. - Fail the publish-release step when gh release create exits non-zero so In-case-of-failure removes the just-pushed tag rather than leaving it orphaned. The manual-instructions output still prints. - Tighten the new publish-time checkout condition to also gate on release_version != '', mirroring the tag step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the release GitHub Actions workflow to avoid creating/pushing the release git tag until after all build artifacts have been downloaded and verified in the publish job, preventing premature tags when sanity checks fail.
Changes:
- Remove tag creation from
build-release, keeping only a fast-fail “tag already exists” check. - After artifact verification in
publish-release, checkout the resolved build commit and create/push the release tag there. - Add
--verify-tagtogh release create, and ensure failures propagate so the rollback job can remove the pushed tag.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…fort - Comment the tag-existence recheck in publish-release: it guards against a concurrent release run pushing the same tag between build-release's check and the push here. - Make `gh release view` after `gh release create` non-fatal. The release is already created at that point, so a transient view failure must not fail the job and trigger the In-case-of-failure rollback (which would delete the tag the freshly-created release points at). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop --target from the live gh release create. With --verify-tag the tag is required to already exist, and the GitHub API ignores target_commitish when the tag exists, so --target was dead code. Remove the now-unused GITHUB_RELEASE_TARGET env var. - Manual fallback: print a NOTE that the just-pushed tag will be removed by the rollback job, and change the printed gh release create snippet to --target the build commit-id. Previously it printed --target <checkout_ref> (a branch), which after rollback would recreate the tag at branch tip rather than the verified build commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten the publish-time tag handling so a concurrent release dispatch of the same version cannot cause this run's rollback to delete the other run's tag. - Tag push is now idempotent against this run's build commit. If the tag is already on remote at the same commit-id, continue rather than fail; only a mismatching tag is an error. - Rollback compares the remote tag's commit to the build commit-id and only deletes when they match. If the tag points elsewhere (e.g. a concurrent run pushed it) it is left alone. An unknown/empty build commit (e.g. build-release was cancelled before resolving it) is treated as "not ours" and skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git ls-remote --tags <tag> returns two lines for an annotated tag:
the tag object SHA on the bare ref and the commit SHA on the peeled
${tag}^{} ref. awk '{print $1}' then yields a multi-line string,
which breaks the equality check against BUILD_COMMIT — both the
idempotent fast-path in the tag-push step and the "is this ours"
check in the rollback step would silently misfire.
Apply the same fix to both: query the peeled ^{} ref first for the
commit SHA, fall back to the bare ref for lightweight tags (which
we create) or for missing tags (returns empty).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Close the narrow race where two concurrent release dispatches of the
same version both reach publish-release with the same build commit:
Run A pushes the tag and creates the release; Run B takes the
idempotent fast-path, then fails at gh release create ("release
already exists"). Without this gate, Run B's rollback would see the
tag at its BUILD_COMMIT and delete it, orphaning Run A's release.
- Tag-push step records tag_pushed=true to step output, but only on
the path that actually pushed the tag — not the idempotent
fast-path, which means a different run owns the tag.
- Expose as publish-release job output.
- Gate In-case-of-failure on tag_pushed == 'true' so the rollback
runs only when this run pushed the tag. The step-level SHA-match
check remains as a safety net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pushed; include checksums in snippet In the manual-recovery branch of publish-release, the printed NOTE assumed the rollback would remove the just-pushed tag — but rollback is gated on tag_pushed, so in the idempotent fast-path (another run already owned the tag) the tag is left in place. Split the message on steps.push_tag.outputs.tag_pushed so the operator sees accurate guidance in both paths. Also add the checksums file to the example gh release create command so a manual publish produces the same asset set as the automated path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s when release exists Two follow-ups raised by Copilot on the previous revision: push_tag: a concurrent run that pushes the same tag at the same BUILD_COMMIT between our ls-remote check and our git push made the push fail noisily even though the end state was correct. Factor the SHA resolution into a small function and re-resolve on push failure: if the remote tag now matches BUILD_COMMIT, treat it as the idempotent fast-path (tag_pushed stays unset); any other state is a real error. In-case-of-failure rollback: SHA equality alone wasn't enough to guarantee safe deletion. If a concurrent run took the idempotent push path and successfully created the GitHub Release while ours failed, deleting the tag would orphan its Release. Before deleting, gh release view the tag and skip when a release already exists. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p core fix Revert the concurrent-dispatch protections added across the last few commits. They defend against a scenario (two simultaneous manual dispatches of the same release version) that is operationally rare for this workflow_dispatch-only workflow, while adding non-trivial shell complexity. The pre-flight "tag must not exist" check in build-release already catches the realistic operator-error case (re-dispatch after a typo). What remains is the minimal fix for erigontech#20648 plus two defensible nice-to-haves: - Tag creation moved out of build-release into publish-release, after artifacts are downloaded and verified. - Tag pinned to needs.build-release.outputs.commit-id (not branch HEAD). - gh release create --verify-tag (drops the now-dead --target). - Manual-fallback instructions stay, with the checksums file in the printed snippet so a manual publish produces the same asset set. - gh release view after success is best-effort (|| true) so a transient view failure does not trigger rollback against an already-published release. Rollback returns to the original simple model: if publish-release did not succeed, delete the tag if present on the remote. tag_pushed output, SHA re-verification, peeled ^{} resolution, and the gh release view race-guard are all gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
yperbasis
approved these changes
May 12, 2026
…fast-failed
The rollback's gate was `publish-release.result != 'success'`, which also
fires when publish-release is `skipped` because an upstream job failed —
including the tag-already-exists pre-flight in build-release. In that
case publish-release never reached the tag-push step, so any tag named
${RELEASE_VERSION} on the remote is pre-existing. Deleting it undoes the
protection the pre-flight was offering.
Exclude `skipped` from the rollback gate so it fires only when
publish-release actually ran (failure or cancellation), the cases where
this run may have pushed the tag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #20648.
Move release tag creation out of
build-releaseand intopublish-release, after all artifacts are downloaded and verified. The tag is pinned to the exact commit that built the artifacts (needs.build-release.outputs.commit-id), not to branch HEAD.build-releasekeeps an early "tag must not exist" pre-flight so an operator-error re-dispatch fails fast.Other changes
gh release creategets--verify-tag; the now-dead--targetis removed (GitHub ignorestarget_commitishfor existing tags).gh release createfailure exits non-zero so the In-case-of-failure rollback removes the just-pushed tag.gh release viewafter success is best-effort (|| true) so a transient view failure can't trigger a rollback against an already-published release.gh release createfails; the snippet recreates the tag at the verified build commit via--targetand uploads the same asset set as the automated path, including the checksums file.publish-releaseactually ran and didn't succeed (failureorcancelled), not when it wasskippeddue to an upstream failure — so abuild-releasepre-flight rejecting a pre-existing tag does not cause the rollback to delete that tag.