Use actions/setup-node@v7 to select a Node.js version in GitHub Actions. Set node-version or node-version-file, check out the repository before restoring dependencies, and use cache: npm with a committed lockfile. The action selects the runtime and restores the package manager's cache; npm ci still installs the dependencies.
The examples below use Node 24, Active LTS on 21 September 2026. Keep your CI version aligned with the project's supported runtime and test changes before deployment.
Jump to:
- Which actions/setup-node version is current?
- The minimal setup-node workflow
- Why you must pin the Node version
- Enable dependency caching (the biggest speedup)
- The version matrix across LTS lines
- Reading .nvmrc so CI matches local dev
- yarn and pnpm instead of npm
- Private registries and auth tokens
- setup-node inputs reference
- Common mistakes
- FAQ
Which actions/setup-node version is current?
The official setup-node documentation uses v7 as of 21 September 2026. Its internal Node runtime is separate from the version you select for your application. Self-hosted runners need a compatible runner version; the action documents v2.327.1 or later for its Node 24 runtime.
V7 changes the action internals to ESM and removes the dummy NODE_AUTH_TOKEN fallback. Supply a real token when your registry requires authentication. Do not add registry-url to a public install unless you need that configuration.
The examples use major action tags for readability. A tag such as @v7 receives updates within that major; a reviewed full commit SHA fixes the action to one revision and needs an explicit update process. The release notes explain migration changes.
The minimal setup-node workflow
Drop this in .github/workflows/ci.yml. It checks out the repo, installs a pinned Node major, restores the npm cache, and runs install plus tests:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm testFour things matter here. actions/setup-node@v7 is the current major of the action (pin the action major, not a floating @main). node-version: 24 pins the Node major so CI does not move under you. cache: npm restores the dependency cache keyed on the lockfile. And npm ci, not npm install, is what you want in CI: it installs strictly from package-lock.json and errors if the lockfile and package.json disagree, which is exactly the determinism you want in a pipeline.
Why you must pin the Node version
If neither node-version nor node-version-file is set, the action uses Node from the runner's PATH. That can change with runner-image updates.
Choose one of these inputs:
node-version: '24' # a matching 24.x release; may use the runner cache
node-version: '24.21.0' # one exact runtime version; update the pin deliberately
node-version: 'lts/*' # latest LTS major; moves when a new line enters LTSThose lines are alternatives, not three entries to put in the same YAML mapping. A major range prevents an unplanned major upgrade, but does not guarantee the latest patch. Add check-latest: true to resolve the newest release matching the range. An exact Node version pins the runtime, not every input to a reproducible build.
lts/* changes on an LTS promotion, not when a new major first ships as Current. See LTS vs Current for the current support schedule.
Enable dependency caching (the biggest speedup)
cache: npm can avoid repeated package downloads when a usable cache exists. Installation and lifecycle scripts still run, so a cache hit does not guarantee a near-instant build.
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npmIn setup-node@v7, if your package.json declares a packageManager (or devEngines.packageManager) field set to npm, the action caches npm automatically even without cache: npm. That is convenient, but it also means a workflow that looks cache-free may still be writing a cache. Set package-manager-cache: false and omit an explicit cache input to turn caching off, which is what you want on a privileged or secrets-bearing job where you would rather not persist a cache at all. yarn and pnpm are never auto-cached; you still pass cache: explicitly for those.
cache accepts npm, yarn, or pnpm. By default setup-node looks for the lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml) at the repo root. In a monorepo where the lockfile lives elsewhere, point at it explicitly:
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: packages/api/package-lock.json
- run: npm ci
working-directory: packages/apiOne caveat worth internalising: cache here caches the package manager's download cache, not your installed node_modules. You still run npm ci on every job; the cache just makes the download step fast. That is the right tradeoff, because restoring a full node_modules across Node majors invites the stale-native-binary problems covered in the Node.js upgrade reference.
The version matrix across LTS lines
If you maintain a library, you should test against every Node line you claim to support. A matrix runs the same job in parallel across each version:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [22.x, 24.x]
steps:
- uses: actions/checkout@v7
- name: Node ${{ matrix.node-version }}
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm testWhen choosing the matrix:
fail-fast: falselets every version finish even if one fails. The default (true) cancels the whole matrix the moment one leg fails, which hides whether the bug is version-specific or universal. For a compatibility matrix you almost always wantfalse.- On 21 September 2026, Node 24 is Active LTS and Node 22 is Maintenance LTS. Node 20 is end of life. Test the versions your package supports; Node 26 (Current) can be a separate compatibility job. The release schedule supplies the transition dates.
- You can fan the matrix across operating systems too:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [22.x, 24.x]
runs-on: ${{ matrix.os }}That produces six jobs (three OSes times two Node lines). Worth it if you ship native modules or hit filesystem-path edge cases; overkill for a pure-JS library where Linux coverage is representative.
Reading .nvmrc so CI matches local dev
The cleanest way to stop CI and local dev from drifting is to keep the Node version in one file that both read. setup-node reads .nvmrc, .node-version, or the volta/engines field in package.json via node-version-file:
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npmNow your developers run nvm use (or fnm, which auto-switches on cd once you wire up its shell hook) against the same .nvmrc, and CI reads the identical file. Bump the version in one place and everything follows. This is the pattern I reach for on any app with more than one contributor; the mechanics of the pinning files themselves are in the Node version pinning guide.
If you provide both inputs, node-version takes precedence over node-version-file. This is useful for an explicit matrix override, but easy to miss when you expect .nvmrc to control the job. Use one input when you do not need that override.
yarn and pnpm instead of npm
The package manager must be available before setup-node tries to cache its store. This pnpm 10 example installs pnpm first, selects the application runtime, and then installs from the committed lockfile:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
with:
version: 10
run_install: false
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm testThe pnpm action documentation also describes pnpm/setup for newer pnpm releases. Use the package-manager major your project supports; do not upgrade it just to copy a workflow.
For Yarn, provision the project's Yarn version before a cache: yarn setup step. Modern Yarn uses yarn install --immutable; Yarn Classic 1.x uses yarn install --frozen-lockfile. These flags are not interchangeable across Yarn generations.
Do not assume corepack enable always exists: Corepack is no longer bundled starting with Node 25. If your workflow uses Corepack, install a supported version explicitly when needed, then enable it before using the package manager.
Private registries and auth tokens
setup-node can write an .npmrc that points at a private or scoped registry, so installs authenticate without you hand-rolling the config:
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://npm.pkg.github.com
scope: '@my-org'
cache: npm
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}The key detail people miss: registry-url is what makes the action generate the authenticated .npmrc, and the token has to be passed as the NODE_AUTH_TOKEN environment variable on the install step, not as an input to setup-node. For GitHub Packages, grant the job contents: read and packages: read, and ensure the package grants this repository access; then the built-in secrets.GITHUB_TOKEN can work. Keep publishing permissions in a separate release job. For npmjs.com private packages or a third-party registry, store a token in repository secrets and reference that instead. The same NODE_AUTH_TOKEN mechanism is also what you use on a publish step (npm publish) at the end of a release workflow.
setup-node inputs reference
The inputs you will actually reach for, and what each one does:
| Input | Example | What it does |
|---|---|---|
node-version | 24, 24.21.0, lts/* | The Node version (or range) to install. Pin this. |
node-version-file | .nvmrc, package.json | Read the version from a file instead of inlining it. |
cache | npm, yarn, pnpm | Restore the package manager's download cache, keyed on the lockfile. |
cache-dependency-path | packages/api/pnpm-lock.yaml | Where the lockfile lives, for monorepos or non-root setups. |
package-manager-cache | false | Disable automatic npm caching when package.json has a packageManager field. |
registry-url | https://npm.pkg.github.com | Write an authenticated .npmrc for a private/scoped registry. |
scope | @my-org | The npm scope tied to the registry above. |
architecture | x64, arm64 | Target architecture (rarely needed; defaults to the runner's). |
check-latest | true | Always resolve the newest matching version from the dist server instead of preferring the runner's cached copy. |
When both version inputs are present, node-version wins. Use check-latest: true when you want the newest patch matching your range, rather than accepting a compatible cached runtime.
Common mistakes
No node-version at all. You inherit the runner's default Node, which moves when GitHub updates the image. Pin a major.
npm install instead of npm ci in CI. npm install can mutate the lockfile and resolve different versions than your developers have locally. npm ci installs strictly from package-lock.json and fails loudly on drift, which is what you want in a pipeline.
Assuming caching installs packages. A restored download cache can reduce network work, but the install command still needs to run. Measure the effect on your own workflow.
Floating action ref. actions/setup-node@main (or no ref) pulls in whatever the action's default branch is today. Use a versioned ref: these examples use @v7 for both actions. For stricter supply-chain hygiene, pin to a commit SHA.
Wrong cache path in a monorepo. If the lockfile is not at the repo root, cache setup can fail because it cannot find a dependency file. Set cache-dependency-path to the real lockfile location.
Expecting cache to restore node_modules. It does not; it caches the package manager's download store. You still run npm ci. That is by design, and it is why a Node major bump does not leave you with a stale node_modules full of binaries compiled against the old ABI, the exact condition behind a NODE_MODULE_VERSION mismatch.
Token passed as an input instead of an env var. For private registries the auth token goes in NODE_AUTH_TOKEN on the install step, not as a setup-node input. The action writes the .npmrc; the env var supplies the credential at install time.
See also
- How to Update Node.js: nvm, fnm, Volta, Direct Install: the full reference for every realistic Node upgrade route, including the CI/CD and Docker base-image angles that pair with this workflow.
- Pinning a Node version per project with .nvmrc: the local side of the
node-version-filepattern above, so dev and CI read the same pin. - How to Dockerize a Node.js App: when your CI builds a container instead of running
setup-node, this is the multi-stage Dockerfile that pins the runtime. - Node.js LTS vs Current: how to choose which majors belong in your matrix and which one to set as the project baseline.
- Fix NODE_MODULE_VERSION mismatch: what goes wrong when a cached or copied
node_modulescarries native binaries built against a different Node ABI than the one CI runs. - Catching errors from Node.js child_process: when a build step shells out from a Node script in CI, this is how to make a nonzero exit code actually fail the job instead of passing silently.
FAQ
It falls back to the Node version preinstalled on the GitHub-hosted runner image. That version changes whenever GitHub updates ubuntu-latest (or whichever runner you target), so a build can break with no code change when the runner default moves to a new major.
Always set node-version (a bare major like 22 is the usual choice) or node-version-file so CI is deterministic.
Add cache: npm to the actions/setup-node step. It restores the npm download cache keyed on your package-lock.json hash, which can reduce downloads; it does not restore node_modules or skip installation.
For yarn or pnpm use cache: yarn or cache: pnpm. If the lockfile is not at the repo root (a monorepo, for example), set cache-dependency-path to its real location or the cache key will never resolve.
Use a matrix: strategy.matrix.node-version: [22.x, 24.x] and reference ${{ matrix.node-version }} in the setup-node step. GitHub runs the job in parallel once per version.
Set fail-fast: false so one failing leg does not cancel the others, otherwise you cannot tell whether a failure is version-specific. Test the supported versions you advertise, and optionally Current as a compatibility check; see the Node.js release schedule for which majors those currently are.
Yes. Use node-version-file: .nvmrc instead of node-version. The action also reads .node-version and the volta or engines field in package.json.
This keeps CI and local dev on one source of truth: developers run nvm use or fnm against the same file. See the project pinning guide for the local-side mechanics. If both inputs are present, node-version takes precedence.
Use npm ci in CI. It installs strictly from package-lock.json, deletes any existing node_modules first, and errors if the lockfile and package.json have drifted, which is exactly the determinism a pipeline needs.
npm install can rewrite the lockfile and resolve different versions than your team has locally. Use yarn install --immutable for modern Yarn, yarn install --frozen-lockfile for Yarn Classic, or pnpm install --frozen-lockfile.
For caching, yes. setup-node can install Node without a checkout, but cache: npm needs to hash your lockfile, and that file only exists on the runner after actions/checkout has pulled the repo. Put checkout first or cache setup can fail when the lockfile is missing.
You also need the checkout before any npm ci / npm test step, since those read package.json and your source. The only time you can skip it is a job that just resolves a Node binary and does nothing with your code.
Set the full version: node-version: 24.21.0 installs exactly that patch and nothing else. A bare major like 24 can resolve from the runner cache. Add check-latest: true to request the newest matching patch.
An exact patch is useful when reproducing a runtime-specific bug. Keep that pin updated; pinning Node alone does not make the entire build reproducible.
Sources
Authoritative references this article was fact-checked against.
- actions/setup-node (GitHub Actions) READMEgithub.com
- Building and testing Node.js (GitHub Actions docs)docs.github.com
- Node.js release schedule and LTS timeline (nodejs/Release)github.com
- Using a matrix for your jobs (GitHub Actions docs)docs.github.com
- actions/setup-node releases (current major and changelog)github.com





