Skip to content

fix(release): rebuild native modules for Electron in fork releases - #80

Merged
64ix merged 1 commit into
fork-mainfrom
fix/fork-release-native-rebuild
Aug 5, 2026
Merged

fix(release): rebuild native modules for Electron in fork releases#80
64ix merged 1 commit into
fork-mainfrom
fix/fork-release-native-rebuild

Conversation

@64ix

@64ix 64ix commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Description

v1.2.8 was a broken release. Its macOS and Windows builds packaged a better-sqlite3
compiled for the CI runner's system Node, so the app threw on its first require and never
opened a window. It was published, caught, and reverted to a draft within minutes; the tag and
draft are deleted and this ships as v1.2.9 — a bump rather than a re-cut of 1.2.8, because
a client that had already updated would never re-download the same version.

Root cause. rebuild-native.ts was given only a buildPath, and @electron/rebuild uses
that for two separate questions: which modules are prod dependencies (answered by a
package.json) and where they are installed (answered by a node_modules tree). Under pnpm's
hoisted linker no single directory answers both — the app directory says "not installed"
because the native modules are hoisted out of it, the workspace root says "not a dependency" —
so it rebuilt nothing, printed success, and exited 0 in about a second. Upstream never hits
this: build.ts packages from a pnpm deploy --legacy --prod tree where both hold at once.
The fork workflow cannot use that tree, because it must package with the fork config, whose
files mappings are relative to the app directory.

This fails silently by construction. electron-builder runs with npmRebuild: false, so it
packages whatever pnpm install compiled, and scripts/postinstall.ts deliberately skips
electron-rebuild when CI is set. Electron keeps its own NODE_MODULE_VERSION namespace, so
the two can never coincidentally agree — Electron 40 requires 143 where Node 24 produces
137.

Fix. Thread projectRootPath through rebuild-native.ts and pass
--project-root "$GITHUB_WORKSPACE" from both platform jobs. The flag is additive: with
--deploy-dir it defaults to the deploy directory, exactly today's behaviour for upstream's
pipeline.

The guard that was missing. Nothing caught this, which matters more than the bug.
codesign --verify signs a bundle that cannot boot, and verify-mac.ts probes for a module
named sqlite3 — not one this app uses — and only warns when absent. New
scripts/release/verify-native-abi.ts runs the packaged Electron binary as Node
(ELECTRON_RUN_AS_NODE=1, which reports Electron's module version) and dlopens the packaged
.node files, so the ABI is judged by the binary that will actually load them. It runs after
upload, so a failure leaves an updater-invisible draft rather than a shipped release.

Also. Drops the trust-setting removal from the macOS cleanup step. security remove-trusted-cert -d waits on an authorization a headless runner cannot grant and blocks
forever — a hang is not something || true rescues. It held the v1.2.8 mac job open at 100%
complete until it was cancelled. Runners are destroyed after the job, so there is nothing to
clean up.

Related issues

Follows #74. No issue; found while releasing.

Testing

Verified the new guard both ways against the real v1.2.8 artifact downloaded from the
release:

  • as shipped → fails with compiled against a different Node.js version using NODE_MODULE_VERSION 137. This version of Node.js requires NODE_MODULE_VERSION 143
  • same bundle with the .node replaced by an electron-rebuilt one → passes,
    Verified 1 packaged native module(s)
  • also confirmed it needed absolute paths: the hardened runtime rejects a relative dlopen
    ("relative path not allowed in hardened program"), which would have failed good bundles

And the fix itself:

  • rebuild-native.ts with projectRootPath set → 11.7s of real work; without it → ~0s and
    nothing rebuilt
  • the resulting module loads under Electron 40.10.2 (NODE_MODULE_VERSION 143) and is
    correctly rejected by plain Node, proving it is Electron-targeted
  • pnpm typecheck (app + release scripts), pnpm exec oxlint, pnpm run format
  • workflow YAML parsed and the job/step graph checked

Screenshot/Recording (if applicable)

Not applicable; release tooling only.

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks
  • I updated docs when behavior or setup changed (FORK.md, incl. the v1.2.8 incident)
  • I only added comments where the logic is not obvious
  • I used Conventional Commits
  • Release-critical: called out above

v1.2.8 shipped a macOS and Windows build whose better-sqlite3 was compiled for the
runner's system Node, so the app threw on its first require and never opened a
window. It was published, caught, and pulled back to a draft within minutes.

`rebuild-native.ts` was given only a `buildPath`, and @electron/rebuild uses that for
two separate questions: which modules are prod dependencies (the app's package.json)
and where they are installed (a node_modules tree). Under pnpm's hoisted linker no
single directory answers both — the app directory says "not installed" because they
are hoisted out of it, the workspace root says "not a dependency" — so it rebuilt
nothing, printed success and exited 0 in a second. Upstream never hits this because
build.ts packages from a `pnpm deploy --legacy --prod` tree where both hold at once;
the fork workflow cannot use that tree, since it must package with the fork config,
whose file mappings are relative to the app directory.

So thread `projectRootPath` through and pass it from both platform jobs. The flag is
additive: with `--deploy-dir` it defaults to the deploy directory, exactly today's
behaviour for upstream's pipeline.

Nothing caught this, which is the more important half. `codesign --verify` happily
signs a bundle that cannot boot, and verify-mac.ts probes for a module named
`sqlite3` — not one this app uses — and only warns when it is absent. The new
verify-native-abi.ts runs the *packaged* Electron as Node and dlopens the packaged
.node files, so the ABI is judged by the binary that will actually load them.
Verified both ways against the real v1.2.8 artifact: it reports
`NODE_MODULE_VERSION 137 ... requires 143` on the shipped bundle, and passes once
the module is replaced with an electron-rebuilt one.

Also drops the trust-setting removal from the macOS cleanup step. `security
remove-trusted-cert -d` waits on an authorization the runner has no GUI to grant and
blocks forever, which `|| true` cannot rescue; it held the v1.2.8 job open at 100%
complete until it was cancelled. Runners are destroyed after the job anyway.
@64ix
64ix merged commit 58e5b34 into fork-main Aug 5, 2026
1 check passed
@64ix
64ix deleted the fix/fork-release-native-rebuild branch August 5, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant