Skip to content

fix(release): make the GitHub release step idempotent so a failed publish can actually be retried - #15

Merged
wshallwshall merged 1 commit into
mainfrom
release-idempotent
Jul 27, 2026
Merged

fix(release): make the GitHub release step idempotent so a failed publish can actually be retried#15
wshallwshall merged 1 commit into
mainfrom
release-idempotent

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

gh release create fails with "a release with the same tag name already exists" on any re-run. Because that step sits before the PyPI publish, one publish failure was terminal: the release now existed, so every re-run died at creation and skipped the publish entirely. The retry could not reach the thing it was retrying.

v0.3.1 walked the whole trap

attempt outcome
1 created the release, then 403 at PyPI (Trusted Publisher mismatch)
2, 3 died at "Create GitHub release" — publish skipped
4 after a human deleted the public release: published cleanly

Attempt 3 looked like a test of the publisher fix and tested nothing.

The fix

Probe with gh release view, then edit + upload --clobber if it exists, create if not.

  • --clobber because a re-run regenerates every artifact with fresh signatures — replacing assets is the correct semantic, not an error.
  • --prerelease=true|false explicitly: a bare --prerelease only ever sets the flag, so a re-run could never demote a mis-marked pre-release.

The step order is unchanged, deliberately

Publishing to PyPI first looks like the tidier fix and is worse. A PyPI upload burns that version forever; a GitHub release is deletable. Doing the reversible half first is exactly what made the v0.3.1 failure recoverable — we could delete a release and retry, rather than holding an un-retryable PyPI version with no release.

The defect was never the ordering; it was that the reversible half couldn't be repeated. The existing ordering test now says so, so nobody "fixes" it later.

A test pinned to wording, again

Renaming the step to "Create or update the GitHub release" broke the ordering test's marker — caught before pushing. Same shape as the version==tag comparison canary in the previous release commit: a test pinned to the exact wording of the thing it guards fails the moment that thing improves. Second instance in two days.

Also: a comment that became a lie at the cutover

dependabot-lock-resync.yml asserted the App credentials were "configured correctly … nothing to add". True of the private repo; it didn't survive the move. MEFORORG carried only MEFOR_FORBIDDEN_TOKENS, so the job would have skipped on every Dependabot uv PR while the comment claimed otherwise — and it cost me a wrong assumption today. Credentials added to MEFORORG 2026-07-27; the comment now names the repo.

Verification

Mutation-verified both ways — neutralise the gh release view probe and the test fails; swap --prerelease=false for a bare --prerelease and it fails. Shell validated with bash -n against the YAML-de-indented run block.

🤖 Generated with Claude Code

…lish can actually be retried

`gh release create` fails with "a release with the same tag name already exists" on any re-run. Because
that step sits BEFORE the PyPI publish, one publish failure was terminal: the release now existed, so
every subsequent re-run died at creation and SKIPPED the publish entirely. The retry could not reach
the thing it was retrying.

v0.3.1 walked the whole trap. Attempt 1 created the release, then failed at PyPI with a 403 (Trusted
Publisher mismatch). Attempts 2 and 3 both died at "Create GitHub release" -- so attempt 3 looked like
a test of the publisher fix and tested nothing, because the publish step never ran. Only deleting the
public release by hand let attempt 4 through, and it then published cleanly.

Now: probe with `gh release view`, then `edit` + `upload --clobber` if it exists, `create` if not.
`--clobber` because a re-run regenerates every artifact with fresh signatures, so replacing assets is
the correct semantic rather than an error. `edit` takes `--prerelease=true|false` explicitly -- a bare
`--prerelease` only ever SETS the flag, so a re-run could never demote a mis-marked pre-release.

THE STEP ORDER IS UNCHANGED, DELIBERATELY. Publishing to PyPI first would look like the tidier fix and
is worse: a PyPI upload burns that version number forever, while a GitHub release is deletable. Doing
the reversible half first is exactly what made the v0.3.1 failure recoverable -- we could delete a
release and retry, instead of holding an un-retryable PyPI version with no release. The defect was
never the ordering; it was that the reversible half could not be repeated. The existing ordering test
now says so, so nobody "fixes" it later.

The step was renamed to "Create or update the GitHub release", which broke that ordering test's marker
-- caught before pushing. Same shape as the `version==tag comparison` canary in the previous release
commit: a test pinned to the exact WORDING of the thing it guards fails the moment the thing improves.

Also corrects a comment in dependabot-lock-resync.yml asserting the App credentials were "configured
correctly ... nothing to add". True of the PRIVATE repo; it did not survive the cutover. MEFORORG
carried only MEFOR_FORBIDDEN_TOKENS, so the job would have skipped on every Dependabot uv PR while the
comment claimed otherwise -- and it cost me a wrong assumption today. The credentials were added to
MEFORORG on 2026-07-27. A configuration fact stated without the repository it applies to becomes a
confident lie at the next migration.

Mutation-verified both ways: neutralise the `gh release view` probe and the test fails; replace
`--prerelease=false` with a bare `--prerelease` and it fails. Shell validated with `bash -n` on the
YAML-de-indented run block.
@wshallwshall
wshallwshall merged commit da38a09 into main Jul 27, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the release-idempotent branch July 27, 2026 18:59
Sign up for free to 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.

1 participant