← Back to Node Version Drift Check
A Node.js project states which runtime it wants in up to five places: engines.node in package.json, an .nvmrc or .node-version file, an asdf .tool-versions line, the FROM node: line of a Dockerfile, and the node-version of a GitHub Actions actions/setup-node step (Volta users add volta.node). Each is read by a different tool. None of those tools reads the others.
| Finding | Example | What goes wrong |
|---|---|---|
| Pin violates engines.node | engines.node is >=22, .nvmrc says 20.11.1 | Local development runs a Node.js your own package says it does not support; installs warn, or fail where engine-strict is on. |
| Majors disagree | Dockerfile on node:22, CI on node-version: 20 | Tests pass on one major and production runs another. |
| End-of-life Node.js | A CI matrix still lists 18 | The line no longer receives security fixes, and you are testing a version you should not be shipping. |
| Unbounded engines | >=18 and no pin anywhere | No file records which version was ever tested. |
The dates come from the Node.js release schedule at nodejs.org/en/about/previous-releases: 18 ended 2025-04-30, 20 ended 2026-04-30, 22 ends 2027-04-30 and 24 ends 2028-04-30. Odd majors are never long-term support and end about six months after release. The table inside the tool is dated 2026-10-11 and is only a snapshot, which is why every finding says "per the table dated" and why --today exists: it makes the verdict reproducible in a test.
node cli.mjs --fail-on-risk
Exit status 2 means at least one risk finding. Notices (aliases the tool will not guess at, an engines range that still admits an end-of-life major, a major that ends within 90 days) are printed but do not fail the run.
Each file is edited for a different reason by a different person. Someone bumps the Dockerfile for a security fix, someone else raises engines.node when a dependency needs it, and the .nvmrc and the CI workflow are only touched when something breaks. Nothing in npm, Docker or GitHub Actions compares them to each other, so the disagreement lasts until a bug makes it visible.
A range like >=18 says every future major will do. It will happily install on a Node.js version released next year that none of your dependencies or tests have ever run on, and with nothing pinned in .nvmrc, the Dockerfile or CI there is no file that says which version you actually tested. A bounded range such as >=22 <25, or a pin, records the versions you stand behind.
Odd-numbered releases never enter long-term support and stop receiving fixes roughly six months after release, so the table marks their dates as approximate. Even-numbered releases become long-term support and are maintained until April 30 of their third calendar year. A pin on an odd major in a Dockerfile or CI is therefore usually a leftover rather than a decision.
It does not run Node.js, install packages, or ask any registry which versions exist. It cannot tell you what lts/* or latest mean today, so those are listed as notices. It treats prerelease tags as their release, only reads the first nodejs line of a .tool-versions file, and judges end-of-life from a static table that you should confirm against nodejs.org before acting on a finding.