How to check node version of a project
Read all four declarations, not one. node -v gives the runtime in use, npm pkg get engines the range the package requires, .nvmrc the version a manager selects, and the CI workflow the version the pipeline installs. In the demo project below they read 22.23.2, a range ending at 21, 20.11.1 and 22.
Why check this
A project does not have a Node version, it has several declarations of one, and they drift apart independently. The runtime on your machine is set by whatever a version manager last selected. engines is edited when a dependency demands it. .nvmrc is edited when a developer upgrades. The CI workflow is edited when a runner image is deprecated.
Run this when you pick up an unfamiliar repository, and again when a test passes locally and fails in CI. The failure it prevents is the one that produces no useful message: a syntax or API difference between two Node majors surfaces as a stack trace inside a dependency, and the pipeline log never mentions a version at all.
Prerequisites
- Node.js and npm on PATH.
node -vandnpm -vboth answer in a second. - The checkout, including
.github/workflows. A version declared in CI is invisible frompackage.json. - The output below comes from a four-file demo project, so every declaration disagrees on purpose. Create it in a scratch directory:
mkdir versiondemo && cd versiondemo && mkdir -p .github/workflows
printf '{\n "name": "versiondemo",\n "version": "1.0.0",\n "private": true,\n "engines": { "node": ">=18 <21" }\n}\n' > package.json
printf '20.11.1\n' > .nvmrc
printf "name: ci\non: [push]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/setup-node@v4\n with:\n node-version: '22'\n - run: npm ci\n" > .github/workflows/ci.yml
- The engines field is advisory by default. Step 7 shows what turns it into a gate.
- Step 5 runs this file. Save it as
node-version-sources.mjsnext topackage.json:
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { execSync } from 'node:child_process';
const say = (where, value) => console.log(where.padEnd(22) + ' ' + value);
const read = (f) => (existsSync(f) ? readFileSync(f, 'utf8').trim() : '(absent)');
say('installed node', process.version);
say('installed npm', execSync('npm -v').toString().trim());
const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf8')) : {};
say('package.json engines', pkg.engines?.node ?? '(absent)');
say('.nvmrc', read('.nvmrc'));
say('.node-version', read('.node-version'));
const dir = '.github/workflows';
const files = existsSync(dir) ? readdirSync(dir) : [];
for (const f of files) {
for (const line of readFileSync(dir + '/' + f, 'utf8').split('\n')) {
const m = /node-version:\s*'?"?([^'"\s]+)/.exec(line);
if (m) say(dir + '/' + f, m[1]);
}
}
if (files.length === 0) say(dir, '(absent)');
Steps
- Step 1.
Read the runtime that is going to execute the code right now.
node -vv22.23.2This is the only one of the four numbers that describes a fact rather than an intention.
- Step 2.
Read the range the package declares.
npm pkg get engines{ "node": ">=18 <21" }npm pkg getparses the manifest, so it answers{}rather than an error when the field is absent. - Step 3.
Read what a version manager would select.
cat .nvmrc20.11.1nvm, fnm and asdf read this file. npm does not, and neither does Node. Nothing enforces it until somebody types
nvm use. - Step 4.
Read what the pipeline installs.
grep -n "node-version" .github/workflows/ci.yml10: node-version: '22'This is the number that decides whether the build goes green, and it is the one nobody looks at.
- Step 5.
Collect all four in one pass with the script from Prerequisites.
node node-version-sources.mjsinstalled node v22.23.2 installed npm 10.9.8 package.json engines >=18 <21 .nvmrc 20.11.1 .node-version (absent) .github/workflows/ci.yml 22Four declarations, three different answers, and the running version satisfies none of them. The same script on the h2check repository prints
>=22and(absent)three times, which is a different kind of finding: nothing states what CI installs. - Step 6.
Ask npm what it makes of the mismatch.
npm install --no-audit --no-fundnpm warn EBADENGINE Unsupported engine { npm warn EBADENGINE package: 'versiondemo@1.0.0', npm warn EBADENGINE required: { node: '>=18 <21' }, npm warn EBADENGINE current: { node: 'v22.23.2', npm: '10.9.8' } npm warn EBADENGINE } up to date in 392msA warning, and exit code 0. The install succeeded on a runtime the package says it does not support.
- Step 7.
Turn the warning into a failure, then run the same install again.
printf 'engine-strict=true\n' > .npmrc && npm install --no-audit --no-fundnpm error code EBADENGINE npm error engine Unsupported engine npm error engine Not compatible with your version of node/npm: versiondemo@1.0.0 npm error notsup Not compatible with your version of node/npm: versiondemo@1.0.0 npm error notsup Required: {"node":">=18 <21"} npm error notsup Actual: {"npm":"10.9.8","node":"v22.23.2"} npm error A complete log of this run can be found in: …Exit code 1. Commit that
.npmrcand the mismatch stops the pipeline instead of reaching a test run.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| engines absent | The project states no requirement | Add the range you test against, then add engine-strict=true. |
| .nvmrc outside the engines range | Two files disagree | Pick one number and derive the other from it. |
| CI version outside the engines range | The pipeline tests a runtime the package rejects | Point actions/setup-node at node-version-file: .nvmrc. |
| EBADENGINE warning, exit 0 | engines is advisory here | Expected without engine-strict. It is a note, not a gate. |
| node -v differs from .nvmrc | No nvm use was run in this shell | Run it, or the numbers on this page describe someone else's machine. |
Common mistakes
What to check next
- How to check environment parity: the same comparison across two machines rather than four files.
- How to check if package lock is in sync: the second thing that differs when a build works locally and fails in CI.
- How to check if a npm package is installed globally: the other invisible machine dependency.
- How to check installed npm package version: the declared range against what is on disk, for packages rather than Node.
- Pull request checklist: where the version gate belongs in a review.
FAQ
How do I check what node version a project is using?
node -v in the project directory, but that is the shell you are in, not the project. Read engines, .nvmrc and the CI workflow as well, as in step 5, and treat a disagreement between them as the finding.
Which of the four wins?
The runtime that executes the code. engines only blocks an install when engine-strict=true, .nvmrc applies when a manager reads it, and the CI value applies inside the pipeline. Everything else is documentation.
Does npm read .nvmrc?
No. It is a convention shared by nvm, fnm and asdf. Step 6 shows npm comparing the running runtime against engines and never mentioning the file, which held a different version at the time.
How do I check the Node version a CI job used?
Add node -v as the first step of the job, before the install. The workflow file states what was requested, and actions/setup-node resolves a range such as 22 to whatever minor the runner image carries.
Why does the demo fail its own engines range?
It is built that way, so the warning in step 6 has something to report. Node 22.23.2 sits above the upper bound the manifest declares. In a healthy project the installed version satisfies engines, and so does the version CI installs.
Verified
Verified by Maks Vernynode 22.23.2npm 10.9.8git bash grep 3.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic5 minpublished updated Maks Verny