Short answer: a VCS revert of pnpm-lock.yaml is sufficient when the upgrade only bumped lockfileVersion and the dependency graph was untouched by real resolution changes. A full regeneration with the pinned older pnpm is required when the upgrade actually re-resolved versions, changed hoisting, or when you cannot trust that the old lockfile still matches package.json. The lockfile itself gives you no reliable signal about resolution changes — only the schema number — so that judgment must come from release notes and from comparing the resolved graph.
Confirmed facts vs. likely explanation
Confirmed: pnpm has no in-place lockfile downgrade. Older pnpm cannot parse a newer lockfileVersion, so the only paths are restoring the old file or regenerating it. Also confirmed: the lockfile records lockfileVersion but nothing that says "this migration also changed how versions were resolved." There is no documented in-file flag for that.
Likely (not guaranteed): a schema-only migration usually preserves the resolved versions verbatim, which is why a revert often "just works." But pnpm major releases sometimes pair schema changes with resolution or hoisting changes, so treat that as an assumption to verify, not a rule.
Decision criteria
- Revert is sufficient when: the old lockfile is still in git history,
package.json has not gained new dependencies since, and pnpm install --frozen-lockfile with the pinned old pnpm exits 0. That command failing is itself the test — a frozen install validates lockfile/manifest consistency. - Regeneration is required when: the old lockfile is lost or stale, the upgrade window included dependency edits, or the major release notes mention resolution/hoisting changes. In that case the regenerated file is a new artifact: run the full test suite and diff top-level versions with
pnpm ls --depth 0 against the pre-upgrade state. - CI caching: a revert keeps the lockfile hash stable, so store caches stay valid. Regeneration invalidates them — rebuild the virtual store rather than reusing a cache keyed to the old hash.
Preventing accidental re-upgrade
Pin the version in package.json and let Corepack enforce it:
{
"packageManager": "pnpm@8.15.9"
}
Corepack shims then refuse (or auto-switch) when a contributor's global pnpm differs, which closes the mixed-tooling gap — but only if everyone actually uses the Corepack shim. Add a CI guard as the backstop:
pnpm --version
node -e "const l=require('fs').readFileSync('pnpm-lock.yaml','utf8');
if(!l.match(/^lockfileVersion: '6\.0'/m)) process.exit(1)"
(Adjust the expected version string for your pnpm line — 6.0 for pnpm 8, 9.0 for pnpm 9.) A pre-commit hook running the same check catches it earlier. This is worth doing because pnpm install with a newer pnpm silently migrates the lockfile, and the diff is easy to miss in review.
Verifying the rollback
pnpm install --frozen-lockfile — must exit 0.pnpm ls --depth 0 — compare against the pre-upgrade snapshot.- Run the test suite; resolution differences most often surface as peer-dependency or duplicate-instance failures.
One caveat for review: whether a given pnpm major release changed resolution behavior alongside the schema is release-specific — check the release notes for the exact versions involved rather than assuming the lockfileVersion bump tells you.