Interpret `engines.node` When Local Works but CI Rejects the Install
Node engine ranges communicate compatibility but may be advisory or enforced depending on installer settings. Align the declared range with the actual test matrix.
A range is a promise about tested runtimes
The engines.node field describes which Node versions a package supports. Package managers may warn or fail on a mismatch depending on configuration, so “it installed locally” does not establish that CI's runtime is supported. Conversely, setting a broad range to silence a warning makes a promise the project may not have tested.
Inspect the declaration and actual runtime:
node --version
node -p "require('./package.json').engines"
pnpm config get engine-strictFor a monorepo, check whether the root and individual workspaces publish different support ranges.
Choose a support policy, then test it
If the app requires Node 22, express that intent and configure CI accordingly. If a published library supports several majors, test the minimum and current supported versions. Runtime APIs, native modules, and syntax transforms can differ even when dependencies install cleanly.
Do not set engine-strict=false globally as the first repair. If a dependency declares an overly narrow range but is known to work, document a scoped decision and test the relevant code path. If the project itself uses an API absent from the CI Node version, the proper repair is to change the runtime or code, not hide the mismatch.
Keep .nvmrc, .node-version, engines.node, container images, and CI in agreement according to the chosen policy. Print node --version in CI logs so an image update is observable. A range becomes useful when it is backed by a repeatable matrix rather than merely copied into metadata.
Trace the runtime from editor to package manager
Local and CI commands can resolve different Node executables even on the same workstation. A terminal may load nvm, asdf, Volta, or another manager, while an IDE task or GUI-launched process inherits a different PATH. Run the version check inside the failing script and print process.execPath, not only the version displayed by a separate terminal. Check the package manager too, because an engine-strict setting can turn a warning into an installation error.
Inspect the root engines.node declaration and the engine range of any published workspace package involved. These declarations have different responsibilities: the application describes its supported runtime for development and deployment, while a library makes a compatibility promise to consumers. Do not widen a library range because one app happens to run on a newer version. Test the minimum supported major and the version used by production before publishing that promise.
Align every runtime selector
Compare engines.node, .nvmrc or .node-version, the package manager's packageManager field, the container base image, and CI setup. They need not use identical syntax, but they must describe a compatible policy. If CI uses a floating image tag, an image update can change Node without a source diff; pinning or logging the resolved version makes that change visible. In a workspace, document whether the root runtime governs all packages or whether some packages have separate support matrices.
When changing the supported range, run tests on the oldest version you continue to claim and the current deployment version. Include native dependencies and build tools, because an install that succeeds does not guarantee that native bindings load. If a third-party package has an inaccurate engine range but the project depends on its behavior, isolate and document that exception rather than turning off strict checks globally.
The resolution should be explicit: either update the runtime in local setup and CI, or correct the project's stated support range after testing. Keep the chosen version visible in build logs and add a small CI assertion. That turns “works on my machine” into a concrete comparison between the Node executable, package-manager policy, and tested project support.
Native modules deserve their own check because a JavaScript install can pass while a binary fails to load at runtime. Test the oldest claimed Node version with the production build and any postinstall scripts. If a third-party engine range is inaccurate, isolate the exception and track it for removal rather than turning off strict checks globally. The engine field should describe tested behavior, not merely a version that happens to install.
Print process.execPath in the failing job so a version-manager mismatch is visible. Align .nvmrc, .node-version, container images, and CI setup with the selected range. A floating image can silently change the runtime without a source diff, so log or pin it. The practical support policy is the intersection of what the metadata claims and what the test matrix actually exercises.