Repository navigation
ci(pnpm): install with pnpm everywhere and retire package-lock.json - #11859
Conversation
CI tests the graph release ships only if both install it the same way, so this moves both, keeping every `npm run` script and `npm publish` as they are. - ci.yml (7), release.yml (3) and release-vscode-companion.yml (2) install with `corepack pnpm install --frozen-lockfile`; release keeps --ignore-scripts plus its explicit postinstall and generate steps. - setup-node drops the npm cache and sets package-manager-cache: false, which setup-node v5+ needs once package.json declares a pnpm packageManager. - A composite action caches the pnpm store on the three always-hosted jobs; the ECS pool keeps its store on local disk. - version.js no longer lets npm reify node_modules: --no-workspaces-update on both `npm version` calls, and a package-lock-only refresh in step 9. - The notices generator, prepare-package's sharp pin and the standalone release's native package specs read the installed tree or pnpm-lock.yaml instead of package-lock.json keys. - New size baselines for the three workflows, which were already over them.
Generated in CI (pnpm Worktree Smoke on ubuntu-latest) by the ported generator. Same entry set as before except the five packages npm and pnpm resolve differently: fdir 6.5.0, picomatch 4.0.5, mdurl 2.1.0, @types/node 22.20.1 and mime-db 1.52.0 (form-data's pinned copy). No entry lacks a license text or repository.
The first CI run on this branch failed the export renderer's size budget: 1,930,015 bytes against 1,930,000, where the same commit built on npm's tree comes to 1,858,186. Comparing both trees' esbuild metafiles and prebuilt bundles put the whole difference in web-shell's transcript.js (+100,903 bytes pre-minify), and the added code is zod 4 plus the @modelcontextprotocol/ext-apps schemas; the sdk daemon bundle is identical. web-shell bundles ext-apps, whose zod and MCP SDK are peers, and declares no zod itself, so pnpm filled the peer with the highest locked zod (4.4.3) while npm serves the hoisted 3.25.76. Declaring zod 3.25.76 as a root devDependency makes pnpm resolve every such peer to it (resolve-peers-from-workspace-root is on by default), which is npm's hoisting behaviour. mobile-mcp keeps its own zod 4.4.3, as under npm; the npm tree does not change.
The no-AK integration gate typechecks integration-tests/, which takes Node's types from the root node_modules. npm hoists @types/node 20.19.1 there, because acp-bridge and core pin it; pnpm, with no root declaration, hoisted 22.20.1, whose spawnSync overloads type process-registry.ts's stdout as string | NonSharedBuffer and fail TS2322. The same code passes on every npm-installed PR. Declaring 20.19.1 as a root devDependency puts npm's copy back where a root-level tsconfig looks. The npm tree does not change.
The follow-up to switching CI and release: every other workflow that installs the root workspace now uses the pinned pnpm with --frozen-lockfile, so no job tests a graph that release does not ship. - e2e, tui-parity, repo-hygiene, web-shell-visuals, serve-ab, sdk-java, windows-runner-smoke, qwen-autofix, the SDK releases, finalize-release, sync-release-to-oss, the root install in desktop-release and cd-cua-driver, and the sandbox Dockerfile. - setup-node drops the root npm cache (package-manager-cache: false); hosted jobs restore the pnpm store through the shared composite action, and qwen-autofix keeps af-012's rule that the persistent pool restores no remote cache. - serve-ab and web-shell-visuals install a separate base checkout that can predate pnpm-lock.yaml, so those installs fall back to npm ci there. - Out of scope, unchanged: packages with their own lockfile (cua-driver's TypeScript SDK, desktop-shell, live-host, mobile-mcp), and the triage lanes with npm-cache.yml and the npm audit gate, which move with the package-lock.json retirement. - New size baselines for the touched workflows; qwen-autofix stays under the 470000-byte gate.
npm links every workspace package into the root node_modules, so integration-tests/ and scripts/, which are not workspace packages themselves, can import @qwen-code/* by name. pnpm links a workspace package only where it is a declared dependency, and the root declared none, so the no-AK integration gate could not find @qwen-code/qwen-code-core. The root now declares the seven packages that root-level code imports at runtime as file: devDependencies: acp-bridge, channel-base, the CLI, core, sdk, web-shell and web-templates. npm already links them, so its tree does not change (letting npm re-resolve the lock reproduces the same root entry); .pnpmfile.mjs rewrites them to workspace:*, which pnpm links at the root.
- qwen-triage's verify and tmux-testing lanes install the PR with `corepack pnpm install --frozen-lockfile` from a restored pnpm store. The one retry clears node_modules first, because pnpm install, unlike npm ci, keeps it. - npm-cache.yml becomes pnpm-store.yml, keyed on pnpm-lock.yaml. - security-checks audits the root workspace with `pnpm audit`, which reads the lockfile, so the root install step goes. The vendored lockfiles keep npm, and the retry recognises pnpm's endpoint error too. - qwen-autofix drops the hosted-only store cache: its heavy jobs forbid local `uses: './...'` actions, and the file sits at the size gate. - cd-cua-driver: setup-node@v4 has no package-manager-cache input. - package-assets fixtures install sharp rather than listing it in package-lock.json, matching how prepare-package now resolves it. - AGENTS.md, CONTRIBUTING.md and the npm workspace guide install with pnpm.
qwen review installed dependencies only for a repo with package-lock.json and reported a pnpm repo as unsupported, so reviews of qwen-code would lose their build and test once its npm lockfile goes. - lockfileInstaller() picks npm or pnpm from the committed lockfile. With both, `packageManager: pnpm@...` chooses pnpm; anything else keeps npm. - A pnpm repo installs with `corepack pnpm install --frozen-lockfile` through the same disk, budget and sandbox path as `npm ci`, and counts as installed once node_modules/.modules.yaml exists. - Scripts still run through `npm run`, and the npm path is unchanged.
pnpm-lock.yaml is now the only root lockfile. - check-lockfile keeps the pnpm integrity check and Playwright parity, which now reads pnpm importers and snapshots. The npm-vs-pnpm agreement and build-approval checks go: pnpm 11's strictDepBuilds and every CI install's --frozen-lockfile enforce what they covered. - pnpm-lock-freshness.yml goes; it regenerated pnpm-lock.yaml from package-lock.json. - Release: version.js drops its package-lock refresh, the release commit stages pnpm-lock.yaml and the SDK release stages no lockfile, so neither `git add` fails on the missing file. `npm version` in the SDK and mobile-mcp releases passes --no-workspaces-update so it cannot reinstall the root with npm. - cd-mobile-mcp installs from the root pnpm lockfile. setup-node in cd-mobile-mcp, qwen-code-pr-review and desktop-release no longer keys an npm cache on the root lockfile. - check-desktop-isolation reads pnpm importers; build.js, build_sandbox.js and preflight install with pnpm. - pnpm-lock.yaml, pnpm-workspace.yaml and .pnpmfile.mjs join the autofix supply-chain class and repo-hygiene's deny list; pnpm-lock.yaml joins autofix's generated-diff exclusions, sdk-java's path filter and the platform-sensitivity manifests. - .gitignore lists /package-lock.json so an npm install cannot commit it back. Tests and docs follow.
Resolve the .github/workflows/.size-baseline conflict on ci.yml by recording the merged file's byte size (137778): main raised the same entry for the stale workflow size baselines (#11921) and this branch adds its own ci.yml steps. The merged baseline passes the check-workflow-size.sh ratchet for all 56 tracked workflow files.
Conflicts resolved: - .github/workflows/.size-baseline: recorded sizes refreshed from the merged tree for all 56 workflow files. Both sides carried stale entries for files the other side had changed (e.g. main recorded desktop-packaging-check.yml at 1634 while the file was 2033), so the conflict is settled by recording what the merged tree actually contains. - package-lock.json: kept this branch's deletion. The branch retires the root npm lockfile (this PR's title says so); main only modified the file we remove. The lanes that still call `npm ci` are scoped to subpackages with their own lockfiles (packages/cua-driver/typescript, packages/desktop-shell, packages/*/package-lock.json), and `check:lockfile` reads pnpm-lock.yaml. - packages/vscode-ide-companion/NOTICES.txt: kept both sides' regenerated content (the branch's @types/node and mime-db versions, main's @shikijs and @types/hast blocks).
…endency tree The committed NOTICES.txt was generated from an npm install and does not match the tree produced by the pnpm install the release pipeline now uses (659 resolved dependencies, differing @types/node and mime-db versions). Regenerating with the pinned pnpm install makes the freshness check reproducible.
PR 11859 verification — round 2 (follow-up, Linux aarch64)Verdict: 中文摘要结论:
Previous-finding status (R1 @
|
| # | Finding (severity) | Status at new head | Evidence |
|---|---|---|---|
| R1-7 | Playwright parity gate no-ops on a missed snapshot key (Suggestion, defence-in-depth) | fixed — verified load-bearing | probe-r17 arm 2: renamed key now fails the gate with has no snapshot for [email protected]; arm 1 (split revision) still fires; vacuity check below |
| R1-26 | version.js step 9 rmSync deletes pnpm's live per-channel symlinks (cosmetic today) |
stands, unchanged | probe-r26: 10 live channel-base symlinks removed verbatim; @qwen-code/channel-base still resolves via the root file: link this PR adds |
| C1 | Export-renderer size gate nearly exhausted | worsened (main-driven) | 1,912,859 B → 1,928,222 B on the pnpm tree; headroom 17,141 B → 1,778 B (0.09%); npm tree 1,927,783 B. Δ between installers still 439 B. Not caused by this PR, but the first post-merge dependency bump that adds ≥1.8 KB to the document graph fails Lint & Static |
| C2 | NOTICES.txt is now installer-sensitive |
stands, unchanged | regenerated on pnpm tree: byte-identical to committed; on npm tree: 34+/36- = 70 drift lines (same shape as R1) |
| C3 | 51 root-slot version differences is the graph CI adopts | stands (now 50) | census below; esbuild 0.25.6 → 0.25.12 still in the set |
| C4 | Root zod: 3.25.76 line is no longer load-bearing |
stands (re-measured, §4) | mutation arm at this head: identical layout and bundle without the pin; lockfile peer edge flips to [email protected] as before |
Central claim + A/B
Central claim: CI/release install with corepack pnpm install --frozen-lockfile produces the dependency graph release ships, and retiring package-lock.json breaks nothing. Control arm: same head with the base's package-lock.json restored, npm install (R1's recipe).
| Probe | npm arm | pnpm arm | |
|---|---|---|---|
| install | exit 0 | exit 0, 8m09s cold store | both OK |
root @types/node |
20.19.1 | 20.19.1 | parity |
zod from packages/web-shell |
3.25.76 | 3.25.76 | parity |
| zod census (whole tree) | 3.25.76 root · 4.4.3 ×3 (mcp/client, mcp/core, mobile-mcp) | identical | parity |
import @qwen-code/qwen-code-core from / and integration-tests/ |
OK | OK | the 7 root file: links suffice |
root node_modules packages (maxdepth-3 census) |
1475 | 1538 | 50 version diffs, 8 only-npm, 71 only-pnpm |
Build-artifact parity
packages/web-shell/dist/transcript.js— the artifact the zod-4 regression grew by 100,903 B — is byte-identical across arms: sha25635b11c725045617e28e1c023c6abfbc6d0b47fcaf989a33e2c1f1a4ffac70cd9, 3,645,360 B both. (R1 measured a different hash at the old head because main has since moved; the parity claim is what carries forward.)- Export renderer JS (
packages/web-templates/src/export-html/build.mjs): pnpm 1,928,222 B, npm 1,927,783 B; hard gate 1,930,000 → 1,778 B headroom, warning threshold 1,870,000 already crossed on both. NOTICES.txtregenerated on the pnpm tree: byte-identical to committed (CI check passes); npm tree drifts 70 lines (@types/node,mime-db, …).- npm re-resolution control:
npm installagainst this head'spackage.jsonchanges the restored base lockfile by 13 content lines, all inside the root importer'sdevDependencies— zero version resolutions changed. "npm's tree does not change" holds at this head.
Gates
| Gate | Result |
|---|---|
node scripts/check-lockfile.js (pnpm tree) |
passed |
pnpm audit --prod --audit-level high |
1 low, 4 moderate, exit 0 — matches PR text |
npm ci at root (pnpm tree) |
EUSAGE as documented (expected failure, counted as pass) |
npm run typecheck incl. typecheck:integration |
exit 0, 0 errors — the TS2322 class stays fixed |
Delta script tests (10 files touched by 9861c497/51aa6fc1) |
472 passed, 25 skipped |
check-lockfile.test.js at head |
13/13 |
| Merge hygiene (scripted) | 4/4: lockfile deleted, .gitignore backstop, release-sdk.yml stages only packages/sdk-typescript/package.json, --no-workspaces-update in 3 files |
Findings re-measured
- R1-7 — fixed and pinned. On the real lockfile: injecting a split chromium revision on the live
@playwright/test→playwrightedge fails the gate (splitting the chromium revision); renaming the snapshot key — the shape that passed green pre-fix — now fails withhas no snapshot for [email protected]. Vacuity check: reverting thekey === undefinedhunk turns exactly the new test red on the intended assertion (expected '…has no snapshot…', received 'Playwright parity check passed.'), other 12 tests green; restored, 13/13. - R1-26 — stands, still cosmetic. Step 9's verbatim
rmSyncremoves 10 livechannel-basesymlinks on the pnpm tree;@qwen-code/channel-basestill resolves via the rootfile:link. Unchanged risk: becomes a release-breaker if those rootfile:devDependencies are ever trimmed.
4. Mutation arm: root zod declaration removed
Removed "zod": "3.25.76" from the root package.json, ran pnpm install (incremental re-resolution, R1's caveat applies), re-probed: zod physical layout identical (3.25.76 root + 4.4.3 ×3), transcript.js still byte-identical (sha256 35b11c72…). The only change is lockfile peer bookkeeping — ext-apps' nested @modelcontextprotocol/sdk edge flips to [email protected], exactly as R1 measured. C4 stands at the new head: keep the declaration (it pins that edge and mirrors npm), but the guarantee comes from the committed lockfile plus nodeLinker: hoisted. Tracked files restored afterwards.
Not covered
- Runner-side behaviour: ECS pool's first Corepack fetch, the hosted pnpm-store composite action, the triage/tmux lanes, the release workflow end-to-end at this head (the author's dry-run covered
fcee5770, two heads back). - Windows and macOS installs (R1 covered macOS; this run is Linux aarch64 only).
- Full
test:scriptsand package unit suites — the PR's own CI lanes cover these; I ran only the delta-touched files plus the check-lockfile suite. qwen review's pnpm-install path (packages/clireview toolchain) — R1 ran its 120 tests at the old head; not re-run here (files untouched by the delta).
Methodology
Orange Pi 6 Plus, Linux aarch64, Node v24.14.0, pnpm 11.24.0 via Corepack. Head 34ab9f2349887cc2385fd29e95d0003c31ff27ec and base 9e6d058b41c7dc092b1eaba642c72e05159648a5 fetched from QwenLM/qwen-code and verified against the PR metadata. Two worktrees of the head commit: pnpm arm installed with CI's exact corepack pnpm install --frozen-lockfile; npm control arm with the base's package-lock.json restored and npm install (dependency tree unchanged by the PR, so the control differs only by installer). All probes are scripted .mjs/shell assertions in the artifact dir (tmp/pr11859-verify-20260920-062101/): probes.mjs (tree parity), compare-trees.mjs (version census), probe-r17.mjs (lockfile-mutation gate probe), probe-r26.mjs (step-9 probe), plus raw logs per step (logs-*.txt, out-*.txt). Expected failures (npm ci EUSAGE) are encoded as passing assertions.
|
@qwen-code /resolve |
|
Qwen Code attempted to resolve merge conflicts but the run did not complete successfully. Check the workflow run for full logs. |
Main moved 13 commits past this branch's base, which left the PR CONFLICTING and unmergeable. The only conflict is .github/workflows/.size-baseline: e2e.yml grew on both sides (main's #12128 download retry, this branch's pnpm migration), so the entry now records the real merged size, 33227 bytes, instead of either side's stale number. finalize-release.yml keeps this branch's 11634, which is the merged file's real recorded value and within the ratchet allowance. Merge integrity, measured rather than assumed: every one of the 76 files main changed alone is byte-identical to origin/main, and this branch's own diff against main is unchanged at 79 files / +2016 / -34725, so the merge adds no scope to the PR. Verified locally on the merged tree: - .github/scripts/check-workflow-size.sh exits 0 (only the pre-existing qwen-autofix.yml near-gate warning). - scripts/tests/workflow-size.test.js passes, the vitest mirror of that ratchet. - node --test on e2e-build.test.mjs and ci-runner-routing.test.mjs passes 43/43, including main's new "e2e build artifact download retry (consumer legs)" suite reading the merged e2e.yml. - e2e-workflow / ci-platform-lanes / no-ak-integration-ci pass 84/84. - prettier --check is clean on all 79 merged files. - the combined vitest run of workflow-size.test.js and install-script.test.js reports 331 passed / 11 skipped / 1 failed; the failure, "does not package audio-capture test artifacts", is a local environment gap (packages/audio-capture/dist is not built here) on a package neither side touched, not a merge regression. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmu93gu117j
Picks up the pnpm migration (#11859) and the current lint gate, so the freshness check stops short-circuiting this lane and the branch stops carrying a retired package-lock.json. Only conflict was .github/workflows/.size-baseline. It now records the merged byte size of qwen-code-pr-review.yml (279174, verified with wc -c and by check-workflow-size.sh); qwen-fleet-shepherd.yml keeps main's ratchet value because this branch never touched that file. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmu99wbcw7u
Main's pnpm migration (#11859) replaced every e2e.yml install step's `npm ci` with `corepack pnpm install --frozen-lockfile`. Resolve by keeping this PR's bounded install retry and ::warning:: annotation wrapped around the new command, re-pin scripts/tests/e2e-workflow.test.js to the pnpm shape, and re-record e2e.yml's size baseline. Co-authored-by: Qwen-Coder <[email protected]>




What this PR does
Switches dependency installation in CI and release to the pinned pnpm, so CI tests the same dependency graph that release ships. This completes Stages 2 and 3 of #10444, including retiring the root
package-lock.json;npm publishand everynpm run …script invocation are unchanged.CI and release install with
corepack pnpm install --frozen-lockfile. That covers the seven install steps in the main CI workflow, the three in the release workflow (which keep--ignore-scriptsand their explicitpostinstallandgeneratesteps), and the two in the VS Code companion release. The hoisted pnpm layout letsnpm runkeep working unchanged.setup-node no longer restores an npm cache. Nothing reads it any more.
package-manager-cache: falseis now explicit on every touched setup-node step. Without it, setup-node v5+ seespackageManager: pnpm@…inpackage.jsonand tries to cache pnpm before pnpm is on the PATH, which fails the step.A new composite action caches the pnpm store on hosted runners. It is keyed on
pnpm-lock.yamland used by the three jobs that always run on GitHub-hosted runners (web-shell E2E, macOS, Windows). The self-hosted ECS pool keeps its pnpm store on local disk between jobs, just as it does its npm cache today.Release versioning no longer lets npm rewrite the installed tree.
npm versionin workspaces reifiesnode_modulesby default, and the old step 9 ran a fullnpm install. On a pnpm-installed tree that would not fail — it would silently turn the tree back into npm's layout, and the release would build against npm's graph after all. Bothnpm versioncalls now pass--no-workspaces-update, and step 9 refreshespackage-lock.jsononly.pnpm-lock.yamldoes not change on a version bump, because.pnpmfile.mjsrewrites every internal dependency toworkspace:*.Scripts that located packages through
package-lock.jsonkeys now read what is installed.pnpm-lock.yaml. Only the build host's own platform package is installed, so the installed tree cannot answer for the other targets; the lockfile the release installed from can.Undeclared zod peers resolve to the repo's zod, as they do under npm. The first CI run on this PR caught a real divergence. web-shell bundles
@modelcontextprotocol/ext-apps, whose zod and MCP SDK are peers. web-shell declares no zod of its own, so pnpm filled that peer with the highest locked zod (4.4.3), while npm serves the hoisted 3.25.76.transcript.js, and the added code is zod 4 plus the ext-apps schemas; the sdk daemon bundle is byte-identical.zod: 3.25.76as a root devDependency. Withresolve-peers-from-workspace-root(on by default in pnpm 11), every zod peer that a package does not declare itself now resolves to it, which is npm's hoisting behaviour. mobile-mcp keeps its own zod 4.4.3, as it does under npm. The npm tree does not change.Root-level typechecks see npm's root
@types/node. The second CI run caught a second divergence of the same shape. The no-AK integration gate typechecksintegration-tests/, which takes Node's types from the rootnode_modules.@types/node20.19.1 there, because acp-bridge and core pin it.spawnSyncoverloads typeprocess-registry.ts's stdout asstring | NonSharedBuffer, which fails TS2322.@types/node: 20.19.1at the root puts npm's copy back where a root-level tsconfig looks. The npm tree does not change.Root-level code can import workspace packages, as it can under npm. The third CI run caught a third divergence. npm links every workspace package into the root
node_modules, sointegration-tests/andscripts/— which are not workspace packages themselves — import@qwen-code/*by name. pnpm links a workspace package only where it is a declared dependency, and the root declared none, so the no-AK integration gate could not find@qwen-code/qwen-code-core.file:devDependencies, the seven packages that root-level code imports at runtime: acp-bridge, channel-base, the CLI, core, sdk, web-shell and web-templates..pnpmfile.mjsrewrites them toworkspace:*, which pnpm links at the root.NOTICES.txt is regenerated from a pnpm-installed tree.
Every other workflow that installs the root workspace switches too, so no job tests a graph that release does not ship. That covers:
Hosted jobs restore the pnpm store through the same composite action, except in qwen-autofix. Its heavy jobs forbid local
uses: './…'actions, so its hosted fallback installs cold, and the persistent pool still restores no remote cache (af-012). serve-ab and web-shell-visuals also install a separate base checkout, which can predatepnpm-lock.yaml, so those installs fall back tonpm ciwhen the lockfile is absent.The qwen-triage verify and tmux-testing lanes install the PR with pnpm. They still restore a shared store read-only, now a pnpm store keyed on
pnpm-lock.yaml, and runcorepack pnpm install --frozen-lockfile --store-dir …as the unprivilegednodeuser. The single install retry now clearsnode_modulesfirst, becausepnpm install, unlikenpm ci, keeps it.npm-cache.ymlbecomespnpm-store.yml, which fills and saves that store on the same runner and container image.The dependency CVE gate audits the root workspace with
pnpm audit --prod --audit-level high. pnpm readspnpm-lock.yamldirectly, so the root install step goes. The two vendored lockfiles outside the pnpm workspace keepnpm ciandnpm audit. The retry also recognises pnpm's endpoint error,ERR_PNPM_AUDIT_BAD_RESPONSE. Run against this lockfile,pnpm auditreports 1 low and 4 moderate advisories, and no high.The root
package-lock.jsonis retired;pnpm-lock.yamlis the only root lockfile.npm run check:lockfilekeeps the pnpm integrity check and the Playwright parity check, which now reads pnpm's importers and snapshots. The npm-vs-pnpm version agreement and the build-approval check go: pnpm 11'sstrictDepBuildsand every CI install's--frozen-lockfilealready enforce what they covered.pnpm-lock-freshness.ymlgoes, since it regeneratedpnpm-lock.yamlfrompackage-lock.json.version.jsdrops its package-lock refresh, the release commit stagespnpm-lock.yamlinstead, and the SDK release stages no lockfile; bothgit adds would otherwise fail on the missing file.npm versionin the SDK and mobile-mcp releases passes--no-workspaces-update, so it cannot reinstall the root with npm.npm ciinside the workspace used to read the npm one. setup-node in cd-mobile-mcp, qwen-code-pr-review and desktop-release no longer keys an npm cache on the root lockfile, which would fail the step once the file is gone.check:desktop-isolationreads pnpm importers;build.js,build_sandbox.jsandpreflightinstall with pnpm.pnpm-lock.yaml,pnpm-workspace.yamland.pnpmfile.mjsjoin the autofix supply-chain class and repo-hygiene's write deny list.pnpm-lock.yamlalso joins autofix's generated-diff exclusions, sdk-java's path filter and the platform-sensitivity manifests..gitignorelists/package-lock.json, so annpm installcannot commit it back.qwen reviewinstalls pnpm repos. Its build/test step installed only a repo withpackage-lock.jsonand reported a pnpm repo as unsupported, so reviews of qwen-code would have lost their build and test. It now picks the installer from the committed lockfile (with both,packageManager: pnpm@…chooses pnpm), runscorepack pnpm install --frozen-lockfilethrough the same disk, budget and sandbox path asnpm ci, and counts the tree as installed oncenode_modules/.modules.yamlexists. Scripts still run throughnpm run, and npm repos are unaffected.Deliberately unchanged: packages with their own lockfile keep
npm ci— cua-driver's TypeScript SDK, desktop-shell and live-host.Touched workflows get new size baselines. Most were already over their recorded baselines on
main, so any edit engages the ratchet; the new numbers are their sizes after this change.qwen-autofix.ymlstays under the 470,000-byte absolute gate, with 3,261 bytes of headroom.AGENTS.md, CONTRIBUTING.md and the npm workspace guide install with pnpm. Dependency changes now go through
corepack pnpm installorcorepack pnpm add, committingpnpm-lock.yaml.Why it's needed
Switching only CI would leave CI testing a different graph from the one release ships. Measured against the committed lockfiles, 13 direct dependencies still resolve to different versions under npm and pnpm. Among them are
fdirandpicomatch, which core bundles into the CLI, andesbuild, which produces the bundle. Moving release installs in the same change keeps a single graph. The version-bump fix is what makes that true in practice rather than on paper.Reviewer Test Plan
How to verify
Testlanes are skipped by PR classification. Every executed lane installs with pnpm and runs the existingnpm runscripts unchanged. Green here is the evidence the switch needs.pnpm-lock.yamlis unchanged (@teddyzhu/clipboard*@0.0.5for every target), and the existing install-script test pins it.dry_runinput. It is the only thing that exercises release installation, the version bump and the standalone archives end to end.Evidence (Before & After)
N/A. Tooling only.
Tested on
Run locally: Prettier's experimental CLI and ESLint on every changed file, syntax and YAML parsing, and the clipboard spec reader against the committed
pnpm-lock.yaml. Not run locally: any install, build, test suite, or workflow — the PR CI provides the full Linux test lane plus cross-platform installation coverage from the macOS and Windows install jobs and Windows Desktop Shell.Environment (optional)
N/A
Risk & Scope
minimumReleaseAgedefaults to one day, so a dependency bump to a version published less than 24 hours ago fails CI until it ages.strictDepBuildsfails an install script nobody approved. Both are the point of the migration, but they are new failure modes for dependency PRs.pnpm-store.ymlruns on the merge push, because this PR changespnpm-lock.yaml. Until it finishes, triage restores miss and download from the registry.@qwen-code/qwen-code@latest, so reviews keep today's behaviour until a release ships this PR'sqwen reviewchange. That behaviour is not fatal: withoutpackage-lock.jsonit reports the repo as unsupported and tells the reviewer to install dependencies itself, so reviews are slower and less scoped until then.npm cino longer works at the repository root, becausepackage-lock.jsonis gone. Install withcorepack pnpm install --frozen-lockfile, as CI does, and change dependencies withcorepack pnpm addorcorepack pnpm installso thatpnpm-lock.yamlis updated. Open PRs that editpackage-lock.jsonneed to make that change inpnpm-lock.yamlinstead.Linked Issues
Part of #10444: completes Stage 2 and Stage 3.
中文说明
这个 PR 做了什么
把 CI 和发布的依赖安装切到钉住的 pnpm,让 CI 测试的依赖图就是发布出去的那一张。这完成了 #10444 的 Stage 2 和 Stage 3,包括退役根目录的
package-lock.json;npm publish以及所有npm run …的脚本调用都不变。CI 和发布改用
corepack pnpm install --frozen-lockfile安装。 覆盖主 CI workflow 的 7 处安装、发布 workflow 的 3 处(保留--ignore-scripts以及显式的postinstall和generate两步),以及 VS Code 扩展发布的 2 处。pnpm 的 hoisted 布局让npm run可以原样继续使用。setup-node 不再恢复 npm 缓存,因为已经没有东西读它。所有改动过的 setup-node 步骤都显式写上
package-manager-cache: false。否则 setup-node v5+ 会看到package.json里的packageManager: pnpm@…,在 pnpm 还没进入 PATH 时就尝试缓存 pnpm,导致该步骤失败。新增一个复合 action,在 GitHub 托管 runner 上缓存 pnpm store。 缓存按
pnpm-lock.yaml做 key,用在始终跑在托管 runner 上的 3 个 job(web-shell E2E、macOS、Windows)。自托管的 ECS 池会像今天保存 npm 缓存一样,把 pnpm store 留在本地磁盘上跨 job 复用。发布时的版本号更新不再让 npm 改写已安装的依赖树。 在 workspaces 里执行
npm version默认会重排node_modules,原来的第 9 步还会跑一次完整的npm install。在 pnpm 装好的树上,这不会报错,而是悄悄把树改回 npm 的布局,发布最终仍然用 npm 的依赖图构建。现在两处npm version都加上--no-workspaces-update,第 9 步只刷新package-lock.json。版本号提升不会改动pnpm-lock.yaml,因为.pnpmfile.mjs会把所有内部依赖改写为workspace:*。原先靠
package-lock.json的键来定位包的脚本,现在改读实际安装的内容。pnpm-lock.yaml读取每个目标平台的 clipboard 和 OpenTUI 原生包版本。构建机只装了自己平台的原生包,已安装的树回答不了其他目标平台;发布安装时依据的 lockfile 可以。未被声明的 zod peer 解析到仓库统一的 zod,与 npm 一致。 本 PR 的第一轮 CI 抓到了一处真实的差异。web-shell 会打包
@modelcontextprotocol/ext-apps,而它把 zod 和 MCP SDK 声明为 peer。web-shell 自己没有声明 zod,于是 pnpm 用满足范围的最高版本 zod(4.4.3)去满足这个 peer,而 npm 用的是提升到根目录的 3.25.76。transcript.js,新增的正是 zod 4 和 ext-apps 的 schema;sdk 的 daemon 产物逐字节相同。zod: 3.25.76声明为 devDependency。pnpm 11 默认开启resolve-peers-from-workspace-root,于是凡是包自己没有声明的 zod peer,现在都解析到这一份,也就是 npm 的提升行为。mobile-mcp 继续使用它自己声明的 zod 4.4.3,与 npm 下一致。npm 的依赖树不变。根目录层面的类型检查看到的是 npm 的根
@types/node。 第二轮 CI 抓到了第二处同类差异。no-AK 集成门禁会对integration-tests/做类型检查,它从根目录的node_modules取 Node 的类型定义。@types/node20.19.1 提升到那里,因为 acp-bridge 和 core 都钉死了这个版本。spawnSync重载会把process-registry.ts里的 stdout 推断为string | NonSharedBuffer,于是报 TS2322。@types/node: 20.19.1,就把 npm 的那一份放回了根目录 tsconfig 查找的位置。npm 的依赖树不变。根目录下的代码可以像在 npm 下一样导入 workspace 包。 第三轮 CI 抓到了第三处差异。npm 会把每个 workspace 包都链接到根目录的
node_modules,因此integration-tests/和scripts/(它们自己不是 workspace 包)可以按包名导入@qwen-code/*。pnpm 只在某个项目声明了依赖时才链接对应的 workspace 包,而根项目一个都没有声明,所以 no-AK 集成门禁找不到@qwen-code/qwen-code-core。file:devDependencies 的形式,声明根目录代码在运行时导入的 7 个包:acp-bridge、channel-base、CLI、core、sdk、web-shell 和 web-templates。.pnpmfile.mjs会把它们改写为workspace:*,pnpm 于是在根目录建立链接。NOTICES.txt 按 pnpm 安装的树重新生成。
其余所有在仓库根目录安装依赖的 workflow 也一起切换, 让任何 job 测试的都不是发布之外的另一张依赖图。包括:
托管 runner 上的 job 通过同一个复合 action 恢复 pnpm store,qwen-autofix 除外:它的重型 job 禁止使用本地
uses: './…'action,所以托管回退冷安装,自托管池仍不恢复任何远程缓存(af-012)。serve-ab 和 web-shell-visuals 还会另外检出一份 base 代码来安装,而它可能早于pnpm-lock.yaml,所以这两处在没有该 lockfile 时退回npm ci。qwen-triage 的 verify 和 tmux-testing 两条 lane 用 pnpm 安装 PR。 它们仍以只读方式恢复共享存储,只是换成按
pnpm-lock.yaml取键的 pnpm store,并以低权限的node用户运行corepack pnpm install --frozen-lockfile --store-dir …。那一次安装重试现在会先清掉node_modules,因为pnpm install与npm ci不同,会保留它。npm-cache.yml改为pnpm-store.yml,在相同的 runner 和容器镜像上填充并保存这个 store。依赖 CVE 门禁用
pnpm audit --prod --audit-level high审计根 workspace。 pnpm 直接读取pnpm-lock.yaml,所以去掉了根目录的安装步骤。pnpm workspace 之外的两个独立 lockfile 继续用npm ci和npm audit。重试逻辑也能识别 pnpm 的端点错误ERR_PNPM_AUDIT_BAD_RESPONSE。对当前 lockfile 运行pnpm audit,结果是 1 个低危、4 个中危,没有高危。退役根目录的
package-lock.json,pnpm-lock.yaml成为唯一的根 lockfile。npm run check:lockfile保留 pnpm 完整性检查和 Playwright 版本一致性检查,后者改为读取 pnpm 的 importers 和 snapshots。npm 与 pnpm 的版本一致性检查、构建审批检查被移除:pnpm 11 的strictDepBuilds和每次 CI 安装的--frozen-lockfile已经覆盖了它们检查的内容。pnpm-lock-freshness.yml,它的作用是从package-lock.json重新生成pnpm-lock.yaml。version.js不再刷新 package-lock;发布提交改为暂存pnpm-lock.yaml,SDK 发布不再暂存 lockfile,否则这两处git add会因文件不存在而失败。SDK 与 mobile-mcp 发布中的npm version加上--no-workspaces-update,避免用 npm 重新安装根目录。npm ci读的就是根目录的 npm lockfile。cd-mobile-mcp、qwen-code-pr-review 和 desktop-release 中的 setup-node 不再以根 lockfile 作为 npm 缓存的 key,否则文件删除后该步骤会失败。check:desktop-isolation改读 pnpm importers;build.js、build_sandbox.js和preflight改用 pnpm 安装。pnpm-lock.yaml、pnpm-workspace.yaml和.pnpmfile.mjs加入 autofix 的 supply-chain 分类和 repo-hygiene 的写入禁止列表;pnpm-lock.yaml还加入 autofix 的生成文件 diff 排除、sdk-java 的路径过滤和平台敏感清单。.gitignore加入/package-lock.json,避免npm install把它提交回来。qwen review支持安装 pnpm 仓库。 它的构建/测试步骤原来只在有package-lock.json时安装依赖,并把 pnpm 仓库报告为不支持,因此对 qwen-code 的审查会失去构建和测试。现在它按提交的 lockfile 选择安装器(两者都有时,packageManager: pnpm@…选 pnpm),通过与npm ci相同的磁盘、预算和沙箱路径运行corepack pnpm install --frozen-lockfile,并在出现node_modules/.modules.yaml后视为安装完成。脚本仍通过npm run执行,npm 仓库不受影响。刻意保持不变: 带有独立 lockfile 的包继续使用
npm ci,即 cua-driver 的 TypeScript SDK、desktop-shell 和 live-host。改动过的 workflow 更新了体积基线。 其中大多数在 main 上本来就已超出记录的基线,任何改动都会触发 ratchet;新的数字是本次改动后的实际大小。
qwen-autofix.yml仍在 470,000 字节的绝对上限以内,余量为 3,261 字节。AGENTS.md、CONTRIBUTING.md 和 npm workspace 指南改为用 pnpm 安装。 依赖变更现在通过
corepack pnpm install或corepack pnpm add完成,并提交pnpm-lock.yaml。为什么需要
只切 CI 的话,CI 测的依赖图就会和发布的不同。按已提交的 lockfile 实测,仍有 13 个直接依赖在 npm 和 pnpm 下解析到不同版本。其中
fdir和picomatch会被 core 打进 CLI,esbuild则是产出 bundle 的工具。把发布的安装放在同一个改动里一起切,才能保持只有一张依赖图。版本号更新那处修复,是让这一点真正成立、而不只是在纸面上成立的关键。审阅者测试计划
如何验证
Test通道由 PR 分类器跳过。所有实际执行的通道都用 pnpm 安装,并原样运行现有的npm run脚本。这里全绿,就是这次切换所需的证据。pnpm-lock.yaml读出的 clipboard 清单没有变化(每个目标平台都是@teddyzhu/clipboard*@0.0.5),现有的 install-script 测试固定了这一点。dry_run输入)。这是唯一能端到端覆盖发布安装、版本号更新和独立归档包的途径。证据(前后对比)
N/A。只涉及工具链。
测试平台
macOS⚠️ 、Windows ⚠️ 、Linux ⚠️ 。本地运行过:对所有改动文件跑 Prettier 的 experimental CLI 和 ESLint、语法与 YAML 解析检查,以及对照已提交的
pnpm-lock.yaml调用 clipboard 清单读取函数。本地没有运行任何安装、构建、测试套件或 workflow——PR CI 提供完整的 Linux 测试通道,并由 macOS/Windows 安装任务和 Windows Desktop Shell 补充跨平台安装覆盖。环境(可选)
N/A
风险与范围
minimumReleaseAge默认为一天,所以把依赖升级到发布不足 24 小时的版本,CI 会失败,直到它满一天为止。strictDepBuilds会让未经批准的安装脚本直接失败。这两点正是迁移的目的,但对依赖类 PR 来说是新的失败方式。pnpm-lock.yaml,所以合并时的 push 会触发pnpm-store.yml。在它跑完之前,triage 的恢复都会未命中,改从 registry 下载。@qwen-code/qwen-code@latest,所以在包含本 PRqwen review改动的版本发布之前,审查仍保持现在的行为。该行为不会导致失败:没有package-lock.json时,它把仓库报告为不支持,并让审查者自行安装依赖,因此在那之前审查会更慢、范围更粗。package-lock.json已删除,npm ci在仓库根目录不再可用。请像 CI 一样用corepack pnpm install --frozen-lockfile安装,并用corepack pnpm add或corepack pnpm install修改依赖,以更新pnpm-lock.yaml。仍在修改package-lock.json的开放 PR 需要把改动改到pnpm-lock.yaml上。关联 Issue
属于 #10444 的一部分:完成 Stage 2 和 Stage 3。