Skip to content

ci(publish): make publishing idempotent so a second run is green, not E403 - #33

Merged
catomean merged 1 commit into
mainfrom
ci/publish-idempotent
Sep 11, 2026
Merged

catomean merged 1 commit into
mainfrom
ci/publish-idempotent

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Why

The Publish workflow can run twice for one merge — once from the push (the auto-merge sweep merges with a PAT, whose pushes do trigger workflows) and once from the sweep's workflow_dispatch re-arm (rearm_workflows: "ci.yml publish.yml"). Two runs asking the registry the same question at the same moment both hear "not published", both build, and the second npm publish gets E403 cannot publish over previously published versions. A red run that means "nothing is wrong" trains everyone to ignore the workflow; the next thing ignored will be a real failure.

The re-arm stays: it exists so a merge whose push trigger was suppressed still publishes. Idempotency is what makes both paths safe.

Measurement (gh run list --workflow publish.yml --limit 10)

Pattern not present in the last 10 runs (all hourly schedule ticks against 31099dc, all success — no version bump has merged since the sweep switched to a PAT). The workflow has the same registry check and the same push + workflow_dispatch triggers as bip-kit, so the next version bump would hit the identical race.

Fix — publishing is idempotent, at three layers

  1. concurrency: { group: publish, cancel-in-progress: false } — the second run waits for the first instead of racing it, then finds the version on the registry and skips. Never cancel: a cancelled publish is a half-shipped release.
  2. Registry check before anything else (npm view <name>@<version> version): if the exact version is already published, write a green step summary ("already published — nothing to do") and skip install/verify/publish.
  3. Race-tolerant publish step: if npm publish fails but the version is now on the registry, that is the goal state — green with a "published by a concurrent run" summary. A real publish failure still exits 1.

Everything else is unchanged: OIDC trusted publishing, verify-before-publish, tag checks, triggers.

Mutation proof — the guard's shell snippet run locally against the real registry

=== threadkit (a) current version
→ threadkit@0.1.1 is already published; nothing to do.
  outputs: name=threadkit version=0.1.1 publish=false 
  step summary: ✅ `threadkit@0.1.1` is already published — nothing to do.
exit=0
=== threadkit (b) 9.9.9-proof
→ threadkit@9.9.9-proof is not on the registry; releasing it.
  outputs: name=threadkit version=9.9.9-proof publish=true 
  step summary: 
exit=0

(a) current version → "already published", exit 0. (b) fake bump 9.9.9-proof → "not on the registry; releasing it". Nothing was published.

Verification

pnpm run verify green locally (log: /tmp/verify-threadkit.log, exit 0). Prettier passes on the workflow file.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn

… E403

One merge starts Publish twice — the PAT-made push and the auto-merge
sweep's workflow_dispatch re-arm, seconds apart. Both ask the registry
before either publishes, both build, and the loser gets E403 "cannot
publish over previously published versions" (bip-kit 0.2.6, 0.2.7 on
2026-09-11). A red run that means nothing is wrong teaches everyone to
ignore the workflow.

- concurrency group "publish", never cancelled: the second run waits,
  then sees the version on the registry and skips
- registry check writes a green step summary ("already published —
  nothing to do") and gates install/verify/publish
- npm publish failure is re-checked against the registry: if the
  version is there, that is the goal state, not an error

OIDC trusted publishing, verify-before-publish, tag checks and the
re-arm list are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn
@catomean
catomean merged commit 8508451 into main Sep 11, 2026
1 check passed
@catomean
catomean deleted the ci/publish-idempotent branch September 11, 2026 01:07
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