Skip to content

ci: make release creation idempotent across the mac and windows jobs - #37

Merged
z33b0t merged 1 commit into
mainfrom
ci/release-race
Jul 29, 2026
Merged

ci: make release creation idempotent across the mac and windows jobs#37
z33b0t merged 1 commit into
mainfrom
ci/release-race

Conversation

@z33b0t

Copy link
Copy Markdown
Contributor

Both workflows fire on the same v* tag and both used check-then-create, so both could miss, both create, and one gets a 422.

The issue's preferred option isn't available

Or split release creation into its own job that both build jobs needs:.

mac.yml and windows.yml are separate workflows, and needs: doesn't cross workflow boundaries — there's no job in one that the other can depend on. Making creation genuinely idempotent (the issue's first option) is what works with the current structure. A separate release workflow the builds poll for would be a bigger change and re-introduces waiting; happy to go there if you'd prefer it, but it seemed like the wrong trade for this.

Verified against a stubbed gh

Ran the exact logic with a stub modelling the race — first view misses, the other job creates in the gap, create then returns 422:

=== OLD logic (check-then-create) in the race ===
exit=1 <- job dies, never uploads
=== NEW logic in the same race ===
::notice::release already exists — the Windows job created it first
-> proceeded to upload
exit=0

And all three outcomes of the new logic independently:

caseresult
we win the race (create succeeds)proceeds to upload, exit 0
we lose the race (422, release exists)notice, proceeds to upload, exit 0
create fails and no release exists::error::, exit 1, no upload

Windows now fails loudly

Added $ErrorActionPreference = 'Stop' and $PSNativeCommandUseErrorActionPreference = $true, per the issue's last paragraph. Knock-on: the trailing if ($LASTEXITCODE -ne 0) after gh release upload is now unreachable, so I removed it rather than leave config that reads as load-bearing and isn't (the exact failure mode #9 is about).

Verification gap

The real proof is a tag push publishing a release, which I'm obviously not doing. YAML parses, the bash block passes bash -n, and the logic is tested as above — but the pwsh side is not executed anywhere: no pwsh on this machine, and the Windows job only runs its publish step on a v* tag. The PowerShell is reviewed, not run. Worth your eye on that block specifically before the next release.

Closes#19

Both workflows trigger on the same v* tag and both need the Release to exist
before uploading. Both used check-then-create, which is a TOCTOU race between
two jobs that start at the same moment: both view, both miss, both create, and
one gets a 422.
The two then failed differently, which is what made it confusing to diagnose.
mac.yml runs under `set -euo pipefail` with the failing create on the right of
a `||`, so the job died before uploading. windows.yml uses pwsh, which does not
stop on a native command's non-zero exit, so the failed create was swallowed
and the job went green. Same race, red mac, green windows.
Both now attempt creation and treat "it exists now" as success, whichever
platform won; a create that fails with no release behind it still fails the job
loudly. This was previously self-healing via `gh release upload --clobber`, but
--clobber is deliberately gone (#5), so a re-run now needs manual asset
deletion — which is what made this worth fixing rather than retrying past.
windows.yml also gets $ErrorActionPreference = 'Stop' and
$PSNativeCommandUseErrorActionPreference = $true, so a failing gh command fails
the step instead of being ignored. That makes the trailing $LASTEXITCODE check
on `gh release upload` unreachable, so it's removed rather than left as config
that reads as load-bearing and isn't.
Note the issue's preferred option — a separate release-creation job that both
builds `needs:` — isn't reachable here: mac.yml and windows.yml are separate
workflows and `needs:` doesn't cross workflow boundaries. Idempotent creation is
the option that works with the current structure.
Closes#19
@z33b0t

Copy link
Copy Markdown
ContributorAuthor

@z33b0t ready to merge. All 8 checks green.

Two things to weigh before merging, both in the PR body:

  1. The issue's preferred fix isn't reachable. It suggests a separate release-creation job both builds needs: — but mac.yml and windows.yml are separate workflows and needs: doesn't cross workflows. This does the other option (genuinely idempotent creation). If you'd rather have the single-creator design, it'd mean merging the two workflows or a poll loop; say so and I'll do it.
  2. The pwsh half is reviewed, not run. No pwsh on this machine, and the publish step only executes on a v* tag, so CI never touches it either. The bash half is tested against a stubbed gh across all three race outcomes; the PowerShell is a careful translation of the same logic. Worth your eye on that block specifically — a mistake there surfaces during a release.

@z33b0t
z33b0t merged commit 9edade7 into mainJul 29, 2026
8 checks passed
@z33b0t
z33b0t deleted the ci/release-race branch July 29, 2026 16:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release creation races between the mac and windows jobs on the same tag

1 participant

@z33b0t