diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2c214f3..1c163f7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 diff --git a/dist/index.js b/dist/index.js index 4a6f130..fa1f176 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/src/install-pnpm/run.ts b/src/install-pnpm/run.ts index f87bcb6..e6c4464 100644 --- a/src/install-pnpm/run.ts +++ b/src/install-pnpm/run.ts @@ -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 { - 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') + const pnpmHome = path.join(dest, 'node_modules', '.bin') + addPath(pnpmHome) + exportVariable('PNPM_HOME', pnpmHome) - // 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 })) + const bootstrapPnpm = path.join(dest, 'node_modules', 'pnpm', 'bin', 'pnpm.cjs') - // 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 + // 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 } } - // 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') - addPath(pnpmHome) - exportVariable('PNPM_HOME', pnpmHome) - - // Clean up bootstrap directory - await rm(bootstrapDir, { recursive: true, force: true }).catch(() => {}) - } - return exitCode + return 0 } -function runCommand(cmd: string, args: string[], opts: { cwd: string }): Promise { - return new Promise((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. + 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 { + return new Promise((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