Skip to content

feat(cli): prune the bundled lockfile to the code bundle's contents [RED-886] - #1442

Open
sorccu wants to merge 28 commits into
mainfrom
simo/red-886-prune-bundle-lockfile
Open

feat(cli): prune the bundled lockfile to the code bundle's contents [RED-886]#1442
sorccu wants to merge 28 commits into
mainfrom
simo/red-886-prune-bundle-lockfile

Conversation

@sorccu

@sorccu sorccu commented Aug 23, 2026

Copy link
Copy Markdown
Member

Linear: RED-886

What this does

When a workspace (monorepo) project's code bundle covers only part of the workspace, the bundled lockfile used to ship unchanged — describing dependencies of workspace members that were omitted or shipped as dependency-free placeholder manifests. The remote install would then try to fetch those dependencies, which fails outright for private packages.

This PR makes the CLI prune the bundled lockfile at finalize time: the resolution-relevant bundle entries (manifests, lockfile, .npmrc, pnpmfiles, patches) are materialized into a temp dir and the package manager's lockfile-only install regenerates the lockfile there, so it references exactly what the bundle contains. The result is verified against the original before it ships (same lockfile version, resolution subset, workspace links stay links, bundled importers preserved) — any doubt means the original ships unchanged with a warning or note. CHECKLY_LOCKFILE_PRUNE=0 disables pruning entirely.

Supported: pnpm-lock.yaml v6/v9, package-lock.json v2/v3, bun's text bun.lock v1 (the binary bun.lockb is not supported and skips with a pointer at bun install --save-text-lockfile), and Yarn Berry yarn.lock (metadata versions 6/8/10 — yarn 3 and 4). Yarn Classic (v1) is not supported and skips with a note.

Building on that:

  • bundle.packages.embed follows the pruned lockfile. Embedded-package tarballs are now materialized at finalize, after pruning, and filtered to what the pruned lockfile still references — tarballs whose only referents were pruned away are neither embedded nor downloaded. This avoids downloading potentially very large private packages that the runner could never use, instead of relying on CI caches to absorb the cost.
  • Faux (placeholder) workspace manifests carry real versions, and manifests for members that the lockfile references as links but that aren't in the bundle are backfilled automatically, so the pruned lockfile and the bundle stay consistent.
  • pnpm projects with a pnpmfile now bundle the workspace-root pnpmfile when the lockfile records a pnpmfileChecksum and the pnpmfile is self-contained (side-effect-free builtins only); otherwise pruning skips with a note.
  • bun support required some care, all verified empirically against bun 1.3.11: resolutions are compared as a presence set of whole lockfile tuples (pruning re-keys surviving member-scoped entries), workspace links are resolved per edge (a member name can also be consumed from the registry by a different importer), and — because bun re-roots at any ancestor package.json whose workspaces glob matches the working directory, writing the lockfile outside the temp dir — the pruner refuses to run when its temp dir sits inside a workspace.
  • Yarn Berry support (yarn install --mode=update-lockfile), verified against yarn 4.18.0 and 3.8.7: workspace entries are the importers (their dependency/peer maps are the edges) and every other entry feeds a serialized presence set immune to descriptor re-keying; regeneration runs with the network disabled (a stale lockfile then fails fast instead of resolving a missing descriptor against the public registry and leaking its name), scripts disabled, hardened mode disabled only for a probe-confirmed yarn 4, and — because yarn honors a .yarnrc/.yarnrc.yml from any ancestor of the temp dir — yarnPath neutralized (YARN_IGNORE_PATH plus a randomized rc filename) so an uncontrolled ancestor rc on a shared host cannot execute code during the version probe. A pre-spawn probe refuses Yarn Classic (which silently full-installs on this flag) and reports a lockfile/binary generation mismatch as an actionable skip. bundle.packages.embed also works for Yarn Berry: because Berry lockfiles record no npm tarball integrity (their checksum hashes yarn's own cache, not the tarball), the CLI resolves the tarball integrity from the registry's package metadata (per-version route, packument fallback) at finalize time.
  • The dependency-cache hash now covers the pruned lockfile, backfilled manifests, bundled pnpmfiles and the filtered embedded set, so cache keys always reflect what actually ships.

Behavior changes to note

  • The workspace-root pnpmfile is now uploaded with the bundle for pnpm projects whose lockfile records a checksum (previously the remote install could re-resolve without it). Avoid secrets in pnpmfiles; the docs say so explicitly.
  • Embedded-package download failures (EmbeddedPackageError) now surface at finalize rather than during per-check bundling — same command, same error type, marginally later.
  • bundle.packages.embed starts working for bun projects (it previously errored with "unsupported lockfile").
  • Lockfile pruning and bundle.packages.embed start working for Yarn Berry projects (both previously skipped/errored as unsupported).
  • A Yarn Berry project whose yarn resolves to Yarn Classic (a yarnPath-pinned setup with no packageManager field) now reports a clean "not pruned" skip pointing at Corepack, instead of a full Classic install or a confusing failure.
  • A missing package-manager binary now produces a "not pruned" note instead of a misleading "could not prune" warning.

Known limitations / accepted trade-offs

  • Pruning (and the cache key) depends on the locally installed package manager; environment-specific config (user-level ~/.npmrc, pnpm env settings) participates in regeneration. Drift is caught by verification and fails closed to the unpruned lockfile.
  • pnpm may serialize the regenerated lockfile slightly differently across versions; verification compares content, not bytes.
  • bun: a workspace member whose name collides with a registry dependency makes bun reject its own lockfile and silently re-resolve; verification bounds this fail-closed (unpruned ship + warning). The temp-dir workspace-ancestor guard is check-then-act, so a hostile writer to a shared temp root could theoretically race it.
  • bun: bunfig.toml is not bundled or carried into regeneration/downloads — registry credentials must live in .npmrc (documented). A lockfile bun declines to reuse re-resolves against the default registry, disclosing package names to it, before verification rejects the result.
  • yarn: .yarnrc.yml/.yarnrc is not bundled or carried into regeneration — registry credentials for embed downloads must live in .npmrc (documented). Berry lockfiles are registry-agnostic, so pruning needs no network; the network is disabled outright so a stale lockfile fails closed without disclosing names.
  • yarn: Berry projects provisioned via yarnPath rather than the packageManager field land on the Classic skip — Corepack (a packageManager field) is required for v6/v8/v10 pruning. A cross-generation yarn/lockfile mismatch skips with an actionable message.
  • yarn embed: the tarball integrity is resolved from registry metadata rather than the lockfile, so it shares the registry's trust root (the runner's own yarn re-verifies its checksums against the served content at install time) and needs registry reachability on every deploy — one small metadata request per embedded package, even with a warm tarball cache.
  • Embed validation still resolves specs against the original lockfile; a spec whose matches are all pruned away embeds nothing (debug-logged, not an error).
  • Embedded downloads run serially at finalize instead of overlapping with per-check bundling; the set is strictly smaller, so this is a net win for the motivating case.
  • The temp dir can leak on SIGINT mid-prune; npm's nested node_modules re-hoisting is covered at unit level only.
  • CI pins bun 1.3.11 (fixture-serialization determinism); the pin has no automated updater. The real-bun tests run on Windows CI for the first time — if flaky, the gate can be narrowed to non-Windows.
  • PathLookup.detectPresence has a pre-existing missing-await bug that makes executable detection vacuously succeed; it is documented in place and tracked as RED-887 rather than fixed here (fixing it changes package-manager detection outcomes).
  • One test fixture commits a genuine registry tarball (ms@2.1.3) because pnpm re-resolves all importers of a stale lockfile, so a fake kept-side package would 404.

Testing

Unit + integration: 2207 tests green (150 files), including end-to-end prune fixtures for pnpm (Playwright-level, with embed filtering and no-download proofs), and unit-level real-package-manager prunes for pnpm, npm, bun and Yarn Berry (both yarn 3 and 4, from committed real fixtures). Real-bun/-yarn tests skip when the manager is not on PATH; CI provisions both (Corepack for yarn) and asserts the provisioning worked via CHECKLY_EXPECT_BUN/CHECKLY_EXPECT_YARN. The ancestor-.yarnrc code-execution guard was verified against real yarn 1.22.22, 3.8.7 and 4.18.0.

Pre-merge: a runner-side acceptance check with real credentials is still advisable, covering a pnpmfile project, an embed project, a bun project, and a Yarn Berry project (to confirm the runner installs against the embedded-package registry — the same open product question for bun and yarn).

🤖 Generated with Claude Code

sorccu and others added 28 commits August 23, 2026 05:06
…ts [RED-886]

Faux workspace-package manifests (shims for declared-but-unimported
workspace members) hardcoded version 0.0.0, which breaks `workspace:^x.y.z`
specifier resolution under pnpm and makes npm silently substitute a
same-named registry package when a semver range does not match 0.0.0.

Carry the member's real version on the Package model (populated at every
construction site, including from `pnpm list --json` output) and emit it in
the faux manifest, falling back to 0.0.0 only when the member declares no
usable version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm records a checksum of the pnpmfile in pnpm-lock.yaml
(pnpmfileChecksum), and installing without the file makes pnpm treat the
lockfile as out of date and silently re-resolve every dependency — which is
what happens on the remote runner today for every pnpm project with a
pnpmfile, since nothing bundled it.

Bundle the workspace-root pnpmfile (pnpm workspaces only) verbatim into
Playwright code bundles, and add its contents to the dependency cache hash
as a new `pnpmfile:` record (position pinned by a new cross-language parity
fixture). Only self-contained pnpmfiles are bundled: the remote install
loads the pnpmfile before any dependencies exist and in a different
environment, so anything that loads modules beyond a small allowlist of
side-effect-free builtins, or references process/__dirname/require.resolve
and similar escape hatches, is skipped with a warning — preserving today's
behavior for such projects. The `pnpmfile` and `ignore-pnpmfile` settings
in .npmrc / pnpm-workspace.yaml are honored, and when both .pnpmfile.cjs
and .pnpmfile.mjs exist the situation is version-ambiguous (pnpm 10 loads
only the cjs, pnpm 11 only the mjs) and both are skipped.

Pnpmfiles are never parsed as check code; they are install-time
configuration, and parsing them would report constructs like optional
`try { require(...) } catch {}` blocks as missing dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pm [RED-886]

Expose a lockfileOnlyInstallCommand() on the PackageManager interface that
regenerates the lockfile from the manifests on disk without installing
anything. pnpm uses `install --lockfile-only --ignore-scripts
--no-frozen-lockfile` (--no-frozen-lockfile matters because pnpm
auto-enables frozen mode when CI=true); npm uses `install
--package-lock-only --ignore-scripts --no-audit --no-fund`. Other package
managers report the mode as unsupported so callers can skip lockfile
pruning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pruneBundledLockfile() regenerates a code bundle's lockfile so it matches
the bundle's actual set of manifests: the resolution-relevant bundle
entries (manifests including faux shims, .npmrc, pnpm-workspace.yaml,
pnpmfiles, patches, and the lockfile) are materialized into a temp dir and
the package manager's lockfile-only install runs there — dependencies of
workspace members that were shimmed or omitted disappear from the result.

The regenerated lockfile is only accepted if it is provably a pruned copy
of the original: the format version and pnpmfile checksum must be
unchanged, every resolution must already exist in the original (a stale
lockfile would otherwise be silently re-resolved from the registry),
workspace links must stay links (npm silently substitutes registry
packages for version-mismatched members), and no importer whose manifest
is bundled may disappear. Anything else falls back to the original
lockfile.

Workspace members that bundled manifests reference as links but that have
no manifest in the bundle (e.g. declared only by the workspace root) get a
synthesized faux manifest, returned to the caller for registration so the
bundle and the lockfile stay consistent.

Supported for pnpm-lock.yaml (v6/v9) and package-lock.json (v2/v3);
everything else, plus special setups like excludeLinksFromLockfile, is
skipped. CHECKLY_LOCKFILE_PRUNE=0 disables pruning. Not yet wired into
the bundler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ED-886]

Wire the lockfile pruner into Bundler.finalize(): once the bundle's file
set is final, the lockfile is regenerated against the bundle's actual
manifests, so dependencies of workspace members that were shimmed or
omitted no longer appear in it — previously the runner's install resolved
(and tried to fetch) them, which breaks outright for private packages.
Workspace members that bundled manifests reference as links but that had
no manifest in the bundle get their faux manifest registered too. On any
verification failure the original lockfile ships unchanged, with a warning
explaining the fallback and the CHECKLY_LOCKFILE_PRUNE=0 escape hatch.

The dependency cache hash now reflects the bundle's real install inputs:
finalize() recomputes it with a record per synthesized (faux) manifest —
whose version, unlike on-disk manifests, is load-bearing for install
resolution — and a record for the pruned lockfile bytes. Payloads capture
the hash through a mutable CacheHashMarker (same toJSON pattern as
BundlePathMarker) because checks copy it during bundle(), before
finalize() runs. Bundles with no faux manifests and no pruning keep a
byte-identical hash. Cross-language parity fixtures pin the new record
groups for the terraform-provider mirror.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the automatic lockfile pruning behavior in the Playwright
configuration reference (supported lockfile formats, silent-skip
conditions and their DEBUG namespace, warned fallbacks, and the
CHECKLY_LOCKFILE_PRUNE=0 escape hatch), and describe the dependency cache
key accurately: it covers the workspace's own install inputs — including
members outside the bundle — plus the synthesized placeholder manifests
and the pruned lockfile as additional bundle-time inputs. The
caching.dependencyCache.version JSDoc now points at ComposeCacheHashInput
as the single authoritative input list. CLAUDE.md lists
CHECKLY_LOCKFILE_PRUNE (and the previously undocumented
CHECKLY_CACHE_DIR).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Backfill also covers workspace members referenced through
  peerDependencies (plugin-style monorepos previously failed pruning on
  every run) and optionalDependencies, which the parser's faux-manifest
  mechanism does not see; the integration fixture now exercises the
  backfill registration path end to end via an optionalDependencies link.
- Skips that occur after the bundle is known to be a partial workspace
  ("pruning needed but unavailable": unsupported package manager or
  lockfile format, excludeLinksFromLockfile, missing pnpmfile, unknown
  member version) now print a one-line notice instead of being
  debug-only, so users whose remote installs will still fail get a
  pointer; nothing-to-do skips stay quiet.
- The pnpmfile is only bundled when the lockfile records a
  pnpmfileChecksum: without one there is nothing to reproduce, and
  shipping a pnpmfile alongside a lockfile that does not record its
  checksum would itself make pnpm treat the lockfile as out of date.
  This also silences the not-bundling warning for pnpmfiles that cannot
  affect the remote install.
- The pnpmfile self-containedness analysis rejects `Function` (an eval
  equivalent that could reach process via `Function('return process')`)
  and no longer misreports `new.target` as `import.meta`.
- Structurally valid pnpm lockfiles at out-of-range versions are covered
  by a fail-closed test, and the pnpmfile warning dedup uses object
  identity (WeakSet) instead of a module-global set with a test-only
  reset export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ps [RED-886]

pnpm's lockfile-only install now passes --lockfile-dir '.' so a
lockfile-dir setting inherited from any config layer (project, user or
global npmrc, pnpm-workspace.yaml, env) cannot redirect the subprocess's
lockfile write outside the prune temp dir — potentially over the user's
real lockfile. npm_config_lockfile_dir is additionally stripped from the
child env as defense in depth. The unsupported-package-manager check is
documented and test-pinned to run before the lockfile read, so yarn/bun
workspaces always get the notable skip instead of a misleading failure
warning. Optional workspace peers are deliberately still backfilled:
pnpm resolves workspace: peer specs regardless of
peerDependenciesMeta.optional when auto-install-peers is on, verified by
a new real-pnpm test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s scope [RED-886]

The checksum gate in loadWorkspacePnpmfiles is merged into the function
body instead of delegating to a separate helper, and logs a debug line
when it suppresses pnpmfile bundling. analyzePnpmfile's docstring now
records its threat model: the analysis assumes a non-adversarial author
and deliberately does not chase reflective escape hatches such as
constructor-property access, because rules broad enough to catch them
would false-positive on ubiquitous pnpmfile patterns like
pkg.dependencies[name]. A new fixture covers the gate's wiring: a
workspace with a .pnpmfile.cjs whose lockfile records no pnpmfileChecksum
reports no pnpmfiles at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…RED-886]

The note printed when a partial-workspace bundle ships an unpruned
lockfile now points at CHECKLY_LOCKFILE_PRUNE=0, framed as a last resort
for setups that cannot be pruned. Both note paths are now covered by
bundler tests: an unsupported package manager prints the note, and a
full-workspace bundle stays silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[RED-886]

The pnpmfile bullet now states the pnpmfileChecksum precondition for
bundling and lists Function among the forbidden references. The pruning
bullet distinguishes the note-printing skips (a bundled lockfile
over-describes a partial-workspace bundle but pruning cannot run) from
the silent ones (nothing to prune, no bundled lockfile, or pruning
explicitly disabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…filter [RED-886]

parseLockfilePackagesContent extracts the format dispatch from
loadLockfilePackages so lockfiles that exist only in memory — such as a
pruned copy of the bundled lockfile — can be parsed. The new
filterTarballsByLockfile splits a planned embedded-tarball set by
name@version presence in a lockfile's registry packages; since a pruned
lockfile's resolutions are verified to be a subset of the original's,
presence filtering is exactly equivalent to re-running spec matching.
An unparseable lockfile fail-safes to keeping every tarball.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…finalize [RED-886]

Embedded package tarballs (bundle.packages.embed) were downloaded and
registered during per-check bundling, before the bundled lockfile was
pruned — so tarballs whose only referents are dependencies of workspace
members outside the bundle were downloaded and shipped even though the
runner's install could never use them. For projects with large multi-
version private packages this meant 100+ MB of pointless cold-cache
downloads.

Materialization now happens once, in Bundler.finalize(), after pruning:
the planned set is filtered to what the (possibly pruned) bundled
lockfile still references, only that subset is sourced (cache or
download) via the new EmbeddedPackagesMaterializer.materializeTarballs,
and the cache hash's embedded-package records reflect exactly the
shipped set — every hash computation passes it explicitly. The
memoized materialize() is gone with its per-check caller. A stderr line
announces the tarball preparation, and an empty bundle never triggers
downloads (every command calls finalize() unconditionally, and embed
validation is skipped for projects without Playwright checks). Download
failures (EmbeddedPackageError) now surface at finalize instead of
during per-check bundling — same command, same error type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New workspace fixture where the imported member depends on ms@2.1.3
(embedded via a genuine registry tarball committed as a fixture — a fake
package cannot survive pnpm's re-resolution of a stale lockfile) and the
shimmed-only member on the fake @acme/private-utils@1.2.3. The sandbox
test runs a real pnpm prune and asserts the dropped package's tarball is
neither shipped nor requested (only the kept tarball is seeded, so any
attempt to materialize the dropped one would fail loudly), the pruned
lockfile no longer references it, and the payload cacheHash matches the
kept-only embedded set exactly. parseProjectWithOptions now returns the
sandbox run's stderr, which also turns two previously vacuous
String(result.stderr) assertions in the symlink workspace test into real
ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ED-886]

The embed and caching bullets (and the bundle.packages.embed JSDoc) now
state that when the bundled lockfile is pruned, the embedded set follows
it — packages the pruned lockfile no longer references are neither
embedded nor downloaded — with the benign explanation first (an
out-of-bundle workspace member needing the package means the runner does
not need it), bringing the member into the bundle as the remedy, and
CHECKLY_LOCKFILE_PRUNE=0 framed as a last resort. The cold-cache
registry-access note is scoped to the tarballs actually shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'Pruning the bundled lockfile...' and 'Preparing N embedded package
tarball(s)...' stderr lines are not worth showing until the overall CLI
output UX improves; the tarball line is now a debug log and the pruner's
onRun hook is removed outright (its only remaining purpose was a debug
line the pruner's own 'Running ...' debug output already covers).
Warnings and the notable-skip note stay user-facing. The bundler spec's
stderr spies filter out debug-library writes so the tightened
empty-stderr assertions hold even with DEBUG enabled, and the
full-workspace E2E test observes the prune skip through scoped DEBUG
output — its previous stderr assertion would have passed vacuously once
the progress line left stderr.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The lockfile pruner gains tests that run a real bun install and skip
themselves when bun is not on PATH. Both unit-test jobs install a
pinned bun and set CHECKLY_EXPECT_BUN, which makes the suite assert
that the provisioning worked instead of skipping the coverage silently.
The version is pinned because the tests regenerate a committed bun.lock
fixture and a future bun may serialize it differently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bun projects now get the same bundled-lockfile pruning as pnpm and npm:
BunDetector gains a lockfile-only install command (bun install
--lockfile-only --ignore-scripts, verified against bun 1.3.11), and the
pruner parses bun.lock (JSONC, lockfileVersion 1) into its
format-agnostic snapshot. Resolutions are compared as a presence set of
whole tuples because pruning re-keys surviving member-scoped entries,
and dependency edges resolve workspace links per edge (member-scoped
packages key first, then hoisted) because a workspace member's name may
also be consumed from the registry by a different importer. The binary
bun.lockb format skips notably with a pointer at
`bun install --save-text-lockfile`.

Because bun re-roots at any ancestor package.json whose workspaces glob
matches the working directory — writing the regenerated lockfile at
that root, outside the temp dir and over a real file — the pruner
refuses to run when its temp dir sits inside a workspace, parsing
ancestor manifests with bun's own JSONC leniency and failing safe on
unparseable ones. A missing package manager binary and a regenerated
lockfile that bun deleted (a lockfile describing no packages) surface
as notable skips rather than failures blaming the user's lockfile.
Patch files are auto-included for bun as they already were for pnpm, so
bun patchedDependencies survive both bundling and the temp-dir install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bundle.packages.embed now accepts bun's text bun.lock: packages parse
from the lockfile tuples (registry URL at index 1, sha512 integrity at
index 3), with workspace tuples excluded under the precise
workspace-package reason and git/file/URL refs excluded as unfetchable.
The name@ref splitting and registry-entry classification that the bun
parser shares with the pnpm parser move into common helpers. The
pruned-lockfile tarball filter accepts bun.lock through the same
dispatch, and bun.lockb keeps failing with a --save-text-lockfile hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pruning and embedded-packages references and the
bundle.packages.embed JSDoc now name bun (text bun.lock, version 1)
alongside pnpm and npm, including the bunfig.toml limitations: it is
not carried into the lockfile regeneration or the tarball downloads, so
registry credentials must live in .npmrc (referenced through
environment variables, never plaintext, since .npmrc ships with the
bundle), and a lockfile bun declines to reuse re-resolves against the
default registry — disclosing package names to it — before pruning
rejects the result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s [RED-886]

On Windows a missing executable never surfaces as a spawn ENOENT: execa
spawns through cross-spawn, which wraps an unresolvable command in
cmd.exe, so the child "runs" and exits non-zero with cmd.exe's
not-recognized message — and the pruner reported that as a failed prune
with advice to refresh a perfectly fine lockfile (seen on the Windows
CI leg). The failure branch now classifies after the fact with a PATH
probe and maps a missing binary to the intended notable skip. Probing
only an already-failed run means a probe/spawn resolution disagreement
can never block a working prune; only the reported reason is at stake.

PathLookup is hardened toward parity with the spawn's own resolver:
individually quoted Path entries are unquoted and PATHEXT falls back to
.EXE/.CMD/.BAT/.COM when unset. Its detectPresence() sibling has a
pre-existing missing-await bug that makes it vacuously succeed; that is
now documented in place and tracked as RED-887 rather than fixed in
passing, since fixing it changes package-manager detection outcomes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lisions [RED-886]

On win32, node's spawn sorts env keys and deduplicates them
case-insensitively keeping the first — uppercase sorts before
lowercase, so the CI runner's ambient NPM_CONFIG_REGISTRY silently
displaced the test's lowercase npm_config_registry sentinel and the
kept-variable assertion read undefined. The test now drops every
ambient case-variant before setting the sentinel, so exactly one casing
exists regardless of the runner's environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend bundled-lockfile pruning to Yarn Berry (yarn.lock metadata versions
6/8/10; Yarn Classic stays unsupported and fails closed with a clear skip).
YarnDetector gains `yarn install --mode=update-lockfile`, and the pruner
grows a yarn.lock snapshot parser that treats workspace entries as importers
(their dependency/peer maps as edges) and every other entry as a serialized
presence set immune to descriptor re-keying.

Regeneration runs under strict environment isolation: the network is
disabled (a stale lockfile then fails fast instead of resolving a missing
descriptor against the public registry and disclosing its name), scripts are
disabled, hardened mode is turned off only for a probe-confirmed Yarn 4
(Yarn 3 rejects the unknown setting), and — because yarn honors a .yarnrc
from any ancestor of the temp dir — yarnPath is neutralized (YARN_IGNORE_PATH
plus a randomized rc filename) so an uncontrolled ancestor rc on a shared
host cannot execute arbitrary code during the version probe. A pre-spawn
version probe refuses Yarn Classic (which silently full-installs on this
flag) and reports a lockfile/binary generation mismatch as an actionable
skip rather than a blocked-registry failure.

yarn.lock is parsed with the YAML failsafe schema so Yarn 3's unquoted
numeric ranges survive as strings. `.yarn/patches` is auto-included so
patch: dependencies regenerate. Committed real-yarn fixtures for both
generations (4.18.0 and 3.8.7) drive end-to-end prune tests, gated on a
Corepack-provisioned yarn and asserted present in CI via CHECKLY_EXPECT_YARN.

This commit also fixes a pre-existing patches auto-include prefix-match bug
(a sibling directory like patches-archive suppressed the include) for pnpm
and bun alongside yarn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enable a Corepack-managed yarn (scoped to yarn so its pnpm shim cannot
shadow the pinned pnpm) in both unit-test jobs and pre-download the two
fixture-pinned Yarn Berry versions, so a corepack/registry failure surfaces
as an obvious step failure rather than inside the pruner's timed subprocess.
CHECKLY_EXPECT_YARN makes the suite assert yarn resolves to the fixtures'
Berry version, so a lost provisioning step or a shadowing Classic yarn fails
the job instead of silently skipping the real-yarn lockfile-pruner tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D-886]

A prune timeout below the install floor can never succeed, but the check
ran after the version probe — so under a tiny timeout the probe itself
timed out first, yielding a misleading "timed out" result (and a flaky
test). Move the pure check ahead of the probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend `bundle.packages.embed` to Yarn Berry. Berry lockfiles record no npm
tarball integrity (their checksum hashes yarn's own cache archive, not the
registry tarball), so parseYarnLockfilePackages classifies registry entries
without an SRI integrity — carrying the Berry checksum as a content pin for
the dependency-cache hash instead — and the materializer resolves the real
tarball integrity from the registry's package metadata at finalize time. It
tries the abbreviated per-version route first and falls back to the full
packument when that route is absent, using the same npmrc-derived registry
and credentials as the tarball download; an old-style sha1 shasum is
accepted when the metadata carries no SRI. A checksum-less entry, a
workspace/portal/link/patch/git ref, or an unresolved range is excluded, and
Yarn Classic lockfiles are rejected with a clear message.

LockfileRegistryPackage.integrity becomes optional (present for
pnpm/npm/bun, absent for yarn) alongside a new lockfileChecksum; the two
cache-hash call sites map planned tarballs through embeddedPackageHashInputs,
which uses the integrity or, for yarn, the Berry checksum — a stable pin
known at plan time — so the eager and finalize hashes stay consistent
without depending on the metadata roundtrip.

Trade-off: because the CLI/npm caches are keyed by integrity, yarn embeds
incur a small per-package metadata request on every deploy even with a warm
cache; this is documented where embed is configured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…D-886]

Note in the bundle.packages.embed config JSDoc and the Playwright
AI-context reference that pruning and embedding now cover Yarn Berry
yarn.lock (Classic excepted), that Berry integrity is resolved from
registry metadata (per-version route with a packument fallback, needing
registry reachability on every deploy), and that yarn credentials for
embed downloads must live in .npmrc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cross-spawn wraps every cmd.exe argument in quotes, so the fake yarn.cmd
saw "--version" (quoted) and never took its version branch — the probe
fell through to a failed install and the test saw 'failed' instead of the
Yarn Classic skip. Use %~1 to strip the quotes, matching how the real
corepack yarn.cmd forwards args to node. Test-only; win32-gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant