pnpm/action-setup: the step order setup-node needs, and one store cache
pnpm/action-setup installs pnpm and puts it on PATH; it does not install Node.js, and since v4.3.0 it can cache the pnpm store itself. The order that works is checkout, then pnpm/action-setup, then actions/setup-node with cache: pnpm, because setup-node asks pnpm for its store path and fails if pnpm is not installed yet.
Most pnpm workflows have three setup steps and one install, and most of the failures come from the order of the first three or from two of them disagreeing about a version. The action reads package.json before it installs anything, setup-node runs pnpm store path before it restores anything, and both can cache the same store with different keys.
The version comes from one of two places, the version input or packageManager in package.json, and setting both to strings that differ fails the step even when they agree on semver. Set neither and you get no pnpm version is specified, which has its own page.

Seven inputs, and an undeclared cache-hit
Seven inputs and two declared outputs. The restore step also sets a third output, cache-hit, which action.yml does not declare; it is true only when the exact key matched.
| Input | Default | What it does |
|---|---|---|
version | empty | Exact version, a range such as 10 or ^10.9.8, or latest. Required unless package.json names pnpm |
dest | ~/setup-pnpm | Where the bootstrap pnpm is installed |
run_install | off | Run pnpm install inside this step: true, an object or a list |
cache | false | Cache the pnpm store with this action |
cache_dependency_path | pnpm-lock.yaml | File or newline-separated files hashed into the cache key |
package_json_file | package.json | Manifest to read, relative to the repository root; package.yaml also works |
standalone | false | Install @pnpm/exe for pnpm 11 and earlier; no effect on pnpm 12 |
Which pnpm version gets installed
With version empty, the action reads devEngines.packageManager first and packageManager second, stripping any +sha512 suffix. With version set and packageManager also present, it compares the two as plain strings and throws Multiple versions of pnpm specified: when they differ. That comparison is not semver: version: 12 against "packageManager": "pnpm@12.18.0" throws, even though 12.18.0 satisfies 12. Pick one source.
The install itself is a bootstrap pnpm from a lockfile committed in the action, followed by pnpm self-update to your version. For pnpm 11 and earlier, the action also checks the node on PATH and installs the standalone @pnpm/exe when that Node is older than 22.13, whatever standalone says. pnpm 12 ships a native executable and needs no Node at all.
# package.json carries "packageManager": "pnpm@12.18.0"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
# no version input: the manifest decidesThe step order, and why it is fixed
Checkout comes first whenever the version lives in package.json, because the action reads the file from the workspace and a missing file is silently treated as no version at all. pnpm/action-setup comes before setup-node whenever setup-node has cache: pnpm: setup-node finds the store with pnpm store path --silent, and without pnpm on PATH it stops with Unable to locate executable file: pnpm. A setup-node maintainer closed the report of that error as expected behavior, since pnpm must be installed before the setup-node step.
Keep the install as its own step after setup-node rather than using run_install. run_install runs inside the pnpm/action-setup step, so it runs before any later setup-node and uses whichever Node happened to be on PATH at that moment.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm testTwo caches for one store: keep one
Both actions cache the directory pnpm store path prints, and both hash pnpm-lock.yaml by default. They differ in the fallback and in the cleanup. setup-node passes no restore keys, so a lockfile change restores nothing. pnpm/action-setup passes a prefix restore key, so a lockfile change restores the previous store and pnpm downloads only the difference.
The README says the post step prunes the store for you. In the source it runs pnpm store prune only when run_install was set; with a separate install step it logs Pruning is unnecessary. and saves the store unpruned. Turning both caches on uploads the same directory twice under two keys.
One more reason the version matters for caching: the store path contains the store layout version, and an earlier bootstrap pnpm reported a different path from the one your pnpm wrote to, which broke both caches on a cold run. The action now self-updates to your version before asking for the path, and its source names issue #233 as the reason.
pnpm/action-setup cache: true | setup-node cache: pnpm | |
|---|---|---|
| Key | pnpm-cache-<OS>-<arch>-<hash> | node-cache-<OS>-<arch>-pnpm-<hash> |
| Restore fallback | Prefix pnpm-cache-<OS>-<arch>- | None |
| Store prune before save | Only with run_install | No |
| Step order constraint | None beyond checkout | pnpm must already be installed |
run_install, when you do want it
It accepts true (a recursive install), one object, or a list of objects, each with recursive, cwd and args. It is a convenience: a separate step after setup-node does the same work with the Node you chose, and the action exports PNPM_HOME either way, so global installs work in both places.
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
with:
run_install: |
- recursive: true
args: [--frozen-lockfile]
- args: [--global, prettier]From v2 to pnpm 12 support
v2 stopped working with newer Node.js versions, and the README tells anyone on it to upgrade. For pnpm 11 and later, the same maintainers also publish pnpm/setup, which installs pnpm and a JavaScript runtime in one step; pnpm/action-setup still supports pnpm 12 alongside setup-node.
| Release | Change |
|---|---|
| v3.0.0 | Runs on Node.js 20 |
| v4.0.0 | Throws when version and packageManager disagree |
| v4.2.0 | Fetches pnpm from the registry in a root .npmrc |
| v4.3.0 | cache input added |
| v5.0.0 | Runs on Node.js 24 |
| v6.0.0 | pnpm 11 support |
| v6.1.0 | pnpm 12 support |
Key takeaways
- Checkout, then pnpm/action-setup, then setup-node with
cache: pnpm, then the install. - Set the version in
package.jsonor inversion, not both: the two are compared as strings. - The action's own cache falls back to the previous store; setup-node's does not.
- The post step prunes the store only when
run_installwas used.
Frequently asked questions
Why does setup-node fail with Unable to locate executable file: pnpm?
cache: pnpm makes setup-node run pnpm store path --silent during its own step, and pnpm is not installed yet. Move pnpm/action-setup above setup-node, or drop cache: pnpm and use the action's own cache: true, which has no ordering constraint beyond checkout.How does pnpm/action-setup compare to run: corepack enable?
version input and the standalone executable on old Node.Is the action's cache setup equivalent to actions/setup-node's pnpm cache?
Why does pnpm/action-setup throw Multiple versions of pnpm specified?
version and packageManager are both set and are not the same string. A major-only version: 12 against pnpm@12.18.0 counts as a mismatch. Remove version to let the manifest decide, which is also what your local tooling reads.Related guides
References
- pnpm/action-setup README and action.yml at v6.1.1 (read 2026-11-12)
- pnpm/action-setup src/install-pnpm/run.ts: version resolution
- actions/setup-node advanced usage: caching pnpm
- actions/setup-node#530: Unable to locate executable file: pnpm
- pnpm/action-setup#105: comparison with corepack
- Node.js documentation
- npm CLI documentation
- GitHub Actions documentation