Skip to content
Latchkey LogoLatchkey home

actions/setup-node: version files, the cache that turns itself on, and publishing

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.

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.

Flow of three questions deciding whether setup-node caches, beside the cache key and its rules
Whether setup-node v7 caches at all, and the key it uses when it does. Exact match only, saved only when the job succeeds.

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.

.github/workflows/ci.yml
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
InputDefaultWhat it does
node-versionNoneA version spec: 24, 22.17.1, lts/*, latest. Wins over node-version-file
node-version-fileNone.nvmrc, .node-version, .tool-versions, mise.toml or package.json
architectureThe runner'sx86, x64 and so on; ignored with a warning unless node-version is set
check-latestfalseLook for a newer version than the cached one that still satisfies the spec
registry-urlNoneWrites an .npmrc whose auth line reads NODE_AUTH_TOKEN
scopeNone; the repository owner for npm.pkg.github.comScope for a scoped registry
tokengithub.token on github.comDownloads Node builds from actions/node-versions
cacheNonenpm, yarn or pnpm
package-manager-cachetrueSet false to stop automatic npm caching
cache-dependency-pathNoneLockfile path, glob or list used for the key
mirrorNoneAlternative source for Node binaries
mirror-tokenNoneSent as the Authorization header to the mirror

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.

.github/workflows/ci.yml
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

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.

VersionReleasedWhat changedWhat to do
v4.0.02023-10-23Runtime moved from node16 to node20Nothing on hosted runners
v5.0.02025-09-04Runtime moved to node24; caching on by default when packageManager is setRunner v2.327.1 or later on self-hosted machines
v6.0.02025-10-14Automatic caching limited to npm; always-auth input removedAdd cache: pnpm or cache: yarn explicitly; delete always-auth
v7.0.02026-07-14Placeholder NODE_AUTH_TOKEN removed; cache-primary-key and cache-matched-key outputs; ESM internalsSet NODE_AUTH_TOKEN yourself where a registry needs it
v7.1.02026-10-08mise.toml support; absolute node-version-file paths; version validation and manifest fetch retryNothing

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.

.github/workflows/publish.yml
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.

MessageCauseFix
The specified node version file at: <path> does not existThe input, joined to GITHUB_WORKSPACE, names no fileNode version file does not exist
Dependencies lock file is not found in <workspace>. Supported file patterns: package-lock.json,npm-shrinkwrap.json,yarn.lockcache is set and no lockfile is at the rootDependencies lock file is not found
Some specified paths were not resolved, unable to cache dependencies.cache-dependency-path matched nothingFix 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 platformCheck the spec and the architecture
Unable to locate executable file: pnpm.cache: pnpm ran before pnpm was installedMove the pnpm setup step above setup-node

Key takeaways

  • Set exactly one of node-version and node-version-file; a relative file path resolves against GITHUB_WORKSPACE.
  • Automatic caching covers npm only, and only when package.json names npm; pnpm and yarn need cache: and must be installed first.
  • The cache key is exact and saved only on success, and it holds the global cache, not node_modules.
  • v7 stopped exporting a placeholder NODE_AUTH_TOKEN: set it on the publish step, or use trusted publishing.

Frequently asked questions

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.

References

An exact-key npm cache stored next to the runner. Latchkey runners from $0.0025/min. Start free → 30-day trial · No credit card