Skip to content

feat(runtime): tested Node 20.9 engines floor - launcher preflight, ABI guard, engine-matrix CI - #142

Merged
cevheri merged 6 commits into
mainfrom
feat/node-engine-floor
Jul 5, 2026
Merged

feat(runtime): tested Node 20.9 engines floor - launcher preflight, ABI guard, engine-matrix CI#142
cevheri merged 6 commits into
mainfrom
feat/node-engine-floor

Conversation

@cevheri

@cevheri cevheri commented Jul 5, 2026

Copy link
Copy Markdown
Member

Problem

npm's version picker avoids engine-incompatible versions for bare specs. With engines.node >=24.0.0, every Node 20/22 user running bare npx @libredb/studio silently 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 helped npm install users 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:

  1. Launcher preflight (assessNodeRuntime, pure + unit-tested): 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+.
  2. Storage ABI guard: the bundled better-sqlite3 binding targets the Node 24 ABI; STORAGE_PROVIDER=sqlite on an older Node now fails with an actionable message (Node 24 requirement + STORAGE_PROVIDER=postgres/local alternatives, original error chained as cause) instead of a raw NODE_MODULE_VERSION dump.
  3. Engine-matrix CI (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:sqlite availability, 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

  • Local gates: format / lint / typecheck / test (0 fail; +11 new tests) / build / build:lib / attw / knip - all green.
  • Local engine matrix with the exact CI script in containers:
    • 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 including STORAGE_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)

…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.
@cevheri cevheri added the enhancement New feature or request label Jul 5, 2026
@cevheri

cevheri commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.node to >=20.9.0 and 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.

Comment thread src/lib/storage/providers/sqlite.ts Outdated
Comment on lines +23 to +29
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)
);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +271 to +275
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\+/);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

cevheri added 2 commits July 5, 2026 04:24
… 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.
…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.
Comment thread .github/workflows/npx-engine-smoke.yml Fixed
…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.
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@cevheri
cevheri merged commit 421b6dc into main Jul 5, 2026
15 checks passed
@cevheri
cevheri deleted the feat/node-engine-floor branch July 5, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants