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
97 changes: 97 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,103 @@ jobs:
fi
shell: bash

runtime-survives-context-shims:
# pnpm 12 links the global `node` bin as a context-aware shim that would
# switch to the version the project pins in devEngines.runtime. The
# exported PNPM_CONFIG_GLOBAL_SHIMS must keep the installed runtime
# authoritative for every later step.
name: 'Installed runtime beats context-aware shims'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set up package.json with conflicting devEngines.runtime
run: |
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@12.0.0-rc.2",
"devEngines": {
"runtime": { "name": "node", "version": "20.19.0", "onFail": "download" }
}
}
EOF
shell: bash

- uses: ./
with:
runtime: node@22

- name: 'Test: node 22 runs inside the project'
run: |
set -e
actual="$(node --version)"
echo "node --version: ${actual}"
case "${actual}" in
v22.*) ;;
*) echo "Expected node v22.x, got ${actual} — the context-aware shim switched to the project's pin"; exit 1 ;;
esac
echo "PNPM_CONFIG_GLOBAL_SHIMS: ${PNPM_CONFIG_GLOBAL_SHIMS}"
if [ "${PNPM_CONFIG_GLOBAL_SHIMS}" != '{"node":false}' ]; then
echo 'Expected PNPM_CONFIG_GLOBAL_SHIMS={"node":false}'
exit 1
fi
shell: bash

context-shims-opt-out:
# The counterpart to the job above, and its canary: a workflow that sets
# the setting itself keeps pnpm's switching behaviour, so the project's
# pin wins. If this job stops switching, the job above is no longer
# proving anything. Both spellings pnpm accepts are covered, since
# honouring only one of them would silently override the other.
name: 'Workflow-set ${{ matrix.env_name }} is not overwritten'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env_name: [PNPM_CONFIG_GLOBAL_SHIMS, pnpm_config_global_shims]
steps:
- uses: actions/checkout@v7

- name: Set up package.json with conflicting devEngines.runtime
run: |
rm -f pnpm-lock.yaml
cat > package.json <<'EOF'
{
"packageManager": "pnpm@12.0.0-rc.2",
"devEngines": {
"runtime": { "name": "node", "version": "20.19.0", "onFail": "download" }
}
}
EOF
shell: bash

- name: Opt back into context-aware shims
env:
ENV_NAME: ${{ matrix.env_name }}
run: printf '%s={"node":"auto"}\n' "$ENV_NAME" >> "$GITHUB_ENV"
shell: bash

- uses: ./
with:
runtime: node@22

- name: "Test: the project's pin still wins"
run: |
set -e
echo "PNPM_CONFIG_GLOBAL_SHIMS: ${PNPM_CONFIG_GLOBAL_SHIMS}"
if [ "${PNPM_CONFIG_GLOBAL_SHIMS}" = '{"node":false}' ]; then
echo "The action overwrote the setting the workflow provided"
exit 1
fi
actual="$(node --version)"
echo "node --version: ${actual}"
case "${actual}" in
v20.19.0) ;;
*) echo "Expected node v20.19.0 from the context-aware shim, got ${actual}"; exit 1 ;;
esac
shell: bash

Comment thread
coderabbitai[bot] marked this conversation as resolved.
runtime-version-fallback:
# `runtime: node` without an `@<version>` suffix should fall back to the
# version declared in devEngines.runtime.
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,23 @@ 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.
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`.

### Context-aware global shims

pnpm 12 links global runtime bins as context-aware shims: running `node` inside a project switches to the version that project pins in `devEngines.runtime`, fetching it on demand. In a workflow that is rarely what you want — a matrix job asking for `node@22` would run the repository's pinned version instead, and even when the two versions agree pnpm materializes a second copy outside `$PNPM_HOME`.

So whenever the action installs a runtime, it exports `PNPM_CONFIG_GLOBAL_SHIMS` with that runtime disabled (`{"node":false}`), leaving every other runtime at pnpm's defaults. To keep the switching behaviour, set the variable yourself — the action never overwrites a value the workflow already provides:

```yaml
- uses: pnpm/setup@v2
env:
PNPM_CONFIG_GLOBAL_SHIMS: '{"node":"auto"}'
with:
runtime: node@22
```

## License

[MIT](./LICENSE)
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ inputs:
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.

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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
required: false
cache:
description: Whether to cache the pnpm store directory
Expand Down
258 changes: 129 additions & 129 deletions dist/index.js

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion src/install-runtime/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { setFailed, startGroup, endGroup, info } from '@actions/core'
import { exportVariable, setFailed, startGroup, endGroup, info } from '@actions/core'
import { spawn } from 'child_process'
import { readFileSync } from 'fs'
import path from 'path'
Expand All @@ -8,6 +8,11 @@ import { Inputs, RuntimeName } from '../inputs'

const SUPPORTED_RUNTIMES: ReadonlySet<RuntimeName> = new Set(['node', 'bun', 'deno'])

// The names pnpm reads the `globalShims` setting from, in the order pnpm
// itself checks them — the first one present wins, so a workflow that sets
// either of them must not be overridden by the other.
const GLOBAL_SHIMS_ENV_NAMES = ['PNPM_CONFIG_GLOBAL_SHIMS', 'pnpm_config_global_shims'] as const

export interface InstalledRuntime {
readonly name: RuntimeName
readonly version: string
Expand Down Expand Up @@ -50,9 +55,31 @@ export async function installRuntime(
setFailed(`pnpm runtime set ${request.name} ${request.version} -g exited with code ${exitCode}`)
return undefined
}
keepInstalledRuntimeAuthoritative(request.name)
return { name: request.name, version: request.version }
}

/**
* pnpm 12 links global runtime bins as context-aware shims: running `node`
* from `$PNPM_HOME/bin` inside a project switches to the version that
* project pins in `devEngines.runtime`, fetching it on demand. That defeats
* the version this action was asked to install — a matrix job asking for
* `node@22` would run the repository's pinned version instead — and even
* when the two agree it materializes a second copy outside `$PNPM_HOME`.
* Turn the shim off for the runtime we installed, leaving every other
* runtime at pnpm's defaults. A value the workflow set itself always wins.
*/
function keepInstalledRuntimeAuthoritative(name: RuntimeName) {
// An empty value counts as unset, the same rule pnpm applies when it reads
// these — stepping aside for a value pnpm ignores would leave the shim on.
const configured = GLOBAL_SHIMS_ENV_NAMES.find(envName => process.env[envName])
if (configured) {
info(`\`${configured}\` is already set; leaving pnpm's context-aware shims as configured.`)
return
}
exportVariable(GLOBAL_SHIMS_ENV_NAMES[0], JSON.stringify({ [name]: false }))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function logSkippedRuntime() {
info('No runtime requested (no `runtime` input and no `devEngines.runtime` in package.json). Skipping runtime install.')
}
Expand Down