Skip to content

Commit 03a4972

Browse files
joaoh82claude
andauthored
Phase 6g: add publish-nodejs to release.yml (npm via OIDC) (#28)
Two new jobs following the Phase 6f pattern: - build-nodejs-binaries (matrix, 4 cells) - publish-nodejs (aggregate + npm publish + GitHub Release) **Bundled-binaries architecture**: the `sqlrite` npm package ships every platform's `.node` binary inside one tarball (~15 MiB), picked at require time by napi's generated `index.js` dispatcher based on process.platform / process.arch. Simpler than managing N+1 npm packages (main + one per platform) — the cost is a bigger download, acceptable for a database driver people install once. **Why build/publish split**: same reason as publish-python — if each matrix cell ran `npm publish` independently, a mid-matrix failure would leave npm with some-but-not-all binaries and no clean rollback. Aggregator downloads every platform's `.node` binary into sdk/nodejs/ alongside the napi-generated `index.js` dispatcher (uploaded by the Linux x86_64 cell only — it's identical across build platforms), then does one atomic `npm publish --provenance`. **Matrix** mirrors publish-ffi / publish-desktop / publish-python so all publish jobs share one consistent OS/arch pattern: ubuntu-latest → linux-x64-gnu (sqlrite.linux-x64-gnu.node) ubuntu-24.04-arm → linux-arm64-gnu (sqlrite.linux-arm64-gnu.node) macos-latest → darwin-arm64 (sqlrite.darwin-arm64.node) windows-latest → win32-x64-msvc (sqlrite.win32-x64-msvc.node) **Authentication via npm OIDC trusted publishing** — zero long-lived NPM_TOKEN. The publish-nodejs job has `permissions: id-token: write` and lives in the `release` GitHub environment. npm-side config is one-time trusted-publisher registration on npmjs.com (docs/release-secrets.md). `--provenance` flag attaches a sigstore-signed attestation linking the published package to this exact GitHub Actions run — npm's equivalent of PyPI's PEP 740 attestations that worked first-try in the v0.1.4 canary. **Wiring:** - tag-all → pushes sqlrite-node-v<V> - finalize → needs publish-nodejs - umbrella release body → 🟢 Node.js link with npm + per-product release pointers Verified locally: `cargo check -p sqlrite-nodejs` clean, release.yml parses as valid YAML, npm name `sqlrite` available (404 on registry.npmjs.org). Existing package.json already names it `sqlrite` and has the `sqlrite.*.node` glob in `files`, so no SDK source changes needed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1e9a76 commit 03a4972

2 files changed

Lines changed: 245 additions & 9 deletions

File tree

.github/workflows/release.yml

Lines changed: 237 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
# Phase 6d: tag-all + publish-crate + publish-ffi + finalize.
88
# Phase 6e adds publish-desktop.
99
# Phase 6f adds build-python-wheels + publish-python.
10-
# Phases 6g–6i add publish-nodejs / publish-wasm / publish-go as
11-
# separate jobs to this same file.
10+
# Phase 6g adds build-nodejs-binaries + publish-nodejs.
11+
# Phases 6h–6i add publish-wasm / publish-go as separate jobs
12+
# to this same file.
1213
#
1314
# Design doc: docs/release-plan.md.
1415
# One-time registry / branch-protection setup: docs/release-secrets.md.
@@ -86,15 +87,16 @@ jobs:
8687
# BEFORE any publish step so a bad version number (e.g., tag
8788
# already exists for some reason) aborts the whole release cleanly.
8889
#
89-
# As of Phase 6f, we tag:
90+
# As of Phase 6g, we tag:
9091
# - sqlrite-v<V> (Rust engine)
9192
# - sqlrite-ffi-v<V> (C FFI prebuilt binaries)
9293
# - sqlrite-desktop-v<V> (Tauri desktop installers)
9394
# - sqlrite-py-v<V> (Python wheels on PyPI)
95+
# - sqlrite-node-v<V> (Node.js N-API bindings on npm)
9496
# - v<V> (umbrella)
9597
#
96-
# Later phases add sqlrite-node-v<V>, sqlrite-wasm-v<V>,
97-
# sdk/go/v<V> as their publish jobs come online.
98+
# Later phases add sqlrite-wasm-v<V>, sdk/go/v<V> as their
99+
# publish jobs come online.
98100
#
99101
# Idempotent on re-run: if a tag already exists (partial-failure
100102
# scenario where publish-crate succeeded but publish-ffi failed,
@@ -124,6 +126,7 @@ jobs:
124126
"sqlrite-ffi-v$V"
125127
"sqlrite-desktop-v$V"
126128
"sqlrite-py-v$V"
129+
"sqlrite-node-v$V"
127130
"v$V"
128131
)
129132
for tag in "${TAGS[@]}"; do
@@ -633,6 +636,232 @@ jobs:
633636
files: dist/*
634637
generate_release_notes: true
635638

639+
# ---------------------------------------------------------------------------
640+
# Step 3g: build Node.js N-API binaries for every supported
641+
# platform. (Phase 6g — build half; publish half lives in the
642+
# next job.)
643+
#
644+
# Architecture: the "bundled binaries" approach, not napi-rs's
645+
# newer optional-deps-per-platform pattern. The main `sqlrite`
646+
# npm package ships every platform's `.node` binary inside one
647+
# tarball; napi-rs's generated `index.js` dispatcher picks the
648+
# right one at require time based on process.platform / arch.
649+
# Simpler for MVP than maintaining N+1 npm packages; the cost
650+
# is a ~15 MiB tarball instead of a ~4 MiB per-platform download,
651+
# which is fine for a database driver people install once.
652+
#
653+
# Same build/publish split as publish-python for the same
654+
# reason: npm expects one `npm publish` invocation per package
655+
# version. If every matrix cell published independently, a
656+
# partial-failure would put some-but-not-all binaries on npm
657+
# with no clean rollback.
658+
#
659+
# Matrix mirrors publish-ffi / publish-desktop / publish-python.
660+
# Naming convention for the `.node` file is napi-rs's own: the
661+
# platform triple is baked into the filename, e.g.,
662+
# `sqlrite.linux-x64-gnu.node` vs `sqlrite.darwin-arm64.node`.
663+
# The `files` glob in sdk/nodejs/package.json matches on
664+
# `sqlrite.*.node`, so whichever binaries land in the directory
665+
# at publish time get included in the tarball.
666+
build-nodejs-binaries:
667+
name: Build Node.js binary (${{ matrix.platform }})
668+
needs: [detect, tag-all]
669+
if: needs.detect.outputs.should_release == 'true'
670+
runs-on: ${{ matrix.os }}
671+
strategy:
672+
fail-fast: false
673+
matrix:
674+
include:
675+
- os: ubuntu-latest
676+
platform: linux-x86_64
677+
napi_triple: linux-x64-gnu
678+
- os: ubuntu-24.04-arm
679+
platform: linux-aarch64
680+
napi_triple: linux-arm64-gnu
681+
- os: macos-latest
682+
platform: macos-aarch64
683+
napi_triple: darwin-arm64
684+
- os: windows-latest
685+
platform: windows-x86_64
686+
napi_triple: win32-x64-msvc
687+
steps:
688+
- uses: actions/checkout@v4
689+
690+
- uses: actions/setup-node@v4
691+
with:
692+
node-version: '20'
693+
cache: 'npm'
694+
cache-dependency-path: sdk/nodejs/package-lock.json
695+
696+
- uses: dtolnay/rust-toolchain@stable
697+
698+
- uses: Swatinem/rust-cache@v2
699+
with:
700+
shared-key: build-nodejs-${{ matrix.platform }}
701+
702+
- name: Install npm deps
703+
working-directory: sdk/nodejs
704+
run: npm ci
705+
706+
# `napi build --platform --release` produces:
707+
# - sqlrite.<napi_triple>.node (the actual binary)
708+
# - index.js (platform-dispatch loader)
709+
# - index.d.ts (TypeScript types)
710+
# We upload all three but only index.js/d.ts from the
711+
# Linux x86_64 cell (they're identical across platforms
712+
# since napi generates platform-agnostic dispatch code).
713+
- name: Build native binary
714+
working-directory: sdk/nodejs
715+
run: npm run build
716+
717+
- name: Verify binary exists
718+
working-directory: sdk/nodejs
719+
shell: bash
720+
run: |
721+
ls -la sqlrite.*.node
722+
# Fail early if napi produced an unexpected filename —
723+
# otherwise the publish step would silently ship an
724+
# incomplete tarball.
725+
test -f "sqlrite.${{ matrix.napi_triple }}.node"
726+
727+
- name: Upload .node binary
728+
uses: actions/upload-artifact@v4
729+
with:
730+
name: nodejs-binary-${{ matrix.platform }}
731+
path: sdk/nodejs/sqlrite.${{ matrix.napi_triple }}.node
732+
if-no-files-found: error
733+
retention-days: 1
734+
735+
# Only Linux x86_64 uploads the shared dispatcher files.
736+
# These are identical regardless of build platform (they're
737+
# just require-the-right-.node glue), so we only need one
738+
# copy in the final npm tarball.
739+
- name: Upload JS dispatcher (linux-x86_64 only)
740+
if: matrix.platform == 'linux-x86_64'
741+
uses: actions/upload-artifact@v4
742+
with:
743+
name: nodejs-dispatcher
744+
path: |
745+
sdk/nodejs/index.js
746+
sdk/nodejs/index.d.ts
747+
if-no-files-found: error
748+
retention-days: 1
749+
750+
# ---------------------------------------------------------------------------
751+
# Step 3h: aggregate every platform's `.node` binary + the JS
752+
# dispatcher into sdk/nodejs/, publish to npm via OIDC trusted
753+
# publishing, and cut the per-product `sqlrite-node-v<V>`
754+
# GitHub Release.
755+
#
756+
# OIDC trusted publishing: similar to the PyPI setup in
757+
# publish-python. `permissions: id-token: write` lets npm mint
758+
# a short-lived OIDC token, which `npm publish --provenance`
759+
# exchanges for a one-time upload token. No NPM_TOKEN secret.
760+
# One-time trusted-publisher config on npmjs.com — see
761+
# docs/release-secrets.md.
762+
#
763+
# `--provenance` also attaches a signed attestation linking
764+
# the package to this exact GitHub Actions workflow run (via
765+
# sigstore, same mechanism as PyPI's PEP 740). Users who care
766+
# about supply-chain can verify it with `npm audit signatures`.
767+
publish-nodejs:
768+
name: Publish Node.js package to npm
769+
needs: [detect, tag-all, build-nodejs-binaries]
770+
if: needs.detect.outputs.should_release == 'true'
771+
runs-on: ubuntu-latest
772+
environment: release
773+
permissions:
774+
# OIDC for npm trusted publisher + provenance signing.
775+
id-token: write
776+
# For softprops/action-gh-release step at the end.
777+
contents: write
778+
steps:
779+
- uses: actions/checkout@v4
780+
781+
- uses: actions/setup-node@v4
782+
with:
783+
node-version: '20'
784+
registry-url: 'https://registry.npmjs.org'
785+
786+
# Pull every platform's `.node` binary plus the JS
787+
# dispatcher into sdk/nodejs/, overlaying them on top of
788+
# the checked-out package.json / package-lock.json / etc.
789+
- name: Download .node binaries
790+
uses: actions/download-artifact@v4
791+
with:
792+
pattern: nodejs-binary-*
793+
path: sdk/nodejs
794+
merge-multiple: true
795+
796+
- name: Download JS dispatcher
797+
uses: actions/download-artifact@v4
798+
with:
799+
name: nodejs-dispatcher
800+
path: sdk/nodejs
801+
802+
- name: List publish payload
803+
working-directory: sdk/nodejs
804+
run: |
805+
ls -la
806+
echo "---"
807+
# Dry-run the pack to see exactly what ends up in the
808+
# published tarball. A missing .node file or a stray
809+
# devDep pulled in by accident would be visible here.
810+
npm pack --dry-run
811+
812+
# Single atomic publish. `--provenance` signs the package
813+
# with a sigstore-backed attestation linking it to this
814+
# workflow run. `--access public` because scoped packages
815+
# default to private — we're using an unscoped name, but
816+
# the flag is harmless and future-proofs against a rename.
817+
- name: Publish to npm
818+
working-directory: sdk/nodejs
819+
run: npm publish --provenance --access public
820+
env:
821+
# OIDC trusted publisher — no static token.
822+
NPM_CONFIG_PROVENANCE: "true"
823+
824+
- name: GitHub Release
825+
uses: softprops/action-gh-release@v2
826+
with:
827+
tag_name: sqlrite-node-v${{ needs.detect.outputs.version }}
828+
name: Node.js v${{ needs.detect.outputs.version }}
829+
body: |
830+
Published to npm: https://www.npmjs.com/package/sqlrite/v/${{ needs.detect.outputs.version }}
831+
832+
```bash
833+
npm install sqlrite@${{ needs.detect.outputs.version }}
834+
```
835+
836+
```javascript
837+
const { Database } = require('sqlrite');
838+
839+
const db = new Database(':memory:');
840+
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
841+
const stmt = db.prepare('INSERT INTO users (name) VALUES (?)');
842+
stmt.run('alice');
843+
844+
for (const row of db.prepare('SELECT * FROM users').iterate()) {
845+
console.log(row);
846+
}
847+
```
848+
849+
**Binaries bundled in this release:**
850+
- Linux x86_64 (`sqlrite.linux-x64-gnu.node`)
851+
- Linux aarch64 (`sqlrite.linux-arm64-gnu.node`)
852+
- macOS aarch64 (`sqlrite.darwin-arm64.node`)
853+
- Windows x86_64 (`sqlrite.win32-x64-msvc.node`)
854+
855+
The package's `index.js` dispatcher auto-selects the right binary at require time — no platform-specific install step.
856+
857+
Verify package provenance:
858+
```bash
859+
npm audit signatures
860+
```
861+
862+
See the umbrella release [v${{ needs.detect.outputs.version }}](../../releases/tag/v${{ needs.detect.outputs.version }}) for the full changelog.
863+
generate_release_notes: true
864+
636865
# ---------------------------------------------------------------------------
637866
# Step 4: create the umbrella GitHub Release. Runs after all
638867
# publish-* jobs succeed. Uses GitHub's native auto-generated
@@ -641,7 +870,7 @@ jobs:
641870
# config if we add one later.
642871
finalize:
643872
name: Finalize umbrella release
644-
needs: [detect, publish-crate, publish-ffi, publish-desktop, publish-python]
873+
needs: [detect, publish-crate, publish-ffi, publish-desktop, publish-python, publish-nodejs]
645874
if: needs.detect.outputs.should_release == 'true'
646875
runs-on: ubuntu-latest
647876
steps:
@@ -663,8 +892,9 @@ jobs:
663892
- 🔧 [C FFI](../../releases/tag/sqlrite-ffi-v${{ needs.detect.outputs.version }}) — prebuilt `libsqlrite_c` for Linux x86_64/aarch64, macOS aarch64, Windows x86_64
664893
- 🖥️ [Desktop](../../releases/tag/sqlrite-desktop-v${{ needs.detect.outputs.version }}) — unsigned installers for Linux (AppImage + deb), macOS (dmg aarch64), Windows (msi)
665894
- 🐍 [Python](../../releases/tag/sqlrite-py-v${{ needs.detect.outputs.version }}) → [PyPI](https://pypi.org/project/sqlrite/${{ needs.detect.outputs.version }}/) — abi3-py38 wheels for Linux x86_64/aarch64, macOS aarch64, Windows x86_64 + sdist
895+
- 🟢 [Node.js](../../releases/tag/sqlrite-node-v${{ needs.detect.outputs.version }}) → [npm](https://www.npmjs.com/package/sqlrite/v/${{ needs.detect.outputs.version }}) — N-API bindings with prebuilt `.node` binaries for Linux x86_64/aarch64, macOS aarch64, Windows x86_64
666896
667-
_Node.js / WASM / Go SDKs land as their publish jobs come online (Phases 6g–6i)._
897+
_WASM / Go SDKs land as their publish jobs come online (Phases 6h–6i)._
668898
669899
---
670900

docs/roadmap.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,15 @@ Wheel matrix mirrors publish-ffi + publish-desktop: Linux x86_64 (manylinux2014
419419

420420
Authentication via PyPI trusted publishing (OIDC) — zero long-lived tokens. `permissions: id-token: write` on the publish job plus the `release` GitHub environment (one-time trusted-publisher config on PyPI's web UI, documented in `docs/release-secrets.md`).
421421

422-
### Phase 6g — Node.js SDK publish
422+
### Phase 6g — Node.js SDK publish
423423

424-
Adds `publish-nodejs` job. `@napi-rs/cli` builds `.node` binaries per platform; npm publish via OIDC.
424+
Adds two jobs to `release.yml``build-nodejs-binaries` (matrix of 4 platforms) + `publish-nodejs` (aggregator + npm upload + GitHub Release).
425+
426+
**Bundled-binaries architecture**: the main `sqlrite` npm package ships every platform's `.node` binary inside one tarball (~15 MiB), not the per-platform optional-dep packages `@napi-rs/*` projects use. Simpler for an MVP (one npm publish, one package to manage); the tradeoff is a bigger install, acceptable for a database driver people install once. The `index.js` dispatcher napi generates picks the right binary at require time via `process.platform` + `process.arch`.
427+
428+
Same build/publish split as publish-python — matrix cells upload `.node` artifacts, a single aggregator job downloads everything into `sdk/nodejs/`, runs `npm publish --provenance` once. `--provenance` attaches a sigstore-signed attestation linking the published package to this exact workflow run (npm's equivalent of PyPI's PEP 740).
429+
430+
Authentication via npm OIDC trusted publishing — zero long-lived `NPM_TOKEN`. One-time trusted-publisher registration on npmjs.com, documented in `docs/release-secrets.md`.
425431

426432
### Phase 6h — WASM publish
427433

0 commit comments

Comments
 (0)