Compare commits

...

14 Commits

Author SHA1 Message Date
Eric Nemchik 0977fd9972 docs: Update README to include devEngines.packageManager (#273)
Support added in #211 and #256
2026-08-03 13:44:13 +02:00
Jamie Tanna 48261aca05 fix: update pnpm to v11.19.0 (#283)
* fix: update pnpm to v11.19.0

Via `scripts/update-bootstrap.mjs 11.19.0`

Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>

* fixup! fix: update pnpm to v11.19.0

As it needs to be rebuilt on Linux.

---------

Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
2026-08-03 11:54:52 +02:00
Jamie Tanna 75677f717d ci: use pnpm 11 for pr-check (#284)
Noticed while working on #283.
2026-08-03 11:02:17 +02:00
Sukka 769ae71fb3 refactor: introduce restore keys for cache (#280) 2026-08-03 10:32:23 +02:00
Kevin Cui 6fed91f804 docs(README): point users to the successor pnpm/setup action (#282)
* docs(README): point users to the successor pnpm/setup action

The pnpm setup action has moved to `pnpm/setup`, which installs
pnpm v11+ as a self-contained native executable and can install a
JavaScript runtime (Node.js, Bun, or Deno) in the same step,
replacing `actions/setup-node`.

Add a notice at the top of the README, a migration section with a
before/after workflow example and an input/output mapping table,
and mention the successor in the Notes section. The rest of the
docs stay intact since `pnpm/action-setup` remains the way to
install pnpm v10 and older.

Signed-off-by: Kevin Cui <bh@bugs.cc>

* docs(README): address review feedback on migration section

Add an explicit version: 11 to the migration example and note when the
input can be omitted, since pnpm/setup requires pnpm v11+ and a repo
migrating from version: 10 may have no packageManager field or one that
pins v10.

Move the v2 upgrade warning below the page title so the two callouts
are no longer adjacent blockquotes (markdownlint MD028), keeping the
successor notice and the legacy warning as separate blocks.

Fix the comma splice and use "set up" as the verb in the Notes
section.

Signed-off-by: Kevin Cui <bh@bugs.cc>

---------

Signed-off-by: Kevin Cui <bh@bugs.cc>
2026-08-02 22:56:56 -04:00
Zoltan Kochan 0ebf47130e fix: update pnpm to v11.7.0 (#267)
* fix: update pnpm to v11.7.0

* fix: update bundle
2026-06-15 14:04:13 +02:00
mungodewar 0e279bb959 fix: update pnpm to 11.1.1 (#248)
* fix: update pnpm to v11.1.1

* fix: update bundle

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-12 14:32:25 +02:00
Zoltan Kochan 3e835812ef fix: drop patchPnpmEnv so standalone+self-update works on Windows (#258)
`patchPnpmEnv` prepended `dest/node_modules/.bin` to PATH before
spawning `pnpm install` / `pnpm store prune`. On Windows in standalone
mode, `.bin/pnpm.cmd` is an npm-created shim that always points at the
BOOTSTRAP pnpm (currently 11.0.4) — the binary npm linked when it
installed `@pnpm/exe` into `node_modules`. The self-updated pnpm
written by `pnpm self-update` lives at `$PNPM_HOME/bin`, which is
separately added to PATH via `addPath()` in install-pnpm.

When the user requested a pnpm version different from the bootstrap
under `standalone: true` on Windows, patchPnpmEnv's `.bin` entry
shadowed the self-updated `$PNPM_HOME/bin` and the action's internal
`pnpm install` ran on the bootstrap. On a pnpm 11.0.x bootstrap this
broke any 11.1+ install flag (e.g. `--no-runtime`), reporting:

    ERROR  Unknown option: 'runtime'

POSIX standalone got lucky because `.bin` and `$PNPM_HOME` resolve to
the same directory there. Non-standalone never tripped on this since
the `.bin/pnpm` symlink for a regular `pnpm` package keeps working
across self-updates.

Removed `patchPnpmEnv` and the now-empty `src/utils/` module.
`spawnSync` now inherits `process.env`, whose PATH is already
correctly fronted by `$PNPM_HOME/bin` and `$PNPM_HOME` via the
`addPath` calls in install-pnpm.

Added `standalone_windows_self_update` to test.yaml as a regression
guard: standalone on Windows + target 11.1.0 + `run_install` with
`--no-runtime`. With the previous code, the install would have run
under the bootstrap (11.0.4) and errored on the unknown flag.

Originally found while building pnpm/setup (the new combined
pnpm + runtime action).
2026-05-11 22:48:51 +02:00
Andrew Haines 551b42e879 docs(README): fix cache_dependency_path type (#257)
* docs(README): fix `cache_dependency_path` type

* docs(README): add multiple lockfile example

* docs: update input description to match README
2026-05-11 16:41:41 +02:00
Zoltan Kochan 739bfe42ca fix: self-update bootstrap to packageManager-pinned version (#233) (#256)
* fix: self-update bootstrap to packageManager-pinned version (#233)

When package.json pins pnpm via `packageManager` or an exact
`devEngines.packageManager.version`, self-update the bootstrap up
front. The bootstrap's `pnpm store path` skips pnpm's auto-switch
(the `store` command sets `skipPackageManagerCheck = true`), so it
reports the bootstrap's `STORE_VERSION` while the user's actual
install runs under the pinned version and writes to a different
STORE_VERSION — breaking `cache: true` and `setup-node`'s
`cache: pnpm` on cold caches.

* fix: check bin/ dir instead of pnpm.exe for self-update detection

On Windows pnpm self-update writes `.bin/bin/pnpm` (a JS launcher),
not `.bin/bin/pnpm.exe`, so the previous existsSync probe always
fell back to pnpmHome and the bin_dest output pointed at the
bootstrap pnpm. Check whether the `bin` directory itself exists.

* fix: self-update accepts ranges; drop exact-semver gate

`pnpm self-update` resolves semver ranges to a specific version, so
`devEngines.packageManager.version: ">=10 <11"` can also go through
the self-update path. That makes readTargetVersion total — it always
returns a string or throws — so the runtime auto-switch fallback (and
the `pnpm_config_pm_on_fail=download` export from #252) is no longer
reachable and gets removed.

Adds a range case to the cache_store_path matrix.
2026-05-11 14:06:33 +02:00
Zoltan Kochan f61705d907 chore: add CODEOWNERS 2026-05-11 12:46:12 +02:00
Andrew Haines 7a5507b117 fix: restore inputs from state in post (#255) 2026-05-11 12:44:49 +02:00
Zoltan Kochan 1155470f3e fix: honor devEngines.packageManager.onFail=error (#252) (#254)
## Summary
- Export `pnpm_config_pm_on_fail=download` from the action so the bootstrap pnpm switches versions via `manage-package-manager-versions` instead of throwing `BAD_PM_VERSION` when a project pins `devEngines.packageManager.onFail = "error"` without supplying a `version:` input.
- Adds a `test_dev_engines_on_fail_error` workflow job (Linux/macOS/Windows, exact + range) that reproduces #252.

Fixes #252.
2026-05-11 01:51:30 +02:00
Zoltan Kochan 91ab88e261 fix: bin_dest output points to self-updated pnpm, not bootstrap (#249)
* fix: bin_dest output points to self-updated pnpm, not bootstrap (#247)

`pnpm self-update <version>` writes the target binary to
`${PNPM_HOME}/bin/`, leaving the bootstrap symlink at `${PNPM_HOME}/pnpm`
untouched. The `bin_dest` output was set to `${PNPM_HOME}`, so consumers
invoking `${{ steps.pnpm.outputs.bin_dest }}/pnpm` got the bootstrap
version (currently 11.0.4) instead of the version they requested.

PATH lookup hid the bug: `${PNPM_HOME}/bin` was prepended ahead of
`${PNPM_HOME}`, so `pnpm` resolved from PATH was the right one. Existing
version-respect tests only checked `pnpm --version`, not `bin_dest`.

Resolve `binDest` inside `runSelfInstaller` (target lives in
`${PNPM_HOME}/bin` after self-update, otherwise stays at `${PNPM_HOME}`)
and plumb it through to `setOutputs`. Add a regression test that invokes
`${bin_dest}/pnpm --version` directly across Linux/macOS/Windows.

* test(ci): pass bin_dest via env to survive Windows backslashes

Direct GitHub-expression interpolation of `${{ steps.pnpm.outputs.bin_dest }}`
into the bash script let bash eat the backslashes in the Windows path
(`C:Usersrunneradminsetup-pnpmnode_modules.binbin/pnpm`), failing with
"No such file or directory". Forward the value via env so the path
reaches bash unmangled.

* build: rebuild dist with clean lockfile-matched deps
2026-05-07 12:58:58 +02:00
16 changed files with 573 additions and 459 deletions
+1
View File
@@ -0,0 +1 @@
* @zkochan
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with: with:
run_install: true run_install: true
version: 9 version: 11
- name: Update dist/index.js - name: Update dist/index.js
run: pnpm run build run: pnpm run build
+213 -185
View File
@@ -1,43 +1,73 @@
name: Test Action name: Test Action
on: on:
- push pull_request:
- pull_request push:
- workflow_dispatch branches:
- master
workflow_dispatch:
jobs: jobs:
test_default_inputs: smoke:
name: Test with default inputs # Cross-OS coverage. Exercises the bootstrap install + PATH on each platform,
# the version-respects-request regression (#225 / #230 — Windows PATH shadow),
# and the bin_dest output regression (#247). Multi-version coverage on Linux
# so we don't pay 3x for major-version differences.
name: 'Smoke (${{ matrix.name }})'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pnpm: include:
- 9.15.5 - name: 'ubuntu / v9.15.5'
os: os: ubuntu-latest
- ubuntu-latest version: '9.15.5'
- macos-latest - name: 'ubuntu / v10.33.0'
- windows-latest os: ubuntu-latest
version: '10.33.0'
- name: 'ubuntu / v9.15.5 / custom-dest'
os: ubuntu-latest
version: '9.15.5'
dest: '~/test/pnpm'
- name: 'macos / v9.15.5'
os: macos-latest
version: '9.15.5'
- name: 'windows / v9.15.5'
os: windows-latest
version: '9.15.5'
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Run the action - id: pnpm
name: Run the action
uses: ./ uses: ./
with: with:
version: 9.15.5 version: ${{ matrix.version }}
dest: ${{ matrix.dest || '~/setup-pnpm' }}
- name: 'Test: which' - name: 'Test: pnpm/pnpx on PATH report the requested version (incl. via bin_dest)'
run: which pnpm; which pnpx # Pass paths via env, not template interpolation, so Windows
# backslashes in `bin_dest` aren't eaten by bash's escape handling.
- name: 'Test: version' env:
BIN_DEST: ${{ steps.pnpm.outputs.bin_dest }}
REQUIRED: ${{ matrix.version }}
run: | run: |
set -e
which pnpm
which pnpx
actual="$(pnpm --version)" actual="$(pnpm --version)"
echo "pnpm version: ${actual}" echo "pnpm --version: ${actual}"
if [[ ! "${actual}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then if [ "${actual}" != "${REQUIRED}" ]; then
echo "ERROR: pnpm --version did not produce valid output" echo "Expected pnpm version ${REQUIRED}, but got ${actual}"
exit 1
fi
bin_dest_version="$("$BIN_DEST/pnpm" --version)"
echo "bin_dest pnpm --version: ${bin_dest_version}"
if [ "${bin_dest_version}" != "${REQUIRED}" ]; then
echo "Expected ${REQUIRED} via bin_dest, but got ${bin_dest_version}"
exit 1 exit 1
fi fi
shell: bash shell: bash
@@ -50,54 +80,95 @@ jobs:
pnpm add is-odd pnpm add is-odd
shell: bash shell: bash
test_dest: manifest_pin:
name: Test with dest # Folds the old test_package_manager_field, test_dev_engines, and
# test_dev_engines_on_fail_error jobs. The action's manifest handling is
# OS-independent, so ubuntu-only is sufficient.
name: 'Manifest pin: ${{ matrix.label }}'
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pnpm: include:
- 9.15.5 - label: 'packageManager pnpm@9.15.5 (#227)'
os: manifest: '{"packageManager":"pnpm@9.15.5"}'
- ubuntu-latest version: '9.15.5'
- macos-latest - label: 'packageManager pnpm@10.33.0'
- windows-latest manifest: '{"packageManager":"pnpm@10.33.0"}'
version: '10.33.0'
- label: 'devEngines onFail=download, exact'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":"9.15.5","onFail":"download"}}}'
version: '9.15.5'
- label: 'devEngines onFail=download, range'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":">=9.15.0","onFail":"download"}}}'
version: '>=9.15.0'
- label: 'devEngines onFail=error, exact (#252)'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":"9.15.5","onFail":"error"}}}'
version: '9.15.5'
- label: 'devEngines onFail=error, range (#252)'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":">=9.15.0","onFail":"error"}}}'
version: '>=9.15.0'
- label: 'explicit version: pnpm_config_pm_on_fail not exported'
# Regression guard for the af8e203 scope fix: when the user passes an
# explicit `version:` input, the action must NOT export
# pnpm_config_pm_on_fail=download, so the user's strict onFail policy
# is preserved. Asserted directly on the env var rather than pnpm
# runtime behavior — different pnpm majors read devEngines
# differently (v10 ignores it, v11+ honors it).
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":"9.15.5","onFail":"error"}}}'
explicit_version: '10.33.0'
expect_pm_on_fail_unset: true
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up package.json
run: echo '${{ matrix.manifest }}' > package.json
shell: bash
- name: Run the action - name: Run the action
uses: ./ uses: ./
with: with:
version: 9.15.5 version: ${{ matrix.explicit_version }}
dest: ~/test/pnpm
- name: 'Test: which' - name: 'Test: pnpm reports the pinned version'
run: which pnpm && which pnpx if: ${{ !matrix.expect_pm_on_fail_unset }}
env:
- name: 'Test: version' REQUIRED: ${{ matrix.version }}
run: | run: |
set -e
actual="$(pnpm --version)" actual="$(pnpm --version)"
echo "pnpm version: ${actual}" echo "pnpm version: ${actual}"
if [[ ! "${actual}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then if [ "${REQUIRED}" = ">=9.15.0" ]; then
echo "ERROR: pnpm --version did not produce valid output" min="9.15.0"
if [ "$(printf '%s\n' "${min}" "${actual}" | sort -V | head -n1)" != "${min}" ]; then
echo "Expected pnpm version >= ${min}, but got ${actual}"
exit 1 exit 1
fi fi
else
if [ "${actual}" != "${REQUIRED}" ]; then
echo "Expected pnpm version ${REQUIRED}, but got ${actual}"
exit 1
fi
fi
shell: bash shell: bash
test_standalone: - name: 'Test: pnpm_config_pm_on_fail not exported (explicit version preserves strict policy)'
name: Test with standalone if: ${{ matrix.expect_pm_on_fail_unset }}
run: |
if [ -n "${pnpm_config_pm_on_fail:-}" ]; then
echo "Expected pnpm_config_pm_on_fail to be unset, but got: '${pnpm_config_pm_on_fail}'"
exit 1
fi
echo "pnpm_config_pm_on_fail is unset, as expected"
shell: bash
runs-on: ${{ matrix.os }} standalone:
name: Standalone mode
strategy: runs-on: ubuntu-latest
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -108,167 +179,125 @@ jobs:
version: 9.15.0 version: 9.15.0
standalone: true standalone: true
- name: 'Test: which' - name: 'Test: pnpm works'
run: which pnpm
- name: 'Test: version'
run: | run: |
set -e
which pnpm
actual="$(pnpm --version)" actual="$(pnpm --version)"
echo "pnpm version: ${actual}" if [ "${actual}" != "9.15.0" ]; then
if [[ ! "${actual}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then echo "Expected 9.15.0, got ${actual}"
echo "ERROR: pnpm --version did not produce valid output"
exit 1 exit 1
fi fi
shell: bash
- name: 'Test: install in a fresh project'
run: |
mkdir /tmp/test-standalone mkdir /tmp/test-standalone
cd /tmp/test-standalone cd /tmp/test-standalone
pnpm init pnpm init
pnpm add is-odd pnpm add is-odd
shell: bash shell: bash
test_version_respects_request: standalone_windows_self_update:
name: 'Test version input is actually installed (${{ matrix.version }}, ${{ matrix.os }})' # Regression guard for the patchPnpmEnv PATH-shadow bug. When
# Regression test for #225 / #230: the bootstrap pnpm on PATH was shadowing the self-updated binary, # standalone: true on Windows AND the requested pnpm differs from the
# so a user requesting e.g. `version: 9.15.5` would silently get the bootstrap version. # bootstrap, the previous patchPnpmEnv prepended node_modules/.bin to
# PATH; that directory contains an npm-created pnpm.cmd shim pointing
runs-on: ${{ matrix.os }} # at the BOOTSTRAP pnpm, which shadowed the self-updated pnpm at
# $PNPM_HOME/bin and caused `pnpm install` inside the action to run
strategy: # under the bootstrap version. Exercising a newer-pnpm-only flag
fail-fast: false # (`--no-runtime`, added in 11.1.0) makes the regression assertable:
matrix: # if the bootstrap (11.0.4) handles the install, it errors with
os: # "Unknown option: 'runtime'".
- ubuntu-latest name: 'Standalone Windows self-update (PATH regression)'
- macos-latest runs-on: windows-latest
- windows-latest
version:
- '9.15.5'
- '10.33.0'
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up package.json with a minimal manifest
# run_install needs a manifest to install against. Removing the
# repo's existing pnpm-lock.yaml avoids frozen-lockfile mismatch.
run: |
rm -f pnpm-lock.yaml
echo '{"name":"sw","private":true,"packageManager":"pnpm@11.1.0"}' > package.json
shell: bash
- name: Run the action - name: Run the action
uses: ./ uses: ./
with: with:
version: ${{ matrix.version }} version: 11.1.0
standalone: true
run_install: |
args: ['--no-runtime']
- name: 'Test: exact version installed' - name: 'Test: pnpm install completed under the self-updated pnpm'
run: | # If the bug recurs, the previous step's run_install will have failed
required='${{ matrix.version }}' # the job with "Unknown option: 'runtime'", so reaching this step
actual="$(pnpm --version)" # implies success. Still verify the version on PATH matches request.
echo "pnpm version: ${actual}" env:
if [ "${actual}" != "${required}" ]; then REQUIRED: '11.1.0'
echo "Expected pnpm version ${required}, but got ${actual}"
exit 1
fi
shell: bash
test_package_manager_field:
name: 'Test packageManager field is respected (${{ matrix.version }}, ${{ matrix.os }})'
# Reproduces #227: when `packageManager` is set in package.json and no `version:` input is given,
# the action should install the version specified there.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
version:
- '9.15.5'
- '10.33.0'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up package.json with packageManager field
run: echo '{"packageManager":"pnpm@${{ matrix.version }}"}' > package.json
shell: bash
- name: Run the action
uses: ./
- name: 'Test: exact version installed'
run: |
required='${{ matrix.version }}'
actual="$(pnpm --version)"
echo "pnpm version: ${actual}"
if [ "${actual}" != "${required}" ]; then
echo "Expected pnpm version ${required}, but got ${actual}"
exit 1
fi
shell: bash
test_dev_engines:
name: Test with devEngines.packageManager
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
version:
- '9.15.5'
- '>=9.15.0'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up package.json with devEngines.packageManager
run: echo '{"devEngines":{"packageManager":{"name":"pnpm","version":"${{ matrix.version }}","onFail":"download"}}}' > package.json
shell: bash
- name: Run the action
uses: ./
- name: 'Test: which'
run: which pnpm; which pnpx
- name: 'Test: version'
run: | run: |
set -e set -e
required='${{ matrix.version }}'
actual="$(pnpm --version)" actual="$(pnpm --version)"
echo "pnpm version: ${actual}" echo "pnpm --version: ${actual}"
if [ "${actual}" != "${REQUIRED}" ]; then
if [ "${required}" = ">=9.15.0" ]; then echo "Expected pnpm ${REQUIRED}, got ${actual}"
min="9.15.0"
if [ "$(printf '%s\n' "${min}" "${actual}" | sort -V | head -n1)" != "${min}" ]; then
echo "Expected pnpm version >= ${min}, but got ${actual}"
exit 1 exit 1
fi fi
else
if [ "${actual}" != "${required}" ]; then
echo "Expected pnpm version ${required}, but got ${actual}"
exit 1
fi
fi
shell: bash shell: bash
test_run_install: cache_store_path:
name: 'Test with run_install (${{ matrix.run_install.name }}, ${{ matrix.os }})' # Regression guard for #233. When package.json pins a pnpm major that
# differs from the bootstrap pnpm's major, the bootstrap reports its
# own STORE_VERSION from `pnpm store path` (the `store` command skips
# pnpm's auto-switch). The user's actual `pnpm install` runs under the
# pinned version and writes to a different STORE_VERSION, so the post
# step's saveCache then fails with "Path Validation Error". The fix is
# to self-update the bootstrap to the pinned version up front.
name: 'Cache store path matches install (#233): ${{ matrix.label }}'
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- label: 'packageManager pnpm@10.33.0'
manifest: '{"packageManager":"pnpm@10.33.0","dependencies":{"is-odd":"3.0.1"}}'
- label: 'devEngines exact pnpm@10.33.0'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":"10.33.0"}},"dependencies":{"is-odd":"3.0.1"}}'
- label: 'devEngines range >=10 <11'
manifest: '{"devEngines":{"packageManager":{"name":"pnpm","version":">=10 <11"}},"dependencies":{"is-odd":"3.0.1"}}'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Set up package.json
run: echo '${{ matrix.manifest }}' > package.json
shell: bash
- id: pnpm
uses: ./
with:
cache: true
run_install: |
- args: [--no-frozen-lockfile]
- name: 'Test: store path computed by the action exists on disk'
run: |
set -e
actual="$(pnpm store path --silent)"
echo "pnpm store path: ${actual}"
if [ ! -d "${actual}" ]; then
echo "Expected store path to exist on disk; cache save would fail"
exit 1
fi
shell: bash
run_install:
name: 'run_install (${{ matrix.run_install.name }})'
runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pnpm:
- 9.15.5
os:
- ubuntu-latest
- macos-latest
- windows-latest
run_install: run_install:
- name: 'null' - name: 'null'
value: 'null' value: 'null'
@@ -289,15 +318,14 @@ jobs:
version: 9.15.5 version: 9.15.5
run_install: ${{ matrix.run_install.value }} run_install: ${{ matrix.run_install.value }}
- name: 'Test: which' - name: 'Test: pnpm works'
run: which pnpm; which pnpx
- name: 'Test: version'
run: | run: |
set -e
which pnpm
which pnpx
actual="$(pnpm --version)" actual="$(pnpm --version)"
echo "pnpm version: ${actual}" if [ "${actual}" != "9.15.5" ]; then
if [[ ! "${actual}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then echo "Expected 9.15.5, got ${actual}"
echo "ERROR: pnpm --version did not produce valid output"
exit 1 exit 1
fi fi
shell: bash shell: bash
+85 -8
View File
@@ -1,18 +1,68 @@
> ## :warning: Upgrade from v2! > [!IMPORTANT]
> **This action has a successor: [`pnpm/setup`](https://github.com/pnpm/setup).**
> >
> The v2 version of this action [has stopped working](https://github.com/pnpm/action-setup/issues/135) with newer Node.js versions. Please, upgrade to the latest version to fix any issues. > For pnpm v11 and newer, use [`pnpm/setup`](https://github.com/pnpm/setup) instead. It downloads pnpm's self-contained release binary (no Node.js or npm required) and can install a JavaScript runtime (Node.js, Bun, or Deno) in the same step, replacing `actions/setup-node`.
>
> `pnpm/action-setup` remains the action to use for installing pnpm v10 and older. See [Migrating to pnpm/setup](#migrating-to-pnpmsetup) below.
# Setup pnpm # Setup pnpm
Install pnpm package manager. Install pnpm package manager.
> ## :warning: Upgrade from v2!
>
> The v2 version of this action [has stopped working](https://github.com/pnpm/action-setup/issues/135) with newer Node.js versions. Please, upgrade to the latest version to fix any issues.
## Migrating to pnpm/setup
[`pnpm/setup`](https://github.com/pnpm/setup) installs pnpm v11+ as a native standalone executable and can install Node.js, Bun, or Deno in the same step, so a typical workflow no longer needs `actions/setup-node` or an explicit `pnpm install` step:
```yaml
steps:
- uses: actions/checkout@v6
# Before:
# - uses: pnpm/action-setup@v6
# with:
# version: 10
# cache: true
# - uses: actions/setup-node@v4
# with:
# node-version: 22
# - run: pnpm install
# After:
- uses: pnpm/setup@v1
with:
version: 11
runtime: node@22
cache: true
```
The `version` input can be omitted only when `packageManager` (or `devEngines.packageManager`) in `package.json` declares pnpm v11 or newer; otherwise keep it explicit, since `pnpm/setup` requires pnpm v11+.
Input and output changes:
| `pnpm/action-setup` | `pnpm/setup` | Notes |
| ------------------- | ------------ | ----- |
| `version` | `version` | Must resolve to pnpm v11 or newer. As before, it can be omitted when `packageManager` (or `devEngines.packageManager`) is set in `package.json`. |
| `dest` | `dest` | Unchanged. |
| `run_install` | `install` | `pnpm/setup` runs `pnpm install` automatically when a `package.json` is present (`install: true` by default); set `install: false` to skip it. The object/array form (`recursive`, `cwd`, `args`) is not supported — run those commands in separate steps. |
| `cache` | `cache` | Unchanged. |
| `cache_dependency_path` | `cache-dependency-path` | Renamed to kebab-case. |
| `package_json_file` | `package-json-file` | Renamed to kebab-case. |
| `standalone` | removed | `pnpm/setup` always installs the standalone native executable. |
| n/a | `runtime` | New: installs Node.js, Bun, or Deno (e.g. `node@22`, `bun@latest`, `deno@2`), or reads `devEngines.runtime` from `package.json`. |
| n/a | `token` | New: GitHub token for release lookup; defaults to `${{ github.token }}` and rarely needs to be set. |
| `bin_dest` (output) | `bin-dest` (output) | Renamed to kebab-case. New outputs `runtime-name` and `runtime-version` describe the installed runtime. |
## Inputs ## Inputs
### `version` ### `version`
Version of pnpm to install. Version of pnpm to install.
**Optional** when there is a [`packageManager` field in the `package.json`](https://nodejs.org/api/corepack.html). **Optional** when there is a [`packageManager` or `devEngines.packageManager` field in the `package.json`](https://nodejs.org/api/corepack.html).
otherwise, this field is **required** It supports npm versioning scheme, it could be an exact version (such as `10.9.8`), or a version range (such as `10`, `10.x.x`, `10.9.x`, `^10.9.8`, `*`, etc.), or `latest`. otherwise, this field is **required** It supports npm versioning scheme, it could be an exact version (such as `10.9.8`), or a version range (such as `10`, `10.x.x`, `10.9.x`, `^10.9.8`, `*`, etc.), or `latest`.
@@ -48,11 +98,11 @@ If `run_install` is a YAML string representation of either an object or an array
### `cache_dependency_path` ### `cache_dependency_path`
**Optional** (_type:_ `string|string[]`, _default:_ `pnpm-lock.yaml`) File path to the pnpm lockfile, which contents hash will be used as a cache key. **Optional** (_type:_ `string`, _default:_ `pnpm-lock.yaml`) File path to the pnpm lockfile, whose contents hash will be used as a cache key. Accepts multiple paths delimited by newlines.
### `package_json_file` ### `package_json_file`
**Optional** (_type:_ `string`, _default:_ `package.json`) File path to the `package.json`/[`package.yaml`](https://github.com/pnpm/pnpm/pull/1799) to read "packageManager" configuration. **Optional** (_type:_ `string`, _default:_ `package.json`) File path to the `package.json`/[`package.yaml`](https://github.com/pnpm/pnpm/pull/1799) to read `packageManager` or `devEngines.packageManager` configuration.
### `standalone` ### `standalone`
@@ -74,7 +124,7 @@ Location of `pnpm` and `pnpx` command.
### Install only pnpm without `packageManager` ### Install only pnpm without `packageManager`
This works when the repo either doesn't have a `package.json` or has a `package.json` but it doesn't specify `packageManager`. This works when the repo either doesn't have a `package.json` or has a `package.json` but it doesn't specify `packageManager` or `devEngines.packageManager`.
```yaml ```yaml
on: on:
@@ -93,7 +143,7 @@ jobs:
### Install only pnpm with `packageManager` ### Install only pnpm with `packageManager`
Omit `version` input to use the version in the [`packageManager` field in the `package.json`](https://nodejs.org/api/corepack.html). Omit `version` input to use the version in the [`packageManager` or `devEngines.packageManager` field in the `package.json`](https://nodejs.org/api/corepack.html).
```yaml ```yaml
on: on:
@@ -158,9 +208,36 @@ jobs:
**Note:** You don't need to run `pnpm store prune` at the end; post-action has already taken care of that. **Note:** You don't need to run `pnpm store prune` at the end; post-action has already taken care of that.
### Cache dependencies from multiple lockfiles
```yaml
on:
- push
- pull_request
jobs:
cache-and-install-multiple:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
with:
version: 10
cache: true
cache_dependency_path: |
one/pnpm-lock.yaml
two/pnpm-lock.yaml
run_install: |
- cwd: one
- cwd: two
```
## Notes ## Notes
This action does not setup Node.js for you, use [actions/setup-node](https://github.com/actions/setup-node) yourself. This action does not set up Node.js. Use [actions/setup-node](https://github.com/actions/setup-node) yourself. If you are on pnpm v11 or newer, [`pnpm/setup`](https://github.com/pnpm/setup) can install pnpm and Node.js in a single step.
## License ## License
+1 -1
View File
@@ -20,7 +20,7 @@ inputs:
required: false required: false
default: 'false' default: 'false'
cache_dependency_path: cache_dependency_path:
description: File path to the pnpm lockfile, which contents hash will be used as a cache key description: File path to the pnpm lockfile, whose contents hash will be used as a cache key. Accepts multiple paths delimited by newlines.
required: false required: false
default: 'pnpm-lock.yaml' default: 'pnpm-lock.yaml'
package_json_file: package_json_file:
+150 -151
View File
File diff suppressed because one or more lines are too long
+11 -2
View File
@@ -18,9 +18,18 @@ export async function runRestoreCache(inputs: Inputs) {
debug(`Primary key is ${primaryKey}`) debug(`Primary key is ${primaryKey}`)
saveState('cache_primary_key', primaryKey) saveState('cache_primary_key', primaryKey)
let cacheKey = await restoreCache([cachePath], primaryKey) // We don't need to download everything again if only one dependency changed
// We can still re-use previous store to cache the rest of the unchanged dependencies
const restoreKeys = [
`pnpm-cache-${process.env.RUNNER_OS}-${os.arch()}-`
];
setOutput('cache-hit', Boolean(cacheKey)) let cacheKey = await restoreCache([cachePath], primaryKey, restoreKeys)
// A restore-key (prefix) match still restores an older store, but "cache-hit"
// must only report an exact primary-key match, so dependency installation
// is not skipped when the lockfile has changed.
setOutput('cache-hit', cacheKey === primaryKey)
if (!cacheKey) { if (!cacheKey) {
info(`Cache is not found`) info(`Cache is not found`)
+10 -8
View File
@@ -8,28 +8,30 @@ import pnpmInstall from './pnpm-install'
import pruneStore from './pnpm-store-prune' import pruneStore from './pnpm-store-prune'
async function main() { async function main() {
const inputs = getInputs()
if (getState('is_post') === 'true') { if (getState('is_post') === 'true') {
await runPost(inputs) await runPost()
} else { } else {
await runMain(inputs) await runMain()
} }
} }
async function runMain(inputs: Inputs) { async function runMain() {
const inputs = getInputs()
saveState('inputs', inputs)
saveState('is_post', 'true') saveState('is_post', 'true')
await installPnpm(inputs) const binDest = await installPnpm(inputs)
if (binDest === undefined) return
console.log('Installation Completed!') console.log('Installation Completed!')
setOutputs(inputs) setOutputs(inputs, binDest)
await restoreCache(inputs) await restoreCache(inputs)
pnpmInstall(inputs) pnpmInstall(inputs)
} }
async function runPost(inputs: Inputs) { async function runPost() {
const inputs = JSON.parse(getState('inputs')) as Inputs
pruneStore(inputs) pruneStore(inputs)
await saveCache(inputs) await saveCache(inputs)
} }
+33 -50
View File
@@ -5,18 +5,18 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@pnpm/exe": "11.0.4" "@pnpm/exe": "11.19.0"
} }
}, },
"node_modules/@pnpm/exe": { "node_modules/@pnpm/exe": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.19.0.tgz",
"integrity": "sha512-3OwYqbbj1KtuUqoMo5OEkY8nU/WutZ7L5ADFl0bbW9oyqU55U37aDqA3NJNSk28CyszNARfrjerAF2DW2TsV7w==", "integrity": "sha512-P1mw8BZaNkEpttlyzKsxTj7PVs94bMB4cQ8pJhgbrCTSBP7xCzKS3VlOwIInS7aS5by6KAbfO5Vs9yK/ekugrg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@reflink/reflink": "0.1.19", "@reflink/reflink": "0.1.19",
"detect-libc": "^2.0.3" "detect-libc": "^2.1.2"
}, },
"bin": { "bin": {
"pn": "pn", "pn": "pn",
@@ -28,20 +28,19 @@
"url": "https://opencollective.com/pnpm" "url": "https://opencollective.com/pnpm"
}, },
"optionalDependencies": { "optionalDependencies": {
"@pnpm/linux-arm64": "11.0.4", "@pnpm/linux-arm64": "11.19.0",
"@pnpm/linux-x64": "11.0.4", "@pnpm/linux-x64": "11.19.0",
"@pnpm/linuxstatic-arm64": "11.0.4", "@pnpm/linuxstatic-arm64": "11.19.0",
"@pnpm/linuxstatic-x64": "11.0.4", "@pnpm/linuxstatic-x64": "11.19.0",
"@pnpm/macos-arm64": "11.0.4", "@pnpm/macos-arm64": "11.19.0",
"@pnpm/macos-x64": "11.0.4", "@pnpm/win-arm64": "11.19.0",
"@pnpm/win-arm64": "11.0.4", "@pnpm/win-x64": "11.19.0"
"@pnpm/win-x64": "11.0.4"
} }
}, },
"node_modules/@pnpm/linux-arm64": { "node_modules/@pnpm/linux-arm64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.19.0.tgz",
"integrity": "sha512-Bz7V2sFypoGHX/t5w/w7jnCw5DCK3C8s5q8whHJJ3iS5kRznX3Q1F4LwSjjy+lsi777fHyNIvD7qtNmdt9IKoA==", "integrity": "sha512-c5AJqnsj0BMqMCOtctOzsCqyfY+afFe1kdM9F2FrNhYwtnQRMHoJy+51qfee4yuTPOVRyk3Yh1dwvyAadiznvA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -55,9 +54,9 @@
} }
}, },
"node_modules/@pnpm/linux-x64": { "node_modules/@pnpm/linux-x64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.19.0.tgz",
"integrity": "sha512-u0Yn1gytR1vKdPk6fYF500H8ZWQlj0cTuIQPp+5GYVPkMmA5bSw41RNIDPBfjDlE8ERmQWaQcrgmTcmTZ+n22A==", "integrity": "sha512-KOKpA9o75SvmRQgWO+EqEpQzJg1b9uHk5y61PAx90sSpdYtKDPua2eBXBglzv8YK1xM3DsYXFEf7Scs+3HeDsA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -71,9 +70,9 @@
} }
}, },
"node_modules/@pnpm/linuxstatic-arm64": { "node_modules/@pnpm/linuxstatic-arm64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-arm64/-/linuxstatic-arm64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-arm64/-/linuxstatic-arm64-11.19.0.tgz",
"integrity": "sha512-0aitEcfhWNXNZhfJGt/kJaRvfcdtJzXZpV+toJN94kfawSJnhuawfnUSXMi/3m0G97HkJc7BH8rOz3sojUKt0g==", "integrity": "sha512-Ci9WxgCInZc3F43R6BfaHevi+dYqaI5CLi6421egFsK649eHvYPytvu+zABhTNlYGNJbD8j9w+AUYInz/TPsyQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -90,9 +89,9 @@
} }
}, },
"node_modules/@pnpm/linuxstatic-x64": { "node_modules/@pnpm/linuxstatic-x64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-x64/-/linuxstatic-x64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-x64/-/linuxstatic-x64-11.19.0.tgz",
"integrity": "sha512-xDJdeJ7D2YvDBy2/IH9lEqMKiSuZiV8190XKWOgQgxUGGeuW4z3j6Ewpl0S5bXsWuNjAgC+uCKp7Qp3P7cXAvw==", "integrity": "sha512-GNMk96vNJ4uEWigekarjHofXNDFsTYRL0Mw2YlDkv/W+u0cn/UAVPHfd5aAfsf6Arel2ZQfxPUTXlpunFujWnA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -109,9 +108,9 @@
} }
}, },
"node_modules/@pnpm/macos-arm64": { "node_modules/@pnpm/macos-arm64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.19.0.tgz",
"integrity": "sha512-dNR69jUARtGFuyyLE9VuyxhRUKC8MO/7/xIyAdeIMZAD5ej0Y/Ct0DYCa/FLbgFL1nXaXmp4+gRMfJBkkrKfQQ==", "integrity": "sha512-I3Ee/GQlPxEOeBSzqxBNwcTHxVebjMiN2xLdTBS6OK2TAPzbWMIExjU5brxhr8rAk73p7mbv/a/2tFC/3gl6FA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -124,26 +123,10 @@
"url": "https://opencollective.com/pnpm" "url": "https://opencollective.com/pnpm"
} }
}, },
"node_modules/@pnpm/macos-x64": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@pnpm/macos-x64/-/macos-x64-11.0.4.tgz",
"integrity": "sha512-RfyrxSBajeEU16dZsgFjbdagDV9F4HNCJfbBgm8IbGjL0+J95naM/VmCDLd6S3+1tISeI2MxtcyCxqjKJsD/BA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/pnpm"
}
},
"node_modules/@pnpm/win-arm64": { "node_modules/@pnpm/win-arm64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.19.0.tgz",
"integrity": "sha512-fOQEv8b9KxZlUAxPPXSQQUUIrt2nY24Qwd4RzCPpatacBnsE4JIadlr/B4V5z2zFxmV7FdHr7nYUhv2RqTlY/w==", "integrity": "sha512-rOiWuJ9wjaFY9ZDVPGVjpvjIQHZNB72rioxvuyzDKJ7dqKQ97VF27a4rmOtjhMB5u83cdDGEJ+OY+H1+TP+frw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -157,9 +140,9 @@
} }
}, },
"node_modules/@pnpm/win-x64": { "node_modules/@pnpm/win-x64": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.0.4.tgz", "resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.19.0.tgz",
"integrity": "sha512-pErHAV8m3NZuPSeCmH3senTSHX0nwkH5lLzQSpiFuyt08hq8sqL3jDymT4ri9N7ixPN9RFZghZIiT3h+Croaew==", "integrity": "sha512-l26XeTxoGxfU+mVcZ5jFdP9hNe5yrOlLPSkAlwwpXPI1iyyU4JUSxm6Jdyxu1UAU1FerD/BvLK2vrlR6d9ogag==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"pnpm": "11.0.4" "pnpm": "11.19.0"
} }
}, },
"node_modules/pnpm": { "node_modules/pnpm": {
"version": "11.0.4", "version": "11.19.0",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.0.4.tgz", "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.19.0.tgz",
"integrity": "sha512-CjlxZQB6AU7VKRmmHl9GxIubyohATDA+yuzGP2Le9WOJjTxril1epYEes5jP4DqwXaGlzpY/Em1erUwC+TuDww==", "integrity": "sha512-eIHz7VkNRyxKlV4riLISF5ERYGbcyIy8o4SeybYPG7qm0syyIfqR2k4cZb7yvL43k2Wup6xTnHv4be3DobItzg==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"pn": "bin/pnpm.mjs", "pn": "bin/pnpm.mjs",
+6 -4
View File
@@ -4,13 +4,15 @@ import runSelfInstaller from './run'
export { runSelfInstaller } export { runSelfInstaller }
export async function install(inputs: Inputs) { export async function install(inputs: Inputs): Promise<string | undefined> {
startGroup('Running self-installer...') startGroup('Running self-installer...')
const status = await runSelfInstaller(inputs) const { exitCode, binDest } = await runSelfInstaller(inputs)
endGroup() endGroup()
if (status) { if (exitCode) {
return setFailed(`Something went wrong, self-installer exits with code ${status}`) setFailed(`Something went wrong, self-installer exits with code ${exitCode}`)
return undefined
} }
return binDest
} }
export default install export default install
+43 -20
View File
@@ -12,7 +12,12 @@ import exeLock from './bootstrap/exe-lock.json'
const BOOTSTRAP_PNPM_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { pnpm: pnpmLock.packages['node_modules/pnpm'].version } }) const BOOTSTRAP_PNPM_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { pnpm: pnpmLock.packages['node_modules/pnpm'].version } })
const BOOTSTRAP_EXE_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { '@pnpm/exe': exeLock.packages['node_modules/@pnpm/exe'].version } }) const BOOTSTRAP_EXE_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { '@pnpm/exe': exeLock.packages['node_modules/@pnpm/exe'].version } })
export async function runSelfInstaller(inputs: Inputs): Promise<number> { export interface SelfInstallerResult {
exitCode: number
binDest: string
}
export async function runSelfInstaller(inputs: Inputs): Promise<SelfInstallerResult> {
const { version, dest, packageJsonFile } = inputs const { version, dest, packageJsonFile } = inputs
// pnpm v11 requires Node >= 22.13; use standalone (exe) bootstrap which // pnpm v11 requires Node >= 22.13; use standalone (exe) bootstrap which
@@ -45,7 +50,7 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
const npmEnv = { ...process.env, [pathKey]: currentPath ? currentPath + path.delimiter + nodeDir : nodeDir } const npmEnv = { ...process.env, [pathKey]: currentPath ? currentPath + path.delimiter + nodeDir : nodeDir }
const npmExitCode = await runCommand('npm', ['ci'], { cwd: dest, env: npmEnv }) const npmExitCode = await runCommand('npm', ['ci'], { cwd: dest, env: npmEnv })
if (npmExitCode !== 0) { if (npmExitCode !== 0) {
return npmExitCode return { exitCode: npmExitCode, binDest: path.join(dest, 'node_modules', '.bin') }
} }
// On Windows with standalone mode, npm's .bin shims can't properly // On Windows with standalone mode, npm's .bin shims can't properly
@@ -79,25 +84,31 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
? path.join(dest, 'node_modules', '@pnpm', 'exe', process.platform === 'win32' ? 'pnpm.exe' : 'pnpm') ? path.join(dest, 'node_modules', '@pnpm', 'exe', process.platform === 'win32' ? 'pnpm.exe' : 'pnpm')
: path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs') : path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
// Determine the target version // Self-update the bootstrap to the requested pnpm version. readTargetVersion
// either returns a value or throws, so this always runs.
const targetVersion = readTargetVersion({ version, packageJsonFile }) const targetVersion = readTargetVersion({ version, packageJsonFile })
if (targetVersion) {
const cmd = standalone ? bootstrapPnpm : process.execPath const cmd = standalone ? bootstrapPnpm : process.execPath
const args = standalone ? ['self-update', targetVersion] : [bootstrapPnpm, 'self-update', targetVersion] const args = standalone ? ['self-update', targetVersion] : [bootstrapPnpm, 'self-update', targetVersion]
const exitCode = await runCommand(cmd, args, { cwd: dest }) const exitCode = await runCommand(cmd, args, { cwd: dest })
if (exitCode !== 0) { if (exitCode !== 0) {
return exitCode return { exitCode, binDest: pnpmHome }
} }
} // self-update writes the target pnpm/pnpx into PNPM_HOME/bin, leaving
// the bootstrap symlinks in pnpmHome pointing at the old version. Use
return 0 // PNPM_HOME/bin so consumers of the bin_dest output (e.g.
// `${steps.pnpm.outputs.bin_dest}/pnpm`) invoke the requested version.
//
// When the requested version resolves to the bootstrap version, self-update
// is a no-op and PNPM_HOME/bin is not created — fall back to pnpmHome,
// whose symlinks already point at the right version.
const updatedBinDir = path.join(pnpmHome, 'bin')
return { exitCode: 0, binDest: existsSync(updatedBinDir) ? updatedBinDir : pnpmHome }
} }
function readTargetVersion(opts: { function readTargetVersion(opts: {
readonly version?: string | undefined readonly version?: string | undefined
readonly packageJsonFile: string readonly packageJsonFile: string
}): string | undefined { }): string {
const { version, packageJsonFile } = opts const { version, packageJsonFile } = opts
const { GITHUB_WORKSPACE } = process.env const { GITHUB_WORKSPACE } = process.env
@@ -118,12 +129,15 @@ function readTargetVersion(opts: {
} }
} }
// packageManager is always exact `pnpm@<version>[+<integrity>]` per spec.
// Strip the integrity hash for self-update.
const packageManagerVersion =
typeof packageManager === 'string' && packageManager.startsWith('pnpm@')
? packageManager.slice('pnpm@'.length).split('+')[0]
: undefined
if (version) { if (version) {
if ( if (packageManagerVersion && packageManagerVersion !== version) {
typeof packageManager === 'string' &&
packageManager.startsWith('pnpm@') &&
packageManager.replace('pnpm@', '') !== version
) {
throw new Error(`Multiple versions of pnpm specified: throw new Error(`Multiple versions of pnpm specified:
- version ${version} in the GitHub Action config with the key "version" - version ${version} in the GitHub Action config with the key "version"
- version ${packageManager} in the package.json with the key "packageManager" - version ${packageManager} in the package.json with the key "packageManager"
@@ -133,13 +147,22 @@ Remove one of these versions to avoid version mismatch errors like ERR_PNPM_BAD_
return version return version
} }
// pnpm will automatically download and switch to the right version // Self-update the bootstrap pnpm to the version pinned in package.json so
if (typeof packageManager === 'string' && packageManager.startsWith('pnpm@')) { // PATH-resolved `pnpm` (and the bin_dest output) reflect the target
return undefined // version. Without this, `pnpm store path` runs as the bootstrap and
// reports a different STORE_VERSION than the one the user's actual
// install writes to — breaking cache: true and actions/setup-node's
// `cache: pnpm` on cold caches (issue #233).
//
// devEngines.packageManager takes priority over packageManager, matching
// pnpm's getWantedPackageManager logic. `pnpm self-update` accepts both
// exact versions and semver ranges, so we pass either through directly.
if (devEngines?.packageManager?.name === 'pnpm' && devEngines.packageManager.version) {
return devEngines.packageManager.version
} }
if (devEngines?.packageManager?.name === 'pnpm' && devEngines.packageManager.version) { if (packageManagerVersion) {
return undefined return packageManagerVersion
} }
if (!GITHUB_WORKSPACE) { if (!GITHUB_WORKSPACE) {
+1 -3
View File
@@ -1,9 +1,7 @@
import { setOutput } from '@actions/core' import { setOutput } from '@actions/core'
import { Inputs } from '../inputs' import { Inputs } from '../inputs'
import { getBinDest } from '../utils'
export function setOutputs(inputs: Inputs) { export function setOutputs(inputs: Inputs, binDest: string) {
const binDest = getBinDest(inputs)
// NOTE: addPath is already called in installPnpm — do not call it again // NOTE: addPath is already called in installPnpm — do not call it again
// here, as a second addPath would shadow the correct entry on Windows. // here, as a second addPath would shadow the correct entry on Windows.
setOutput('dest', inputs.dest) setOutput('dest', inputs.dest)
+6 -4
View File
@@ -1,11 +1,8 @@
import { setFailed, startGroup, endGroup } from '@actions/core' import { setFailed, startGroup, endGroup } from '@actions/core'
import { spawnSync } from 'child_process' import { spawnSync } from 'child_process'
import { Inputs } from '../inputs' import { Inputs } from '../inputs'
import { patchPnpmEnv } from '../utils'
export function runPnpmInstall(inputs: Inputs) { export function runPnpmInstall(inputs: Inputs) {
const env = patchPnpmEnv(inputs)
for (const options of inputs.runInstall) { for (const options of inputs.runInstall) {
const args = ['install'] const args = ['install']
if (options.recursive) args.unshift('recursive') if (options.recursive) args.unshift('recursive')
@@ -14,11 +11,16 @@ export function runPnpmInstall(inputs: Inputs) {
const cmdStr = ['pnpm', ...args].join(' ') const cmdStr = ['pnpm', ...args].join(' ')
startGroup(`Running ${cmdStr}...`) startGroup(`Running ${cmdStr}...`)
// spawnSync inherits process.env, which already has $PNPM_HOME/bin and
// $PNPM_HOME prepended via addPath() in install-pnpm. Do NOT pass a
// hand-patched env that adds node_modules/.bin to the front — on
// Windows standalone, .bin/pnpm.cmd is an npm shim pointing at the
// BOOTSTRAP pnpm, which would shadow the self-updated one and break
// newer-pnpm-only behavior.
const { error, status } = spawnSync('pnpm', args, { const { error, status } = spawnSync('pnpm', args, {
stdio: 'inherit', stdio: 'inherit',
cwd: options.cwd, cwd: options.cwd,
shell: true, shell: true,
env,
}) })
endGroup() endGroup()
+2 -2
View File
@@ -1,7 +1,6 @@
import { warning, startGroup, endGroup } from '@actions/core' import { warning, startGroup, endGroup } from '@actions/core'
import { spawnSync } from 'child_process' import { spawnSync } from 'child_process'
import { Inputs } from '../inputs' import { Inputs } from '../inputs'
import { patchPnpmEnv } from '../utils'
export function pruneStore(inputs: Inputs) { export function pruneStore(inputs: Inputs) {
if (inputs.runInstall.length === 0) { if (inputs.runInstall.length === 0) {
@@ -10,10 +9,11 @@ export function pruneStore(inputs: Inputs) {
} }
startGroup('Running pnpm store prune...') startGroup('Running pnpm store prune...')
// spawnSync inherits process.env (which has the right PATH from addPath
// in install-pnpm). See pnpm-install/index.ts for the rationale.
const { error, status } = spawnSync('pnpm', ['store', 'prune'], { const { error, status } = spawnSync('pnpm', ['store', 'prune'], {
stdio: 'inherit', stdio: 'inherit',
shell: true, shell: true,
env: patchPnpmEnv(inputs),
}) })
endGroup() endGroup()
-10
View File
@@ -1,10 +0,0 @@
import path from 'path'
import process from 'process'
import { Inputs } from '../inputs'
export const getBinDest = (inputs: Inputs): string => path.join(inputs.dest, 'node_modules', '.bin')
export const patchPnpmEnv = (inputs: Inputs): NodeJS.ProcessEnv => ({
...process.env,
PATH: path.join(getBinDest(inputs), 'bin') + path.delimiter + getBinDest(inputs) + path.delimiter + process.env.PATH,
})