Skip to content

fix(self-update): do not downgrade when latest dist-tag is older#11435

Merged
zkochan merged 3 commits into
mainfrom
self-update-no-downgrade
May 2, 2026
Merged

fix(self-update): do not downgrade when latest dist-tag is older#11435
zkochan merged 3 commits into
mainfrom
self-update-no-downgrade

Conversation

@zkochan

@zkochan zkochan commented May 2, 2026

Copy link
Copy Markdown
Member

Summary

  • pnpm self-update (with no version arg) 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. In that case, the command silently downgraded users to the older release.
  • The handler now refuses to downgrade when implicit latest resolves 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 against wantedPackageManager.version, including ranges via semver.minVersion).
  • An explicit 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 compile passes
  • NODE_OPTIONS="--experimental-vm-modules" npx jest selfUpdate — 36/36 tests pass, including 4 new cases (implicit latest refuses downgrade globally, explicit latest forces it, explicit older version skips the guard, project pin refuses downgrade)
  • pnpm --filter pnpm run compile rebuilds the bundle cleanly

Summary by CodeRabbit

  • Bug Fixes

    • pnpm self-update now prevents unintended downgrades when the registry’s latest resolves to an older release; use pnpm self-update latest to force a downgrade.
  • Documentation

    • Updated self-update guidance to explain downgrade-prevention behavior.
  • Tests

    • Added test coverage for downgrade prevention and for explicit-bypass behaviors.

`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
Copilot AI review requested due to automatic review settings May 2, 2026 22:15
@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Prevents implicit pnpm self-update (no args) from downgrading when the registry latest resolves to an older version than the active global pnpm or the project-pinned pnpm; pnpm self-update latest or explicit versions still allow downgrades.

Changes

Downgrade Prevention in Self-Update

Layer / File(s) Summary
Behavior spec / Docs
.changeset/self-update-no-downgrade.md
Documented that pnpm self-update will not downgrade when latest resolves to an older release; instructs pnpm self-update latest to force a downgrade.
Data / Version resolution
engine/pm/commands/src/self-updater/selfUpdate.ts
Adds isImplicitLatest detection for no-arg invocations and imports readEnvLockfile to resolve project-installed pnpm versions when pinned by range.
Core guard logic
engine/pm/commands/src/self-updater/selfUpdate.ts
When latest is implicit, adds two early-return downgrade guards: (1) compares registry latest to project-pinned/resolved pnpm and (2) compares registry latest to active global pnpm; returns "No update performed" when downgrade would occur. Adds readProjectPinnedPnpmVersion helper to derive effective project pnpm version from lockfile or semver minimum.
Wiring / build config
engine/pm/commands/package.json, engine/pm/commands/tsconfig.json
Adds dependency @pnpm/lockfile.fs (workspace:*) and a tsconfig project reference to ../../../lockfile/fs to enable lockfile reading.
Tests
engine/pm/commands/test/self-updater/selfUpdate.test.ts
Adds tests covering: implicit-latest downgrade refusal, explicit latest forcing install, explicit older exact version bypassing guard, project packageManager pinned refusal, and lockfile-resolved pin refusal. Asserts messages and side effects (global install dir, unchanged package.json).

Sequence Diagram

sequenceDiagram
    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"
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibble code and guard the gate,
No slipping back by fickle fate,
If "latest" hides an older crown,
I hold the line — don't let it down.
Say "latest" loud to wear the gown.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: preventing downgrade when the registry's latest dist-tag is older than the current version.
Linked Issues check ✅ Passed The PR addresses issue #11418 by implementing logic to prevent unintended downgrades when latest is older, matching the issue's expectation for stable behavior.
Out of Scope Changes check ✅ Passed All changes directly support the downgrade-prevention objective: implementation in selfUpdate.ts, tests, documentation, and necessary dependency/config updates. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch self-update-no-downgrade

Review rate limit: 8/10 reviews remaining, refill in 10 minutes and 56 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 192d67b and 4f32437.

📒 Files selected for processing (3)
  • .changeset/self-update-no-downgrade.md
  • engine/pm/commands/src/self-updater/selfUpdate.ts
  • engine/pm/commands/test/self-updater/selfUpdate.test.ts

Comment thread engine/pm/commands/test/self-updater/selfUpdate.test.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-latest downgrade guard in selfUpdate.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.

Comment thread engine/pm/commands/src/self-updater/selfUpdate.ts Outdated
Comment thread engine/pm/commands/test/self-updater/selfUpdate.test.ts Outdated
zkochan added 2 commits May 3, 2026 00:24
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
engine/pm/commands/src/self-updater/selfUpdate.ts (1)

253-258: 💤 Low value

Consider fuller optional chaining for defensive property access.

The current chain envLockfile?.importers['.'] would throw a TypeError if importers exists but the '.' key is missing. While the try/catch handles 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4f32437 and 2e581cd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • engine/pm/commands/package.json
  • engine/pm/commands/src/self-updater/selfUpdate.ts
  • engine/pm/commands/test/self-updater/selfUpdate.test.ts
  • engine/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

@zkochan zkochan merged commit 72a7020 into main May 2, 2026
13 checks passed
@zkochan zkochan deleted the self-update-no-downgrade branch May 2, 2026 23:11
zkochan added a commit that referenced this pull request May 2, 2026
)

* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v11 is not tagged as latest in the npm registry

2 participants