Skip to main content
All posts
Tooling6 min read

Electron Builds Pass Locally but Fail on CI With a Missing Native Module

The desktop build worked on my machine and on every teammate machine, then failed on the build server with a module that supposedly could not be found. It was there the whole time, compiled for the wrong runtime.

Portrait of Benjamin Fazli, the author of bfzli.com

Benjamin Fazli

Principal EngineerSkopje, North Macedonia

Hands typing on a laptop at a minimal desk in black and white

The symptom

npm run build produced a working installer locally. The same command on CI failed while packaging with a message claiming a native module could not be found, naming a file that was plainly sitting in node_modules. Deleting the cache did nothing. Pinning the dependency did nothing.

The actual problem

Electron ships its own build of Node, and it is almost never the same version as the Node that runs on your build server. Native modules are compiled against a specific ABI. A module built for Node 22 will not load inside an Electron runtime expecting a different ABI, and the error you get is a resolution failure rather than a version complaint, which sends everyone hunting in the wrong direction.

It worked locally because I had run the app in development weeks earlier, which had quietly rebuilt the module for Electron and left the result in my node_modules. CI started from a clean install every time and never performed that step.

The fix

Rebuild native modules against the Electron ABI as an explicit step, so it happens on every machine rather than by accident:

json
{
  "scripts": {
    "postinstall": "electron-builder install-app-deps",
    "build": "electron-builder --publish never"
  }
}

install-app-deps reads the Electron version from your dependencies and rebuilds anything native against the matching ABI. Because it runs as postinstall, CI performs it as part of the clean install and the packaging step finds exactly what it expects.

Two supporting changes made the pipeline predictable:

  • Pin the Node version on CI with a .nvmrc file and have the workflow read it. Matching your local major version removes an entire class of difference.
  • Cache ~/.cache/electron and ~/.cache/electron-builder rather than node_modules. Caching node_modules is what preserved my accidental local rebuild in the first place, and it will hide this bug again the moment you restore it.

Proving it locally

The reason this took a day rather than an hour is that I kept testing on a machine that could not reproduce the failure. Once I started with a genuinely clean tree, the loop tightened immediately:

bash
rm -rf node_modules
npm ci
npm run build
If a build only fails on CI, the difference is almost never CI. It is the state your machine has accumulated. Reproduce on a clean tree before you touch the pipeline configuration.