feat(runtime): tested Node 20.9 engines floor - launcher preflight, ABI guard, engine-matrix CI - #142
Conversation
…her preflight and ABI guard Bare npx resolves the highest engine-compatible version, so an engines floor above a user's runtime silently drops them onto an ancient bin-less release (the #130 class - still live for every Node < 24 user). Instead of excluding those runtimes, support them honestly: - engines.node >=24.0.0 -> >=20.9.0 (Next.js 16's own floor) and version bump to 0.9.44. - Launcher preflight (assessNodeRuntime): refuses < 20.9 with clear guidance before any download; warns 20.9-22.12 that all SQLite features are unavailable (no unflagged node:sqlite); warns 22.13-23.x that server-side SQLite storage needs Node 24; silent on 24+. - Storage ABI guard: better-sqlite3 is compiled against the Node 24 ABI, so STORAGE_PROVIDER=sqlite on an older Node failed with a raw NODE_MODULE_VERSION dump; it now throws an actionable error naming the Node 24 requirement and the postgres/local alternatives. - sqlite-driver guidance updated (node:sqlite exists unflagged from 22.13, not only 24); docs/providers/sqlite.md synced. - The floor is a tested claim: scripts/engine-smoke.sh boots the standalone payload through the launcher and asserts each tier's documented behaviour; ci.yml runs it on Node 20.9/22/24 per PR (payload built once, matrix legs download the artifact). - npx-engine-smoke.yml (workflow_dispatch): post-release bare-npx check against the live registry on all three tiers; wired into the DISTRIBUTION.md runbook. - Unit tests: assessNodeRuntime version matrix; ABI-guard translation, cause chaining, and unrelated-error passthrough. Node 24 users see zero behaviour change; bundled runtimes (Docker/deb/rpm/brew) are untouched. All three tiers validated locally in node:20.9 / node:22 / node:24 containers with the exact CI script, plus the refusal path on node:18.
|
related: #122 |
…he release ABI build-standalone-payload.sh probes better-sqlite3 with the ambient node and rebuilds against it when the binding does not load. The runner's default node (22) rebuilt the binding to ABI 127, inverting the matrix expectations: storage 'worked' on the Node 22 leg and broke on Node 24. release-artifacts.yml already pins Node 24 before building for exactly this reason - mirror it so CI asserts against the payload releases actually ship.
There was a problem hiding this comment.
Pull request overview
This PR widens the npm package’s supported Node.js engine floor to >=20.9.0 (while keeping Node 24 LTS as the fully supported runtime) and backs that claim with launcher preflight checks, a server-side SQLite ABI guard, and CI engine-matrix smoke tests to prevent bare-npx regressions.
Changes:
- Lower
engines.nodeto>=20.9.0and add a launcher runtime preflight (assessNodeRuntime) with unit tests. - Add a better-sqlite3 Node-ABI mismatch guard for
STORAGE_PROVIDER=sqlite, plus unit tests for the translated error behavior. - Add engine-matrix smoke coverage (
scripts/engine-smoke.sh) and CI workflows to validate behavior across Node 20.9 / 22 / 24; update docs accordingly.
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Bumps version and lowers engines.node to >=20.9.0. |
bin/lib/launcher-utils.mjs |
Adds assessNodeRuntime tiering logic for runtime preflight messaging. |
bin/studio.js |
Runs the runtime assessment before downloading/extracting payloads; warns/fails early. |
tests/unit/launcher-utils.test.ts |
Adds coverage for assessNodeRuntime across version tiers and edge cases. |
src/lib/storage/providers/sqlite.ts |
Translates better-sqlite3 load/ABI failures into a more actionable storage error (with cause). |
tests/unit/lib/storage/providers/sqlite.test.ts |
Adds tests for the new ABI/load-failure translation behavior. |
src/lib/db/providers/sql/sqlite-driver.ts |
Updates comments/error guidance around node:sqlite availability (22.13+). |
scripts/engine-smoke.sh |
Adds a local/CI smoke harness to validate tier-specific runtime behavior end-to-end. |
.github/workflows/ci.yml |
Adds payload build + Node-tier smoke jobs to enforce the tested engines floor. |
.github/workflows/npx-engine-smoke.yml |
Adds post-release, live-registry bare-npx validation across Node tiers. |
README.md |
Updates the npx support line to reflect Node 20.9+ with Node 24 LTS recommended. |
docs/providers/sqlite.md |
Updates SQLite runtime/driver support notes (Node 22.13+ for node:sqlite). |
docs/DISTRIBUTION.md |
Documents the tiered Node support matrix and the post-release npx smoke workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const code = (error as { code?: string })?.code; | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| return ( | ||
| code === "ERR_DLOPEN_FAILED" || | ||
| /NODE_MODULE_VERSION|was compiled against a different Node\.js version/i.test(message) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Agreed the predicate was too broad, but requiring a better-sqlite3 mention would not actually exclude the false-positive cases: libc-mismatch and missing-shared-lib loader errors also contain the better_sqlite3.node path. Fixed more precisely in c9494ef: the ERR_DLOPEN_FAILED branch is removed entirely and only the NODE_MODULE_VERSION / 'compiled against a different Node.js version' text (Node's module-register check, the actual ABI signal - verified empirically on Node 20/22) classifies as an ABI mismatch. Non-ABI dlopen failures now pass through untouched.
| const dlopenError = new Error("could not load the native binding") as Error & { code?: string }; | ||
| dlopenError.code = "ERR_DLOPEN_FAILED"; | ||
| constructorError = dlopenError; | ||
| const freshProvider = new SQLiteStorageProvider(":memory:"); | ||
| await expect(freshProvider.initialize()).rejects.toThrow(/requires Node\.js 24\+/); |
There was a problem hiding this comment.
Fixed in c9494ef together with the guard tightening: the fixture now uses realistic glibc loader text (ERR_DLOPEN_FAILED + the better_sqlite3.node path, no NODE_MODULE_VERSION) and asserts the original error passes through UNTOUCHED - so the test now fails if the guard ever becomes broad enough to translate non-ABI dlopen failures.
… not bare ERR_DLOPEN_FAILED A bare ERR_DLOPEN_FAILED also covers missing shared libraries, libc mismatches (e.g. musl), and corrupted files - on any Node version - so treating it as an ABI mismatch could surface a misleading 'requires Node 24' claim over the real failure. Node's module-register check always emits the NODE_MODULE_VERSION text for genuine ABI mismatches (verified empirically on Node 20/22), so that message is the precise and sufficient signal. Non-ABI dlopen failures now pass through untouched, with a regression test using realistic glibc loader text. Addresses both Copilot review comments on the PR.
…cher log-line contracts Review follow-ups from the external PR review notes: - New drift test reads package.json engines.node and asserts assessNodeRuntime's fail boundary matches it - bumping either floor without the other now fails a unit test instead of shipping a drift. - Two-component version strings documented as fail-closed by a test (process.versions.node is always three-component). - 'Checksum verified' and 'Starting LibreDB Studio <version>' are asserted verbatim by npx-engine-smoke.yml - marked as log-line contracts at their emit sites. - engine-smoke.sh: the fresh-credentials-per-boot assumption is now annotated with the issue it leans on (#132) and what to change when that defect is fixed.
6081895 to
1412a9d
Compare
…PM Publish Review follow-up (external review, non-blocking note): the post-release bare-npx validation was workflow_dispatch only, so its execution depended on a maintainer remembering the runbook step. It now also triggers on workflow_run after a successful NPM Publish: the expected version is read from the released commit's package.json, the job waits (up to 10 minutes) for the registry to actually serve that version to avoid publish-propagation races, and then asserts bare npx resolves exactly it on Node 20.9/22/24. Manual dispatch stays available for re-runs; the DISTRIBUTION.md runbook step now says verify-it-went-green instead of remember-to-run-it.
…n from the release tag CodeQL flagged the head_sha checkout in the workflow_run-triggered smoke (checkout of untrusted code in a trusted context). Exploitation was not possible here - the triggering NPM Publish only fires on maintainer releases, the job holds no secrets and contents:read only, and the checkout was never executed (package.json parse only) - but the flagged pattern is unnecessary anyway: on release-triggered runs workflow_run.head_branch IS the release tag, and the release guards already enforce tag == package.json version. The version now comes from the tag behind the same strict semver validation, non-semver triggers (e.g. a branch dispatch) fall back to the runnable-server-only assertion, and the checkout step is gone entirely.
|



Problem
npm's version picker avoids engine-incompatible versions for bare specs. With
engines.node >=24.0.0, every Node 20/22 user running barenpx @libredb/studiosilently resolves the ancient bin-less 0.9.13 and dies with "could not determine executable to run" - the #130 failure class, still live for the pre-24 population (Node 22 is maintenance LTS until April 2027). npm-deprecating the old versions helpednpm installusers but npm 10's npx flow does not surface deprecation messages (verified empirically).Approach: support the honest floor instead of excluding it
engines.node>=24.0.0->>=20.9.0(Next.js 16's own floor) + version bump to 0.9.44, backed by three guards so the widened floor is a tested claim, not a hope:assessNodeRuntime, pure + unit-tested): refuses< 20.9with clear guidance before any download; warns20.9-22.12that all SQLite features are unavailable (no unflaggednode:sqlite); warns22.13-23.xthat server-side SQLite storage needs Node 24; silent on 24+.STORAGE_PROVIDER=sqliteon an older Node now fails with an actionable message (Node 24 requirement +STORAGE_PROVIDER=postgres/localalternatives, original error chained ascause) instead of a rawNODE_MODULE_VERSIONdump.scripts/engine-smoke.sh): builds the standalone payload once, then boots it through the npx launcher on Node 20.9 / 22 / 24 and asserts each tier's documented behaviour - health, zero-config login, embedded sample query,node:sqliteavailability, preflight warning text, and the ABI guard.Plus
npx-engine-smoke.yml(workflow_dispatch): post-release bare-npx check against the live registry on all three tiers (wired into the DISTRIBUTION.md runbook), docs support matrix, and the sqlite provider doc/message sync (node:sqlite is unflagged from 22.13, not only 24).Node 24 users see zero behaviour change (preflight is silent, guard never fires, bundled runtimes untouched); the library consumer only gets a relaxed range.
Verification
node:20.9(v20.9.0, tier legacy20): 8/8 PASS - boots, login, sample query; sqlite connection fails with node:sqlite guidance; ABI guard message on storage.node:22(v22.23.1, tier node22): 8/8 PASS - sqlite connections work via node:sqlite; storage guard fires with the Node 24 message.node:24(v24.18.0, tier node24): 7/7 PASS - no warnings, everything works includingSTORAGE_PROVIDER=sqlite.node:18: launcher refuses cleanly ("requires Node.js 20.9 or newer", exit 1) before any download.Context: #130, follow-up to the 0.9.43 channel-validation run on epic #108.
Related issues (none closed by this PR)
--archivere-extracts per boot, hence fresh credentials per server) and documents it inline. The wipe-on-reextract defect remains open.