Repository navigation
ci(release): serialize release runs and pin Latest to the newest main tag - #56
Merged
Merged
Conversation
… tag One merge train (#50, #51, #54) produced v2.260926.3, .4 and .5 within seconds. Their release runs published in parallel, GoReleaser marked each release Latest as it published, and .4 finished one second after .5, so Latest landed on v2.260926.4 (7d1a335), which lacks #54. - auto-release.yml: a `concurrency: auto-release` group (cancel-in-progress: false) runs tagging one push at a time, so a run reads the tags the previous run pushed; a commit that is already tagged is skipped. - release.yml: runs for the same tag share a group and wait for each other. A new `latest` job, in a global `release-latest` group (cancel-in-progress: false), runs after GoReleaser and marks Latest only on the release whose tag is on the newest tagged main commit, and only once that release is published (.github/scripts/set-latest-release.sh). - .goreleaser.yaml: make_latest "false", so publishing never moves Latest. - docs/RELEASING.md: how tagging, releases and Latest interact, and how to check or repair Latest by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review follow-ups for the release race fix. - auto-release: the job-level `if` read only head_commit.message. When a `[skip release]` run replaced the pending run of an earlier push that wanted a release, neither commit was tagged. The skip markers are now read inside the job over every untagged first-parent commit since the last tag (.github/scripts/next-release-tag.sh): HEAD is tagged when any of them lacks a marker, and skipped only when all of them carry one. - set-latest-release.sh: lists releases once and fails on any API error instead of reading it as "missing"; a 404 from releases/latest means no Latest yet. It walks main from the tip and promotes the newest published release (non-draft, non-prerelease, with assets), never past the current Latest. A newer unpublished tag gets a ::warning:: naming it, and the job fails when its own tag (RELEASE_TAG) is that unpublished tag. - GoReleaser creates releases as drafts and publishes only after every upload (v2 internal/client/github.go CreateRelease/PublishRelease), so a non-draft release is complete; a failed upload leaves a draft. - docs/RELEASING.md: replaced pending runs show as cancelled and lose nothing; a failed `latest` job is the signal. - internal/releasescripts: drives both scripts against throwaway git repos and a fake gh, so `make ci` covers the tagging and Latest rules. Co-Authored-By: Claude Opus 5.5 <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.
Problem
One merge train (#50, #51, #54) pushed three merge commits to
mainwithin 14 seconds. Each push ranauto-release, which tagged it (v2.260926.3 on 2d6b6b6, .4 on 7d1a335, .5 on 6f908d4) and dispatchedrelease.yml. The three release runs built in parallel, and GoReleaser marks every release it publishes as Latest (make_latestdefaults to true). v2.260926.5 published at 20:57:42Z and v2.260926.4 at 20:57:43Z, so the Latest badge landed on v2.260926.4, a commit without #54.Two races were involved:
auto-releaseruns each read the tag list before the others had pushed, so they could compute the sameNor tag out of order.Fix
.github/workflows/auto-release.ymlconcurrency: {group: auto-release, cancel-in-progress: false}. The job-levelifonhead_commit.messageis gone; the job runs.github/scripts/next-release-tag.sh..github/scripts/next-release-tag.sh[skip release]and[skip ci]from every untagged first-parent commit since the last tag. It tags HEAD when any of them lacks a marker and skips only when all of them carry one. CalverNis computed as before, without thesedshellcheck flagged..github/workflows/release.ymlconcurrency: release-<tag>: runs for the same tag wait for each other. A newlatestjob runs aftergoreleaserin a globalrelease-latestgroup (cancel-in-progress: false) and passes its own tag asRELEASE_TAG..github/scripts/set-latest-release.sh.goreleaser.yamlrelease.make_latest: "false", so publishing never moves Latest.docs/RELEASING.mdlatestjob is the signal to act on. How to check or repair Latest by hand.internal/releasescripts/release_scripts_test.gogh(it applies the script's--jqfilters with realjq), somake cicovers these rules.set-latest-release.shgh api --paginate repos/…/releases). Any API error fails the job; it is never read as "no release".releases/latestreturning 404 means there is no Latest yet; any other error fails the job.main's first-parent history from the tip and promotes the newest published release. Published means not a draft, not a prerelease, and with assets. The walk stops at the current Latest's commit, so Latest never moves backwards.mainwhose release is missing, a draft or incomplete gets a::warning::naming it, and Latest falls back to the newest published release.Asset completeness (review item 4)
GoReleaser v2 creates the release as a draft, uploads every artifact, and only then publishes it. This is
internal/client/github.go,CreateReleasewith the comment "Always start with a draft release while uploading artifacts. PublishRelease will undraft it", andinternal/pipe/release/release.go, which callsPublishReleaseafterg.Wait(). Checked in the v2.12.7 source. v2.260926.5 matches:published_at20:57:42Z comes after its last asset (20:57:41Z), with 8 assets.So a non-draft GoReleaser release is complete, and a failed upload leaves a draft, which is never promoted. The script also refuses a published release with zero assets. I did not add a per-release expected asset count: it would duplicate the GoReleaser target matrix.
Why builds stay parallel
GitHub keeps only one pending run per concurrency group. A newer queued run replaces an older pending one, even with
cancel-in-progress: false, and the replaced run shows as cancelled. With one global group around the whole release workflow, a train of three would silently lose the middle release. So only the Latest step is serialized globally. It derives its target frommainand not from its own run, so a replaced pendinglatestjob loses nothing. Inauto-release, the replacing run reads every untagged commit, so an earlier push that wanted a release still gets one; the tag lands on the newer commit that contains it.Simulation matrix (
go test ./internal/releasescripts/, all PASS)TestNextReleaseTag:v2.260926.1v2.260926.3v2.260926.1[skip release]replaced B's pending runv2.260926.2[skip release]/[skip ci][skip ci]only on a PR branch commit[skip release]on the merge commitTestSetLatestRelease(main has v2.260926.3/.4/.5 on first-parent merges):::warning::v2.260926.5 … missing::error::This run's tag v2.260926.5::error::Could not list the releases::error::Could not read the current LatestRevert checks
set-latest-release.shfrom the first head of this PR (7d0d165), run with a fake that also answers itsgh release view: 9 of 15 cases fail. The failures are both missing-release fallbacks, both API-error cases, both own-run failures, the draft-other-run warning, without-assets (it promoted a release with no assets) and never-backwards. The 6 cases that pass are behaviours the first version already had.next-release-tag.shjudging only HEAD (the old job-levelif):skip head replaces a pending releasefails, and the other 7 pass.Evidence
gh apishowedv2.260926.5dereferences to6f908d436e8fd9c16d122e352d2e029f9d6a5957=commits/main. Thengh release edit v2.260926.5 --latest(rc 0).shellcheck .github/scripts/*.sh: clean.actionlintv1.7.7: only the pre-existing custom runner-label notices; the SC2001 on the old calversedis gone.goreleaser check(v2.12.7): valid. The threearchives.*deprecations are identical onorigin/main.make ciat 2c4baea (throwaway HOME,WK_*unset, own TMPDIR,GOFLAGS=-timeout=45m): rc 0,deadcode baseline check: OK,coverage total: 70.6% (min 70.0%).The workflow changes take effect on the next push to
mainafter merge, because release runs are dispatched with--ref main.🤖 Generated with Claude Code