Install from the lockfile, so CI cannot be broken by someone else's release #53

Merged
jknapp merged 1 commits from fix/ci-npm-lockfile into main 2026-09-03 16:41:15 +00:00
Owner

build-linux fails on every workflow, before tauri build runs, at Install frontend dependencies:

npm error Cannot read properties of null (reading 'edgesOut')

Confirmed in the logs of preview jobs 5644 and 5636. No AppImage has been produced by CI since.

Root cause

Reproduced exactly, first attempt, by running the step's own commands locally on the same Node 22.23.2 the runner installs. The npm debug log gives the frame the CI output omits:

at #loadPeerSet (.../@npmcli/arborist/lib/arborist/build-ideal-tree.js:1289:38)

A null dereference in npm 10.9.8's peer-set resolver, reached through this chain:

vite → @vitejs/devtools@^0.4.0 || ^0.5.0 → @vitejs/devtools-vitest@^0.5.2
     → vitest@* → @vitest/browser-playwright@5.0.0 → vitest@4.1.11 → jsdom@* → canvas@^3.2.3

Nothing in this repo changed to cause it. The step deleted package-lock.json before installing, so every build re-resolved the entire dependency tree against the registry — through ranges like vitest@*. Someone else published a version whose peer graph npm cannot resolve, and our CI broke. The same command succeeded fifteen hours earlier for v0.4.21.

That is the actual defect: the build was never reproducible. The crash is only how we found out. Any dependency's release could have done this, at any time, to a release build.

Fix

Linux installs with npm ci, from the committed lockfile — which is what Windows already did. macOS moves too: it kept the lockfile but still ran npm install, which is free to re-resolve. All three platforms now install identically, and none can re-resolve mid-release.

The reason the lockfile was deleted is obsolete, not ignored

2d4fce9 added the deletion "to ensure correct platform-specific bindings", which was a real problem once — rollup and esbuild ship per-platform native packages, and an older npm would record only the host's.

That no longer holds, and I checked rather than assumed:

  • the committed lockfile records 25 rollup platform variants
  • npm ci on Linux installs precisely rollup-linux-x64-gnu, rollup-linux-x64-musl and @esbuild/linux-x64

The step carries a comment saying so, and saying not to reach for deleting the lockfile again: if npm ci refuses, package.json and the lockfile have genuinely diverged, and the fix is to commit an updated lockfile.

Verification

From the tree npm ci produces:

  • npx tsc --noEmit — clean
  • npm run build — successful
  • npm test -- --run752 passed, 62 files

Also checked that the next step cannot reintroduce a fresh resolution: npx tauri --version || npm install @tauri-apps/cli is unreachable in its fallback, because @tauri-apps/cli is a pinned devDependency that npm ci installs (tauri-cli 2.11.0).

Why this goes first

#52 (the update-channel fixes) has never had its scripts execute in CI — build-linux dies before tauri build on both of its commits, which is why preview-63f282b carries only .dmg/.msi/.exe and no AppImage. Merging this unblocks validating that one for real rather than only locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm

`build-linux` fails on every workflow, before `tauri build` runs, at **Install frontend dependencies**: ``` npm error Cannot read properties of null (reading 'edgesOut') ``` Confirmed in the logs of preview jobs 5644 and 5636. No AppImage has been produced by CI since. ## Root cause Reproduced exactly, first attempt, by running the step's own commands locally on the same Node 22.23.2 the runner installs. The npm debug log gives the frame the CI output omits: ``` at #loadPeerSet (.../@npmcli/arborist/lib/arborist/build-ideal-tree.js:1289:38) ``` A null dereference in npm 10.9.8's peer-set resolver, reached through this chain: ``` vite → @vitejs/devtools@^0.4.0 || ^0.5.0 → @vitejs/devtools-vitest@^0.5.2 → vitest@* → @vitest/browser-playwright@5.0.0 → vitest@4.1.11 → jsdom@* → canvas@^3.2.3 ``` **Nothing in this repo changed to cause it.** The step deleted `package-lock.json` before installing, so every build re-resolved the entire dependency tree against the registry — through ranges like `vitest@*`. Someone else published a version whose peer graph npm cannot resolve, and our CI broke. The same command succeeded fifteen hours earlier for v0.4.21. That is the actual defect: **the build was never reproducible.** The crash is only how we found out. Any dependency's release could have done this, at any time, to a release build. ## Fix Linux installs with `npm ci`, from the committed lockfile — which is what Windows already did. macOS moves too: it kept the lockfile but still ran `npm install`, which is free to re-resolve. All three platforms now install identically, and none can re-resolve mid-release. ## The reason the lockfile was deleted is obsolete, not ignored `2d4fce9` added the deletion "to ensure correct platform-specific bindings", which was a real problem once — rollup and esbuild ship per-platform native packages, and an older npm would record only the host's. That no longer holds, and I checked rather than assumed: - the committed lockfile records **25** rollup platform variants - `npm ci` on Linux installs precisely `rollup-linux-x64-gnu`, `rollup-linux-x64-musl` and `@esbuild/linux-x64` The step carries a comment saying so, and saying not to reach for deleting the lockfile again: if `npm ci` refuses, `package.json` and the lockfile have genuinely diverged, and the fix is to commit an updated lockfile. ## Verification From the tree `npm ci` produces: - `npx tsc --noEmit` — clean - `npm run build` — successful - `npm test -- --run` — **752 passed, 62 files** Also checked that the next step cannot reintroduce a fresh resolution: `npx tauri --version || npm install @tauri-apps/cli` is unreachable in its fallback, because `@tauri-apps/cli` is a pinned devDependency that `npm ci` installs (`tauri-cli 2.11.0`). ## Why this goes first **#52** (the update-channel fixes) has never had its scripts execute in CI — `build-linux` dies before `tauri build` on both of its commits, which is why `preview-63f282b` carries only `.dmg`/`.msi`/`.exe` and no AppImage. Merging this unblocks validating that one for real rather than only locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
jknapp added 1 commit 2026-09-03 16:28:30 +00:00
Install from the lockfile, so CI cannot be broken by someone else's release
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m40s
Build App (Preview) / build-windows (pull_request) Successful in 4m52s
Build App (Preview) / build-linux (pull_request) Successful in 5m0s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
f41b1d9054
`build-linux` fails before `tauri build` runs, on every workflow, at "Install
frontend dependencies":

    npm error Cannot read properties of null (reading 'edgesOut')

Reproduced exactly on the first attempt by running the step's own commands
locally on the same Node 22.23.2 the runner installs. The debug log gives the
frame the CI output omits:

    at #loadPeerSet (.../@npmcli/arborist/lib/arborist/build-ideal-tree.js:1289:38)

It is a null dereference in npm 10.9.8's peer-set resolver, reached through
vite → @vitejs/devtools → @vitejs/devtools-vitest → vitest@* →
@vitest/browser-playwright → vitest@4.1.11 → jsdom@* → canvas.

**Nothing in this repo changed to cause it.** The step deleted
`package-lock.json` before installing, so every build re-resolved the entire
tree against the registry against ranges like `vitest@*`. A dependency
published a version that produces a peer graph npm cannot resolve, and our CI
broke — the same command succeeded fifteen hours earlier for 0.4.21. That is
the real defect: the build was never reproducible, and the crash is only how we
found out.

So Linux installs with `npm ci`, from the committed lockfile, like Windows
already did. macOS moves too — it kept the lockfile but still ran `npm
install`, which is free to re-resolve; all three platforms now install
identically and none can re-resolve mid-release.

**The reason the lockfile was being deleted is obsolete, not ignored.** 2d4fce9
removed it "to ensure correct platform-specific bindings", which was a real
problem once. The committed lockfile now records 25 rollup platform variants,
and `npm ci` on Linux installs precisely rollup-linux-x64-{gnu,musl} and
@esbuild/linux-x64 — checked directly. A comment on the step says so, and says
not to reach for deleting the lockfile again: if `npm ci` refuses, package.json
and the lockfile have genuinely diverged and the fix is to commit an updated
lockfile.

Verified from the resulting tree: `tsc --noEmit` clean, `npm run build`
successful, 752 tests across 62 files passing. The `npx tauri --version ||
npm install @tauri-apps/cli` fallback in the next step cannot reintroduce a
fresh resolution — the CLI is a pinned devDependency that `npm ci` installs, so
the fallback is unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
jknapp merged commit b21a568bf5 into main 2026-09-03 16:41:15 +00:00
jknapp deleted branch fix/ci-npm-lockfile 2026-09-03 16:41:15 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: CyberCoveLLC/Triple-C#53