Skip to content

Commit 6bfbb82

Browse files
authored
feat!: install pnpm v12 as a standalone native executable (#11)
* feat!: install pnpm v12 as a standalone native executable Since v12, pnpm is a Rust rewrite shipped as a native executable that needs neither Node.js nor npm. The action now resolves the requested version (exact, semver range, or dist-tag) against the npm registry, downloads the @pnpm/exe.<os>-<arch> tarball for the runner's platform, verifies its sha512 integrity, and places the pnpm binary (plus the pnpx, pn, and pnx aliases) directly into dest. This removes the whole npm-based bootstrap: the committed exe-lock.json, the update-bootstrap script, the npm ci step, the node-on-PATH hack, the Windows .bin shim workarounds, and the pnpm self-update step. Versions below 12 are rejected with a pointer to pnpm/action-setup. Adjustments for the Rust CLI: - 'pnpm store path' no longer accepts --silent - $PNPM_HOME/bin is created upfront: 'pnpm runtime set -g' fails with ERR_PNPM_PNPM_DIR_NOT_WRITABLE when it is missing - the --no-runtime version probe is gone (always supported in v12) * chore: update pnpm to v12.0.0-alpha.18 The new release includes the pacquet parity fixes from pnpm/pnpm#13194: runtime set -g now creates a missing global bin dir, and the --silent shorthand is accepted again. The upfront dest/bin creation stays for users pinning alpha.17 or older. * chore: retrigger CI * fix: address code scanning and review findings - add a least-privilege permissions block to the test workflow (CodeQL: workflow does not limit GITHUB_TOKEN permissions) - use replaceAll when encoding the scoped package name for the registry URL (CodeQL: replace only rewrites the first occurrence) - extract only the pnpm binary entry from the downloaded tarball instead of the whole archive, and fail with an actionable message when no tar executable is on PATH (self-hosted minimal runners) * chore: update pnpm to v12.0.0-alpha.19 Includes the fresh-install --no-runtime fix (pnpm/pnpm#13225), which unblocks the devEngines test jobs that install without a lockfile.
1 parent 77cf068 commit 6bfbb82

16 files changed

Lines changed: 549 additions & 734 deletions

File tree

‎.github/workflows/test.yaml‎

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,26 @@ on:
77
- main
88
workflow_dispatch:
99

10+
permissions:
11+
contents: read
12+
1013
jobs:
1114
smoke:
12-
# Bootstrap install + pnpm on PATH across OSes.
15+
# Direct binary download + pnpm on PATH across OSes and architectures.
1316
name: 'Smoke (${{ matrix.os }} / pnpm ${{ matrix.version }})'
1417
runs-on: ${{ matrix.os }}
1518
strategy:
1619
fail-fast: false
1720
matrix:
1821
include:
1922
- os: ubuntu-latest
20-
version: '11.1.0'
23+
version: '12.0.0-alpha.19'
24+
- os: ubuntu-24.04-arm
25+
version: '12.0.0-alpha.19'
2126
- os: macos-latest
22-
version: '11.1.0'
27+
version: '12.0.0-alpha.19'
2328
- os: windows-latest
24-
version: '11.1.0'
29+
version: '12.0.0-alpha.19'
2530

2631
steps:
2732
- uses: actions/checkout@v6
@@ -51,6 +56,55 @@ jobs:
5156
fi
5257
shell: bash
5358

59+
version-from-dist-tag:
60+
# `version` may be an npm dist-tag; it is resolved against the main
61+
# `pnpm` package's dist-tags.
62+
name: 'Version from dist-tag (next-12)'
63+
runs-on: ubuntu-latest
64+
steps:
65+
- uses: actions/checkout@v6
66+
67+
- uses: ./
68+
with:
69+
version: next-12
70+
install: false
71+
72+
- name: 'Test: dist-tag resolves to a 12.x version'
73+
run: |
74+
set -e
75+
which pnpm
76+
actual="$(pnpm --version)"
77+
echo "pnpm --version: ${actual}"
78+
case "${actual}" in
79+
12.*) echo "ok" ;;
80+
*) echo "Expected pnpm 12.x, got ${actual}"; exit 1 ;;
81+
esac
82+
shell: bash
83+
84+
version-range:
85+
# `version` may be a semver range. While v12 only has prereleases the
86+
# range resolution falls back to prerelease versions.
87+
name: 'Version from semver range (^12.0.0-0)'
88+
runs-on: ubuntu-latest
89+
steps:
90+
- uses: actions/checkout@v6
91+
92+
- uses: ./
93+
with:
94+
version: '^12.0.0-0'
95+
install: false
96+
97+
- name: 'Test: range resolves to a 12.x version'
98+
run: |
99+
set -e
100+
actual="$(pnpm --version)"
101+
echo "pnpm --version: ${actual}"
102+
case "${actual}" in
103+
12.*) echo "ok" ;;
104+
*) echo "Expected pnpm 12.x, got ${actual}"; exit 1 ;;
105+
esac
106+
shell: bash
107+
54108
runtime-node:
55109
# Explicit runtime input across OSes. Asserts node binary is on PATH and
56110
# the action's outputs reflect what was installed.
@@ -73,7 +127,7 @@ jobs:
73127
- id: pnpm
74128
uses: ./
75129
with:
76-
version: '11.1.0'
130+
version: '12.0.0-alpha.19'
77131
runtime: node@${{ matrix.major }}
78132

79133
- name: 'Test: node binary on PATH'
@@ -114,7 +168,7 @@ jobs:
114168
- uses: actions/checkout@v6
115169
- uses: ./
116170
with:
117-
version: '11.1.0'
171+
version: '12.0.0-alpha.19'
118172
runtime: bun@latest
119173
- name: 'Test: bun on PATH'
120174
run: |
@@ -130,7 +184,7 @@ jobs:
130184
- uses: actions/checkout@v6
131185
- uses: ./
132186
with:
133-
version: '11.1.0'
187+
version: '12.0.0-alpha.19'
134188
runtime: deno@2
135189
- name: 'Test: deno on PATH'
136190
run: |
@@ -155,7 +209,7 @@ jobs:
155209
rm -f pnpm-lock.yaml
156210
cat > package.json <<'EOF'
157211
{
158-
"packageManager": "pnpm@11.1.0",
212+
"packageManager": "pnpm@12.0.0-alpha.19",
159213
"devEngines": {
160214
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
161215
},
@@ -201,7 +255,7 @@ jobs:
201255
# Explicit `runtime` input conflicts with `devEngines.runtime` in
202256
# package.json. The action should install the explicit version AND pass
203257
# `--no-runtime` to pnpm install so the devEngines runtime doesn't shadow
204-
# the matrix one. Requires pnpm >= 11.1.0.
258+
# the matrix one.
205259
name: 'Explicit runtime overrides devEngines.runtime'
206260
runs-on: ubuntu-latest
207261
steps:
@@ -212,7 +266,7 @@ jobs:
212266
rm -f pnpm-lock.yaml
213267
cat > package.json <<'EOF'
214268
{
215-
"packageManager": "pnpm@11.1.0",
269+
"packageManager": "pnpm@12.0.0-alpha.19",
216270
"devEngines": {
217271
"runtime": { "name": "node", "version": "^20.0.0", "onFail": "download" }
218272
},
@@ -257,7 +311,7 @@ jobs:
257311
rm -f pnpm-lock.yaml
258312
cat > package.json <<'EOF'
259313
{
260-
"packageManager": "pnpm@11.1.0",
314+
"packageManager": "pnpm@12.0.0-alpha.19",
261315
"devEngines": {
262316
"runtime": { "name": "node", "version": "^20.0.0", "onFail": "download" }
263317
}
@@ -294,7 +348,7 @@ jobs:
294348
rm -f pnpm-lock.yaml
295349
cat > package.json <<'EOF'
296350
{
297-
"packageManager": "pnpm@11.1.0",
351+
"packageManager": "pnpm@12.0.0-alpha.19",
298352
"dependencies": {
299353
"is-odd": "3.0.1"
300354
}
@@ -304,7 +358,7 @@ jobs:
304358

305359
- uses: ./
306360
with:
307-
version: '11.1.0'
361+
version: '12.0.0-alpha.19'
308362
install: false
309363

310364
- name: 'Test: node_modules was not populated'
@@ -331,7 +385,7 @@ jobs:
331385
- id: pnpm
332386
uses: ./
333387
with:
334-
version: '11.1.0'
388+
version: '12.0.0-alpha.19'
335389

336390
- name: 'Test: pnpm works, runtime outputs are empty'
337391
env:

‎README.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

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

5-
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.
5+
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.
6+
7+
> [!NOTE]
8+
> 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.
69
710
If your `package.json` declares `devEngines.runtime`, the action picks up the runtime and version from there automatically — no inputs required.
811

912
## Inputs
1013

1114
| Name | Description |
1215
|------|-------------|
13-
| `version` | Version of pnpm to install. Optional when `packageManager` is set in `package.json`. |
16+
| `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`. |
1417
| `dest` | Where to store pnpm files. Defaults to `~/setup-pnpm`. |
1518
| `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`. |
1619
| `cache` | Cache the pnpm store directory. Default: `false`. |
@@ -34,7 +37,7 @@ If your `package.json` declares `devEngines.runtime`, the action picks up the ru
3437
```json
3538
// package.json
3639
{
37-
"packageManager": "pnpm@11.0.4",
40+
"packageManager": "pnpm@12.0.0-alpha.19",
3841
"devEngines": {
3942
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
4043
}
@@ -104,10 +107,10 @@ For jobs that only need pnpm itself — e.g. `pnpm audit`, lockfile-only regener
104107

105108
## How it works
106109

107-
1. The action installs `@pnpm/exe` (a Node.js-bundled standalone build of pnpm) into `dest`, then self-updates to the requested pnpm version.
108-
2. `PNPM_HOME` is exported and `$PNPM_HOME/bin` is added to `PATH`.
110+
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.
111+
2. `PNPM_HOME` is exported and `dest` plus `$PNPM_HOME/bin` are added to `PATH`.
109112
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.
110-
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.
113+
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`.
111114

112115
## License
113116

‎action.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ branding:
55
color: orange
66
inputs:
77
version:
8-
description: Version of pnpm to install
8+
description: |
9+
Version of pnpm to install: an exact version (`12.0.0-alpha.17`), a
10+
semver range (`^12.0.0`), or an npm dist-tag (`next-12`). Must resolve
11+
to pnpm v12 or newer — since v12, pnpm is a standalone executable, and
12+
the action downloads it directly without needing Node.js or npm.
13+
14+
When omitted, the version is read from `devEngines.packageManager` or
15+
`packageManager` in the project's package.json.
916
required: false
1017
dest:
1118
description: Where to store pnpm files

‎dist/index.js‎

Lines changed: 180 additions & 176 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
{
22
"private": true,
33
"scripts": {
4-
"build:bundle": "esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --minify --outfile=dist/index.js --loader:.json=json",
4+
"build:bundle": "esbuild src/index.ts --bundle --platform=node --target=node24 --format=cjs --minify --outfile=dist/index.js",
55
"build": "pnpm run build:bundle",
6-
"start": "pnpm run build && sh ./run.sh",
7-
"update-bootstrap": "node scripts/update-bootstrap.mjs"
6+
"start": "pnpm run build && sh ./run.sh"
87
},
98
"dependencies": {
109
"@actions/cache": "^4.1.0",
1110
"@actions/core": "^1.10.1",
1211
"@actions/exec": "^1.1.1",
1312
"@actions/glob": "^0.5.0",
13+
"@actions/http-client": "^2.2.3",
1414
"@types/expand-tilde": "^2.0.2",
1515
"@types/node": "^22.0.0",
16+
"@types/semver": "^7.5.8",
1617
"expand-tilde": "^2.0.2",
17-
"yaml": "^2.3.4",
18-
"zod": "^3.22.4"
18+
"semver": "^7.6.3",
19+
"yaml": "^2.3.4"
1920
},
2021
"devDependencies": {
2122
"esbuild": "^0.27.4",

‎pnpm-lock.yaml‎

Lines changed: 22 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎run.sh‎

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

0 commit comments

Comments
 (0)