diff --git a/.github/workflows/napi-build.yml b/.github/workflows/napi-build.yml index c6971d9..4502662 100644 --- a/.github/workflows/napi-build.yml +++ b/.github/workflows/napi-build.yml @@ -129,7 +129,18 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v5 with: name: bindings-${{ matrix.settings.target }} - path: crates/spaghetti-napi/*.node + # index.js rides along with the binary. It is generated by `napi + # build` and gitignored (see RELEASING.md), so it does not exist in a + # fresh checkout — and neither the test job nor the publish job runs + # a build. Shipping it as a build output also means the published + # loader carries the *released* version in its per-platform guards, + # which a committed copy never did. + # + # Both paths share the crates/spaghetti-napi/ prefix, so the archive + # root is unchanged and consumers still find *.node at the top level. + path: | + crates/spaghetti-napi/*.node + crates/spaghetti-napi/index.js if-no-files-found: error test: @@ -232,6 +243,36 @@ jobs: cd crates/spaghetti-napi npx napi artifacts + # `napi artifacts` only relocates *.node, and this job never builds, so + # the generated loader has to be lifted out of an artifact by hand. + # Every target emits the same file — it branches on all platforms napi + # knows about, not the triple it was built for — so any one will do. + # + # Failing loudly here matters: `files` in package.json silently skips + # entries that are missing on disk, so without this the job would happily + # publish a package whose `main` points at a file that isn't in it. + - name: Restore the generated loader + run: | + set -euo pipefail + cd crates/spaghetti-napi + LOADER=$(find artifacts -name index.js -type f -print -quit) + if [ -z "$LOADER" ]; then + echo "::error::No index.js among the downloaded artifacts — refusing to publish a package with no loader." + exit 1 + fi + cp "$LOADER" ./index.js + echo "Restored loader from $LOADER" + + # Load the exact bytes about to be published. ubuntu-latest matches the + # linux-x64-gnu binary that `napi artifacts` just placed alongside it, so + # this exercises the real loader/binary pair rather than trusting that + # the files merely exist. + - name: Smoke test the package about to be published + run: | + set -euo pipefail + cd crates/spaghetti-napi + node -e "const m = require('./index.js'); if (!m.nativeVersion) { throw new Error('nativeVersion export missing'); } console.log('Publishing loader OK, nativeVersion =', m.nativeVersion());" + # We publish ONLY the main package, which fat-bundles all 6 .node binaries # (see its `files: ["*.node"]`). The per-platform packages under # crates/spaghetti-napi/npm/* are deliberately NOT published: nothing diff --git a/RELEASING.md b/RELEASING.md index 36646a0..633a740 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -62,8 +62,18 @@ Two files in the Rust addon are generated and embed the version. Both used to br silently retarget a different crate when dependencies change. - **`crates/spaghetti-napi/index.js`** is deliberately **not** committed (see `.gitignore`). `napi build` regenerates it and stamps the version into a per-platform guard in ~50 places, which no updater can - track. It is still published: `files` in `package.json` is an allowlist that overrides `.gitignore`, - and every publish path builds first. `index.d.ts` stays committed — it is the reviewable API surface - and carries no version. + track. `index.d.ts` stays committed — it is the reviewable API surface and carries no version. + + It travels as a **build artifact**: `napi-build.yml`'s build job uploads it next to the `.node` + binary, the test job gets it from the artifact download, and the publish job lifts it out of an + artifact before packing. Note that **neither the test job nor the publish job runs a build** — they + consume prebuilt binaries — so nothing may assume a fresh checkout contains a loader. `napi` has no + loader-only generator (`napi build` is the sole producer and needs the Rust toolchain), which is why + it moves through artifacts rather than being regenerated on demand. + + Getting this wrong fails quietly in one direction: `files` in `package.json` silently skips entries + that are absent on disk, so a missing loader would publish a package whose `main` points at nothing. + The publish job therefore hard-fails if no loader is found and `require()`s the assembled package + before publishing. Future releases should continue from whatever the manifest currently records — through `release-please`, not through manual version/tag commits.