fix(self-update): do not downgrade when latest dist-tag is older#11435
Conversation
`pnpm self-update` defaults to the `latest` dist-tag, but `latest` on the registry can lag the installed version when a new major has shipped without being tagged. Refuse to downgrade in that case. Users can still run `pnpm self-update latest` (explicit) to force the downgrade. Closes #11418
📝 WalkthroughWalkthroughPrevents implicit ChangesDowngrade Prevention in Self-Update
Sequence DiagramsequenceDiagram
participant User
participant Handler as SelfUpdateHandler
participant Registry as Registry Resolver
participant Project as Project Lockfile Reader
participant Global as Global Binary Inspector
participant Installer as Installer
User->>Handler: pnpm self-update (no args)
Handler->>Handler: isImplicitLatest = true
Handler->>Registry: resolve "latest"
Registry-->>Handler: registryLatest
Handler->>Project: readProjectPinnedPnpmVersion(spec)
Project-->>Handler: projectResolvedVersion (optional)
Handler->>Global: get active global pnpm version
Global-->>Handler: activeGlobalVersion
Handler->>Handler: compare versions (registryLatest vs projectResolvedVersion / activeGlobalVersion)
alt registryLatest is older than either
Handler-->>User: "No update performed" (skip install)
else
Handler->>Installer: perform install/update
Installer-->>User: "Update completed"
end
User->>Handler: pnpm self-update latest (explicit)
Handler->>Handler: isImplicitLatest = false
Handler->>Registry: resolve "latest"
Handler->>Installer: perform install/update (no downgrade guard)
Installer-->>User: "Update completed"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 8/10 reviews remaining, refill in 10 minutes and 56 seconds. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@engine/pm/commands/test/self-updater/selfUpdate.test.ts`:
- Around line 231-239: The test self-update latest forces the downgrade... is
incorrectly mocking the registry so "latest" is newer (createMetadata('9.1.0'))
instead of older; update the mockRegistryForUpdate call in that test (and/or the
createMetadata argument) so the registry metadata reports a version older than
the currently installed pnpm (e.g., createMetadata('8.x.x',
opts.registries.default)) while still reusing the 9.1.0 fixture tarball, so the
test exercises the downgrade-bypass path when calling selfUpdate.handler with
['latest'].
🪄 Autofix (Beta)
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3cd30723-14fa-45cd-b3ec-30d1549cf3a4
📒 Files selected for processing (3)
.changeset/self-update-no-downgrade.mdengine/pm/commands/src/self-updater/selfUpdate.tsengine/pm/commands/test/self-updater/selfUpdate.test.ts
There was a problem hiding this comment.
Pull request overview
This PR updates the self-update flow so that an implicit pnpm self-update no longer downgrades users when the registry’s latest dist-tag lags behind the currently running or project-pinned pnpm version. It extends the safeguard to both the global self-update path and the project package-manager pin path, and adds regression tests plus a changeset entry.
Changes:
- Add an implicit-
latestdowngrade guard inselfUpdate.handler()for both global installs and project-pinned pnpm. - Introduce tests covering implicit downgrade refusal and explicit-version bypass behavior.
- Add a changeset describing the new self-update behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
engine/pm/commands/src/self-updater/selfUpdate.ts |
Adds the new no-downgrade guard and helper logic in the self-update command. |
engine/pm/commands/test/self-updater/selfUpdate.test.ts |
Adds regression tests for downgrade-prevention and explicit override cases. |
.changeset/self-update-no-downgrade.md |
Documents the behavior change for release notes/versioning. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…de check When a project pins pnpm via a range (e.g. `devEngines.packageManager.version: ">=8.0.0"`) and the env lockfile pins an exact version above the range's lower bound, the previous guard compared the resolved `latest` against `semver.minVersion(spec)` and missed the downgrade. Read `packageManagerDependencies.pnpm.version` from `pnpm-lock.yaml` and use the max of (lockfile-pinned, spec.minVersion) as the current version. Also fix the explicit-`latest` test which mocked `latest` as newer than the current version, defeating its own assertion.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
engine/pm/commands/src/self-updater/selfUpdate.ts (1)
253-258: 💤 Low valueConsider fuller optional chaining for defensive property access.
The current chain
envLockfile?.importers['.']would throw a TypeError ifimportersexists but the'.'key is missing. While thetry/catchhandles this, fuller optional chaining would make the intent clearer and avoid relying on exception handling for expected cases.♻️ Suggested improvement
try { const envLockfile = await readEnvLockfile(rootProjectManifestDir) - lockfilePinned = envLockfile?.importers['.'].packageManagerDependencies?.pnpm?.version + lockfilePinned = envLockfile?.importers?.['.']?.packageManagerDependencies?.pnpm?.version } catch { // ignore — fall through to spec min version }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@engine/pm/commands/src/self-updater/selfUpdate.ts` around lines 253 - 258, The code currently uses envLockfile?.importers['.'] which can throw if importers exists but lacks the '.' key; update the defensive access to use full optional chaining so it reads: envLockfile?.importers?.['.']?.packageManagerDependencies?.pnpm?.version; modify the assignment in the try block that sets lockfilePinned (the line using readEnvLockfile and envLockfile) to use this fuller chain to avoid relying on exceptions for missing keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@engine/pm/commands/src/self-updater/selfUpdate.ts`:
- Around line 253-258: The code currently uses envLockfile?.importers['.'] which
can throw if importers exists but lacks the '.' key; update the defensive access
to use full optional chaining so it reads:
envLockfile?.importers?.['.']?.packageManagerDependencies?.pnpm?.version; modify
the assignment in the try block that sets lockfilePinned (the line using
readEnvLockfile and envLockfile) to use this fuller chain to avoid relying on
exceptions for missing keys.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f2cc4c59-047a-49ce-a84f-199a5549943d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (4)
engine/pm/commands/package.jsonengine/pm/commands/src/self-updater/selfUpdate.tsengine/pm/commands/test/self-updater/selfUpdate.test.tsengine/pm/commands/tsconfig.json
✅ Files skipped from review due to trivial changes (2)
- engine/pm/commands/tsconfig.json
- engine/pm/commands/test/self-updater/selfUpdate.test.ts
) * fix(self-update): do not downgrade when latest dist-tag is older `pnpm self-update` defaults to the `latest` dist-tag, but `latest` on the registry can lag the installed version when a new major has shipped without being tagged. Refuse to downgrade in that case. Users can still run `pnpm self-update latest` (explicit) to force the downgrade. Closes #11418 * fix(self-update): use lockfile-pinned version for project-pin downgrade check When a project pins pnpm via a range (e.g. `devEngines.packageManager.version: ">=8.0.0"`) and the env lockfile pins an exact version above the range's lower bound, the previous guard compared the resolved `latest` against `semver.minVersion(spec)` and missed the downgrade. Read `packageManagerDependencies.pnpm.version` from `pnpm-lock.yaml` and use the max of (lockfile-pinned, spec.minVersion) as the current version. Also fix the explicit-`latest` test which mocked `latest` as newer than the current version, defeating its own assertion. * chore(engine.pm.commands): add lockfile/fs project reference to tsconfig
Summary
pnpm self-update(with no version arg) defaults to thelatestdist-tag, butlateston the registry can lag the installed version when a new major has shipped without being tagged. In that case, the command silently downgraded users to the older release.latestresolves to a version older than the currently active pnpm — both for the global path (compared against the running binary) and the project-pin path (compared againstwantedPackageManager.version, including ranges viasemver.minVersion).pnpm self-update latest(or any explicit version/tag) still updates as before, so the downgrade can be forced when intentional.Closes #11418.
Test plan
pnpm --filter @pnpm/engine.pm.commands run compilepassesNODE_OPTIONS="--experimental-vm-modules" npx jest selfUpdate— 36/36 tests pass, including 4 new cases (implicit latest refuses downgrade globally, explicitlatestforces it, explicit older version skips the guard, project pin refuses downgrade)pnpm --filter pnpm run compilerebuilds the bundle cleanlySummary by CodeRabbit
Bug Fixes
pnpm self-updatenow prevents unintended downgrades when the registry’slatestresolves to an older release; usepnpm self-update latestto force a downgrade.Documentation
Tests