Compare commits

..

8 Commits

Author SHA1 Message Date
Zoltan Kochan 19566048c2 fix: address review feedback — PATH ordering and regex anchoring
- Swap addPath order so pnpmHome (with pnpm.exe) is prepended last
  and has highest precedence over pnpmHome/bin.
- Anchor version regex with $ and allow prerelease suffixes.
2026-03-27 20:38:35 +01:00
Zoltan Kochan 825c5d3936 fix: add PNPM_HOME/bin to PATH on all platforms 2026-03-27 20:16:32 +01:00
Zoltan Kochan 8cb9261afe fix: remove duplicate addPath in setOutputs that shadowed pnpm.exe
setOutputs called addPath(node_modules/.bin) AFTER installPnpm had
already added the correct path (@pnpm/exe on Windows). Since
GITHUB_PATH entries are prepended, .bin ended up first in PATH,
causing PowerShell to find npm's broken shims instead of pnpm.exe.
2026-03-27 20:05:21 +01:00
Zoltan Kochan 43028df61b debug: log pnpm --version output during setup 2026-03-27 19:44:58 +01:00
Zoltan Kochan 54e420f032 test: validate pnpm --version output in CI
All version checks now capture output and assert it matches a semver
pattern. Previously, a silently failing pnpm (exit 0, no output)
would pass the tests.
2026-03-27 19:33:54 +01:00
Zoltan Kochan 5a859cc8cb fix: add @pnpm/exe dir to PATH on Windows instead of .bin shims
On Windows, npm's .bin shims can't properly execute the extensionless
native binaries from @pnpm/exe. Instead of trying to fix the shims,
add the @pnpm/exe directory directly to PATH where pnpm.exe lives.
2026-03-27 19:30:36 +01:00
Zoltan Kochan cd98fe77eb fix: copy pnpm.exe to .bin/ on Windows for standalone mode
The .cmd wrapper approach didn't work because CMD doesn't properly
wait for extensionless PE binaries. Instead, copy the actual .exe
(and .cmd for pnpx) from @pnpm/exe into .bin/ so PATHEXT finds
pnpm.exe directly, bypassing npm's broken node-wrapping shim.
2026-03-27 19:15:32 +01:00
Zoltan Kochan 6bbd5b7dd6 fix: overwrite npm .cmd wrappers for @pnpm/exe on Windows
npm creates .cmd wrappers that invoke bin entries through `node`,
but @pnpm/exe bins are native executables, not JavaScript files.
This causes pnpm commands to silently fail on Windows.
2026-03-27 19:01:02 +01:00
9 changed files with 117 additions and 299 deletions
-76
View File
@@ -129,82 +129,6 @@ jobs:
pnpm add is-odd
shell: bash
test_version_respects_request:
name: 'Test version input is actually installed (${{ matrix.version }}, ${{ matrix.os }})'
# Regression test for #225 / #230: the bootstrap pnpm on PATH was shadowing the self-updated binary,
# so a user requesting e.g. `version: 9.15.5` would silently get the bootstrap version.
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: Run the action
uses: ./
with:
version: ${{ matrix.version }}
- 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_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
-1
View File
@@ -9,4 +9,3 @@ temp
tmp.*
temp.*
.pnpm-store
.claude
+7 -7
View File
@@ -14,7 +14,7 @@ Version of pnpm to install.
**Optional** when there is a [`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 `6.24.1`), or a version range (such as `6`, `6.x.x`, `6.24.x`, `^6.24.1`, `*`, etc.), or `latest`.
### `dest`
@@ -86,7 +86,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
with:
version: 10
```
@@ -105,7 +105,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
```
### Install pnpm and a few npm packages
@@ -120,9 +120,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
with:
version: 10
run_install: |
@@ -144,9 +144,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
name: Install pnpm
with:
version: 10
+70 -70
View File
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -3,8 +3,7 @@
"scripts": {
"build:bundle": "esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --minify --outfile=dist/index.js --loader:.json=json",
"build": "pnpm run build:bundle",
"start": "pnpm run build && sh ./run.sh",
"update-bootstrap": "node scripts/update-bootstrap.mjs"
"start": "pnpm run build && sh ./run.sh"
},
"dependencies": {
"@actions/cache": "^4.1.0",
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env node
// Usage: node scripts/update-bootstrap.mjs [version]
// If version is omitted, fetches the latest next-11 tag from npm.
// Regenerates the bootstrap lockfiles used by action-setup to install pnpm via npm.
import { execSync } from 'child_process'
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
const BOOTSTRAP_DIR = new URL('../src/install-pnpm/bootstrap/', import.meta.url).pathname
const version = process.argv[2] || resolveLatestVersion()
console.log(`Updating bootstrap lockfiles to pnpm@${version} ...`)
generateLock('pnpm-lock.json', { pnpm: version }, 'bootstrap-pnpm')
generateLock('exe-lock.json', { '@pnpm/exe': version }, 'bootstrap-exe')
console.log('Done!')
function resolveLatestVersion() {
const json = execSync('npm view @pnpm/exe dist-tags --json', { encoding: 'utf8' })
const tags = JSON.parse(json)
const version = tags['next-11'] || tags['latest']
if (!version) {
console.error('Could not determine latest pnpm version from npm dist-tags')
process.exit(1)
}
return version
}
function generateLock(filename, dependencies, name) {
const tmp = mkdtempSync(join(tmpdir(), 'pnpm-bootstrap-'))
try {
writeFileSync(join(tmp, 'package.json'), JSON.stringify({ private: true, dependencies }))
execSync('npm install --package-lock-only --ignore-scripts', { cwd: tmp, stdio: 'pipe' })
const lock = readFileSync(join(tmp, 'package-lock.json'), 'utf8')
const parsed = JSON.parse(lock)
parsed.name = name
writeFileSync(join(BOOTSTRAP_DIR, filename), JSON.stringify(parsed, null, 2) + '\n')
console.log(` ${filename} -> ${Object.values(dependencies)[0]}@${version}`)
} finally {
rmSync(tmp, { recursive: true, force: true })
}
}
+29 -73
View File
@@ -5,18 +5,17 @@
"packages": {
"": {
"dependencies": {
"@pnpm/exe": "11.0.0-rc.5"
"@pnpm/exe": "11.0.0-beta.3"
}
},
"node_modules/@pnpm/exe": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/exe/-/exe-11.0.0-rc.5.tgz",
"integrity": "sha512-HT1HxzeFc6RVIMhngQZ7bQgTNzF0IckeFpOvnwCJKfsjfsD/po3LvUVsidCvpALxCWOft1TuBZUkdHq03pEolA==",
"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,
"license": "MIT",
"dependencies": {
"@reflink/reflink": "0.1.19",
"detect-libc": "^2.0.3"
"@reflink/reflink": "0.1.19"
},
"bin": {
"pn": "pn",
@@ -28,20 +27,18 @@
"url": "https://opencollective.com/pnpm"
},
"optionalDependencies": {
"@pnpm/linux-arm64": "11.0.0-rc.5",
"@pnpm/linux-x64": "11.0.0-rc.5",
"@pnpm/linuxstatic-arm64": "11.0.0-rc.5",
"@pnpm/linuxstatic-x64": "11.0.0-rc.5",
"@pnpm/macos-arm64": "11.0.0-rc.5",
"@pnpm/macos-x64": "11.0.0-rc.5",
"@pnpm/win-arm64": "11.0.0-rc.5",
"@pnpm/win-x64": "11.0.0-rc.5"
"@pnpm/linux-arm64": "11.0.0-beta.3",
"@pnpm/linux-x64": "11.0.0-beta.3",
"@pnpm/macos-arm64": "11.0.0-beta.3",
"@pnpm/macos-x64": "11.0.0-beta.3",
"@pnpm/win-arm64": "11.0.0-beta.3",
"@pnpm/win-x64": "11.0.0-beta.3"
}
},
"node_modules/@pnpm/linux-arm64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.0.0-rc.5.tgz",
"integrity": "sha512-AreNJJI0r5oEsv5+i+FMVK8AeYs0MpWTGWc2GQwf7qi/w8uA8UxVlIDwhgwY+R6YgdrYVrEjgbU4WcqIqYfgog==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/linux-arm64/-/linux-arm64-11.0.0-beta.3.tgz",
"integrity": "sha512-TF2fyuCY9GggR4kfhjo1hMmgn+rIohenwNoH0tLPM7JlBK7/UAIFt1LI+o999tRwTCEw7gnxHFwtI2vyQuDfNw==",
"cpu": [
"arm64"
],
@@ -55,41 +52,9 @@
}
},
"node_modules/@pnpm/linux-x64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.0.0-rc.5.tgz",
"integrity": "sha512-NzZPWeIVxCEfQs84wR/O3IND2HSDOClPB2L8vvkWb8KQ4pczOG2x3aNkltXDwYVKxvw4URmwct5u57JGTEvtfg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/pnpm"
}
},
"node_modules/@pnpm/linuxstatic-arm64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-arm64/-/linuxstatic-arm64-11.0.0-rc.5.tgz",
"integrity": "sha512-xK+U/fJDkvzs4ktswrCZ03cTSEAeFTfgUG88r2J+6JEDGuY/foNOMnnSNOiSplpaufY+Ie+uL+PEDlTyIy46Xg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/pnpm"
}
},
"node_modules/@pnpm/linuxstatic-x64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/linuxstatic-x64/-/linuxstatic-x64-11.0.0-rc.5.tgz",
"integrity": "sha512-Z1kSilngaM2URfPhBjam/xhMDAn5jl8V0L5CjG/Gg5unmKkipyF93OYMpfnny7A9p1KWi6sNql/KufzUmRP4Eg==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/linux-x64/-/linux-x64-11.0.0-beta.3.tgz",
"integrity": "sha512-7GrLsnSuDH62y486GUTwJdohGIC1ugz9ZJkbKOHgxIAkNGcSTJ1IkkdARtv7/WMmOEwwESDmtpOQ6LmjnpDMSA==",
"cpu": [
"x64"
],
@@ -103,9 +68,9 @@
}
},
"node_modules/@pnpm/macos-arm64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.0.0-rc.5.tgz",
"integrity": "sha512-98p3ilSzkyusC2bxk7Ya34CWt9MeJy/+kpXfwn9YgnOD7GDqCjYY9dlPB9yrkdtKUUMeOIvOuacAQTWnCg2GOQ==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/macos-arm64/-/macos-arm64-11.0.0-beta.3.tgz",
"integrity": "sha512-NQKgI1DURrEiOUzpxL0Mc+yn7DV4tpShqGnjaJLbz8ZCXsX/qhmybebvCG3r+IfSk3P5KID66lcgC/Osiaz0Dg==",
"cpu": [
"arm64"
],
@@ -119,9 +84,9 @@
}
},
"node_modules/@pnpm/macos-x64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/macos-x64/-/macos-x64-11.0.0-rc.5.tgz",
"integrity": "sha512-WZ9UqjTbZN+dMZcy4qaPDsEo4sxTIrw5H+fDvdxT1GUavsf8SBDpvzZMHrGDQ/k22H8oKvPtJ+RGd/Ie5dvbuA==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/macos-x64/-/macos-x64-11.0.0-beta.3.tgz",
"integrity": "sha512-Ky22KFYHXx8+8WU4KJT9NXVgzFioL2w9pHTQjsqTK70AbxiErscPYhrFIehlCNbXjgs+tGVIy13QNKkiwvmS8w==",
"cpu": [
"x64"
],
@@ -135,9 +100,9 @@
}
},
"node_modules/@pnpm/win-arm64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.0.0-rc.5.tgz",
"integrity": "sha512-hxgDmF4xpSVvUPvH+HdMllvHcV2zuYUn/uK182gzFvZ9DE0xEGVj09XaSn5VMbpa32i25oIqaT89QfMcOw/TJg==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/win-arm64/-/win-arm64-11.0.0-beta.3.tgz",
"integrity": "sha512-7L8TFNDm25m+XYSyhcola3YFd/li6BZzzl56SsyGnZabsvUslMwnDiJad48wOz8IuN7zsrTSGh+X/x6F+GdrFQ==",
"cpu": [
"arm64"
],
@@ -151,9 +116,9 @@
}
},
"node_modules/@pnpm/win-x64": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.0.0-rc.5.tgz",
"integrity": "sha512-B1H/6jhzW/6VLr8Bc3zOhMd2P3srANWytuBq0uppR5c7OJwUlqXqJtB6Q4nOEjWYZ2sA5m2xuFiaMeioZiiqgA==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@pnpm/win-x64/-/win-x64-11.0.0-beta.3.tgz",
"integrity": "sha512-Z/6OpMUaIpggXjCtWEhp6kWjiT/2EImhkJAu8AodOORqeNcWouGEq3sO4XU0em6d+pAHmdV0hWMQ2xCUmPVuiA==",
"cpu": [
"x64"
],
@@ -312,15 +277,6 @@
"engines": {
"node": ">= 10"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
}
}
}
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": {
"": {
"dependencies": {
"pnpm": "11.0.0-rc.5"
"pnpm": "11.0.0-beta.3"
}
},
"node_modules/pnpm": {
"version": "11.0.0-rc.5",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.0.0-rc.5.tgz",
"integrity": "sha512-xGn7aqE6meV67JNc17hv9CJwH0YC7KwiMdPcIJEFhuv7a1CntFXQd47CKuVpEtjY6I6fngoDwIdaakF4OpShvQ==",
"version": "11.0.0-beta.3",
"resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.0.0-beta.3.tgz",
"integrity": "sha512-6PrfRjycZV4vRX6ttG9oR6pOgbI2/OcF2QLOzHm35UcRuvtqP4zf3wQfAAPwEbeu1uAbpSg/Q5cL8h32tumy6Q==",
"license": "MIT",
"bin": {
"pn": "bin/pnpm.mjs",
+6 -19
View File
@@ -29,17 +29,7 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
await writeFile(path.join(dest, 'package.json'), packageJson)
await writeFile(path.join(dest, 'package-lock.json'), JSON.stringify(lockfile))
// Prepend the action's node directory to PATH so npm's
// `#!/usr/bin/env node` shebang resolves on runners (e.g. GHE
// self-hosted) where node isn't already on PATH. npm itself is
// resolved via PATH — on the GitHub Actions runner it is not
// co-located with `process.execPath`.
const nodeDir = path.dirname(process.execPath)
// On Windows, the PATH key casing varies; search case-insensitively.
const pathKey = Object.keys(process.env).find(k => k.toUpperCase() === 'PATH') ?? 'PATH'
const currentPath = process.env[pathKey]
const npmEnv = { ...process.env, [pathKey]: currentPath ? nodeDir + path.delimiter + currentPath : nodeDir }
const npmExitCode = await runCommand('npm', ['ci'], { cwd: dest, env: npmEnv })
const npmExitCode = await runCommand('npm', ['ci'], { cwd: dest })
if (npmExitCode !== 0) {
return npmExitCode
}
@@ -50,13 +40,11 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
const pnpmHome = standalone && process.platform === 'win32'
? path.join(dest, 'node_modules', '@pnpm', 'exe')
: path.join(dest, 'node_modules', '.bin')
// PNPM_HOME/bin is where `pnpm self-update` places the target version
// binary. It must have higher PATH precedence than pnpmHome (which
// contains the bootstrap binary) so the self-updated version is found
// first. The bootstrap pnpm is invoked via absolute path, not PATH,
// so this ordering does not affect the bootstrap step.
addPath(pnpmHome)
// pnpm expects PNPM_HOME/bin in PATH for global binaries (e.g. node
// installed via `pnpm runtime`). Add it first so the next addPath
// (pnpmHome itself, which contains pnpm.exe) has higher precedence.
addPath(path.join(pnpmHome, 'bin'))
addPath(pnpmHome)
exportVariable('PNPM_HOME', pnpmHome)
// Ensure pnpm bin link exists — npm ci sometimes doesn't create it
@@ -165,11 +153,10 @@ function getSystemNodeVersion(): Promise<{ major: number; minor: number }> {
})
}
function runCommand(cmd: string, args: string[], opts: { cwd: string; env?: Record<string, string | undefined> }): Promise<number> {
function runCommand(cmd: string, args: string[], opts: { cwd: string }): Promise<number> {
return new Promise<number>((resolve, reject) => {
const cp = spawn(cmd, args, {
cwd: opts.cwd,
env: opts.env,
stdio: ['pipe', 'inherit', 'inherit'],
shell: process.platform === 'win32',
})