Skip to content

fix(ci): make the GitHub Release step idempotent so same-tag retries work (#198) - #206

Merged
kalwalt merged 1 commit into
devfrom
fix/198-release-idempotent
Sep 12, 2026
Merged

fix(ci): make the GitHub Release step idempotent so same-tag retries work (#198)#206
kalwalt merged 1 commit into
devfrom
fix/198-release-idempotent

Conversation

@kalwalt

@kalwalt kalwalt commented Sep 12, 2026

Copy link
Copy Markdown
Member

The problem

gh release create has no upsert mode — it fails outright if a release already exists for the tag. That step runs before "Publish to npm", so the ordering produced a trap:

  1. Publish fails (registry hiccup, auth/config issue — exactly what happened during 0.16.0).
  2. A real GitHub Release for the tag now exists.
  3. The documented recovery — re-run via Actions → Release → Run workflow with the same tag — dies at "Create GitHub Release" instead of ever reaching the publish step that actually needed retrying.

0.16.0 was only released by running gh release delete 0.16.0 --yes before each retry, an undocumented workaround.

The fix

Detect the existing release and take an edit path:

if gh release view "$TAG" >/dev/null 2>&1; then
    gh release edit "$TAG" --title "$TAG" --notes-file release-notes.md --prerelease="$PRERELEASE"
else
    gh release create "$TAG" --title "$TAG" --notes-file release-notes.md --prerelease="$PRERELEASE"
fi

gh release view exits 0 when the release exists and 1 when it does not — that exit code is the whole detection.

Why the prerelease flag changed shape

It was a bare conditional flag (--prerelease or empty). The edit path cannot use that form: a bare --prerelease on a re-run of a stable release would silently demote it to a prerelease, and there is no way to express "false" without the explicit form. So it is now --prerelease=true|false, driven by the same steps.tag.outputs.prerelease value, on both paths.

Verification

The workflow can't be exercised without cutting a release, so each assumption it rests on was checked directly against gh 2.97.0:

Check Result
gh release view <existing tag> exit 0
gh release view <missing tag> exit 1
gh release edit --prerelease=false parses; fails with "release not found" (not a flag error)
gh release create --prerelease=false parses; fails with "tag required" (not a flag error)
Workflow YAML parses; step's run block extracted and checked with bash -n
Prettier clean on both files

Acceptance criteria

  • Re-running for a tag that already has a Release succeeds at this step and refreshes its notes
  • A fresh tag still works exactly as before (the else branch is the original command)
  • MAINTAINERS.md's "you do not need to re-tag" instructions become true for a publish-step failure, with no manual deletion

Also documented

npm publish is deliberately not idempotent — npm refuses to republish an existing version. So a re-run after a successful publish fails with 403 / EPUBLISHCONFLICT, which means the version is already on npm rather than that something is broken. MAINTAINERS.md now says so, with npm view @webarkit/jsfeat-next versions as the check, so the next person doesn't misread it as a failure.

Closes #198

🤖 Generated with Claude Code

…work

`gh release create` has no upsert mode and fails outright when a release
already exists for the tag. That step runs before "Publish to npm", so any
failure at or after the publish left a real GitHub Release behind and the
same-tag re-run documented in MAINTAINERS.md died at release creation instead
of retrying the step that actually failed. This happened during 0.16.0, where
each retry needed an undocumented `gh release delete 0.16.0 --yes` first.

Detect the existing release with `gh release view` (exit 0 when present, 1 when
not) and take the edit path when it is, refreshing title, notes and prerelease
flag; create as before otherwise. A fresh tag behaves exactly as it did.

The prerelease state is now passed as an explicit `--prerelease=true|false`
instead of a bare conditional flag. The edit path requires it: a bare
`--prerelease` would silently demote a stable release to a prerelease on
re-run, and there is no way to say "false" without the explicit form.

Also documents in MAINTAINERS.md that `npm publish` is deliberately NOT
idempotent, so a re-run after a successful publish fails with EPUBLISHCONFLICT
— that means the version is already on npm, not that something is broken.

Closes #198

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@kalwalt kalwalt self-assigned this Sep 12, 2026
@kalwalt kalwalt added enhancement New feature or request github_actions Pull requests that update GitHub Actions code labels Sep 12, 2026
@kalwalt kalwalt added this to the 1.0.0 milestone Sep 12, 2026
@kalwalt
kalwalt merged commit ffd48c1 into dev Sep 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant