Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .github/workflows/napi-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
16 changes: 13 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading