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
44 changes: 44 additions & 0 deletions .github/workflows/get-pnpm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: get-pnpm package

on:
- push
- pull_request

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
runtime:
# The oldest supported runtime and the current one. `engines` allows
# Node 22, so it has to be exercised there.
- node@22
- node@24

runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: get-pnpm

steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

# pnpm's own action, installing pnpm and the runtime in one step. Nothing
# to install at the repository root, so its `pnpm install` is left off.
- uses: pnpm/setup@v2
with:
version: latest
runtime: ${{ matrix.runtime }}
install: false

- run: pnpm install --frozen-lockfile
- run: pnpm test
60 changes: 60 additions & 0 deletions .github/workflows/npm-keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: npm signing keys

on:
schedule:
# Weekly. A rotation npm has not announced would otherwise surface as a
# failed install for whoever runs the installer next.
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

# One rotation at a time: a manual run alongside the weekly one would otherwise
# race over the same branch.
concurrency:
group: npm-key-rotation
cancel-in-progress: false

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# The script only needs a runtime; pnpm comes along with it.
- uses: pnpm/setup@v2
with:
version: latest
runtime: node@24
install: false

- id: refresh
run: |
if node scripts/update-npm-keys.mjs; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
node scripts/update-npm-keys.mjs --update
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- if: steps.refresh.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
# One long-lived branch, force-pushed: a weekly job must not open a
# second pull request for a rotation that is already waiting, and a
# re-run on the same day must not collide with its own branch.
branch=npm-key-rotation
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git checkout -B "$branch"
git commit -am 'chore: refresh the pinned npm signing keys'
git push --force origin "$branch"
if [ -z "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then
gh pr create --head "$branch" --title 'chore: refresh the pinned npm signing keys' --body \
'npm rotated its registry signing key. Every installer pins it, so all three files change together.

The checksums in `SHASUMS256.txt` cover `install.sh` and `install.ps1`, so this needs a re-sign (`./_update.sh`) before merge.'
fi
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.claude/
get-pnpm/lib
get-pnpm/node_modules
76 changes: 76 additions & 0 deletions get-pnpm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# get-pnpm

> Installs pnpm as a standalone executable

Installs pnpm the same way [the standalone install script](https://github.com/pnpm/get.pnpm.io) does — a self-contained executable in `PNPM_HOME`, on your `PATH`, with no dependency on Node.js afterwards — for people who would rather not pipe a script into a shell.

## Usage

```sh
npx get-pnpm
```

Install a specific version, major, or dist-tag:

```sh
npx get-pnpm 12 # the latest release of pnpm 12
npx get-pnpm next-12 # the pnpm 12 prerelease lane
npx get-pnpm 11.20.0 # an exact version
```

Then open a new terminal, or source the file the installer names in its output.

## How it works

1. Resolves the requested version against the `pnpm` dist-tags.
2. Downloads the executable for your platform from the npm registry.
3. Checks npm's signature over the package's checksum, then the download against that checksum.
4. Runs `pnpm setup`, which installs the executable globally and adds `PNPM_HOME` to your `PATH`.

Step 3 is what makes step 2 worth trusting. The registry publishes both the tarball
and its checksum, so a checksum taken from it proves nothing on its own; npm also
signs `<name>@<version>:<integrity>` with a key that this package pins, and a
download that fails either check is refused rather than installed.

The download goes to the registry npm is configured with (`npm_config_registry`), not to GitHub. Registries that require authentication are not supported yet.

## Environment variables

| Variable | Description |
| --- | --- |
| `PNPM_VERSION` | Version to install when no argument is given. |
| `PNPM_HOME` | Directory to install pnpm into. |
| `npm_config_registry` | Registry to download pnpm from. |

## Using it from a program

`installPnpm` does what the command does: download, verify, then hand over to
`pnpm setup`, which installs globally and edits your `PATH`. A caller that
manages its own directory — a CI action, say — wants `downloadPnpm` instead,
which verifies and places the executable and nothing else:

```js
import { downloadPnpm } from 'get-pnpm'

const { version, binPath } = await downloadPnpm({
versionSpec: 'next-12',
registry: 'https://registry.npmjs.org/',
dest: '/opt/pnpm',
})
```

## Development

```sh
pnpm install
pnpm test # builds, then runs the tests against the build output
```

The published package runs on Node.js 22.13 and newer. The test runner needs
22.18 or newer, which is where Node runs TypeScript without a flag.

## Other ways to install pnpm

See [pnpm.io/installation](https://pnpm.io/installation). The shell and PowerShell
installers in this repository do the same job for machines without Node.js, and pin
the same npm signing key.
11 changes: 11 additions & 0 deletions get-pnpm/bin/get-pnpm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
import { runCli } from '../lib/index.js'

try {
process.exitCode = await runCli(process.argv.slice(2))
} catch (err) {
// Anything can be thrown, and reading `.message` off a non-Error would either
// print `undefined` or throw again, burying the reason under a stack trace.
console.error(err instanceof Error ? err.message : String(err))
process.exitCode = 1
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
43 changes: 43 additions & 0 deletions get-pnpm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "get-pnpm",
"version": "0.0.1",
"description": "Installs pnpm as a standalone executable",
"keywords": [
"pnpm",
"install",
"installer"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/get.pnpm.io/tree/main/get-pnpm",
"homepage": "https://github.com/pnpm/get.pnpm.io/tree/main/get-pnpm#readme",
"bugs": {
"url": "https://github.com/pnpm/get.pnpm.io/issues"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map",
"bin"
],
"bin": {
"get-pnpm": "bin/get-pnpm.js"
},
"scripts": {
"build": "tsc",
"test": "tsc && node --test test/*.test.ts",
"prepublishOnly": "tsc"
},
"devDependencies": {
"@types/node": "^24.10.1",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.13"
}
}
39 changes: 39 additions & 0 deletions get-pnpm/pnpm-lock.yaml

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

24 changes: 24 additions & 0 deletions get-pnpm/src/extractTarball.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'

/**
* Unpacks a package tarball into `dest`, leaving its `package/` root in place.
*
* Shells out to `tar`, which every supported host has: macOS and Linux ship it,
* and Windows has had bsdtar since Windows 10 1803. Member selection and
* `--strip-components` are avoided because busybox tar and bsdtar disagree
* about them.
*/
export function extractTarball (tarball: string, dest: string): void {
fs.mkdirSync(dest, { recursive: true })
const { error, status, stderr } = spawnSync('tar', ['-xzf', tarball, '-C', dest], { encoding: 'utf8' })
if (error != null) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error('This installer needs the `tar` command, which was not found on your PATH.')
}
throw error
}
if (status !== 0) {
throw new Error(`Could not extract ${tarball}: ${stderr.trim()}`)
}
}
Loading
Loading