Repository navigation
feat: add get-pnpm, the installer as an npm package - #51
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for getpnpm ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reached
Next review available in: 58 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (17)
📝 WalkthroughWalkthroughAdds a standalone TypeScript Changesget-pnpm package
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd get-pnpm npm package installer with signature verification and key rotation automation
AI Description
Diagram
High-Level Assessment
Files changed (18)
|
`npx get-pnpm` installs pnpm the way install.sh does, for people who have Node.js and would rather not pipe a script into a shell. It was built in the pnpm monorepo, which is the wrong home: it has no workspace dependencies, its releases have nothing to do with pnpm's, and it needed an exception there to keep its version from being normalised to the `11xx` scheme. Here it sits beside the two installers it duplicates in miniature, and the things all three have to agree on stay in one place — the pinned npm signing key, the platform package names, the refusal messages. Moving it already caught one drift: this one reported a checksum mismatch in different words than install.sh does. The package has no dependencies. libc detection is the probe pnpm's own wrappers use rather than `detect-libc`, because every dependency is one more thing `npx` fetches before pnpm exists. TypeScript compiled to `lib/`, tests through `node --test`, so the repo gains a compiler and nothing else. `scripts/update-npm-keys.mjs` refreshes the pinned key in all three installers at once and fails if any has drifted; a weekly workflow runs the check and opens a pull request when npm rotates. That replaces the mirroring that ran from the pnpm monorepo, which would have reached across repositories to work. CI installs pnpm with pnpm's own action, and runs the tests on Node 22 as well as 24 — `engines` allows 22, so it is exercised there rather than only claimed. The test runner itself needs 22.18, where Node runs TypeScript without a flag; the published package has no such floor.
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (5)
.github/workflows/npm-package.yml (2)
3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe trigger runs every pull request twice.
pushandpull_requestboth fire for branches pushed to this repository. Restrictpushto the default branch to halve the CI runs.♻️ Proposed change
on: - - push - - pull_request + push: + branches: + - main + pull_request:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/npm-package.yml around lines 3 - 5, Update the workflow trigger configuration so the push event runs only for the repository’s default branch, while retaining pull_request triggers for all applicable requests. Adjust the `on` block without changing the workflow jobs or other event behavior.
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
npm ci || npm installhides a missing or stale lockfile.No
package-lock.jsonappears in this PR.npm citherefore fails on every run and the job silently installs floating^ranges instead. A future lockfile drift also passes unnoticed. Commitnpm/package-lock.jsonand usenpm cialone, so dependency resolution stays reproducible andactions/setup-nodecan cache.♻️ Proposed change
- uses: actions/setup-node@v6 with: node-version: 24 - - run: npm ci || npm install + cache: npm + cache-dependency-path: npm/package-lock.json + - run: npm ci🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/npm-package.yml at line 32, Commit the npm/package-lock.json lockfile and update the workflow step to run npm ci without the fallback npm install. Ensure the lockfile is maintained with the package manifests so missing or stale dependency resolutions fail the job and setup-node caching remains reproducible.npm/test/unit.test.ts (1)
67-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the checksum path in
npm/src/registry.ts.The signature tests are thorough. The checksum comparison in
downloadTarballand the algorithm selection inchecksumcarry equal security weight and have no unit tests. Add cases for anintegrityvalue, theshasumfallback, a mismatching digest, and metadata with neither field.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@npm/test/unit.test.ts` around lines 67 - 124, Add unit tests covering the checksum logic used by downloadTarball and checksum: verify integrity-based hashing, shasum fallback, rejection of a mismatching digest, and handling metadata with neither integrity nor shasum. Reuse the existing test fixtures and assert the expected success or failure behavior for each case.npm/tsconfig.json (1)
16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest files are never type-checked.
includecoverssrc/**/*.tsonly. Node.js strips types at runtime without checking them, sonpm/test/*.test.tsreceives no type checking at all. Consider adding a second config (for exampletsconfig.test.jsonwithnoEmit) that includestest/**/*.ts, and running it in thetestscript.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@npm/tsconfig.json` at line 16, Extend the TypeScript test workflow so test files are type-checked: add a noEmit test configuration including the test TypeScript sources, and update the package test script to run that configuration alongside the existing tests. Preserve the current production tsconfig include and test execution behavior.npm/package.json (1)
33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the Node.js baseline or pass
--experimental-strip-typesfor the test script.
npm/package.jsonallows Node.js>=22.13, butnode --test test/*.test.tsneeds TypeScript type stripping unflagged by Node.js: at minimum 22.18 or 23.6. Either raiseengines.node/document a higher development baseline, or add--experimental-strip-typesconditionally for Node.js versions before that baseline.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@npm/package.json` at line 33, Update the test script and Node.js engine declaration so the TypeScript test files run on the supported baseline: either raise engines.node to a version with unflagged type stripping, or conditionally pass --experimental-strip-types for older supported versions. Keep the tsc compilation and node --test execution behavior intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/npm-keys.yml:
- Around line 36-42: Make the npm key rotation workflow use one stable
automation branch instead of the date-based branch generated near git checkout,
and reuse or update an existing rotation pull request rather than creating
duplicates. Add workflow-level concurrency covering scheduled and manual runs,
while preserving the existing commit and push behavior.
In `@npm/bin/get-pnpm.js`:
- Around line 6-9: Update the catch block in the get-pnpm error-handling flow to
safely handle thrown values that are null, undefined, primitives, or Error
objects. Log a meaningful string representation of the caught value without
directly dereferencing err.message, while preserving process.exitCode = 1.
In `@npm/src/index.ts`:
- Around line 112-117: Update the package-fetch flow around Promise.all so both
fetchPackage tasks settle before the surrounding finally cleanup removes tmpDir.
Preserve propagation of any fetch failure after both tasks have completed, while
retaining the existing conditional wrapper fetch behavior for major versions
below 11.
- Around line 101-106: Update the signal-handler setup around installPnpm to
store each registered handler, then remove all three handlers in a finally block
after installPnpm completes. Preserve the existing cleanup and exit behavior
while ensuring repeated calls do not retain stale process-level listeners.
In `@npm/src/registry.ts`:
- Around line 76-90: Update the registry request flow in request to pass an
AbortSignal to fetch, using a normal timeout for metadata requests and a
substantially longer timeout when downloading the tarball stream. Ensure the
timeout covers the entire response and streamed body, while preserving the
existing error handling and request behavior.
- Around line 60-69: Update checksum to parse only the first space-separated SRI
entry, then split that entry at the first hyphen so the full digest remains
intact; remove the shasum/SHA-1 fallback and retain the existing error when no
integrity checksum is available.
In `@npm/src/verifySignature.ts`:
- Around line 51-53: Update the expiry validation in verifySignature to parse
key.expires once and reject it when the parsed date is invalid (its time is
NaN), while preserving the existing expired-date error and opts.now comparison
for valid values.
- Around line 38-50: Update signature selection in the verification flow to
search all entries in opts.signatures for one whose keyid matches the configured
keys, rather than always using opts.signatures[0]. Preserve the existing
no-signature and unexpected-key errors, and add coverage for an unpinned
signature followed by a pinned signature, which must verify successfully.
In `@scripts/update-npm-keys.mjs`:
- Around line 34-37: Update the merge logic in the pinned-key handling to use
the authoritative npm endpoint key set as the complete trusted set, removing the
union that re-adds absent pinned keys. Ensure keys removed from the endpoint are
not retained or trusted by the generated key data.
---
Nitpick comments:
In @.github/workflows/npm-package.yml:
- Around line 3-5: Update the workflow trigger configuration so the push event
runs only for the repository’s default branch, while retaining pull_request
triggers for all applicable requests. Adjust the `on` block without changing the
workflow jobs or other event behavior.
- Line 32: Commit the npm/package-lock.json lockfile and update the workflow
step to run npm ci without the fallback npm install. Ensure the lockfile is
maintained with the package manifests so missing or stale dependency resolutions
fail the job and setup-node caching remains reproducible.
In `@npm/package.json`:
- Line 33: Update the test script and Node.js engine declaration so the
TypeScript test files run on the supported baseline: either raise engines.node
to a version with unflagged type stripping, or conditionally pass
--experimental-strip-types for older supported versions. Keep the tsc
compilation and node --test execution behavior intact.
In `@npm/test/unit.test.ts`:
- Around line 67-124: Add unit tests covering the checksum logic used by
downloadTarball and checksum: verify integrity-based hashing, shasum fallback,
rejection of a mismatching digest, and handling metadata with neither integrity
nor shasum. Reuse the existing test fixtures and assert the expected success or
failure behavior for each case.
In `@npm/tsconfig.json`:
- Line 16: Extend the TypeScript test workflow so test files are type-checked:
add a noEmit test configuration including the test TypeScript sources, and
update the package test script to run that configuration alongside the existing
tests. Preserve the current production tsconfig include and test execution
behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 57e621ea-888d-44d4-8c3a-eb9b15132532
⛔ Files ignored due to path filters (1)
npm/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
.github/workflows/npm-keys.yml.github/workflows/npm-package.yml.gitignorenpm/README.mdnpm/bin/get-pnpm.jsnpm/package.jsonnpm/src/extractTarball.tsnpm/src/index.tsnpm/src/npmSigningKeys.tsnpm/src/platformPackageName.tsnpm/src/registry.tsnpm/src/resolveVersion.tsnpm/src/verifySignature.tsnpm/test/install.test.tsnpm/test/unit.test.tsnpm/tsconfig.jsonscripts/update-npm-keys.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (18)
- GitHub Check: install (dash, next-12, install.sh)
- GitHub Check: install (windows-latest, next-11)
- GitHub Check: install (windows-latest, 10.30.1)
- GitHub Check: install (ubuntu-latest, next-12)
- GitHub Check: install (macos-latest, next-12)
- GitHub Check: test (ubuntu-latest)
- GitHub Check: install (bash, next-12, install.sh)
- GitHub Check: test (windows-latest)
- GitHub Check: install (sh, next-11, install.sh)
- GitHub Check: install (dash, next-12, install.sh)
- GitHub Check: install (bash, next-11, install.sh)
- GitHub Check: install (windows-latest, next-11)
- GitHub Check: install (windows-latest, next-12)
- GitHub Check: install (windows-latest, 10.30.1)
- GitHub Check: install (macos-latest, next-11)
- GitHub Check: install (sh, 10.30.1, install.sh)
- GitHub Check: install (ksh, next-11, install.sh)
- GitHub Check: install (zsh, 10.30.1, install.sh)
🧰 Additional context used
🪛 ast-grep (0.45.0)
npm/src/extractTarball.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
npm/test/install.test.ts
[warning] 46-88: Use https protocol over http
Context: http.createServer((req, res) => {
const url = decodeURIComponent(req.url!)
const json = (body: unknown): void => {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify(body))
}
if (url === '/pnpm') {
json({ 'dist-tags': { latest: VERSION }, versions: { [VERSION]: {} } })
return
}
if (url.endsWith(`/${VERSION}`)) {
const kind = url === `/pnpm/${VERSION}` ? 'pnpm' : 'platform'
const name = url.slice(1, url.lastIndexOf('/'))
const bytes = fs.readFileSync(tarballs[kind])
const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`
// Signing a different checksum than the one served is what a tampered
// checksum looks like: the bytes and the metadata agree, the signature
// does not cover them.
const signed = mode === 'bad-signature' ? `sha512-${'A'.repeat(86)}==` : integrity
json({
dist: {
tarball: `${registry}${kind}.tgz`,
integrity,
signatures: mode === 'unsigned'
? []
: [{
keyid: mode === 'wrong-keyid' ? 'SHA256:not-pinned' : KEYS[0]!.keyid,
sig: createSign('SHA256').update(`${name}@${VERSION}:${signed}`).sign(privateKey, 'base64'),
}],
},
})
return
}
if (url === '/pnpm.tgz' || url === '/platform.tgz') {
const bytes = fs.readFileSync(tarballs[url === '/pnpm.tgz' ? 'pnpm' : 'platform'])
res.writeHead(200, { 'content-type': 'application/octet-stream' })
res.end(mode === 'bad-tarball' ? Buffer.concat([bytes, Buffer.from('tampered')]) : bytes)
return
}
res.writeHead(404)
res.end()
})
Note: [CWE-319] Cleartext Transmission of Sensitive Information. Security best practice.
(https-protocol-missing-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] 60-60: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(tarballs[kind])
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 81-81: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(tarballs[url === '/pnpm.tgz' ? 'pnpm' : 'platform'])
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 103-103: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(setupLog, 'utf8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 143-143: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(dest, content, { mode: 0o755 })
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
npm/src/index.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] 150-150: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(path.join(opts.wrapperDir, 'package.json'), 'utf8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 152-158: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(path.join(opts.tmpDir, 'package.json'), JSON.stringify({
name: '@pnpm/exe',
version: opts.version,
type: 'module',
bin: { pnpm: opts.executable, pn: opts.executable },
dependencies: wrapper.dependencies,
}))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
🪛 Betterleaks (1.7.3)
npm/src/npmSigningKeys.ts
[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
[high] 21-21: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🪛 zizmor (1.29.0)
.github/workflows/npm-keys.yml
[warning] 18-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🔇 Additional comments (14)
npm/src/extractTarball.ts (1)
12-23: LGTM!npm/test/install.test.ts (1)
13-149: LGTM!npm/package.json (1)
1-30: LGTM!Also applies to: 34-43
npm/tsconfig.json (1)
1-15: LGTM!npm/README.md (1)
1-49: LGTM!.gitignore (1)
2-3: LGTM!npm/src/resolveVersion.ts (2)
1-23: LGTM!Also applies to: 28-30
24-27: 🎯 Functional CorrectnessNo change needed for bare-major resolution.
pnpm publishes
latest-11, soresolveVersion(packument, '11')resolves the current stable major through the existing tag.npm/src/platformPackageName.ts (1)
1-71: LGTM!npm/test/unit.test.ts (1)
1-66: LGTM!npm/src/registry.ts (1)
1-59: LGTM!Also applies to: 70-75
.github/workflows/npm-package.yml (1)
26-31: 📐 Maintainability & Code QualityNo change needed for action major versions.
actions/checkout@v7andactions/setup-node@v6are valid major-version references.npm/src/verifySignature.ts (1)
55-61: 🩺 Stability & AvailabilityNo change needed. Node.js accepts these pinned public keys as single-line PEM bodies.
npm/src/npmSigningKeys.ts (1)
8-29: 🔒 Security & PrivacyNo issue found.
These entries are npm registry public signing keys, and
scripts/update-npm-keys.mjskeeps expired pinned keys for pre-rotation artifact validation.
Signature selection was positional. A package can carry several signatures in no guaranteed order, and mid-rotation one of them can come from a key this installer does not pin — which is no reason to refuse a package that a pinned key also signed. The signature is now chosen by matching keyid against the pinned set. An unreadable `expires` compared as `Invalid Date`, which is false against everything, so a key with a corrupt date read as "never expires". It is now rejected. Withdrawn keys are no longer kept. A signature carries no trusted timestamp, so one made before a withdrawal cannot be told apart from one forged after it; these installers only ever fetch current releases, so following npm's advertised set costs nothing and keeps a withdrawal meaning something. `integrity` is an SRI field and may hold several space-separated entries, so splitting on every `-` could yield a corrupt digest and refuse a good download. The SHA-1 `shasum` fallback is gone: a package without `integrity` is now refused by name before anything is downloaded, rather than reaching a weaker digest. Requests had no timeout, so a registry that accepted the connection and then stalled would hang the install with no output. Metadata gets 30s; the tarball gets a budget that survives a slow connection carrying ~150 MB. `installPnpm` is a library function, and it left three signal handlers registered per call. It also used `Promise.all`, so a rejected fetch let the other one keep writing into the directory the cleanup was about to remove. The bin script read `.message` off whatever was thrown, which would print `undefined` — or throw again — for a non-Error. The rotation workflow opened a new branch and pull request every week until one merged, and a same-day re-run collided with its own branch. One long-lived branch, force-pushed, with a pull request opened only when none is waiting, and a concurrency group so a manual run cannot race the scheduled one.
`installPnpm` was the only way in, and it ends by running `pnpm setup --force`: a global install that edits shell rc files and writes GitHub Actions env files. That is right for a person typing `npx get-pnpm` and wrong for anything that manages its own directory, so pnpm/setup could not have used this package without reimplementing the half it needed. `downloadPnpm` is now that half — resolve, verify, place the executable beside its `dist/`, touch nothing else — and `installPnpm` is it plus the setup step. The directory is `get-pnpm/` rather than `npm/`, after the package rather than after the registry, and the version starts at 0.0.0.
0.0.0 is taken: it is the placeholder published under this name in 2018, and npm refuses a re-publish of an existing version.
|
Correction to something I claimed in this PR and in its commit message. I said the registry copy of v11 loses
The GitHub release tarball points the same way: its bundled So the honest position on the manifest: it makes a registry-sourced install match what This has a consequence worth acting on separately: the reason I kept v11 on the GitHub path in pnpm/setup#24 was exactly this |
Summary
Moves
get-pnpm— the installer as an npm package,npx get-pnpm— out of the pnpm monorepo and in here, beside the two installers it duplicates in miniature.It was built in pnpm/pnpm#13732, which is the wrong home for it: no workspace dependencies, a release cadence unrelated to pnpm's, and it needed a
meta-updaterexception to stop its version being normalised to the11xxscheme. Here, the things all three installers must agree on live in one place — the pinned npm signing key, the platform package names, the refusal messages.Shape
detect-libc— every dependency is one more thingnpxfetches before pnpm exists.lib/, tests vianode --test, so the repo gains a compiler and nothing else.install.sh: npm's signature over the checksum, checked against a pinned key, then the download against that checksum.Keeping the key current
scripts/update-npm-keys.mjsrewrites the pinned key in all three installers and exits non-zero if any has drifted. A weekly workflow runs the check and opens a PR when npm rotates.This replaces the mirroring I had added to pnpm's own
update-npm-signing-keys.mjs, which reached across repositories. Self-contained is better: one job, one repo, and a rotation can't reach two installers and miss the third.I rehearsed a rotation rather than assuming it works — pointing all three at a stale keyid, then:
The shell and PowerShell installers carry only the current key; the package keeps the union, so packages published before a rotation still verify.
Testing
22 tests. The verifier against a generated key pair (valid, wrong checksum, wrong package, untrusted key, unpinned keyid, unsigned, expired, not-yet-expired), version resolution, platform naming, and an end-to-end suite against a mock registry covering the happy path plus every refusal — tampered tarball, a checksum the signature doesn't cover, unsigned, unpinned keyid — asserting each leaves nothing installed.
Real installs against the live registry:
next-12→ 12.0.0-rc.1,11.20.0→ 11.20.0, with@reflink/reflinkpresent in the v11 install (its absence silently downgrades copy-on-write cloning to hard linking).Before this can publish
get-pnpmhas never been published from CI. The first release needs either a trusted publisher configured for this repo on npmjs.com, or anNPM_TOKENsecret — I didn't add a publish workflow rather than guess which you want. The name is already yours; the placeholder0.0.0from 2018 is what's on the registry today.Follow-ups
pnpm setup"type": "module"fix, which is a genuine CLI fix and belongs there.downloadPnpmexport instead — verify and place, without thepnpm setupstep an action must not run.Written by an agent (Claude Code, claude-opus-5).
Summary by CodeRabbit
get-pnpmcommand-line tool for installing selected pnpm versions across supported platforms.