# pnpm/action-setup: the step order setup-node needs, and one store cache

> pnpm/action-setup must run before setup-node with cache: pnpm. Learn the step order, how packageManager sets the version, and which cache to keep.

Source: https://latchkey.dev/learn/actions/pnpm-action-setup  
Updated: 2026-11-12

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](/learn/github-actions/pnpm-action-setup-no-version-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 |

> From `action.yml`, `src/inputs` and `src/cache-restore/run.ts` at v6.1.1 (commit 73e4699), read 2026-11-12. Outputs: `dest` and `bin_dest`.

## 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.

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

## The 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.

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

## Two 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 |

> Keys from `src/cache-restore/run.ts` in pnpm/action-setup v6.1.1 and `src/cache-restore.ts` in actions/setup-node v7.1.0, read 2026-11-12.

## 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.

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

> From the GitHub release notes for each tag, read 2026-11-12.

## FAQ

### Why does setup-node fail with Unable to locate executable file: pnpm?

Because `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?

Both get a pinned pnpm onto the runner. A pnpm maintainer answered in the action's issue tracker that there is nothing wrong with corepack, and that pnpm's docs at the time recommended it for every CI except GitHub Actions. The action adds the store cache, the `version` input and the standalone executable on old Node.

### Is the action's cache setup equivalent to actions/setup-node's pnpm cache?

They cache the same directory with different keys and different fallbacks. The action restores the previous store on a lockfile change through a prefix restore key; setup-node passes no restore key and starts cold. Use one of them, not both.

### Why does pnpm/action-setup throw Multiple versions of pnpm specified?

Because `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.

## References

- [pnpm/action-setup README and action.yml at v6.1.1 (read 2026-11-12)](https://github.com/pnpm/action-setup/tree/v6.1.1)
- [pnpm/action-setup src/install-pnpm/run.ts: version resolution](https://github.com/pnpm/action-setup/blob/v6.1.1/src/install-pnpm/run.ts)
- [actions/setup-node advanced usage: caching pnpm](https://github.com/actions/setup-node/blob/v7.1.0/docs/advanced-usage.md)
- [actions/setup-node#530: Unable to locate executable file: pnpm](https://github.com/actions/setup-node/issues/530)
- [pnpm/action-setup#105: comparison with corepack](https://github.com/pnpm/action-setup/issues/105)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
