refactor: use pnpm self-update instead of installing target separately

- Bootstrap pnpm via npm ci (verified by lockfile)
- Use `pnpm self-update <version>` for explicit version
- Let pnpm handle packageManager field automatically
- Remove standalone/exe-specific install logic (pnpm handles this)
- Update tests to not run pnpm install against the action repo itself

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zoltan Kochan
2026-03-16 02:24:28 +01:00
parent 49b8837b49
commit a722bd2d87
3 changed files with 183 additions and 267 deletions

View File

@@ -32,8 +32,16 @@ jobs:
- name: 'Test: which'
run: which pnpm; which pnpx
- name: 'Test: install'
run: pnpm install
- name: 'Test: version'
run: pnpm --version
- name: 'Test: install in a fresh project'
run: |
mkdir /tmp/test-project
cd /tmp/test-project
pnpm init
pnpm add is-odd
shell: bash
test_dest:
name: Test with dest
@@ -62,63 +70,8 @@ jobs:
- name: 'Test: which'
run: which pnpm && which pnpx
- name: 'Test: install'
run: pnpm install
test_standalone:
name: Test with standalone
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
# macos is excluded from this test because node 12 is no longer available on this platform
- ubuntu-latest
- windows-latest
standalone:
- true
- false
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Run the action
uses: ./
with:
version: 9.15.0
standalone: ${{ matrix.standalone }}
- name: install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
# pnpm@7.0.0 is not compatible with Node.js 12
node-version: 12.22.12
- name: 'Test: which (pnpm)'
run: which pnpm
- name: 'Test: which (pnpx)'
if: matrix.standalone == false
run: which pnpx
- name: 'Test: install when standalone is true'
if: matrix.standalone
run: pnpm install
- name: 'Test: install when standalone is false'
if: matrix.standalone == false
# Since the default shell on windows runner is pwsh, we specify bash explicitly
shell: bash
run: |
if pnpm install; then
echo "pnpm install should fail"
exit 1
else
echo "pnpm install failed as expected"
fi
- name: 'Test: version'
run: pnpm --version
test_run_install:
name: 'Test with run_install (${{ matrix.run_install.name }}, ${{ matrix.os }})'
@@ -137,11 +90,6 @@ jobs:
run_install:
- name: 'null'
value: 'null'
- name: 'empty object'
value: '{}'
- name: 'recursive'
value: |
recursive: true
- name: 'global'
value: |
args:
@@ -149,15 +97,6 @@ jobs:
- --global-dir=./pnpm-global
- npm
- yarn
- name: 'array'
value: |
- {}
- recursive: true
- args:
- --global
- --global-dir=./pnpm-global
- npm
- yarn
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -171,5 +110,5 @@ jobs:
- name: 'Test: which'
run: which pnpm; which pnpx
- name: 'Test: install'
run: pnpm install
- name: 'Test: version'
run: pnpm --version

252
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import { addPath, exportVariable } from '@actions/core'
import { spawn } from 'child_process'
import { rm, writeFile, mkdir, copyFile } from 'fs/promises'
import { rm, writeFile, mkdir } from 'fs/promises'
import { readFileSync } from 'fs'
import path from 'path'
import util from 'util'
@@ -11,76 +11,48 @@ import pnpmLock from './bootstrap/pnpm-lock.json'
const BOOTSTRAP_PACKAGE_JSON = JSON.stringify({ private: true, dependencies: { pnpm: pnpmLock.packages['node_modules/pnpm'].version } })
export async function runSelfInstaller(inputs: Inputs): Promise<number> {
const { version, dest, packageJsonFile, standalone } = inputs
const { GITHUB_WORKSPACE } = process.env
const { version, dest, packageJsonFile } = inputs
// Step 1: Install bootstrap pnpm via npm (integrity verified by committed lockfile)
const bootstrapDir = path.join(dest, '..', '.pnpm-bootstrap')
await rm(bootstrapDir, { recursive: true, force: true })
await mkdir(bootstrapDir, { recursive: true })
// Install bootstrap pnpm via npm (integrity verified by committed lockfile)
await rm(dest, { recursive: true, force: true })
await mkdir(dest, { recursive: true })
await writeFile(path.join(bootstrapDir, 'package.json'), BOOTSTRAP_PACKAGE_JSON)
await writeFile(path.join(bootstrapDir, 'package-lock.json'), JSON.stringify(pnpmLock))
await writeFile(path.join(dest, 'package.json'), BOOTSTRAP_PACKAGE_JSON)
await writeFile(path.join(dest, 'package-lock.json'), JSON.stringify(pnpmLock))
const npmExitCode = await runCommand('npm', ['ci', '--ignore-scripts'], { cwd: bootstrapDir })
const npmExitCode = await runCommand('npm', ['ci', '--ignore-scripts'], { cwd: dest })
if (npmExitCode !== 0) {
return npmExitCode
}
const bootstrapPnpm = path.join(bootstrapDir, 'node_modules', 'pnpm', 'bin', 'pnpm.cjs')
// Step 2: Use bootstrap pnpm to install the target version (verified via project's pnpm-lock.yaml)
await rm(dest, { recursive: true, force: true })
await mkdir(dest, { recursive: true })
const pkgJson = path.join(dest, 'package.json')
await writeFile(pkgJson, JSON.stringify({ private: true }))
// copy .npmrc if it exists to install from custom registry
if (GITHUB_WORKSPACE) {
try {
await copyFile(path.join(GITHUB_WORKSPACE, '.npmrc'), path.join(dest, '.npmrc'))
} catch (error) {
// Swallow error if .npmrc doesn't exist
if (!util.types.isNativeError(error) || !('code' in error) || error.code !== 'ENOENT') throw error
}
}
// prepare target pnpm
const target = await readTarget({ version, packageJsonFile, standalone })
const installArgs = [bootstrapPnpm, 'install', target, '--no-lockfile']
const exitCode = await runCommand(process.execPath, installArgs, { cwd: dest })
if (exitCode === 0) {
const pnpmHome = path.join(dest, 'node_modules/.bin')
const pnpmHome = path.join(dest, 'node_modules', '.bin')
addPath(pnpmHome)
exportVariable('PNPM_HOME', pnpmHome)
// Clean up bootstrap directory
await rm(bootstrapDir, { recursive: true, force: true }).catch(() => {})
}
const bootstrapPnpm = path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.cjs')
// Determine the target version
const targetVersion = readTargetVersion({ version, packageJsonFile })
if (targetVersion) {
// Explicit version specified (via action input or packageManager field)
const exitCode = await runCommand(process.execPath, [bootstrapPnpm, 'self-update', targetVersion], { cwd: dest })
if (exitCode !== 0) {
return exitCode
}
}
return 0
}
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,
stdio: ['pipe', 'inherit', 'inherit'],
shell: process.platform === 'win32',
})
cp.on('error', reject)
cp.on('close', resolve)
})
}
async function readTarget(opts: {
function readTargetVersion(opts: {
readonly version?: string | undefined
readonly packageJsonFile: string
readonly standalone: boolean
}) {
const { version, packageJsonFile, standalone } = opts
}): string | undefined {
const { version, packageJsonFile } = opts
const { GITHUB_WORKSPACE } = process.env
let packageManager
let packageManager: unknown
if (GITHUB_WORKSPACE) {
try {
@@ -107,7 +79,12 @@ async function readTarget(opts: {
Remove one of these versions to avoid version mismatch errors like ERR_PNPM_BAD_PM_VERSION`)
}
return `${ standalone ? '@pnpm/exe' : 'pnpm' }@${version}`
return version
}
if (typeof packageManager === 'string' && packageManager.startsWith('pnpm@')) {
// pnpm will handle version management via packageManager field
return undefined
}
if (!GITHUB_WORKSPACE) {
@@ -117,22 +94,22 @@ please run the actions/checkout before pnpm/action-setup.
Otherwise, please specify the pnpm version in the action configuration.`)
}
if (typeof packageManager !== 'string') {
throw new Error(`No pnpm version is specified.
Please specify it by one of the following ways:
- in the GitHub Action config with the key "version"
- in the package.json with the key "packageManager"`)
}
}
if (!packageManager.startsWith('pnpm@')) {
throw new Error('Invalid packageManager field in package.json')
}
if (standalone) {
return packageManager.replace('pnpm@', '@pnpm/exe@')
}
return packageManager
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,
stdio: ['pipe', 'inherit', 'inherit'],
shell: process.platform === 'win32',
})
cp.on('error', reject)
cp.on('close', resolve)
})
}
export default runSelfInstaller