Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ dist
.swc
*.tsbuildinfo

# Sibling-product trees and their build artifacts. None of these are inputs to
# `next build`, and without them a local `docker build` copies whatever the
# developer happens to have built - a Rust target/ dir alone put 2.3 GB of
# desktop-shell objects inside the production image, so the image being smoke
# tested locally was not the image CI publishes (CI checkouts are clean).
desktop
dist-desktop
dist-rel
operator
packaging
charts
.attw

# Test and automation trees
e2e
tests
playwright-report
test-results
data-e2e-offline
.loop
loop
.claude

# Environment files
.env
.env*.local
Expand Down
13 changes: 8 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ jobs:
name: playwright-report
path: playwright-report/

# The engines floor in package.json (>=20.9.0) is a tested claim: build the
# The engines floor in package.json (>=24.0.0) is a tested claim: build the
# standalone payload once, then boot it through the npx launcher on every
# supported Node tier and assert the documented behaviour of each
# (scripts/engine-smoke.sh).
Expand Down Expand Up @@ -248,12 +248,15 @@ jobs:
fail-fast: false
matrix:
include:
- node-version: "20.9"
tier: legacy20
- node-version: "22"
tier: node22
# The reference runtime the payload above was built on.
- node-version: "24"
tier: node24
# The current upper release. Asserts the SAME outcome as node24,
# which is what proves the payload is portable across majors:
# better-sqlite3 is N-API since v13, so the binary built into a
# Node 24 payload has to load and work here too.
- node-version: "26"
tier: node26
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand Down
65 changes: 59 additions & 6 deletions .github/workflows/npx-engine-smoke.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Post-release validation of the bare-npx experience against the LIVE npm
# registry (issue #130 regression class): npm's version picker avoids
# engine-incompatible versions for bare specs, so `npx @libredb/studio` must
# resolve a runnable release on every supported Node tier - not fall back to
# an ancient bin-less version.
# resolve a runnable release on every Node tier - not fall back to an ancient
# bin-less version.
#
# Since the floor moved to Node 24 (#326) this workflow covers two outcomes,
# not one. On 24/26 bare npx must resolve the release just published; on
# 20.9/22 the picker silently pins users to the last <24 release, and the
# assertion is that the pin lands on something that still BOOTS. Note the
# pinning is silent by design - `npm` prints no notice when the floor excludes
# a newer version, which is why the release runbook deprecates the pinned
# version with a pointer instead.
#
# Runs automatically after a successful "NPM Publish" (the expected version is
# the release tag - workflow_run.head_branch - which the release guards pin to
Expand All @@ -27,13 +35,28 @@ permissions:

jobs:
npx-smoke:
name: bare npx on Node ${{ matrix.node-version }}
name: bare npx on Node ${{ matrix.node-version }} (${{ matrix.expect }})
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ["20.9", "22", "24"]
include:
# Supported runtimes: bare npx must resolve the release just published.
- node-version: "24"
expect: exact
- node-version: "26"
expect: exact
# Below the engines floor (>=24.0.0). npm's picker does not error
# here - it silently resolves the newest ENGINE-COMPATIBLE release,
# so these users are pinned to the last <24 version. That pin is only
# acceptable while it lands on a RUNNABLE release; issue #130 was
# exactly this mechanism dropping users onto an ancient bin-less one.
# This leg keeps that guarantee under test.
- node-version: "22"
expect: pinned
- node-version: "20.9"
expect: pinned
steps:
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
Expand Down Expand Up @@ -92,6 +115,7 @@ jobs:
- name: Run bare npx and assert resolution
env:
EXPECTED_VERSION: ${{ steps.expected.outputs.version }}
EXPECT_MODE: ${{ matrix.expect }}
run: |
set -euo pipefail
WORK=$(mktemp -d)
Expand All @@ -111,9 +135,38 @@ jobs:
grep -F "Checksum verified" npx.log
RESOLVED=$(sed -n 's/^Starting LibreDB Studio \([0-9][0-9.]*\) .*/\1/p' npx.log | head -1)
echo "resolved version: ${RESOLVED:-unknown}"
if [ -n "$EXPECTED_VERSION" ] && [ "$RESOLVED" != "$EXPECTED_VERSION" ]; then
echo "FAIL: bare npx resolved '$RESOLVED', expected '$EXPECTED_VERSION'; log:" >&2
if [ -z "$RESOLVED" ]; then
echo "FAIL: could not read the resolved version from the launcher banner; log:" >&2
cat npx.log >&2
exit 1
fi
if [ -z "$EXPECTED_VERSION" ]; then
echo "==> OK (runnable-server assertion only)"
exit 0
fi
case "$EXPECT_MODE" in
exact)
if [ "$RESOLVED" != "$EXPECTED_VERSION" ]; then
echo "FAIL: bare npx resolved '$RESOLVED', expected '$EXPECTED_VERSION'; log:" >&2
cat npx.log >&2
exit 1
fi
;;
pinned)
# Serving health above already proved this release is runnable.
# What is left to prove is that the engines floor actually held:
# resolving the NEW version here would mean npm ignored it and
# the floor is decorative.
if [ "$RESOLVED" = "$EXPECTED_VERSION" ]; then
echo "FAIL: bare npx resolved '$RESOLVED' on a runtime below the engines floor - the floor is not being honoured; log:" >&2
cat npx.log >&2
exit 1
fi
echo "pinned to '$RESOLVED' as expected (floor excludes '$EXPECTED_VERSION')"
;;
*)
echo "FAIL: unknown expect mode '$EXPECT_MODE'" >&2
exit 1
;;
esac
echo "==> OK"
18 changes: 12 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ RUN apt-get update && apt-get install -y python3 make g++ --no-install-recommend
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

# Build with Node.js to avoid Bun/QEMU segfaults on ARM64
# trixie-slim: must match the oven/bun deps stage glibc (Debian 13 / glibc 2.41),
# otherwise native modules (better-sqlite3) compiled in deps fail to load here.
# Build with Node.js to avoid Bun/QEMU segfaults on ARM64.
# trixie-slim keeps the toolchain consistent with the oven/bun deps stage, but
# it is no longer load-bearing for the native module: better-sqlite3 v13 ships
# every prebuild inside the package and picks one in the RUNNING process
# (lib/binding.js reads process.platform/arch and detects musl via
# process.report), so neither the ABI nor the libc of the installing stage
# constrains the stage that requires it.
FROM node:24.16.0-trixie-slim AS builder
WORKDIR /usr/src/app
COPY --from=deps /usr/src/app/node_modules ./node_modules
Expand Down Expand Up @@ -56,10 +60,12 @@ RUN mkdir -p .next data
COPY --from=builder /usr/src/app/.next/standalone ./
COPY --from=builder /usr/src/app/.next/static ./.next/static

# Copy better-sqlite3 native binding for server storage support
# Copy better-sqlite3 native binding for server storage support. Since v13 the
# package is N-API and self-contained: lib/binding.js resolves
# ../prebuilds/<platform>-<arch>.node relative to itself, so the former
# bindings + file-uri-to-path runtime dependencies are gone (they are no longer
# in the lockfile at all). Keep in sync with scripts/build-standalone-payload.sh.
COPY --from=builder /usr/src/app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
COPY --from=builder /usr/src/app/node_modules/bindings ./node_modules/bindings
COPY --from=builder /usr/src/app/node_modules/file-uri-to-path ./node_modules/file-uri-to-path
# prebuild-install is only needed at build time, not runtime

# Copy the embedded LibreDB database package. The libredb provider lazy-imports
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Run a full SQL IDE in one command — no clone, no build:
# Docker (recommended)
docker run -d -p 3000:3000 ghcr.io/libredb/libredb-studio:latest

# or with Node.js 20.9+ (no Docker)
# or with Node.js 24+ (no Docker)
npx @libredb/studio
```

Expand Down Expand Up @@ -234,7 +234,7 @@ And nothing is held back. Single sign-on, ER diagrams, the AI assistant and the
| :--- | :--- | :--- |
| **Docker** | `docker run -d -p 3000:3000 ghcr.io/libredb/libredb-studio:latest` | Zero-config: the admin password is printed to the log on first run |
| **Helm (Kubernetes)** | `helm install libredb oci://ghcr.io/libredb/charts/libredb-studio` | Zero-config: first-run admin credentials are printed to the pod log |
| **npx** | `npx @libredb/studio` | Linux/macOS/Windows, Node 20.9+ (Node 24 LTS recommended); downloads the release server archive |
| **npx** | `npx @libredb/studio` | Linux/macOS/Windows, Node 24+ (24 LTS is the reference runtime); downloads the release server archive |
| **Homebrew** | `brew trust libredb/tap && brew install libredb/tap/libredb-studio` | `brew trust` is required once (Homebrew 6+; run `brew update` if unknown) |
| **deb / rpm** | `sudo dpkg -i libredb-studio_<version>_amd64.deb` | Attached to each GitHub release; systemd service included |
| **Snap** | `sudo snap install libredb-studio` | Zero-config: the admin password is printed to `sudo snap logs libredb-studio` on first run — [Snap Store listing](https://snapcraft.io/libredb-studio) |
Expand Down
4 changes: 2 additions & 2 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# Docker(推奨)
docker run -d -p 3000:3000 ghcr.io/libredb/libredb-studio:latest

# または Node.js 20.9+ で(Dockerなし)
# または Node.js 24+ で(Dockerなし)
npx @libredb/studio
```

Expand Down Expand Up @@ -183,7 +183,7 @@ StudioがMITなのは、あらゆる場所に置ける必要があるからで
- ユニット、API、統合、hooks、コンポーネント、E2Eの6層
- **行カバレッジ100%**、しかもCIの必須ゲート。下がればマージできません
- SonarCloud品質ゲート
- リリースごとにNode 20.9 / 22 / 24でスモークテスト
- リリースごとにNode 24 / 26でスモークテスト

```bash
bun run test # 全テスト
Expand Down
4 changes: 2 additions & 2 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# Docker(推荐)
docker run -d -p 3000:3000 ghcr.io/libredb/libredb-studio:latest

# 或者用 Node.js 20.9+(不装 Docker)
# 或者用 Node.js 24+(不装 Docker)
npx @libredb/studio
```

Expand Down Expand Up @@ -183,7 +183,7 @@ Studio 是 MIT,因为它必须能去任何地方。付费的是 libredb-platfo
- 单元、API、集成、hooks、组件、E2E 六层测试
- **行覆盖率 100%**,并且是 CI 的硬性门禁。覆盖率掉下来,合并就被拦住
- SonarCloud 质量门禁
- 每次发布跨 Node 20.9 / 22 / 24 做冒烟测试
- 每次发布跨 Node 24 / 26 做冒烟测试

```bash
bun run test # 全部测试
Expand Down
41 changes: 13 additions & 28 deletions bin/lib/launcher-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -205,23 +205,27 @@ export function extractArchive(archivePath, destDir) {
}

/**
* Runtime tiers for the payload (Next.js standalone server):
* - Next.js 16 itself needs Node >= 20.9 - below that the server cannot run.
* - node:sqlite (SQLite database connections) exists unflagged from 22.13.
* - The bundled better-sqlite3 binding (server-side SQLite storage,
* STORAGE_PROVIDER=sqlite) targets the Node 24 ABI line.
* Node 24 LTS is the fully supported runtime; older tiers run with the
* degradations spelled out by assessNodeRuntime so users see them up front.
* Runtime requirement for the payload (Next.js standalone server). Node 24 LTS
* is the reference runtime - it is what release-artifacts.yml builds the
* payload on (issue #326 raised the floor here from 20.9 to clear the runway
* for the agent runtime, which needs a modern Node and ESM).
*
* There is a floor but deliberately no ceiling. The payload's only native
* module is better-sqlite3, and since v13 it is built on the N-API: one
* prebuilt binary per platform, valid across Node majors. So a payload built
* on Node 24 runs unchanged on Node 26, and every runtime at or above the
* floor is fully supported rather than degraded - which is what the node26 leg
* of scripts/engine-smoke.sh asserts.
*/
const MINIMUM_NODE = { major: 20, minor: 9 };
const MINIMUM_NODE = { major: 24, minor: 0 };

/**
* Assess a Node.js runtime version (process.versions.node shape, e.g.
* "22.13.0") against the payload's requirements. Pure and injectable for
* unit tests - never reads process state itself.
*
* @param {string} version
* @returns {{ action: "fail" | "warn" | "ok", message: string | null }}
* @returns {{ action: "fail" | "ok", message: string | null }}
*/
export function assessNodeRuntime(version) {
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
Expand All @@ -243,25 +247,6 @@ export function assessNodeRuntime(version) {
].join("\n"),
};
}
if (major < 22 || (major === 22 && minor < 13)) {
return {
action: "warn",
message:
`Node ${version}: SQLite features are unavailable on this runtime - ` +
"SQLite database connections need the built-in node:sqlite module (Node 22.13+) and " +
"server-side SQLite storage (STORAGE_PROVIDER=sqlite) needs Node 24. " +
"Everything else works; use Node 24 LTS for full functionality.",
};
}
if (major < 24) {
return {
action: "warn",
message:
`Node ${version}: server-side SQLite storage (STORAGE_PROVIDER=sqlite) needs Node 24 - ` +
"the bundled native module targets the Node 24 ABI. Everything else works " +
"(node:sqlite may print a one-time ExperimentalWarning); use Node 24 LTS for full functionality.",
};
}
return { action: "ok", message: null };
}

Expand Down
6 changes: 3 additions & 3 deletions bin/studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,11 @@ async function main() {
return;
}

// Refuse runtimes the payload cannot run on and surface degraded tiers
// up front, before any download happens (see assessNodeRuntime).
// Refuse runtimes the payload cannot run on, up front, before any download
// happens (see assessNodeRuntime). There are no degraded tiers above the
// floor - the payload's one native module is ABI-independent.
const runtime = assessNodeRuntime(process.versions.node);
if (runtime.action === "fail") fail(runtime.message);
if (runtime.action === "warn") console.warn(runtime.message);

const cacheDir = resolveCacheDir(pkg.version, os.homedir());
const archive = args.archive || process.env.LIBREDB_STUDIO_ARCHIVE || null;
Expand Down
Loading
Loading