Skip to content

SQLR-12 — make release.yml publish jobs idempotent (skip-if-already-published)#156

Merged
joaoh82 merged 1 commit into
mainfrom
sqlr-12-idempotent-publish
Jun 1, 2026
Merged

SQLR-12 — make release.yml publish jobs idempotent (skip-if-already-published)#156
joaoh82 merged 1 commit into
mainfrom
sqlr-12-idempotent-publish

Conversation

@joaoh82
Copy link
Copy Markdown
Owner

@joaoh82 joaoh82 commented Jun 1, 2026

Context

The v0.11.0 release (run 26738895572) partially failed: sqlrite-engine hit a crates.io 413 (fixed in #154), but by then every other channel — sqlrite-ask, npm, PyPI, C FFI, Go, desktop — had already published at 0.11.0.

The publish jobs were not idempotent: only tag-all skipped existing tags. So a full re-dispatch to finish the partial release failed on every job whose artifact already published, and "Re-run failed jobs" re-checks-out the original SHA (can't pick up a fix). Completing a partial release required manual cargo publish from a local checkout.

What this does

Adds a per-registry "already published?" guard to each registry-publish job. A re-dispatch at the same version skips what's done and publishes only what's missing.

Job(s) Registry Guard
publish-crate / publish-ask / publish-mcp crates.io GET crates.io/api/v1/crates/<name>/<ver> (mandatory User-Agent); 200 → skip, 404 → publish. Transport error → publish (fail-safe).
publish-nodejs / publish-notes-example / publish-wasm npm npm view <pkg>@<ver> version non-empty → skip
publish-python PyPI GET pypi.org/pypi/sqlrite/<ver>/json logged; skip-existing: false → true so a re-run fills in only the missing wheels (file-granular — the right unit for PyPI's multi-wheel wave)

Every guard emits a ::notice:: so the run log explains each skip/publish.

The 4 GitHub-Release-only jobs (publish-ffi, publish-desktop, publish-go, build-mcp-binaries) were already idempotent via softprops/action-gh-release create-or-update and tolerate an existing tag/release — unchanged.

Docs: docs/release-plan.md "Sad path" section rewritten to document same-version re-dispatch recovery (retiring the old "never reuse a tag, always bump past" workaround) and the stale tag-all "aborts on existing tag" claim corrected (it skips with a ::notice::).

Acceptance criteria

  • Re-dispatch after a partial failure publishes only the missing artifacts; already-published ones skip (job succeeds).
  • A fully-successful release re-dispatched at the same version is a clean no-op (all skips), not a wall of "already exists" failures.
  • Guards are per-registry and logged.

Verification

actionlint (incl. shellcheck of every run: block) + YAML parse: clean. A standalone harness mirroring the exact guard shell logic, asserted against the live registries — 14/14 pass:

  • 0.11.0 (published) → crates.io engine/ask/mcp, npm @joaoh82/sqlrite + -wasm, PyPI sqlrite all decide skip (full re-dispatch = no-op).
  • sqlrite-notes@0.11.0 (genuinely missing from npm — a real partial-failure artifact) → decides publish.
  • 0.99.0 (fresh) → every channel decides publish (normal release path unaffected).
  • Transport-failure path → decides publish (fail-safe, preserves old fail-loud behavior).

Refs: SQLR-12. Complements #154.

🤖 Generated with Claude Code

…ublished)

Re-dispatching release.yml at a version where some-but-not-all artifacts
already published (the v0.11.0 wave: the engine crate 413'd but every
other channel had shipped) used to fail on every job whose artifact was
already on its registry ("crate version already uploaded", "cannot
publish over existing version"). Completing a partial release meant
hand-publishing the missing crates from a local checkout.

Add a per-registry "already published?" guard to each registry-publish
job so a re-dispatch at the same version skips what's done and publishes
only what's missing:

- crates.io (publish-crate/-ask/-mcp): GET crates.io/api/v1/crates/
  <name>/<version> with a User-Agent (crates.io 403s without one);
  200 -> skip the cargo publish step, 404 -> publish. Transport error
  falls through to publish (preserves old fail-loud behavior).
- npm (publish-nodejs/-wasm/-notes-example): `npm view <pkg>@<ver>
  version` non-empty -> skip the npm publish step.
- PyPI (publish-python): GET pypi.org/pypi/sqlrite/<ver>/json logged for
  visibility; flip skip-existing false->true so a re-run uploads only the
  missing wheels (file-granular is the right unit for PyPI's multi-wheel
  wave) instead of failing on the first already-present file.

Each guard emits a ::notice:: so the run log explains every skip. The
GitHub-Release-only jobs (publish-ffi/-desktop/-go/build-mcp-binaries)
were already idempotent via softprops/action-gh-release create-or-update
and tolerate an existing tag/release — left unchanged.

docs/release-plan.md: rewrite the "Sad path" section to document
same-version re-dispatch recovery (retiring the old "never reuse a tag,
always bump past" workaround) and correct the stale tag-all "aborts on
existing tag" claim (it skips with a ::notice::).

Verified the guard decisions against the live registries: a re-dispatch
at the published 0.11.0 skips all single-artifact channels; the genuinely
unpublished sqlrite-notes@0.11.0 still publishes; a fresh 0.99.0 publishes
everywhere. actionlint (incl. shellcheck of all run: blocks) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rust-sqlite Ready Ready Preview, Comment Jun 1, 2026 10:13am

Request Review

@joaoh82 joaoh82 merged commit f3d80eb into main Jun 1, 2026
21 checks passed
joaoh82 added a commit that referenced this pull request Jun 1, 2026
…ository-field gotcha (SQLR-13) (#157)

The sqlrite-notes npm trusted publisher had its Repository field set to
the package's npmjs access-page URL instead of the bare repo name, so the
OIDC subject claim (repo:joaoh82/rust_sqlite:environment:release) never
matched the record and every publish-notes-example run failed with "OIDC
token exchange error - package not found". It only surfaced once #156
made the release idempotent and a re-dispatch finally attempted the
first-ever sqlrite-notes publish.

- §3c: mark the trusted publisher resolved (first shipped at 0.11.0) and
  add a gotcha callout — set Repository to exactly `rust_sqlite`, no
  owner prefix, no URL.
- §3b: strengthen the canonical Repository-field note to warn against
  pasting a URL (not just the owner-prefixed form), pointing at §3c.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 mentioned this pull request Jun 2, 2026
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