pnpm outdated recommends versions inside the minimumReleaseAge window
Summary
With minimumReleaseAge configured, pnpm outdated reports an "available
upgrade" to a version that is still inside the cooldown window. The install
path honours the cooldown; outdated does not. Workflows that use
pnpm outdated to decide what to bump are therefore steered straight into the
versions the cooldown exists to hold back.
Repro
pnpm-workspace.yaml: minimumReleaseAge: 10080 (no explicit
minimumReleaseAgeStrict — correctly relying on the strict-by-default that
pnpm auto-enables since 11.0.4 when minimumReleaseAge is set).
- A dependency whose
latest dist-tag points at a build published within the
window. Constantly true for @typescript/native-preview, whose latest is
always a fresh dated -dev prerelease.
- Run
pnpm -r outdated.
Expected: package not reported — there is no mature version newer than
what is installed. Actual: the immature version is reported as an upgrade.
Normal deps look correct because their latest is a stable release with many
mature versions; the bug only shows when no version satisfies the cooldown
for the resolved spec — which is the steady state for prerelease-only packages.
Root cause
outdated resolves the latest dist-tag through
deps/inspection/outdated/src/createManifestGetter.ts:
strictPublishedByCheck: Boolean(opts.minimumReleaseAge) && opts.minimumReleaseAgeStrict === true,
When strictPublishedByCheck is off, the npm resolver's non-strict fallback
applies (resolving/npm-resolver/src/pickPackage.ts,
pickRespectingMinReleaseAge): if no version satisfies the maturity cutoff it
re-picks while ignoring publishedBy, returning the immature latest.
That fallback exists so an install doesn't hard-fail when every candidate is
fresh — but for a read-only outdated report it turns "no mature upgrade"
into "here is an immature upgrade".
This is reachable two ways, both fixed by the same change: (1) users who
deliberately set minimumReleaseAgeStrict: false (legitimate: don't hard-fail
CI, but also don't recommend immature upgrades); (2) the auto-enabled strict
default not reliably reaching the outdated command's resolver.
minimumReleaseAgeStrict is an install-resilience knob; a report has no
install to rescue, so coupling outdated to it is the design flaw.
Suggested fix
outdated should respect the cooldown unconditionally when minimumReleaseAge
is active:
strictPublishedByCheck: Boolean(opts.minimumReleaseAge),
getManifest already maps NO_MATURE_MATCHING_VERSION /
NO_MATCHING_VERSION to "nothing to report", and createManifestGetter is
used only by outdated / outdated --recursive, so install behaviour is
unaffected. Verified locally: a regression test (load has-outdated-deps, set
an all-immature cutoff, assert is-negative@2.1.0 is not offered) goes red →
green with this one-line change; the no-policy baseline keeps passing.
Written by an agent (Claude Code, claude-opus-4-7).
pnpm outdatedrecommends versions inside theminimumReleaseAgewindowSummary
With
minimumReleaseAgeconfigured,pnpm outdatedreports an "availableupgrade" to a version that is still inside the cooldown window. The install
path honours the cooldown;
outdateddoes not. Workflows that usepnpm outdatedto decide what to bump are therefore steered straight into theversions the cooldown exists to hold back.
Repro
pnpm-workspace.yaml:minimumReleaseAge: 10080(no explicitminimumReleaseAgeStrict— correctly relying on the strict-by-default thatpnpm auto-enables since 11.0.4 when
minimumReleaseAgeis set).latestdist-tag points at a build published within thewindow. Constantly true for
@typescript/native-preview, whoselatestisalways a fresh dated
-devprerelease.pnpm -r outdated.Expected: package not reported — there is no mature version newer than
what is installed. Actual: the immature version is reported as an upgrade.
Normal deps look correct because their
latestis a stable release with manymature versions; the bug only shows when no version satisfies the cooldown
for the resolved spec — which is the steady state for prerelease-only packages.
Root cause
outdatedresolves thelatestdist-tag throughdeps/inspection/outdated/src/createManifestGetter.ts:When
strictPublishedByCheckis off, the npm resolver's non-strict fallbackapplies (
resolving/npm-resolver/src/pickPackage.ts,pickRespectingMinReleaseAge): if no version satisfies the maturity cutoff itre-picks while ignoring
publishedBy, returning the immaturelatest.That fallback exists so an install doesn't hard-fail when every candidate is
fresh — but for a read-only
outdatedreport it turns "no mature upgrade"into "here is an immature upgrade".
This is reachable two ways, both fixed by the same change: (1) users who
deliberately set
minimumReleaseAgeStrict: false(legitimate: don't hard-failCI, but also don't recommend immature upgrades); (2) the auto-enabled strict
default not reliably reaching the
outdatedcommand's resolver.minimumReleaseAgeStrictis an install-resilience knob; a report has noinstall to rescue, so coupling
outdatedto it is the design flaw.Suggested fix
outdatedshould respect the cooldown unconditionally whenminimumReleaseAgeis active:
getManifestalready mapsNO_MATURE_MATCHING_VERSION/NO_MATCHING_VERSIONto "nothing to report", andcreateManifestGetterisused only by
outdated/outdated --recursive, so install behaviour isunaffected. Verified locally: a regression test (load
has-outdated-deps, setan all-immature cutoff, assert
is-negative@2.1.0is not offered) goes red →green with this one-line change; the no-policy baseline keeps passing.
Written by an agent (Claude Code, claude-opus-4-7).