Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
82 changes: 68 additions & 14 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@ on:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
smoke:
# Bootstrap install + pnpm on PATH across OSes.
# Direct binary download + pnpm on PATH across OSes and architectures.
name: 'Smoke (${{ matrix.os }} / pnpm ${{ matrix.version }})'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: '11.1.0'
version: '12.0.0-alpha.19'
- os: ubuntu-24.04-arm
version: '12.0.0-alpha.19'
- os: macos-latest
version: '11.1.0'
version: '12.0.0-alpha.19'
- os: windows-latest
version: '11.1.0'
version: '12.0.0-alpha.19'

steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -51,6 +56,55 @@ jobs:
fi
shell: bash

version-from-dist-tag:
# `version` may be an npm dist-tag; it is resolved against the main
# `pnpm` package's dist-tags.
name: 'Version from dist-tag (next-12)'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: ./
with:
version: next-12
install: false

- name: 'Test: dist-tag resolves to a 12.x version'
run: |
set -e
which pnpm
actual="$(pnpm --version)"
echo "pnpm --version: ${actual}"
case "${actual}" in
12.*) echo "ok" ;;
*) echo "Expected pnpm 12.x, got ${actual}"; exit 1 ;;
esac
shell: bash

version-range:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# `version` may be a semver range. While v12 only has prereleases the
# range resolution falls back to prerelease versions.
name: 'Version from semver range (^12.0.0-0)'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: ./
with:
version: '^12.0.0-0'
install: false

- name: 'Test: range resolves to a 12.x version'
run: |
set -e
actual="$(pnpm --version)"
echo "pnpm --version: ${actual}"
case "${actual}" in
12.*) echo "ok" ;;
*) echo "Expected pnpm 12.x, got ${actual}"; exit 1 ;;
esac
shell: bash

runtime-node:
# Explicit runtime input across OSes. Asserts node binary is on PATH and
# the action's outputs reflect what was installed.
Expand All @@ -73,7 +127,7 @@ jobs:
- id: pnpm
uses: ./
with:
version: '11.1.0'
version: '12.0.0-alpha.19'
runtime: node@${{ matrix.major }}

- name: 'Test: node binary on PATH'
Expand Down Expand Up @@ -114,7 +168,7 @@ jobs:
- uses: actions/checkout@v6
- uses: ./
with:
version: '11.1.0'
version: '12.0.0-alpha.19'
runtime: bun@latest
- name: 'Test: bun on PATH'
run: |
Expand All @@ -130,7 +184,7 @@ jobs:
- uses: actions/checkout@v6
- uses: ./
with:
version: '11.1.0'
version: '12.0.0-alpha.19'
runtime: deno@2
- name: 'Test: deno on PATH'
run: |
Expand All @@ -155,7 +209,7 @@ jobs:
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@11.1.0",
"packageManager": "pnpm@12.0.0-alpha.19",
"devEngines": {
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
},
Expand Down Expand Up @@ -201,7 +255,7 @@ jobs:
# Explicit `runtime` input conflicts with `devEngines.runtime` in
# package.json. The action should install the explicit version AND pass
# `--no-runtime` to pnpm install so the devEngines runtime doesn't shadow
# the matrix one. Requires pnpm >= 11.1.0.
# the matrix one.
name: 'Explicit runtime overrides devEngines.runtime'
runs-on: ubuntu-latest
steps:
Expand All @@ -212,7 +266,7 @@ jobs:
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@11.1.0",
"packageManager": "pnpm@12.0.0-alpha.19",
"devEngines": {
"runtime": { "name": "node", "version": "^20.0.0", "onFail": "download" }
},
Expand Down Expand Up @@ -257,7 +311,7 @@ jobs:
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@11.1.0",
"packageManager": "pnpm@12.0.0-alpha.19",
"devEngines": {
"runtime": { "name": "node", "version": "^20.0.0", "onFail": "download" }
}
Expand Down Expand Up @@ -294,7 +348,7 @@ jobs:
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@11.1.0",
"packageManager": "pnpm@12.0.0-alpha.19",
"dependencies": {
"is-odd": "3.0.1"
}
Expand All @@ -304,7 +358,7 @@ jobs:

- uses: ./
with:
version: '11.1.0'
version: '12.0.0-alpha.19'
install: false

- name: 'Test: node_modules was not populated'
Expand All @@ -331,7 +385,7 @@ jobs:
- id: pnpm
uses: ./
with:
version: '11.1.0'
version: '12.0.0-alpha.19'

- name: 'Test: pnpm works, runtime outputs are empty'
env:
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

Install pnpm **and** a JavaScript runtime (Node.js, Bun, or Deno) in a single GitHub Actions step.

The action installs pnpm from the standalone `@pnpm/exe` package (no system Node.js required) and then uses `pnpm runtime set` to install the requested runtime. The runtime binary is placed on `PATH` for subsequent steps, replacing the need for `actions/setup-node`, `oven-sh/setup-bun`, or `denoland/setup-deno`. `pnpm install` runs automatically when a `package.json` is present.
Since v12, pnpm is a standalone executable — the action downloads the native binary for the runner's platform directly from the npm registry (no Node.js or npm needed) and then uses `pnpm runtime set` to install the requested runtime. The runtime binary is placed on `PATH` for subsequent steps, replacing the need for `actions/setup-node`, `oven-sh/setup-bun`, or `denoland/setup-deno`. `pnpm install` runs automatically when a `package.json` is present.

> [!NOTE]
> This action installs pnpm v12 and newer only. To install pnpm 11 or older (which are Node.js programs, not standalone executables), use [`pnpm/action-setup`](https://github.com/pnpm/action-setup) instead.

If your `package.json` declares `devEngines.runtime`, the action picks up the runtime and version from there automatically — no inputs required.

## Inputs

| Name | Description |
|------|-------------|
| `version` | Version of pnpm to install. Optional when `packageManager` is set in `package.json`. |
| `version` | Version of pnpm to install: an exact version, a semver range (`^12.0.0`), or a dist-tag (`next-12`). Must resolve to v12 or newer. Optional when `packageManager` or `devEngines.packageManager` is set in `package.json`. |
| `dest` | Where to store pnpm files. Defaults to `~/setup-pnpm`. |
| `runtime` | Runtime spec, in `<name>` or `<name>@<version>` form (e.g. `node@22`, `node@lts`, `bun@latest`, `deno@2`). Supported names: `node`, `bun`, `deno`. When the version is omitted, falls back to `devEngines.runtime` in `package.json`, then to `lts` (for `node`) / `latest`. If the input itself is omitted, the action reads `devEngines.runtime` from `package.json`. |
| `cache` | Cache the pnpm store directory. Default: `false`. |
Expand All @@ -34,7 +37,7 @@ If your `package.json` declares `devEngines.runtime`, the action picks up the ru
```json
// package.json
{
"packageManager": "pnpm@11.0.4",
"packageManager": "pnpm@12.0.0-alpha.19",
"devEngines": {
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
}
Expand Down Expand Up @@ -104,10 +107,10 @@ For jobs that only need pnpm itself — e.g. `pnpm audit`, lockfile-only regener

## How it works

1. The action installs `@pnpm/exe` (a Node.js-bundled standalone build of pnpm) into `dest`, then self-updates to the requested pnpm version.
2. `PNPM_HOME` is exported and `$PNPM_HOME/bin` is added to `PATH`.
1. The action resolves the requested version (exact, range, or dist-tag) against the npm registry, downloads the `@pnpm/exe.<os>-<arch>` tarball for the runner's platform, verifies its integrity, and places the `pnpm` executable (plus the `pnpx`, `pn`, and `pnx` aliases) into `dest`. No Node.js or npm is involved.
2. `PNPM_HOME` is exported and `dest` plus `$PNPM_HOME/bin` are added to `PATH`.
3. The action runs `pnpm runtime set <name> <version> -g`, which downloads the requested runtime into `$PNPM_HOME/bin` — making `node`, `bun`, or `deno` available to later workflow steps.
4. If a `package.json` exists in the workspace, the action runs `pnpm install` (unless `install: false` is set). When the `runtime` input is set, `--no-runtime` is appended so the just-installed runtime isn't shadowed by a different version declared in `devEngines.runtime`. This flag requires pnpm ≥ 11.1.0.
4. If a `package.json` exists in the workspace, the action runs `pnpm install` (unless `install: false` is set). When the `runtime` input is set, `--no-runtime` is appended so the just-installed runtime isn't shadowed by a different version declared in `devEngines.runtime`.

## License

Expand Down
9 changes: 8 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ branding:
color: orange
inputs:
version:
description: Version of pnpm to install
description: |
Version of pnpm to install: an exact version (`12.0.0-alpha.17`), a
semver range (`^12.0.0`), or an npm dist-tag (`next-12`). Must resolve
to pnpm v12 or newer — since v12, pnpm is a standalone executable, and
the action downloads it directly without needing Node.js or npm.

When omitted, the version is read from `devEngines.packageManager` or
`packageManager` in the project's package.json.
required: false
dest:
description: Where to store pnpm files
Expand Down
356 changes: 180 additions & 176 deletions dist/index.js

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
{
"private": true,
"scripts": {
"build:bundle": "esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --minify --outfile=dist/index.js --loader:.json=json",
"build:bundle": "esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --minify --outfile=dist/index.js",
"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",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/glob": "^0.5.0",
"@actions/http-client": "^2.2.3",
"@types/expand-tilde": "^2.0.2",
"@types/node": "^22.0.0",
"@types/semver": "^7.5.8",
"expand-tilde": "^2.0.2",
"yaml": "^2.3.4",
"zod": "^3.22.4"
"semver": "^7.6.3",
"yaml": "^2.3.4"
},
"devDependencies": {
"esbuild": "^0.27.4",
Expand Down
30 changes: 22 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 12 additions & 7 deletions run.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
#! /bin/sh
# shellcheck disable=SC2155,SC2088
# Input env var names mirror how the Actions runner encodes them: INPUT_ +
# uppercased input name, hyphens preserved. Hyphenated names are not valid
# shell identifiers, so they are passed via `env`.
export HOME="$(pwd)"
export INPUT_VERSION=latest-11
export INPUT_DEST='~/pnpm.temp'
export INPUT_RUNTIME='node@lts'
export INPUT_CACHE=false
export INPUT_CACHE_DEPENDENCY_PATH=pnpm-lock.yaml
export INPUT_PACKAGE_JSON_FILE=package.json
exec node dist/index.js
exec env \
INPUT_VERSION=next-12 \
INPUT_DEST='~/pnpm.temp' \
INPUT_RUNTIME='node@lts' \
INPUT_CACHE=false \
'INPUT_CACHE-DEPENDENCY-PATH=pnpm-lock.yaml' \
'INPUT_PACKAGE-JSON-FILE=package.json' \
INPUT_INSTALL=true \
node dist/index.js
Loading
Loading