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
67 changes: 51 additions & 16 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,10 @@ jobs:
if [ "${OUT_NAME}" != "node" ]; then
echo "Expected outputs.runtime-name=node, got ${OUT_NAME}"; exit 1
fi
if [ "${OUT_VERSION}" != "22" ]; then
echo "Expected outputs.runtime-version=22, got ${OUT_VERSION}"; exit 1
fi
case "${OUT_VERSION}" in
22.*) ;;
*) echo "Expected outputs.runtime-version=22.x, got ${OUT_VERSION}"; exit 1 ;;
esac
if [ ! -d node_modules/is-odd ]; then
echo "Expected pnpm install to populate node_modules/is-odd"; exit 1
fi
Expand Down Expand Up @@ -333,10 +334,10 @@ jobs:
echo "Expected outputs.runtime-name=node, got ${OUT_NAME}"
exit 1
fi
if [ "${OUT_VERSION}" != "${EXPECTED_VERSION}" ]; then
echo "Expected outputs.runtime-version=${EXPECTED_VERSION}, got ${OUT_VERSION}"
exit 1
fi
case "${OUT_VERSION}" in
"${EXPECTED_VERSION}".*) ;;
*) echo "Expected outputs.runtime-version=${EXPECTED_VERSION}.x, got ${OUT_VERSION}"; exit 1 ;;
esac
shell: bash

runtime-bun:
Expand Down Expand Up @@ -373,7 +374,7 @@ jobs:

runtime-from-devengines:
# No `runtime` input — the action should pick up devEngines.runtime from
# package.json and install it. Also asserts that `pnpm install` runs by
# package.json and install every entry. Also asserts that `pnpm install` runs by
# default when a manifest is present.
Comment thread
BlankParticle marked this conversation as resolved.
name: 'Runtime from devEngines.runtime'
runs-on: ubuntu-latest
Expand All @@ -389,7 +390,11 @@ jobs:
{
"packageManager": "pnpm@12.0.0-beta.4",
"devEngines": {
"runtime": { "name": "node", "version": "^22.0.0", "onFail": "download" }
"runtime": [
{ "name": "bun", "version": "1.3.12", "onFail": "warn" },
{ "name": "node", "version": "24", "onFail": "warn" },
{ "name": "bun", "version": "1.3.13", "onFail": "warn" }
]
},
"dependencies": {
"is-odd": "3.0.1"
Expand All @@ -401,27 +406,52 @@ jobs:
- id: pnpm
uses: ./

- name: 'Test: node 22 is installed and dependencies are resolved'
- name: 'Test: Bun and Node are installed and dependencies are resolved'
env:
OUT_NAME: ${{ steps.pnpm.outputs.runtime-name }}
OUT_VERSION: ${{ steps.pnpm.outputs.runtime-version }}
OUT_RUNTIMES: ${{ steps.pnpm.outputs.runtimes }}
run: |
set -e
which bun
bun_actual="$(bun --version)"
echo "bun --version: ${bun_actual}"
if [ "${bun_actual}" != "1.3.13" ]; then
echo "Expected bun 1.3.13, got ${bun_actual}"
exit 1
fi
which node
actual="$(node --version)"
echo "node --version: ${actual}"
case "${actual}" in
v22.*) ;;
*) echo "Expected node v22.x, got ${actual}"; exit 1 ;;
v24.*) ;;
*) echo "Expected node v24.x, got ${actual}"; exit 1 ;;
esac
if [ "${OUT_NAME}" != "node" ]; then
echo "Expected outputs.runtime-name=node, got ${OUT_NAME}"
if [ "${OUT_NAME}" != "bun" ]; then
echo "Expected outputs.runtime-name=bun, got ${OUT_NAME}"
exit 1
fi
if [ "${OUT_VERSION}" != "^22.0.0" ]; then
echo "Expected outputs.runtime-version=^22.0.0, got ${OUT_VERSION}"
if [ "${OUT_VERSION}" != "1.3.13" ]; then
echo "Expected outputs.runtime-version=1.3.13, got ${OUT_VERSION}"
exit 1
fi
# Declaration order is part of the contract, and each version must be
# the one that landed rather than the selector that was asked for.
first="$(printf '%s' "${OUT_RUNTIMES}" | jq -r '.[0].name + "@" + .[0].version')"
second="$(printf '%s' "${OUT_RUNTIMES}" | jq -r '.[1].name + "@" + .[1].version')"
count="$(printf '%s' "${OUT_RUNTIMES}" | jq -r 'length')"
if [ "${count}" != "2" ]; then
echo "Expected 2 entries in outputs.runtimes, got ${count}: ${OUT_RUNTIMES}"
exit 1
fi
if [ "${first}" != "bun@1.3.13" ]; then
echo "Expected outputs.runtimes[0]=bun@1.3.13, got ${first}"
exit 1
fi
case "${second}" in
node@24.*) ;;
*) echo "Expected outputs.runtimes[1]=node@24.x, got ${second}"; exit 1 ;;
esac
# `pnpm install` should have run automatically — node_modules must exist.
if [ ! -d node_modules/is-odd ]; then
echo "Expected pnpm install to have populated node_modules/is-odd"
Expand Down Expand Up @@ -666,6 +696,7 @@ jobs:
env:
OUT_NAME: ${{ steps.pnpm.outputs.runtime-name }}
OUT_VERSION: ${{ steps.pnpm.outputs.runtime-version }}
OUT_RUNTIMES: ${{ steps.pnpm.outputs.runtimes }}
run: |
set -e
which pnpm
Expand All @@ -678,4 +709,8 @@ jobs:
echo "Expected outputs.runtime-version to be empty, got '${OUT_VERSION}'"
exit 1
fi
if [ "${OUT_RUNTIMES}" != "[]" ]; then
echo "Expected outputs.runtimes=[], got '${OUT_RUNTIMES}'"
exit 1
fi
shell: bash
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ pnpm ships a self-contained release binary — the action downloads it for the r
>
> One caveat: pnpm v11 publishes no binary for Intel macOS (`darwin-x64`); use v12 or newer on Intel macOS runners.

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

Only one version of each runtime can be installed globally. If a runtime name is declared more than once, the action emits a GitHub warning annotation and installs the last declared version while retaining the position of its first declaration.

## Inputs

| Name | Description |
|------|-------------|
| `version` | Version of pnpm to install: an exact version, a semver range (`^12.0.0`), or a dist-tag (`next-12`). Must resolve to v11 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 and restore it before installing the runtime. Default: `false`. |
| `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 installs every entry in `devEngines.runtime` from `package.json`. |
| `cache` | Cache the pnpm store directory and restore it before installing the runtimes. Default: `false`. |
| `cache-dependency-path` | Path(s) to the pnpm lockfile, used to compute the cache key. Default: `pnpm-lock.yaml`. |
| `package-json-file` | Path to `package.json` (relative to `GITHUB_WORKSPACE`). Default: `package.json`. |
| `install` | Run `pnpm install` after setup. Default: `true`. Set to `false` for jobs that only need pnpm itself (e.g. `pnpm audit`, lockfile-only regeneration). |
Expand All @@ -30,8 +32,9 @@ If your `package.json` declares `devEngines.runtime`, the action picks up the ru
|------|-------------|
| `dest` | Expanded path of `dest`. |
| `bin-dest` | Directory containing the `pnpm` / `pnpx` binaries. |
| `runtime-name` | Name of the installed runtime, or empty string if none was installed. |
| `runtime-version` | Resolved version of the installed runtime, or empty string if none was installed. |
| `runtime-name` | Name of the first installed runtime, or empty string if none was installed. |
| `runtime-version` | Resolved version of the first installed runtime, or empty string if none was installed. |
| `runtimes` | JSON array of every installed runtime in declaration order, as `{ "name": string, "version": string }` objects. Returns `[]` when none were installed. |
| `cache-hit` | Whether the pnpm store cache matched the exact primary key. |

## Usage
Expand Down Expand Up @@ -98,9 +101,11 @@ jobs:
cache: true
```

The cache is restored before the runtime is installed, so a cached runtime does
not need to be downloaded again. Cache keys include both the requested runtime
selector and the actual installed version.
The cache is restored before the runtimes are installed, so a cached runtime
does not need to be downloaded again. Cache keys include both the requested
runtime selectors and the versions actually installed. Reordering
`devEngines.runtime` does not change the key — the same set of runtimes
produces the same store.

### Skip `pnpm install`

Expand All @@ -117,8 +122,8 @@ For jobs that only need pnpm itself — e.g. `pnpm audit`, lockfile-only regener

1. The action resolves the requested version (exact, range, or dist-tag) against the npm registry, then downloads the matching self-contained release archive for the runner's platform (`pnpm-<os>-<arch>.tar.gz`, or `pnpm-win32-<arch>.zip` on Windows) from pnpm's GitHub releases. It verifies the archive against the SHA-256 digest GitHub publishes for the asset, extracts the `pnpm` executable (and, for pnpm builds that need it, its bundled `dist/`), and links 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. It then exports `PNPM_CONFIG_GLOBAL_SHIMS={"<name>":false}` so that runtime stays the one later steps get; see [Context-aware global shims](#context-aware-global-shims).
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`.
3. The action runs `pnpm runtime set <name> <version> -g` for every requested runtime, which downloads them into `$PNPM_HOME/bin` and makes them available to later workflow steps. It then disables context-aware shims for every installed runtime; see [Context-aware global shims](#context-aware-global-shims).
4. If a `package.json` exists in the workspace, the action runs `pnpm install` (unless `install: false` is set). When runtimes were installed, `--no-runtime` is appended because the action has already processed `devEngines.runtime`.

### Context-aware global shims

Expand Down
21 changes: 11 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ inputs:
declared in `devEngines.runtime` for the same name, or to `lts` (for
`node`) / `latest` for others.

If this input is omitted entirely, the action reads `devEngines.runtime`
from the project's package.json. If that is also missing, no runtime is
installed.
If this input is omitted entirely, the action installs every runtime in
`devEngines.runtime` from the project's package.json. In case of duplicates,
the last declared version wins. If the field is missing, no runtime is installed.

Whenever a runtime is installed, the action exports
`PNPM_CONFIG_GLOBAL_SHIMS` with that runtime's context-aware shim
disabled, so later steps run exactly this version instead of one a
project pins. Set the variable in the workflow to override.
required: false
cache:
description: Whether to cache the pnpm store directory and restore it before installing the runtime
description: Whether to cache the pnpm store directory and restore it before installing the runtimes
required: false
default: 'false'
cache-dependency-path:
Expand All @@ -55,10 +55,9 @@ inputs:
Whether to run `pnpm install` after pnpm and the runtime are set up.

When set to `true` (the default), the action runs `pnpm install` in the
workspace when a package.json is present. When the `runtime` input is
also set, `--no-runtime` is appended automatically so the installed
runtime isn't shadowed by a different version declared in
`devEngines.runtime`.
workspace when a package.json is present. When runtimes are installed,
`--no-runtime` is appended automatically because the action has already
processed `devEngines.runtime`.

Set to `false` to skip the install step — useful for jobs that only
need pnpm itself (e.g. `pnpm audit`, lockfile-only regeneration).
Expand All @@ -78,9 +77,11 @@ outputs:
bin-dest:
description: Location of `pnpm` and `pnpx` command
runtime-name:
description: Name of the installed runtime (`node`, `bun`, or `deno`), or empty if none was installed
description: Name of the first installed runtime (`node`, `bun`, or `deno`), or empty if none was installed
runtime-version:
description: Resolved version of the installed runtime, or empty if none was installed
description: Resolved version of the first installed runtime, or empty if none was installed
runtimes:
description: JSON array of installed runtimes, each containing `name` and `version`
cache-hit:
description: Whether the pnpm store cache matched the exact primary key
runs:
Expand Down
Loading
Loading