actions/setup-node installs a Node.js version, puts it on PATH, can restore your package manager's global cache, and can write an .npmrc for publishing. Since v6 it caches npm on its own when package.json names npm as the package manager, and since v7 (v7.1.0 is current) it no longer exports a placeholder NODE_AUTH_TOKEN when you set registry-url.

Whether setup-node v7 caches at all, and the key it uses when it does. Exact match only, saved only when the job succeeds.
Most workflows carry a setup-node step copied from somewhere else, with a cache: npm line nobody has checked. Then a major version lands and something shifts: caching turns itself on, a pnpm project fails before pnpm is installed, or an OIDC publish breaks on a token nobody set.
Two of those shifts came from caching, whose rules moved twice in two months between v5 and v6, and one from publishing, where v7 stopped exporting a placeholder token. If your workflow predates v6, read the caching section before you bump the pin.
Inputs, and the two new cache outputs
The table is action.yml at the v7.1.0 tag. Four outputs come back: node-version, cache-hit, and, new in v7, cache-primary-key and cache-matched-key, which let a later step see exactly which entry was restored. The cache is saved in a post step that action.yml limits to success(), so a failed job saves nothing for the next run.
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm test
| Input | Default | What it does |
|---|---|---|
node-version |
None | A version spec: 24, 22.17.1, lts/*, latest. Wins over node-version-file
|
node-version-file |
None |
.nvmrc, .node-version, .tool-versions, mise.toml or package.json
|
architecture |
The runner's |
x86, x64 and so on; ignored with a warning unless node-version is set |
check-latest |
false |
Look for a newer version than the cached one that still satisfies the spec |
registry-url |
None | Writes an .npmrc whose auth line reads NODE_AUTH_TOKEN
|
scope |
None; the repository owner for npm.pkg.github.com
|
Scope for a scoped registry |
token |
github.token on github.com |
Downloads Node builds from actions/node-versions
|
cache |
None |
npm, yarn or pnpm
|
package-manager-cache |
true |
Set false to stop automatic npm caching |
cache-dependency-path |
None | Lockfile path, glob or list used for the key |
mirror |
None | Alternative source for Node binaries |
mirror-token |
None | Sent as the Authorization header to the mirror |
Inputs and outputs from actions/setup-node action.yml at tag v7.1.0 (commit 949feb2413d6458794dcd2491c4babbbce0c15c1), read 2026-11-12.
node-version or node-version-file
Set one. If you set both, the action warns "Both node-version and node-version-file inputs are specified, only node-version will be used" and ignores the file. With neither, it uses whatever Node is already on PATH, which changes when the runner image does.
A relative node-version-file is resolved against GITHUB_WORKSPACE, not the step's working directory, and v7.1.0 accepts an absolute path too, so a composite action can pass its own file. For package.json the action reads volta.node first, then a devEngines.runtime entry named node, then engines.node, then follows volta.extends. A file it can read but not parse logs a warning and carries on with no version, which lands you on the PATH Node again; the node version file page shows how to turn that into a failure.
Resolution checks the runner's tool cache for a semver match first, then downloads from actions/node-versions, then from nodejs.org. check-latest: true skips the cached answer when a newer match exists, at the price of a download.
Built-in caching, and when it runs
Three questions decide it. If cache is set, the action caches that package manager, which must already be installed: cache: pnpm before pnpm is on PATH fails the step. If not, and package-manager-cache is left at true, it caches npm only when package.json names npm in devEngines.packageManager or the top-level packageManager field. Otherwise nothing is cached.
What it saves is the package manager's global cache directory, never node_modules, so npm ci still runs and still builds the tree. The key is node-cache-<platform>-<arch>-<manager>-<hash>, built from your lockfile, and the restore is exact: change one dependency and the next run starts cold. The only fallback is for Yarn Berry projects whose dependencies are all managed locally, which may restore an older entry by prefix.
For several lockfiles, point cache-dependency-path at a glob or a list. For what the cache is worth on a real project, speeding up npm install has measured numbers.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
with:
version: 10
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
apps/web/pnpm-lock.yaml
apps/api/pnpm-lock.yaml
Cache rules from src/main.ts, src/cache-restore.ts and src/cache-utils.ts at setup-node v7.1.0, read 2026-11-12.
Caching rules moved twice in two months
The caching rules moved twice in two months. v5 turned caching on for any packageManager field, and projects that declared pnpm and installed it later with corepack failed in Set up Node.js before corepack ran (issue 1357). v6 narrowed automatic caching to npm.
| Version | Released | What changed | What to do |
|---|---|---|---|
| v4.0.0 | 2023-10-23 | Runtime moved from node16 to node20 | Nothing on hosted runners |
| v5.0.0 | 2025-09-04 | Runtime moved to node24; caching on by default when packageManager is set |
Runner v2.327.1 or later on self-hosted machines |
| v6.0.0 | 2025-10-14 | Automatic caching limited to npm; always-auth input removed |
Add cache: pnpm or cache: yarn explicitly; delete always-auth
|
| v7.0.0 | 2026-07-14 | Placeholder NODE_AUTH_TOKEN removed; cache-primary-key and cache-matched-key outputs; ESM internals |
Set NODE_AUTH_TOKEN yourself where a registry needs it |
| v7.1.0 | 2026-10-08 |
mise.toml support; absolute node-version-file paths; version validation and manifest fetch retry |
Nothing |
From the actions/setup-node release notes and the README "What's new in V7" and "Breaking changes" sections, read 2026-11-12.
registry-url, NODE_AUTH_TOKEN and publishing
action.yml says registry-url writes a project-level .npmrc. The v7.1.0 source writes it to $RUNNER_TEMP/.npmrc and exports NPM_CONFIG_USERCONFIG pointing at it, so your repository's own .npmrc is left alone. The auth line it writes is //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}: the token is read at publish time from the step's environment, so put NODE_AUTH_TOKEN on the step that publishes.
Until v7 the action exported a placeholder NODE_AUTH_TOKEN of XXXXX-XXXXX-XXXXX-XXXXX when you had not set one. v7 removed it, and the README warns that Yarn Classic and older npm may now fail with registry-url set and no token. npm trusted publishing needs no token at all: npm 11.5.1 or newer, id-token: write, and a trusted publisher on npm that matches the repository and workflow exactly, or the publish returns E404.
The setup-node docs set package-manager-cache: false in every publishing example, because a poisoned cache in a job that holds a publish credential can expose it. Do the same.
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false
- run: npm ci
- run: npm publish
Missing files, missing lockfiles and missing pnpm
The first two have full pages. The rest are one-line fixes. Failures after the step, in the install itself, are a different family: npm ci lockfile out of sync and no pnpm version is specified are the common two.
| Message | Cause | Fix |
|---|---|---|
The specified node version file at: <path> does not exist |
The input, joined to GITHUB_WORKSPACE, names no file |
Node version file does not exist |
Dependencies lock file is not found in <workspace>. Supported file patterns: package-lock.json,npm-shrinkwrap.json,yarn.lock |
cache is set and no lockfile is at the root |
Dependencies lock file is not found |
Some specified paths were not resolved, unable to cache dependencies. |
cache-dependency-path matched nothing |
Fix the glob; it is relative to the workspace |
Unable to find Node version '<spec>' for platform <os> and architecture <arch>. |
No published build matches the spec on that platform | Check the spec and the architecture |
Unable to locate executable file: pnpm. |
cache: pnpm ran before pnpm was installed |
Move the pnpm setup step above setup-node |
FAQ
Does actions/setup-node cache node_modules?
No. It caches the package manager's global cache directory, ~/.npm for npm, keyed on a hash of the lockfile, so npm ci still runs and still builds node_modules. The setup-node docs give the reason: the global cache can be reused across Node versions, so a matrix of three versions can share one entry.
Why does setup-node v5 fail immediately when using pnpm?
v5 turned caching on whenever package.json had a packageManager field, so a project declaring pnpm made the action look for pnpm before any later step had installed it (issue 1357). v6 limited automatic caching to npm. Upgrade, or set package-manager-cache: false on v5.
Why does an npm OIDC publish fail after setup-node sets registry-url?
Up to v6 the action exported NODE_AUTH_TOKEN=XXXXX-XXXXX-XXXXX-XXXXX when you had not set one, and the issue asking to drop that default (issue 1440) describes having to blank it to get OIDC publishing working. v7.0.0 removed the placeholder. On v7, check the npm version is 11.5.1 or newer and that the trusted publisher matches the workflow file.
Why can GITHUB_TOKEN not install private packages from another repository?
Because a package is only readable by the workflows its settings allow. The setup-node docs point to Manage Actions access in the package's settings, where you add the repositories whose workflows may read it; after that NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} works for packages in the same organization.
Top comments (0)