Skip to content

fix(libnpmexec): honor min-release-age-exclude in npx - #9768

Open
Sanjays2402 wants to merge 5 commits into
npm:latestfrom
Sanjays2402:fix/issue-9765
Open

Sanjays2402 wants to merge 5 commits into
npm:latestfrom
Sanjays2402:fix/issue-9765

Conversation

@Sanjays2402

Copy link
Copy Markdown

Description

npx calls pacote.manifest(spec, { ...flatOptions }) directly, which forwards flatOptions.before (set by min-release-age) to pacote. pacote has no concept of min-release-age-exclude, so even packages whose name matches an exclude pattern are rejected with ETARGET. arborist already handles this per-spec inside npm install by dropping before when the resolved package name matches; libnpmexec needs to do the same.

Change in workspaces/libnpmexec/lib/index.js: before calling pacote.manifest in getManifest, run the spec through a new helper (applyReleaseAgeExclude) that returns a shallow copy of flatOptions with before set to null when the spec's trusted registry name matches minReleaseAgeExclude. npm: aliases are keyed on the alias target, mirroring arborist's trustedSpecName. The pattern semantics match arborist exactly (nonegate, nocomment, noext) so an exemption list can never silently widen into match-all.

Repro before the fix, with ~/.npmrc:

min-release-age=3
min-release-age-exclude[]=@myscope/*
$ npx @myscope/some-package@1.2.3
npm error code ETARGET
npm error notarget No matching version found for @myscope/some-package@1.2.3 with a date before <cutoff>.

After the fix, the exempt package resolves normally; unrelated packages still respect the cutoff.

Existing Issue

Fixes #9765

Test Coverage

New unit tests in workspaces/libnpmexec/test/release-age-exclude.js cover the helper end to end: exact and glob patterns, hardened !/#/extglob semantics, npm: alias resolution, non-mutation of the input options, and the passthrough branches (no before, no match, empty/undefined options). File coverage is 100%. The existing libnpmexec suite continues to pass, since the wire-in is a passthrough whenever before is unset (the norm in existing tests).

Verified both directions on the affected test file: it fails on main (module missing) and passes with the patch applied.

AI Disclosure

This change was written with the assistance of Claude. I have reviewed the diff and the test and take responsibility for the code.

npx calls pacote.manifest directly with flatOptions.before set by
min-release-age, but pacote has no notion of min-release-age-exclude
so npx would fail with ETARGET even for packages that should be
exempt. arborist already does the equivalent per-spec drop inside
npm install; do the same in libnpmexec's getManifest by clearing
before when the resolved spec name matches an exclude pattern.

Fixes: npm#9765
@Sanjays2402
Sanjays2402 requested review from a team as code owners July 15, 2026 09:24
Comment thread workspaces/libnpmexec/lib/index.js Resolved
The manifest lookup cache was module-scoped and keyed only by spec.raw,
while min-release-age-exclude is applied only on a cache miss. The first
exec() for a spec matching the exclude list resolved and cached a manifest
with the `before` cutoff dropped; a later exec() for the same spec without
the exclusion reused that cached manifest and bypassed its release-age
restriction.

Move the cache into a per-exec() Map and thread it through getManifest and
all three missingFromTree() call sites, so caching still avoids duplicate
fetches within a single exec() while policy-specific results no longer leak
across separate exec() invocations.
@Sanjays2402

Copy link
Copy Markdown
Author

Good catch, thanks — the leak is real and your reproduction matches what I see.

I've pushed the cache scoping in add0b21. It follows your sketch: manifestCache is now a Map created inside exec() and threaded through getManifest() and all three missingFromTree() call sites, so the local-tree and npx-tree lookups still share a single fetch within one exec() while applyReleaseAgeExclude() is re-evaluated per invocation.

On the two-call regression test — I don't have one I'm willing to push yet, and I'd rather say that than push something that only looks like coverage. The shapes I tried:

  1. Full setup() + MockRegistry, two sequential exec() calls with a local install so the spec resolves without a reify. pacote.manifest gets called ~6 times because arborist's loadActual/resolution path goes through it too, so filtering by spec.raw to isolate the top-level lookup was fragile, and the first call's recorded before didn't come back null — I think I'm not threading minReleaseAgeExclude into flatOptions the same way the config layer does in that harness.
  2. Mocking pacote.manifest to record { raw, before } and throw immediately. That passes, but it passes without the fix too, since throwing before manifestCache.set() means nothing is ever cached — so it's not a regression test at all.

The thing that makes this awkward under t.mock is that the pre-fix cache is module scoped, so the test has to reuse one mocked module instance across both exec() calls (re-mocking per call resets it and hides the bug) while still driving each call far enough to populate the cache.

Do you have a preference for the shape here? Either:

  • a full MockRegistry test in test/registry.js with two real versions and a real before cutoff, asserting the second call selects the older version; or
  • exporting getManifest/missingFromTree (or accepting the cache as a param on an internal export) so the cache behaviour can be unit tested directly without standing up a registry.

I'd lean toward the first since it tests the actual user-visible symptom, but it's the more expensive one to get deterministic. Happy to write whichever you'd rather review.

Copilot AI 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.

🟡 Changes recommended

The new runtime dependency lacks its generated DEPENDENCIES.md edge, so dependency regeneration will leave a dirty tree and fail the libnpmexec CI check.

3 open issues
What changed in this PR

This PR adds release-age exclusions to libnpmexec manifest resolution so npx can honor min-release-age-exclude, aligning with npm install and addressing #9765.

Changes:

  • Applies exclusions using hardened glob matching and alias-target names.
  • Scopes manifest caching to each execution to prevent policy results leaking across calls.
  • Adds helper and registry regression tests, with dependency and lockfile updates.
File Description
workspaces/​libnpmexec/​test/​release-age-exclude.js Tests matching, aliases, option passthrough, and non-mutation.
workspaces/​libnpmexec/​test/​registry.js Tests exclusion-policy isolation across execution calls.
workspaces/​libnpmexec/​package.json Adds the direct minimatch dependency.
workspaces/​libnpmexec/​lib/​release-age-exclude.js Adds exclusion matching and per-spec cutoff adjustment.
workspaces/​libnpmexec/​lib/​index.js Applies exclusions and confines manifest caching to one call.
package-lock.json Records the workspace dependency and an optional development peer.

Review effort: Max

Comment thread workspaces/libnpmexec/package.json Resolved
Keep release-age cutoffs for non-registry specs, cover hardened matching and alias identity, and update the generated dependency graph.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 067c8fbe-a9f3-4aab-b6d6-c5f730c65705

Copilot AI 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.

🔵 Needs a closer look

The npx min-release-age bypass relaxes a supply-chain control and intentionally differs from arborist for named non-registry specs, requiring maintainer sign-off on the policy semantics.

No open issues 🎉

3 resolved since last review

Review effort: Max

Mock each execution's manifest lookup now that the cache is scoped per call. Remove the unrelated optional peer entry pruned by CI dependency setup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 067c8fbe-a9f3-4aab-b6d6-c5f730c65705
@martinrrm
martinrrm self-requested a review November 10, 2026 01:30
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.

[BUG] min-release-age-exclude is not honored by npx

3 participants