@@ -173,7 +173,33 @@ jobs:
173173 with :
174174 shared-key : publish-crate
175175
176+ # Idempotency guard (SQLR-12). A re-dispatch of release.yml at a
177+ # version where some-but-not-all artifacts already published (the
178+ # v0.11.0 partial failure: the engine 413'd but every other channel
179+ # had already shipped) must SKIP what's done and publish only what's
180+ # missing — instead of erroring with "crate version already uploaded".
181+ # crates.io returns HTTP 200 for an existing crate@version, 404 for a
182+ # missing one. The `User-Agent` header is mandatory: crates.io 403s
183+ # any request without one. On a transport error (curl fails → "000")
184+ # we fall through to publish, preserving the old fail-loud behavior.
185+ - name : Skip if already on crates.io
186+ id : published_check
187+ run : |
188+ V="${{ needs.detect.outputs.version }}"
189+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
190+ -H 'User-Agent: sqlrite-release-ci (https://github.com/joaoh82/rust_sqlite)' \
191+ "https://crates.io/api/v1/crates/sqlrite-engine/$V" || true)
192+ CODE=${CODE:-000}
193+ if [ "$CODE" = "200" ]; then
194+ echo "skip=true" >> "$GITHUB_OUTPUT"
195+ echo "::notice::sqlrite-engine@$V already on crates.io (HTTP 200) — skipping cargo publish"
196+ else
197+ echo "skip=false" >> "$GITHUB_OUTPUT"
198+ echo "::notice::sqlrite-engine@$V not found on crates.io (HTTP $CODE) — will publish"
199+ fi
200+
176201 - name : cargo publish
202+ if : steps.published_check.outputs.skip != 'true'
177203 env :
178204 CARGO_REGISTRY_TOKEN : ${{ secrets.CRATES_IO_TOKEN }}
179205 run : |
@@ -236,7 +262,26 @@ jobs:
236262 with :
237263 shared-key : publish-ask
238264
265+ # Idempotency guard (SQLR-12) — see publish-crate for the full
266+ # rationale. crates.io: 200 = already published (skip), else publish.
267+ - name : Skip if already on crates.io
268+ id : published_check
269+ run : |
270+ V="${{ needs.detect.outputs.version }}"
271+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
272+ -H 'User-Agent: sqlrite-release-ci (https://github.com/joaoh82/rust_sqlite)' \
273+ "https://crates.io/api/v1/crates/sqlrite-ask/$V" || true)
274+ CODE=${CODE:-000}
275+ if [ "$CODE" = "200" ]; then
276+ echo "skip=true" >> "$GITHUB_OUTPUT"
277+ echo "::notice::sqlrite-ask@$V already on crates.io (HTTP 200) — skipping cargo publish"
278+ else
279+ echo "skip=false" >> "$GITHUB_OUTPUT"
280+ echo "::notice::sqlrite-ask@$V not found on crates.io (HTTP $CODE) — will publish"
281+ fi
282+
239283 - name : cargo publish
284+ if : steps.published_check.outputs.skip != 'true'
240285 env :
241286 CARGO_REGISTRY_TOKEN : ${{ secrets.CRATES_IO_TOKEN }}
242287 # `--no-verify` mirrors `publish-crate` — Release-PR CI
@@ -302,7 +347,26 @@ jobs:
302347 with :
303348 shared-key : publish-mcp
304349
350+ # Idempotency guard (SQLR-12) — see publish-crate for the full
351+ # rationale. crates.io: 200 = already published (skip), else publish.
352+ - name : Skip if already on crates.io
353+ id : published_check
354+ run : |
355+ V="${{ needs.detect.outputs.version }}"
356+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
357+ -H 'User-Agent: sqlrite-release-ci (https://github.com/joaoh82/rust_sqlite)' \
358+ "https://crates.io/api/v1/crates/sqlrite-mcp/$V" || true)
359+ CODE=${CODE:-000}
360+ if [ "$CODE" = "200" ]; then
361+ echo "skip=true" >> "$GITHUB_OUTPUT"
362+ echo "::notice::sqlrite-mcp@$V already on crates.io (HTTP 200) — skipping cargo publish"
363+ else
364+ echo "skip=false" >> "$GITHUB_OUTPUT"
365+ echo "::notice::sqlrite-mcp@$V not found on crates.io (HTTP $CODE) — will publish"
366+ fi
367+
305368 - name : cargo publish
369+ if : steps.published_check.outputs.skip != 'true'
306370 env :
307371 CARGO_REGISTRY_TOKEN : ${{ secrets.CRATES_IO_TOKEN }}
308372 # `--no-verify` mirrors `publish-crate` / `publish-ask` —
@@ -827,17 +891,40 @@ jobs:
827891 - name : List files about to be uploaded
828892 run : ls -la dist/
829893
830- # Single atomic upload of all wheels + sdist. If any file
831- # fails to upload, none are published — no partial wave
832- # on PyPI.
894+ # Idempotency log line (SQLR-12). PyPI's JSON API returns HTTP 200
895+ # for an existing project/version, 404 otherwise. Unlike the
896+ # single-artifact crates.io / npm jobs, PyPI's publish unit is a
897+ # *set* of files (one wheel per platform + the sdist), so we do NOT
898+ # gate the whole step on this check: a partial wave (some wheels
899+ # landed before the job died) would leave the version "present" yet
900+ # incomplete, and a version-level skip would strand the missing
901+ # wheels. Instead this step just records the state in the run log;
902+ # `skip-existing: true` below does the actual file-granular skipping.
903+ - name : Report PyPI publish state
904+ run : |
905+ V="${{ needs.detect.outputs.version }}"
906+ CODE=$(curl -sS -o /dev/null -w '%{http_code}' \
907+ "https://pypi.org/pypi/sqlrite/$V/json" || true)
908+ CODE=${CODE:-000}
909+ if [ "$CODE" = "200" ]; then
910+ echo "::notice::sqlrite==$V already present on PyPI (HTTP 200) — upload will skip files already there (skip-existing)"
911+ else
912+ echo "::notice::sqlrite==$V not found on PyPI (HTTP $CODE) — uploading all wheels + sdist"
913+ fi
914+
915+ # Upload all wheels + sdist aggregated from the build matrix.
916+ # `skip-existing: true` (SQLR-12) is what makes this job idempotent:
917+ # a re-dispatch (full or partial-wave recovery) uploads only the
918+ # files not yet on PyPI and silently skips the rest, instead of the
919+ # old `skip-existing: false` which failed loudly the moment any one
920+ # file already existed. File-granular skipping is the right unit for
921+ # PyPI's multi-file publish — it cleanly handles both a fully-shipped
922+ # version (every file skipped → no-op) and a half-shipped one.
833923 - name : Publish to PyPI
834924 uses : pypa/gh-action-pypi-publish@release/v1
835925 with :
836926 packages-dir : dist
837- # Keep `skip-existing: false` so a re-run of this job
838- # (after a partial-failure scenario) fails loudly rather
839- # than silently ignoring already-uploaded files.
840- skip-existing : false
927+ skip-existing : true
841928
842929 - name : GitHub Release
843930 uses : softprops/action-gh-release@v2
@@ -1080,6 +1167,26 @@ jobs:
10801167 # devDep pulled in by accident would be visible here.
10811168 npm pack --dry-run
10821169
1170+ # Idempotency guard (SQLR-12) — see publish-crate for the full
1171+ # rationale. `npm view <pkg>@<version> version` prints the version
1172+ # on a hit and exits non-zero with empty stdout on a miss; the
1173+ # `|| true` keeps the `-e` shell from aborting on the miss path.
1174+ # npm's publish unit is a single tarball per version, so a present
1175+ # version means a complete publish — a clean version-level skip.
1176+ - name : Skip if already on npm
1177+ id : published_check
1178+ working-directory : sdk/nodejs
1179+ run : |
1180+ V="${{ needs.detect.outputs.version }}"
1181+ EXISTING=$(npm view "@joaoh82/sqlrite@$V" version 2>/dev/null || true)
1182+ if [ -n "$EXISTING" ]; then
1183+ echo "skip=true" >> "$GITHUB_OUTPUT"
1184+ echo "::notice::@joaoh82/sqlrite@$V already on npm ($EXISTING) — skipping npm publish"
1185+ else
1186+ echo "skip=false" >> "$GITHUB_OUTPUT"
1187+ echo "::notice::@joaoh82/sqlrite@$V not found on npm — will publish"
1188+ fi
1189+
10831190 # Single atomic publish via OIDC trusted publisher.
10841191 #
10851192 # The `--provenance` flag is what tells npm CLI to use the
@@ -1111,6 +1218,7 @@ jobs:
11111218 # diagnosable from the run log without re-running with
11121219 # debug-on. Cheap insurance against another silent failure.
11131220 - name : Publish to npm
1221+ if : steps.published_check.outputs.skip != 'true'
11141222 working-directory : sdk/nodejs
11151223 run : npm publish --access public --provenance --loglevel verbose
11161224
@@ -1226,7 +1334,24 @@ jobs:
12261334 # node_modules).
12271335 npm pack --dry-run
12281336
1337+ # Idempotency guard (SQLR-12) — see publish-crate / publish-nodejs.
1338+ # Package is the unscoped `sqlrite-notes`.
1339+ - name : Skip if already on npm
1340+ id : published_check
1341+ working-directory : examples/nodejs-notes
1342+ run : |
1343+ V="${{ needs.detect.outputs.version }}"
1344+ EXISTING=$(npm view "sqlrite-notes@$V" version 2>/dev/null || true)
1345+ if [ -n "$EXISTING" ]; then
1346+ echo "skip=true" >> "$GITHUB_OUTPUT"
1347+ echo "::notice::sqlrite-notes@$V already on npm ($EXISTING) — skipping npm publish"
1348+ else
1349+ echo "skip=false" >> "$GITHUB_OUTPUT"
1350+ echo "::notice::sqlrite-notes@$V not found on npm — will publish"
1351+ fi
1352+
12291353 - name : Publish to npm
1354+ if : steps.published_check.outputs.skip != 'true'
12301355 working-directory : examples/nodejs-notes
12311356 run : npm publish --access public --provenance --loglevel verbose
12321357
@@ -1386,12 +1511,32 @@ jobs:
13861511 echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN is set: ${ACTIONS_ID_TOKEN_REQUEST_TOKEN:+yes}${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-NO}"
13871512 npm pack --dry-run
13881513
1514+ # Idempotency guard (SQLR-12) — see publish-crate / publish-nodejs.
1515+ # Package is the scoped `@joaoh82/sqlrite-wasm`. (The wasm-pack build
1516+ # above still runs on a skip — gating only the publish keeps this
1517+ # consistent with the other channels; the build is cheap relative to
1518+ # the rest of the release wave.)
1519+ - name : Skip if already on npm
1520+ id : published_check
1521+ working-directory : sdk/wasm/pkg
1522+ run : |
1523+ V="${{ needs.detect.outputs.version }}"
1524+ EXISTING=$(npm view "@joaoh82/sqlrite-wasm@$V" version 2>/dev/null || true)
1525+ if [ -n "$EXISTING" ]; then
1526+ echo "skip=true" >> "$GITHUB_OUTPUT"
1527+ echo "::notice::@joaoh82/sqlrite-wasm@$V already on npm ($EXISTING) — skipping npm publish"
1528+ else
1529+ echo "skip=false" >> "$GITHUB_OUTPUT"
1530+ echo "::notice::@joaoh82/sqlrite-wasm@$V not found on npm — will publish"
1531+ fi
1532+
13891533 # Atomic OIDC publish. Same flag combo proven in
13901534 # publish-nodejs: `--provenance` to trigger OIDC code path,
13911535 # `--access public` because scoped packages default to
13921536 # private, `--loglevel verbose` to keep error logs
13931537 # diagnosable.
13941538 - name : Publish to npm
1539+ if : steps.published_check.outputs.skip != 'true'
13951540 working-directory : sdk/wasm/pkg
13961541 run : npm publish --access public --provenance --loglevel verbose
13971542
0 commit comments