npm EBADENGINE "Unsupported engine" in CI - Fix Node Version Mismatch
EBADENGINE means a package declares an engines range that the current Node or npm version does not satisfy. It warns by default but can hard-fail when engine-strict is on.
What this error means
During install npm prints EBADENGINE listing the package, the wanted Node range, and the current version. With engine-strict enabled the install aborts instead of warning.
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'vite@5.0.0',
npm WARN EBADENGINE required: { node: '^18.0.0 || >=20.0.0' },
npm WARN EBADENGINE current: { node: 'v16.20.2', npm: '8.19.4' }
npm WARN EBADENGINE }Diagnose it: reproduce the CI install locally
Install failures are usually environment drift rather than a broken lockfile: a different package-manager major, a different Node version, or a cache that is being restored from a run with different inputs. Reproduce the CI conditions before changing the lockfile, because regenerating it hides the real cause.
# match the runner exactly, then install from a clean slate
node --version && npm --version
rm -rf node_modules
npm ci --foreground-scripts
# if that succeeds locally but fails in CI, the difference is the cache
# or the package-manager version, not your lockfileCommon causes
The CI Node version is older than dependencies require
The runner uses a Node major below the engines range a dependency declares, so npm flags the mismatch.
engine-strict turns the warning into a failure
With engine-strict=true in .npmrc, an unsupported engine aborts the install rather than warning.
How to fix it
Pin a supported Node version in CI
- Set the Node version in the setup step to a value inside the required range.
- Re-run the install.
- uses: actions/setup-node@v4
with:
node-version: 20Verify the fix survives a cold cache
A green run immediately after a fix often proves nothing, because it restored a cache written before the change. Force a cold install once to confirm the fix is real.
# temporarily bust the cache key to prove the fix on a cold runner
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
# then bump this suffix once, run, and remove it
# key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}-v2How to prevent it
- Declare your own engines field, pin the Node version in setup-node, and keep it inside the intersection of every dependency engines range so installs stay clean.