# pnpm/action-setup — Context7 Documentation `pnpm/action-setup` is a GitHub Actions composite action that installs the [pnpm](https://pnpm.io/) package manager on a CI runner. It resolves the desired pnpm version from either an explicit `version` input, the `packageManager` field in `package.json`, or `devEngines.packageManager`, and then bootstraps pnpm via a committed, integrity-verified `npm ci` lockfile — ensuring reproducible, tamper-resistant installs. The action targets Node 24 and is compatible with GitHub-hosted and self-hosted runners on Linux, macOS, and Windows. Beyond installation, the action provides an optional post-step pipeline: it can restore a cached pnpm content-addressable store before dependencies are installed, run one or more `pnpm install` / `pnpm recursive install` commands with arbitrary extra arguments, and then automatically prune the store and save an updated cache when the job finishes. The result is a single action that covers the full setup-install-cache lifecycle for pnpm-based projects. --- ## Inputs ### `version` — Specify the pnpm version to install Accepts an exact version (`10.9.8`), a semver range (`10`, `^10.9.8`, `*`), or `latest`. When omitted, the version is read from `packageManager` or `devEngines.packageManager` in `package.json`. Specifying `version` and also having a `packageManager` field with a different value raises an error. ```yaml # Exact version - uses: pnpm/action-setup@v6 with: version: 10.9.8 # Semver range — resolves to latest matching release - uses: pnpm/action-setup@v6 with: version: 10 # Let packageManager field in package.json decide (omit version entirely) # package.json: { "packageManager": "pnpm@10.9.8" } - uses: pnpm/action-setup@v6 ``` --- ### `dest` — Custom pnpm installation directory Where the bootstrap pnpm files are written. Tilde expansion is supported. Defaults to `~/setup-pnpm`. The expanded path is emitted as the `dest` output. ```yaml - uses: pnpm/action-setup@v6 with: version: 10 dest: ~/.local/share/pnpm-setup ``` --- ### `run_install` — Run pnpm install after setup Accepts `null`/`false` (skip), `true` (recursive install from workspace root), or a YAML object/array describing one or more install commands. Each entry supports: - `recursive` (`boolean`) — use `pnpm recursive install` - `cwd` (`string`) — working directory for the install command - `args` (`string[]`) — extra CLI flags appended after `pnpm [recursive] install` ```yaml - uses: actions/checkout@v4 # Single recursive install with extra flags - uses: pnpm/action-setup@v6 with: version: 10 run_install: | recursive: true args: [--strict-peer-dependencies, --frozen-lockfile] # Multiple install commands - uses: pnpm/action-setup@v6 with: version: 10 run_install: | - recursive: true args: [--strict-peer-dependencies] - args: [--global, gulp, prettier, typescript] # Install in a specific sub-directory - uses: pnpm/action-setup@v6 with: version: 10 run_install: | cwd: packages/my-lib args: [--frozen-lockfile] ``` --- ### `cache` — Enable pnpm store caching When `true`, the action saves/restores the pnpm content-addressable store using the GitHub Actions cache service. The cache key is derived from `RUNNER_OS`, the CPU architecture, and a hash of the lockfile. Store pruning runs automatically in the post-step — no manual `pnpm store prune` needed. ```yaml - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v6 with: version: 10 cache: true # cache-hit output is set to true/false after restore - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' # optional: node_modules cache via setup-node - run: pnpm install --frozen-lockfile ``` --- ### `cache_dependency_path` — Custom lockfile path for cache key Override the default `pnpm-lock.yaml` with any glob pattern or explicit path. Useful for monorepos where multiple lockfiles exist. ```yaml - uses: pnpm/action-setup@v6 with: version: 10 cache: true cache_dependency_path: | packages/**/pnpm-lock.yaml apps/**/pnpm-lock.yaml ``` --- ### `package_json_file` — Custom path to `package.json` Defaults to `package.json` relative to `GITHUB_WORKSPACE`. Set this when the manifest is nested or has a `.yaml` extension (pnpm supports `package.yaml`). ```yaml - uses: pnpm/action-setup@v6 with: package_json_file: apps/my-app/package.json ``` --- ### `standalone` — Install pnpm bundled with Node.js When `true`, installs `@pnpm/exe` — a self-contained pnpm binary that bundles its own Node.js runtime. This is automatically activated when the system Node version is older than 22.13 (required by pnpm v11+). Use explicitly when you need pnpm with an incompatible Node version. ```yaml - uses: pnpm/action-setup@v6 with: version: 10 standalone: true # @pnpm/exe — no Node.js dependency at runtime ``` --- ## Outputs ### `dest` / `bin_dest` — Installation paths `dest` is the expanded directory where pnpm was installed; `bin_dest` is the `.bin` directory containing the `pnpm` and `pnpx` executables. Both are automatically added to `PATH`. ```yaml - uses: pnpm/action-setup@v6 id: pnpm-setup with: version: 10 - name: Show pnpm paths run: | echo "Install dir : ${{ steps.pnpm-setup.outputs.dest }}" echo "Bin dir : ${{ steps.pnpm-setup.outputs.bin_dest }}" pnpm --version ``` --- ## Internal Functions ### `getInputs()` — Parse and validate all action inputs Reads every `inputs.*` value from the Actions runtime, expands tilde paths, and returns a typed `Inputs` object. `run_install` is parsed from YAML via `parseRunInstall`. ```typescript // src/inputs/index.ts import getInputs from './inputs' const inputs = getInputs() // inputs.version → "10" | undefined // inputs.dest → "/home/runner/setup-pnpm" // inputs.cache → true | false // inputs.cacheDependencyPath → "/home/runner/work/repo/pnpm-lock.yaml" // inputs.runInstall → [{ recursive: true, args: ["--frozen-lockfile"] }] // inputs.packageJsonFile → "/home/runner/work/repo/package.json" // inputs.standalone → false ``` --- ### `parseRunInstall(inputName)` — Parse YAML run_install config Accepts `null`, `boolean`, an object, or an array of objects. Validates via Zod schema and normalizes to `RunInstall[]`. Exits the process with code 1 on validation failure. ```typescript // src/inputs/run-install.ts import { parseRunInstall } from './inputs/run-install' // Input YAML: "- recursive: true\n args: [--frozen-lockfile]" const commands = parseRunInstall('run_install') // → [{ recursive: true, args: ["--frozen-lockfile"] }] // Input YAML: "true" // → [{ recursive: true }] // Input YAML: "null" // → [] ``` --- ### `runSelfInstaller(inputs)` — Bootstrap and install pnpm The core installation routine. Writes a minimal `package.json` + committed `package-lock.json` to `dest`, runs `npm ci` to get the bootstrap pnpm (or `@pnpm/exe` for standalone mode), then calls `pnpm self-update ` to switch to the requested version. Adds `PNPM_HOME` and the bin directory to PATH via `addPath` / `exportVariable`. ```typescript // src/install-pnpm/run.ts import { runSelfInstaller } from './install-pnpm' const exitCode = await runSelfInstaller({ version: '10', dest: '/home/runner/setup-pnpm', standalone: false, packageJsonFile: 'package.json', cache: false, cacheDependencyPath: 'pnpm-lock.yaml', runInstall: [], }) if (exitCode !== 0) { core.setFailed(`Self-installer exited with code ${exitCode}`) } // On success: PNPM_HOME is set, pnpm is on PATH ``` --- ### `restoreCache(inputs)` / `runRestoreCache(inputs)` — Restore pnpm store cache Resolves the pnpm store path via `pnpm store path`, hashes the lockfile(s), constructs a primary key of the form `pnpm-cache---`, and attempts a cache restore. Saves state for the post-step. Sets the `cache-hit` output. ```typescript // src/cache-restore/run.ts // Primary key format: // pnpm-cache-Linux-x64-abc123def456... // Equivalent workflow output after cache hit: // ✓ Cache restored from key: pnpm-cache-Linux-x64-abc123def456... // Output cache-hit: true // On cache miss: // ℹ Cache is not found // Output cache-hit: false ``` --- ### `runPnpmInstall(inputs)` — Execute pnpm install commands Iterates `inputs.runInstall` and runs each as a synchronous `spawnSync('pnpm', ...)` call with the patched environment. Supports `recursive`, `cwd`, and arbitrary extra `args`. ```typescript // src/pnpm-install/index.ts // Equivalent to running: // pnpm recursive install --strict-peer-dependencies // pnpm install --global gulp prettier runPnpmInstall({ runInstall: [ { recursive: true, args: ['--strict-peer-dependencies'] }, { args: ['--global', 'gulp', 'prettier'] }, ], dest: '/home/runner/setup-pnpm', // ...other inputs }) ``` --- ### `saveCache(inputs)` / `runSaveCache()` — Persist pnpm store cache Post-step routine. Skips saving if the primary key matches the already-restored key (exact cache hit), preventing redundant writes. Otherwise calls `@actions/cache` `saveCache()`. ```typescript // src/cache-save/run.ts // State from restore step: // cache_primary_key = "pnpm-cache-Linux-x64-abc123" // cache_restored_key = "" (was a miss) // cache_path = "/home/runner/.local/share/pnpm/store/v3" // Result: cache saved with key "pnpm-cache-Linux-x64-abc123" // ✓ Cache saved with the key: pnpm-cache-Linux-x64-abc123 // If cache_primary_key === cache_restored_key (exact hit): // ℹ Cache hit occurred on the primary key …, not saving cache. ``` --- ### `pruneStore(inputs)` — Remove unreferenced packages from store Runs `pnpm store prune` synchronously in the post-step, only when `runInstall` was non-empty. Emits a warning (not a failure) on non-zero exit so it never breaks the job. ```typescript // src/pnpm-store-prune/index.ts pruneStore({ runInstall: [{ recursive: true }], ...otherInputs }) // → Running pnpm store prune... // → (orphaned packages removed) pruneStore({ runInstall: [], ...otherInputs }) // → Pruning is unnecessary. ``` --- ### `patchPnpmEnv(inputs)` — Build PATH-patched environment Returns a copy of `process.env` with the pnpm bin directory prepended to `PATH`, ensuring `pnpm` is resolvable during `runPnpmInstall` and `pruneStore`. ```typescript // src/utils/index.ts import { patchPnpmEnv, getBinDest } from './utils' const env = patchPnpmEnv(inputs) // env.PATH = "/home/runner/setup-pnpm/node_modules/.bin/bin: // /home/runner/setup-pnpm/node_modules/.bin: // " const binDest = getBinDest(inputs) // → "/home/runner/setup-pnpm/node_modules/.bin" ``` --- ## Complete Workflow Examples ### Minimal install using `packageManager` field ```yaml # package.json: { "packageManager": "pnpm@10.9.8+sha512.abc..." } name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v6 # version auto-resolved from packageManager - uses: actions/setup-node@v4 with: node-version: 20 - run: pnpm install --frozen-lockfile - run: pnpm test ``` ### Monorepo with caching and multiple install targets ```yaml name: Monorepo CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v6 id: setup with: version: 10 cache: true cache_dependency_path: '**/pnpm-lock.yaml' run_install: | - recursive: true args: [--frozen-lockfile, --strict-peer-dependencies] - args: [--global, turbo] - uses: actions/setup-node@v4 with: node-version: 20 - name: Build all packages run: pnpm turbo run build - name: Test all packages run: pnpm turbo run test ``` ### Standalone mode for incompatible Node/pnpm pairs ```yaml name: Legacy Node CI on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 # older Node, pnpm v11 requires Node >=22.13 - uses: pnpm/action-setup@v6 with: version: 10 standalone: true # installs @pnpm/exe, bundles its own Node runtime - run: pnpm install --frozen-lockfile ``` --- `pnpm/action-setup` fits naturally as the first step in any Node.js CI pipeline that uses pnpm. The most common pattern is: `actions/checkout` → `pnpm/action-setup` (with `cache: true`) → `actions/setup-node` → application-specific build/test commands. Because the post-step handles store pruning and cache saving automatically, no cleanup steps are needed, keeping workflow files concise. For monorepos managed with pnpm workspaces or Turborepo, the `run_install` array input cleanly replaces a separate `run: pnpm install` step, and `cache_dependency_path` can be set to a glob pattern to cover all workspace lockfiles. When Node.js version compatibility is a concern — especially with pnpm v11+ requiring Node ≥ 22.13 — enabling `standalone: true` ensures pnpm runs under its own bundled runtime regardless of the system Node version, making the action suitable for self-hosted runners and legacy environments.