Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support volta, asdf, and package.json engines for declaring version #151

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +10 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is problematic. .tool-versions and package.json are completely different formats and should be treated as different features.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, the only reason I did it this way was to match up with how they do it for node versioning in the referenced lib. I figure if js devs are familiar with any GitHub actions at all, it’s likely that one.

I’ll rethink the inputs throwing out that goal and come back with something else.

Or not. Heh. If I have the time I will.

dest:
description: Where to store pnpm files
required: false
Expand Down
8 changes: 4 additions & 4 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/inputs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'),
Expand Down
75 changes: 71 additions & 4 deletions src/install-pnpm/run.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
const { version, dest, packageJsonFile, standalone } = inputs
const { version, versionFilePath, dest, packageJsonFile, standalone } = inputs

// prepare self install
await rm(dest, { recursive: true, force: true })
Expand All @@ -19,7 +19,7 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
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'],
Expand All @@ -37,12 +37,71 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior would be more predictable if you match the extension of versionFilePath against .json and .tool-versions instead of try. We can even be stricter: match the basename of versionFilePath against package.json and .tool-versions.


// 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
}
Comment on lines +59 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (manifest.volta?.pnpm) {
return manifest.volta.pnpm as string
}
if (manifest.engines?.pnpm) {
return manifest.engines.pnpm as string
}
if (typeof manifest.volta?.pnpm === 'string') {
return manifest.volta.pnpm
}
if (typeof manifest.engines?.pnpm === 'string') {
return manifest.engines.pnpm
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what if both manifest.engines.volta.pnpm and manifest.engines.pnpm are specified but different from each other? Shouldn't it throw an error?


// 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?(?<version>[^\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
happycollision marked this conversation as resolved.
Show resolved Hide resolved
const { GITHUB_WORKSPACE } = process.env

let packageManager
Expand All @@ -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' &&
Expand Down
1 change: 1 addition & 0 deletions test/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm 4.11.1
1 change: 1 addition & 0 deletions test/package.engines.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"engines":{"pnpm":"4.11.1"}}
1 change: 1 addition & 0 deletions test/package.volta-extends.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"volta":{"extends":"./package.volta.json"}}
1 change: 1 addition & 0 deletions test/package.volta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"volta":{"pnpm":"4.11.1"}}