feat: read pnpm version from devEngines.packageManager#211
Conversation
When no version is specified in the action config or the packageManager field of package.json, fall back to devEngines.packageManager. Co-Authored-By: Claude Opus 4.6 <[email protected]>
9504ec4 to
949c415
Compare
There was a problem hiding this comment.
Pull request overview
Updates the pnpm setup action to tolerate pnpm v11+ devEngines.packageManager configuration when determining whether to self-update pnpm, and adjusts bootstrapping to account for pnpm v11’s Node engine requirements.
Changes:
- Auto-selects standalone (@pnpm/exe) bootstrap when the system
nodeis older than the required major version. - Extends manifest parsing to consider
devEngines.packageManagerand updates the “no version specified” error message accordingly. - Pins bootstrap lockfiles to pnpm/@pnpm/exe
11.0.0-beta.3and adds a CI workflow job coveringdevEngines.packageManager.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/install-pnpm/run.ts |
Adds system-Node-based standalone selection and recognizes devEngines.packageManager during version resolution. |
src/install-pnpm/bootstrap/pnpm-lock.json |
Updates bootstrap pnpm dependency to 11.0.0-beta.3 (engine >=22.13). |
src/install-pnpm/bootstrap/exe-lock.json |
Updates bootstrap @pnpm/exe dependency to 11.0.0-beta.3 (plus new dependency tree). |
.github/workflows/test.yaml |
Adds a workflow job to exercise devEngines.packageManager scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "pnpm": "11.0.0-beta.3" | ||
| } | ||
| }, | ||
| "node_modules/pnpm": { | ||
| "version": "10.32.1", | ||
| "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.32.1.tgz", | ||
| "integrity": "sha512-pwaTjw6JrBRWtlY+q07fHR+vM2jRGR/FxZeQ6W3JGORFarLmfWE94QQ9LoyB+HMD5rQNT/7KnfFe8a1Wc0jyvg==", | ||
| "version": "11.0.0-beta.3", | ||
| "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.0.0-beta.3.tgz", | ||
| "integrity": "sha512-6PrfRjycZV4vRX6ttG9oR6pOgbI2/OcF2QLOzHm35UcRuvtqP4zf3wQfAAPwEbeu1uAbpSg/Q5cL8h32tumy6Q==", |
There was a problem hiding this comment.
Bootstrap pnpm is being pinned to a pre-release (11.0.0-beta.3). This increases operational risk (unexpected breaking changes, yanked pre-releases) for every action run because the bootstrap version is always installed before any self-update/version selection. If possible, prefer a stable pnpm/@pnpm/exe version, or gate use of the v11 bootstrap to only when devEngines.packageManager is detected (keeping the stable bootstrap for the common path).
| run: which pnpm; which pnpx | ||
|
|
||
| - name: 'Test: version' | ||
| run: pnpm --version |
There was a problem hiding this comment.
The new test_dev_engines job currently only prints pnpm --version and doesn’t assert that the version selected actually satisfies the devEngines.packageManager.version value (especially for the >=9.15.0 case). Adding an explicit assertion here would prevent regressions where the action installs/keeps the bootstrap pnpm version but ignores devEngines.packageManager.
| run: pnpm --version | |
| run: | | |
| set -e | |
| required='${{ matrix.version }}' | |
| actual="$(pnpm --version)" | |
| echo "pnpm version: ${actual}" | |
| if [ "${required}" = ">=9.15.0" ]; then | |
| 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 | |
| fi | |
| else | |
| if [ "${actual}" != "${required}" ]; then | |
| echo "Expected pnpm version ${required}, but got ${actual}" | |
| exit 1 | |
| fi | |
| fi |
| "@pnpm/exe": "11.0.0-beta.3" | ||
| } | ||
| }, | ||
| "node_modules/@pnpm/exe": { | ||
| "version": "10.32.1", | ||
| "resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-10.32.1.tgz", | ||
| "integrity": "sha512-baEtwHeZwmZAdBuuDDL6tbdGg5KpxhPxr3QFfYTGXvY6ws+Z1bN0mQ7ZjcaXBSC1HuLpVXnZ6NsBiaZ2DMv4vg==", | ||
| "version": "11.0.0-beta.3", | ||
| "resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.0.0-beta.3.tgz", | ||
| "integrity": "sha512-yWNlHHdYmvf4c0MCkCzAa4csJDPdA+7yJCbXBUDXMbUu/0Zv/AxtO77q24MwlnBUC0dWeA+0F/pPmdkR9aTV2A==", | ||
| "hasInstallScript": true, |
There was a problem hiding this comment.
@pnpm/exe is also pinned to the pnpm v11 pre-release (11.0.0-beta.3). Since this is the fallback path for older system Node versions, any instability or packaging changes in the beta can impact a large set of users. Consider pinning to a stable release when available, or documenting why a beta is required.
| // pnpm v11 requires Node >= 22; use standalone (exe) bootstrap which | ||
| // bundles its own Node.js when the system Node is too old | ||
| const systemNodeVersion = await getSystemNodeVersion() | ||
| const standalone = inputs.standalone || systemNodeVersion < 22 |
There was a problem hiding this comment.
standalone is now auto-disabled when node --version reports major >= 22, which means the non-standalone bootstrap path will be used. However pnpm-lock.json was updated to pnpm v11 where the package bin points to bin/pnpm.mjs, while this installer still later references .../pnpm/bin/pnpm.cjs for the non-standalone bootstrap/symlink. This will break the action when standalone is false. Update the non-standalone bootstrap paths to match the pnpm v11 bin (or derive the bin entry dynamically) so Node>=22 runs correctly.
| // pnpm will automatically download and switch to the right version | ||
| if (typeof packageManager === 'string' && packageManager.startsWith('pnpm@')) { | ||
| // Strip the "pnpm@" prefix and any "+sha..." hash suffix | ||
| return packageManager.replace('pnpm@', '').replace(/\+.*$/, '') | ||
| return undefined | ||
| } |
There was a problem hiding this comment.
readTargetVersion no longer returns the version from packageManager: "pnpm@..." and instead returns undefined. That regresses the documented behavior of omitting the version input to install the packageManager-specified pnpm version, and may lead to ERR_PNPM_BAD_PM_VERSION if the bootstrap pnpm version differs. Consider restoring parsing of packageManager (including stripping any +sha... suffix) to return a target version for self-update, and only falling back to devEngines.packageManager when packageManager is absent.
| cp.stdout.on('data', (data: Buffer) => { output += data.toString() }) | ||
| cp.on('close', () => { | ||
| const match = output.match(/^v(\d+)/) | ||
| resolve(match ? parseInt(match[1], 10) : 0) |
There was a problem hiding this comment.
getSystemNodeVersion() only parses the major version and the standalone decision uses < 22, but the pinned pnpm bootstrap in pnpm-lock.json declares engines.node >= 22.13. With Node 22.0–22.12 this would incorrectly pick the non-standalone path and could fail at runtime. Parse the full Node semver (major/minor/patch) and compare against the actual minimum supported version (e.g. 22.13.0), or otherwise ensure the threshold matches the engine requirement.
| cp.stdout.on('data', (data: Buffer) => { output += data.toString() }) | |
| cp.on('close', () => { | |
| const match = output.match(/^v(\d+)/) | |
| resolve(match ? parseInt(match[1], 10) : 0) | |
| cp.stdout.on('data', (data: Buffer) => { | |
| output += data.toString() | |
| }) | |
| cp.on('close', () => { | |
| const match = output.trim().match(/^v(\d+)\.(\d+)\.(\d+)/) | |
| if (!match) { | |
| resolve(0) | |
| return | |
| } | |
| const major = parseInt(match[1], 10) | |
| const minor = parseInt(match[2], 10) | |
| // Align the effective ">= 22" threshold with pnpm's engines.node >= 22.13.0. | |
| // Treat Node 22.0–22.12 as effectively below 22 so that a `< 22` check remains correct. | |
| if (major === 22 && minor < 13) { | |
| resolve(21) | |
| return | |
| } | |
| resolve(major) |
pnpm auto-switches to the right version when devEngines.packageManager is set, so self-update is unnecessary. This also enables range support (e.g. ">=9.15.0") which self-update doesn't handle.
Summary
packageManagerfield ofpackage.json, fall back to readingdevEngines.packageManagerfrompackage.json{ "name": "pnpm", "version": "..." }syntax used by pnpm v11+Test plan
devEngines.packageManagerinpackage.jsonand nopackageManagerfieldpackageManagerfield still takes precedence overdevEngines.packageManagerversionstill takes precedence over both🤖 Generated with Claude Code