diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 40a91d2..a5fee7f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,6 +35,122 @@ jobs: - name: 'Test: install' run: pnpm install + test_version_file_asdf: + name: Test with version file (asdf) + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + pnpm: + - 4.11.1 + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run the action + uses: ./ + with: + version_file_path: test/.tool-versions + + - name: 'Test: which' + run: which pnpm; which pnpx + + - name: 'Test: install' + run: pnpm install + + test_version_file_engines: + name: Test with version file (package.json engines) + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + pnpm: + - 4.11.1 + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run the action + uses: ./ + with: + version_file_path: test/package.engines.json + + - name: 'Test: which' + run: which pnpm; which pnpx + + - name: 'Test: install' + run: pnpm install + + test_version_file_volta: + name: Test with version file (volta) + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + pnpm: + - 4.11.1 + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run the action + uses: ./ + with: + version_file_path: test/package.volta.json + + - name: 'Test: which' + run: which pnpm; which pnpx + + - name: 'Test: install' + run: pnpm install + + test_version_file_volta_extends: + name: Test with version file (volta extends) + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + pnpm: + - 4.11.1 + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Run the action + uses: ./ + with: + version_file_path: test/package.volta-extends.json + + - name: 'Test: which' + run: which pnpm; which pnpx + + - name: 'Test: install' + run: pnpm install + test_dest: name: Test with dest diff --git a/README.md b/README.md index 6d00309..777fbaa 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ Version of pnpm to install. 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`. +### `version_file_path` + +The `version_file_path` input accepts a path to a file containing the version of pnpm to be used. For example `.tool-versions` (if you use `asdf`), or package.json (if you use the engines property or [`volta`](https://volta.sh) instead of corepack). + +The action will search for the version file relative to the repository root. + ### `dest` **Optional** Where to store pnpm files. @@ -83,6 +89,26 @@ jobs: version: 9 ``` +### Install only pnpm with a version file + +This works when you use `volta`, `asdf`, etc. + +```yaml +on: + - push + - pull_request + +jobs: + install: + runs-on: ubuntu-latest + + steps: + - uses: pnpm/action-setup@v4 + with: + version_file_path: ".tool-versions" # with asdf + # version_file_path: "package.json" # with volta +``` + ### 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). diff --git a/action.yml b/action.yml index 831ced0..52f990b 100644 --- a/action.yml +++ b/action.yml @@ -7,6 +7,9 @@ inputs: version: description: Version of pnpm to install required: false + version_file_path: + description: "Path to a version file. Eg: '.tool-versions' for asdf or 'package.json' for volta" + required: false dest: description: Where to store pnpm files required: false diff --git a/dist/index.js b/dist/index.js index d554a1e..9b84d1f 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/src/inputs/index.ts b/src/inputs/index.ts index a9b705a..97b537b 100644 --- a/src/inputs/index.ts +++ b/src/inputs/index.ts @@ -4,6 +4,7 @@ import { RunInstall, parseRunInstall } from './run-install' export interface Inputs { readonly version?: string + readonly versionFilePath?: string readonly dest: string readonly runInstall: RunInstall[] readonly packageJsonFile: string @@ -18,6 +19,7 @@ const parseInputPath = (name: string) => expandTilde(getInput(name, options)) export const getInputs = (): Inputs => ({ version: getInput('version'), + versionFilePath: getInput('version_file_path'), dest: parseInputPath('dest'), runInstall: parseRunInstall('run_install'), packageJsonFile: parseInputPath('package_json_file'), diff --git a/src/install-pnpm/run.ts b/src/install-pnpm/run.ts index e02fb98..d07c900 100644 --- a/src/install-pnpm/run.ts +++ b/src/install-pnpm/run.ts @@ -1,14 +1,14 @@ import { addPath, exportVariable } from '@actions/core' import { spawn } from 'child_process' import { rm, writeFile, mkdir } from 'fs/promises' -import { readFileSync } from 'fs' +import { readFileSync, existsSync } from 'fs' import path from 'path' import { execPath } from 'process' import util from 'util' import { Inputs } from '../inputs' export async function runSelfInstaller(inputs: Inputs): Promise { - const { version, dest, packageJsonFile, standalone } = inputs + const { version, versionFilePath, dest, packageJsonFile, standalone } = inputs // prepare self install await rm(dest, { recursive: true, force: true }) @@ -19,7 +19,7 @@ export async function runSelfInstaller(inputs: Inputs): Promise { await writeFile(pkgJson, JSON.stringify({ private: true })) // prepare target pnpm - const target = await readTarget({ version, packageJsonFile, standalone }) + const target = await readTarget({ version, versionFilePath, packageJsonFile, standalone }) const cp = spawn(execPath, [path.join(__dirname, 'pnpm.cjs'), 'install', target, '--no-lockfile'], { cwd: dest, stdio: ['pipe', 'inherit', 'inherit'], @@ -37,12 +37,71 @@ export async function runSelfInstaller(inputs: Inputs): Promise { return exitCode } +// Nearly identical to the function `actions/setup-node` uses. +// See https://github.com/actions/setup-node/blob/39370e3970a6d050c480ffad4ff0ed4d3fdee5af/src/util.ts#L8 +function getPnpmVersionFromFile(versionFilePath: string) { + if (!existsSync(versionFilePath)) { + throw new Error( + `The specified pnpm version file at: ${versionFilePath} does not exist` + ) + } + + const contents = readFileSync(versionFilePath, 'utf8') + + // Try parsing the file as a `package.json` file. + try { + const manifest = JSON.parse(contents) + + // Presume package.json file. + if (typeof manifest === 'object' && !!manifest) { + // Support Volta. + // See https://docs.volta.sh/guide/understanding#managing-your-project + if (manifest.volta?.pnpm) { + return manifest.volta.pnpm as string + } + + if (manifest.engines?.pnpm) { + return manifest.engines.pnpm as string + } + + // Support Volta workspaces. + // See https://docs.volta.sh/advanced/workspaces + if (manifest.volta?.extends) { + const extendedFilePath = path.resolve( + path.dirname(versionFilePath), + manifest.volta.extends + ) + console.info('Resolving pnpm version from ' + extendedFilePath) + return getPnpmVersionFromFile(extendedFilePath) + } + + // If contents are an object, we parsed JSON + // this can happen if pnpm-version-file is a package.json + // yet contains no volta.pnpm or engines.pnpm + // + // If pnpm-version file is _not_ JSON, control flow + // will not have reached these lines. + // + // And because we've reached here, we know the contents + // *are* JSON, so no further string parsing makes sense. + return + } + } catch { + console.info('pnpm version file is not JSON file') + } + + const found = contents.match(/^(?:pnpm\s+)?v?(?[^\s]+)$/m) + return found?.groups?.version ?? contents.trim() +} + async function readTarget(opts: { readonly version?: string | undefined + readonly versionFilePath?: string | undefined readonly packageJsonFile: string readonly standalone: boolean }) { - const { version, packageJsonFile, standalone } = opts + const { versionFilePath, packageJsonFile, standalone } = opts + let { version } = opts const { GITHUB_WORKSPACE } = process.env let packageManager @@ -56,6 +115,14 @@ async function readTarget(opts: { } } + if (typeof versionFilePath === "string" && typeof version === "string") { + throw new Error("Multiple version determination methods specified: 'version' and 'version_file_path'. Please specify only one.") + } + + if (versionFilePath) { + version = getPnpmVersionFromFile(versionFilePath) + } + if (version) { if ( typeof packageManager === 'string' && diff --git a/test/.tool-versions b/test/.tool-versions new file mode 100644 index 0000000..b5f0550 --- /dev/null +++ b/test/.tool-versions @@ -0,0 +1 @@ +pnpm 4.11.1 \ No newline at end of file diff --git a/test/package.engines.json b/test/package.engines.json new file mode 100644 index 0000000..ae2422c --- /dev/null +++ b/test/package.engines.json @@ -0,0 +1 @@ +{"engines":{"pnpm":"4.11.1"}} \ No newline at end of file diff --git a/test/package.volta-extends.json b/test/package.volta-extends.json new file mode 100644 index 0000000..f6aec47 --- /dev/null +++ b/test/package.volta-extends.json @@ -0,0 +1 @@ +{"volta":{"extends":"./package.volta.json"}} \ No newline at end of file diff --git a/test/package.volta.json b/test/package.volta.json new file mode 100644 index 0000000..4b1551e --- /dev/null +++ b/test/package.volta.json @@ -0,0 +1 @@ +{"volta":{"pnpm":"4.11.1"}} \ No newline at end of file