diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 42e7d1e9..0138bb23 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,16 @@ on: permissions: contents: read +# One release at a time, and never a queue. Three sessions cut 0.228.0, 0.231.1 and 0.232.0 within +# hours of each other on 2026-09-16, and v0.232.0 was pushed four minutes before #1259 merged, so +# the tag published without the change main declared in that version. Two tags racing this +# workflow would both pass the per-tag tip check below and publish out of order; the group makes +# the second wait for the first, and `cancel-in-progress: false` means a tag that is already +# publishing is never interrupted by a later one. +concurrency: + group: publish-agent-runtime + cancel-in-progress: false + jobs: release-source: if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' @@ -79,6 +89,17 @@ jobs: echo "Tag is the tip of main: $TAG_SHA" exit 0 fi + # A tag that was the tip of main when it was pushed, but is not by the time this job + # runs, is the race the concurrency group serialises: another session merged after the + # tag. Publishing it now would ship less than main declares under this version. Name the + # commits it would leave out and stop, exactly as for a stale tag. + if git merge-base --is-ancestor "$TAG_SHA" "$MAIN_SHA" 2>/dev/null; then + echo "::error::Tag $RELEASE_TAG is at $TAG_SHA, but main has moved to $MAIN_SHA since it was pushed." + echo "Commits on main this tag would NOT publish:" + git log --oneline "$TAG_SHA".."$MAIN_SHA" || true + echo "Bump the version on main and re-cut the tag at its tip." + exit 1 + fi VERSION="${RELEASE_TAG#v}" LINE="release/${VERSION%.*}.x" if git fetch --no-tags --depth=100 origin "+refs/heads/$LINE:refs/remotes/origin/$LINE" 2>/dev/null; then diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index c036fa39..4d6ca973 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -73,8 +73,11 @@ console.log( `Prepared ${version}. Next:`, ' git add -A && git commit -m "chore(release): "', ' open a PR, merge it, then tag the tip of main:', + ' git fetch --tags origin && gh run list --workflow=publish.yml --limit 1 # nothing in progress, no newer tag', ` git tag v${version} && git push origin v${version}`, '', - 'Publish rejects a tag that is not the tip of main (or of release/..x).', + 'Publish rejects a tag that is not the tip of main (or of release/..x), and', + 'refuses a tag main has since moved past. Releases run one at a time; say on the PR that', + 'you are cutting before you tag, because more than one session publishes this package.', ].join('\n'), )