Skip to content
Latchkey LogoLatchkey home

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.

Four setup steps in order: checkout, pnpm/action-setup, setup-node with cache pnpm, pnpm install
Each step reads something the previous one produced. Read from pnpm/action-setup v6.1.1 and actions/setup-node v7.1.0 source on 2026-11-12.

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.

InputDefaultWhat it does
versionemptyExact version, a range such as 10 or ^10.9.8, or latest. Required unless package.json names pnpm
dest~/setup-pnpmWhere the bootstrap pnpm is installed
run_installoffRun pnpm install inside this step: true, an object or a list
cachefalseCache the pnpm store with this action
cache_dependency_pathpnpm-lock.yamlFile or newline-separated files hashed into the cache key
package_json_filepackage.jsonManifest to read, relative to the repository root; package.yaml also works
standalonefalseInstall @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.

.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: truesetup-node cache: pnpm
Keypnpm-cache-<OS>-<arch>-<hash>node-cache-<OS>-<arch>-pnpm-<hash>
Restore fallbackPrefix pnpm-cache-<OS>-<arch>-None
Store prune before saveOnly with run_installNo
Step order constraintNone beyond checkoutpnpm 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.

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

ReleaseChange
v3.0.0Runs on Node.js 20
v4.0.0Throws when version and packageManager disagree
v4.2.0Fetches pnpm from the registry in a root .npmrc
v4.3.0cache input added
v5.0.0Runs on Node.js 24
v6.0.0pnpm 11 support
v6.1.0pnpm 12 support

Key takeaways

  • Checkout, then pnpm/action-setup, then setup-node with cache: pnpm, then the install.
  • Set the version in package.json or in version, 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_install was used.

Frequently asked questions

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

One pnpm store cache, keyed on your lockfile. Latchkey cache-action. Start free → 30-day trial · No credit card