Found by CodeRabbit on #430; verified against the code before filing.
What
scripts/release.py's cmd_publish guards on tag_exists(tag) and then calls
gh release create under check=True:
version = sync_version.read_canonical()
tag = f"v{version}"
if tag_exists(tag):
print(f"{tag} already exists — nothing to publish")
return 0
...
subprocess.run(["gh", "release", "create", tag, ...], check=True)
tag_exists is git rev-parse -q --verify refs/tags/<tag> against the local clone.
There is no fetch between checkout and the check, so the guard reflects the tag state at
checkout time, not the live remote. Two runs that both check out before either creates the
tag will both pass the guard, both call gh release create, and the loser exits non-zero →
CalledProcessError → red job.
The concurrency group does not serialize them:
concurrency:
group: release-${{ github.ref }}
Two branches are two refs, so they are two groups.
Why it is newly reachable
Before #429/#430, release.yml fired only on push: branches: [main] — one ref, one
group, cancel-in-progress: false, so publish runs serialized. Adding release/* makes a
second ref able to publish, which is what opens the cross-branch window. The trigger change
is still correct; this is the sharp edge it exposes.
Severity
Low. The release that wins is correct — the failure mode is a spuriously red workflow run
on the losing branch, not a bad or duplicate release. It also needs the same canonical
version to land on two branches close together, which the 0.9.x flow avoids by design (the
forward-port to main does not bump versions, so main's publish no-ops on the existing
tag).
Fix shapes
Either would do; not ranking them without looking at how the 0.10 line will branch.
- Make the concurrency group version-scoped or ref-independent (e.g.
group: release-${{ github.workflow }}) so all publish runs serialize repo-wide. The
second run then re-checks out after the first has pushed the tag — but note that only
helps if the checkout genuinely post-dates the tag push, so pair it with (2).
- Make
cmd_publish tolerate the conflict: drop check=True and treat an
"already exists" failure from gh release create as success, or re-verify against the
remote (git ls-remote --tags) rather than the local clone. This is the one that
actually closes the window, since it does not depend on scheduling.
Deliberately not fixed in #429/#430 — those were scoped to the trigger change alone, and
#429 is already merged and published.
Found by CodeRabbit on #430; verified against the code before filing.
What
scripts/release.py'scmd_publishguards ontag_exists(tag)and then callsgh release createundercheck=True:tag_existsisgit rev-parse -q --verify refs/tags/<tag>against the local clone.There is no fetch between checkout and the check, so the guard reflects the tag state at
checkout time, not the live remote. Two runs that both check out before either creates the
tag will both pass the guard, both call
gh release create, and the loser exits non-zero →CalledProcessError→ red job.The concurrency group does not serialize them:
Two branches are two refs, so they are two groups.
Why it is newly reachable
Before #429/#430,
release.ymlfired only onpush: branches: [main]— one ref, onegroup,
cancel-in-progress: false, so publish runs serialized. Addingrelease/*makes asecond ref able to publish, which is what opens the cross-branch window. The trigger change
is still correct; this is the sharp edge it exposes.
Severity
Low. The release that wins is correct — the failure mode is a spuriously red workflow run
on the losing branch, not a bad or duplicate release. It also needs the same canonical
version to land on two branches close together, which the 0.9.x flow avoids by design (the
forward-port to
maindoes not bump versions, somain's publish no-ops on the existingtag).
Fix shapes
Either would do; not ranking them without looking at how the 0.10 line will branch.
group: release-${{ github.workflow }}) so all publish runs serialize repo-wide. Thesecond run then re-checks out after the first has pushed the tag — but note that only
helps if the checkout genuinely post-dates the tag push, so pair it with (2).
cmd_publishtolerate the conflict: dropcheck=Trueand treat an"already exists" failure from
gh release createas success, or re-verify against theremote (
git ls-remote --tags) rather than the local clone. This is the one thatactually closes the window, since it does not depend on scheduling.
Deliberately not fixed in #429/#430 — those were scoped to the trigger change alone, and
#429 is already merged and published.