Skip to content

ci(pnpm): install with pnpm everywhere and retire package-lock.json - #11859

Merged
yiliang114 merged 36 commits into
mainfrom
feat/pnpm-ci-release-install
Sep 20, 2026
Merged

yiliang114 merged 36 commits into
mainfrom
feat/pnpm-ci-release-install

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

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 publish and every npm 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-scripts and their explicit postinstall and generate steps), and the two in the VS Code companion release. The hoisted pnpm layout lets npm run keep working unchanged.

  • setup-node no longer restores an npm cache. Nothing reads it any more. package-manager-cache: false is now explicit on every touched setup-node step. Without it, setup-node v5+ sees packageManager: pnpm@… in package.json and 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.yaml and 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 version in workspaces reifies node_modules by default, and the old step 9 ran a full npm 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. Both npm version calls now pass --no-workspaces-update, and step 9 refreshes package-lock.json only. pnpm-lock.yaml does not change on a version bump, because .pnpmfile.mjs rewrites every internal dependency to workspace:*.

  • Scripts that located packages through package-lock.json keys now read what is installed.

    • The VS Code notices generator resolves each dependency with Node's own lookup over the installed tree, so the notices describe what the extension bundles, whichever package manager laid the tree out.
    • The sharp version pinned into the published CLI manifest is read the same way.
    • The standalone release reads every target's clipboard and OpenTUI native package version from 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.

    • The result: zod 4 landed in web-shell's transcript bundle (+100,903 bytes pre-minify), and the export renderer built from the same commit grew from 1,858,186 to 1,930,015 bytes, past its 1,930,000-byte budget.
    • This was confirmed by comparing both trees' esbuild metafiles and prebuilt bundles in CI. The whole difference is in transcript.js, and the added code is zod 4 plus the ext-apps schemas; the sdk daemon bundle is byte-identical.
    • The fix declares zod: 3.25.76 as a root devDependency. With resolve-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 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. Its spawnSync overloads type process-registry.ts's stdout as string | NonSharedBuffer, which fails TS2322.
    • The same code passes on every npm-installed PR.
    • Declaring @types/node: 20.19.1 at 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, so integration-tests/ and scripts/ — 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.

    • The root now declares, as 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.
    • 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.
  • 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:

    • e2e, tui-parity, repo-hygiene, web-shell-visuals, serve-ab, sdk-java, windows-runner-smoke and qwen-autofix;
    • the SDK releases, finalize-release and sync-release-to-oss;
    • the root installs in desktop-release and cd-cua-driver;
    • the sandbox Dockerfile.

    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 predate pnpm-lock.yaml, so those installs fall back to npm ci when 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 run corepack pnpm install --frozen-lockfile --store-dir … as the unprivileged node user. The single install retry now clears node_modules first, because pnpm install, unlike npm ci, keeps it. npm-cache.yml becomes pnpm-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 reads pnpm-lock.yaml directly, so the root install step goes. The two vendored lockfiles outside the pnpm workspace keep npm ci and npm audit. The retry also recognises pnpm's endpoint error, ERR_PNPM_AUDIT_BAD_RESPONSE. Run against this lockfile, pnpm audit reports 1 low and 4 moderate advisories, and no high.

  • The root package-lock.json is retired; pnpm-lock.yaml is the only root lockfile.

    • npm run check:lockfile keeps 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's strictDepBuilds and every CI install's --frozen-lockfile already enforce what they covered.
    • pnpm-lock-freshness.yml goes, since it regenerated pnpm-lock.yaml from package-lock.json.
    • Release: version.js drops its package-lock refresh, the release commit stages pnpm-lock.yaml instead, and the SDK release stages no lockfile; both git adds would otherwise fail 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, which is where its npm ci inside 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-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 write deny list. pnpm-lock.yaml also 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.
  • qwen review installs pnpm repos. Its build/test step installed only a repo with package-lock.json and 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), runs corepack pnpm install --frozen-lockfile through the same disk, budget and sandbox path as npm ci, and counts the tree as installed once node_modules/.modules.yaml exists. Scripts still run through npm 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.yml stays 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 install or corepack pnpm add, committing pnpm-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 fdir and picomatch, which core bundles into the CLI, and esbuild, 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

  • This PR's own CI is the parity run. The enabled Linux test, lint/static and integration lanes run on the ECS pool; hosted runners cover web-shell E2E, the macOS and Windows install jobs, and Windows Desktop Shell. The full macOS and Windows Test lanes are skipped by PR classification. Every executed lane installs with pnpm and runs the existing npm run scripts unchanged. Green here is the evidence the switch needs.
  • The NOTICES check in Lint & Static passes against the regenerated file, which proves the generator produces the same output on CI's tree.
  • The standalone release still stages the right native packages: the clipboard spec list read from pnpm-lock.yaml is unchanged (@teddyzhu/clipboard*@0.0.5 for every target), and the existing install-script test pins it.
  • Before merging, run a release dry-run from this branch — the release workflow's dry_run input. 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

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ⚠️

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

  • First pnpm run on the ECS pool. These runners reuse their pre-installed Node, so this is the first time Corepack fetches pnpm through that egress path. The CI on this PR, which runs on the pool, is where that shows up first.
  • pnpm 11's supply-chain policies now apply to every CI run, not only the opt-in smoke job. minimumReleaseAge defaults to one day, so a dependency bump to a version published less than 24 hours ago fails CI until it ages. strictDepBuilds fails an install script nobody approved. Both are the point of the migration, but they are new failure modes for dependency PRs.
  • The pnpm store on ECS hosts grows without pruning. The same is true of the npm cache today. A prune schedule is a follow-up.
  • The triage store starts cold. pnpm-store.yml runs on the merge push, because this PR changes pnpm-lock.yaml. Until it finishes, triage restores miss and download from the registry.
  • Reviews run the published CLI. qwen-code-pr-review installs @qwen-code/qwen-code@latest, so reviews keep today's behaviour until a release ships this PR's qwen review change. That behaviour is not fatal: without package-lock.json it reports the repo as unsupported and tells the reviewer to install dependencies itself, so reviews are slower and less scoped until then.
  • Breaking changes / migration notes: npm ci no longer works at the repository root, because package-lock.json is gone. Install with corepack pnpm install --frozen-lockfile, as CI does, and change dependencies with corepack pnpm add or corepack pnpm install so that pnpm-lock.yaml is updated. Open PRs that edit package-lock.json need to make that change in pnpm-lock.yaml instead.

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 的键来定位包的脚本,现在改读实际安装的内容。

    • VS Code 的 NOTICES 生成器用 Node 自己的查找规则在已安装的树上解析每个依赖,因此 NOTICES 描述的就是扩展实际打包的内容,无论这棵树由哪个包管理器铺设。
    • 写进发布出去的 CLI manifest 的 sharp 版本也按同样方式读取。
    • 独立发布包从 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。

    • 结果:zod 4 被打进了 web-shell 的 transcript 产物(压缩前多 100,903 字节),同一个提交构建出的导出渲染器从 1,858,186 字节涨到 1,930,015 字节,超过了 1,930,000 字节的预算。
    • 这一点是在 CI 上分别对比两棵树的 esbuild metafile 和预构建产物确认的:差异全部在 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 的类型定义。

    • npm 把 @types/node 20.19.1 提升到那里,因为 acp-bridge 和 core 都钉死了这个版本。
    • pnpm 在根项目没有声明它的情况下,提升的是 22.20.1。它的 spawnSync 重载会把 process-registry.ts 里的 stdout 推断为 string | NonSharedBuffer,于是报 TS2322。
    • 同样的代码在所有用 npm 安装的 PR 上都能通过。
    • 在根目录声明 @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。
    • npm 本来就会链接它们,所以它的依赖树不变;让 npm 重新解析 lockfile,得到的根条目完全相同。
    • .pnpmfile.mjs 会把它们改写为 workspace:*,pnpm 于是在根目录建立链接。
  • NOTICES.txt 按 pnpm 安装的树重新生成。

  • 其余所有在仓库根目录安装依赖的 workflow 也一起切换, 让任何 job 测试的都不是发布之外的另一张依赖图。包括:

    • e2e、tui-parity、repo-hygiene、web-shell-visuals、serve-ab、sdk-java、windows-runner-smoke 和 qwen-autofix;
    • 各 SDK 的发布、finalize-release 和 sync-release-to-oss;
    • desktop-release 和 cd-cua-driver 中在根目录安装的那一步;
    • sandbox 的 Dockerfile。

    托管 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 重新安装根目录。
    • cd-mobile-mcp 改为从根目录的 pnpm lockfile 安装——它原来在 workspace 内执行的 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 的工具。把发布的安装放在同一个改动里一起切,才能保持只有一张依赖图。版本号更新那处修复,是让这一点真正成立、而不只是在纸面上成立的关键。

审阅者测试计划

如何验证

  • 本 PR 自己的 CI 就是一致性验证。 当前启用的 Linux Test、Lint & Static 和 integration 通道跑在 ECS 池上;托管 runner 覆盖 web-shell E2E、macOS 和 Windows 安装任务,以及 Windows Desktop Shell。macOS 和 Windows 的完整 Test 通道由 PR 分类器跳过。所有实际执行的通道都用 pnpm 安装,并原样运行现有的 npm run 脚本。这里全绿,就是这次切换所需的证据。
  • Lint & Static 里的 NOTICES 检查通过(对照重新生成的文件),证明生成器在 CI 的树上产出相同的结果。
  • 独立发布包仍然会暂存正确的原生包:从 pnpm-lock.yaml 读出的 clipboard 清单没有变化(每个目标平台都是 @teddyzhu/clipboard*@0.0.5),现有的 install-script 测试固定了这一点。
  • 合并前,请从本分支跑一次发布 dry-run(发布 workflow 的 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

风险与范围

  • ECS 池上第一次跑 pnpm。 这些 runner 复用预装的 Node,所以这是 Corepack 第一次经由那条出口路径去拉取 pnpm。本 PR 的 CI 会在池上运行,问题会最先在那里暴露。
  • pnpm 11 的供应链策略现在作用于每一次 CI,而不只是可选的 smoke job。 minimumReleaseAge 默认为一天,所以把依赖升级到发布不足 24 小时的版本,CI 会失败,直到它满一天为止。strictDepBuilds 会让未经批准的安装脚本直接失败。这两点正是迁移的目的,但对依赖类 PR 来说是新的失败方式。
  • ECS 主机上的 pnpm store 会持续增长,没有清理。 今天的 npm 缓存也是如此。定期清理放到后续处理。
  • triage 的 store 起初是冷的。 本 PR 改动了 pnpm-lock.yaml,所以合并时的 push 会触发 pnpm-store.yml。在它跑完之前,triage 的恢复都会未命中,改从 registry 下载。
  • 审查使用已发布的 CLI。 qwen-code-pr-review 安装的是 @qwen-code/qwen-code@latest,所以在包含本 PR qwen 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。

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.
@yiliang114 yiliang114 changed the title ci(pnpm): install dependencies with pnpm in CI and release ci(pnpm): install dependencies with pnpm in CI, release and every workflow Sep 14, 2026
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.
@yiliang114 yiliang114 changed the title ci(pnpm): install dependencies with pnpm in CI, release and every workflow ci(pnpm): install with pnpm everywhere and retire package-lock.json Sep 14, 2026
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.
yiliang114 and others added 5 commits September 17, 2026 12:23
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.
@yiliang114
yiliang114 marked this pull request as ready for review September 18, 2026 03:07
@wenshao

wenshao commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

PR 11859 verification — round 2 (follow-up, Linux aarch64)

Verdict: findings — 41/41 scripted assertions passed at head 34ab9f2349887cc2385fd29e95d0003c31ff27ec; the carried-forward findings are re-measured below, and one of them (export-renderer size gate) has sharpened from "watch" to "about to trip".

中文摘要

结论:findings(41/41 断言通过,无阻断性缺陷)——本轮是跟进轮,针对新 head 34ab9f23(R1 之后合入了两次 main),在 Linux aarch64 / Node v24.14.0 上重新实测了上一轮的全部负载结论:

  • 上一轮发现状态:R1-7(Playwright 门在快照键缺失时静默空转)→ 已修复,且在真实锁文件上复测两根损坏臂都能触发门禁(见"Findings re-measured"表);R1-26(version.js 第 9 步删掉 pnpm 的活符号链接)→ 仍然存在,仍无可观察损害;四项实测结论全部重测,其中导出渲染器体积门余量从 17,141 B 缩到 1,778 B(0.09%)——这是 main 合入带来的增长,不是本 PR 引入,但合并后第一个超线的将是它。
  • A/B 一致性:同一 head 两棵树(corepack pnpm install --frozen-lockfile vs 还原 base 的 package-lock.json 后 npm install),5/5 探针两臂全绿:根 @types/node 20.19.1、web-shell 的 zod 3.25.76、全树 zod 布局完全相同、根层与 integration-tests 都能导入 @qwen-code/qwen-code-core。transcript.js(当年被 zod 4 撑大 100 KB 的产物)两臂逐字节相同(sha256 35b11c72…,3,645,360 B)。
  • 门禁:check-lockfile 通过;pnpm audit --prod --audit-level high 1 low + 4 moderate、exit 0(与 PR 正文一致);根目录 npm ci 如期 EUSAGE;npm run typecheck(含 integration)0 错误;pnpm 树上重新生成的 NOTICES.txt 与提交文件逐字节相同(npm 树上则漂移 70 行,与 R1 一致)。
  • 增量(9861c497 评审阻断修复 + 51aa6fc desktop 修复):触及的 10 个脚本测试文件 472/472 通过;新测试"rejects a missing snapshot for a pinned package"经变异验证非空转——回退修复代码后恰好这条测试变红,恢复后 13/13 全绿。合并卫生:lockfile 保持删除、.gitignore 兜底、release-sdk 只暂存 manifest、3 处 --no-workspaces-update 均在。
  • 变异臂(删掉根 zod: 3.25.76 声明重装):见正文第 4 节。
  • 未覆盖:ECS 池首次 Corepack 拉取、托管 runner 的 store 缓存 action、triage/tmux 车道、发布 workflow 本体(作者在 fcee5770 的 dry_run 覆盖了旧 head)、Windows/macOS(R1 覆盖 macOS)。

Previous-finding status (R1 @ 5f4b8d8755, macOS) — re-measured @ 34ab9f23, Linux aarch64

# 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

A/B tree parity

Build-artifact parity

artifact parity

  • packages/web-shell/dist/transcript.js — the artifact the zod-4 regression grew by 100,903 B — is byte-identical across arms: sha256 35b11c725045617e28e1c023c6abfbc6d0b47fcaf989a33e2c1f1a4ffac70cd9, 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.txt regenerated 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 install against this head's package.json changes the restored base lockfile by 13 content lines, all inside the root importer's devDependencies — 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

findings re-probed

  • R1-7 — fixed and pinned. On the real lockfile: injecting a split chromium revision on the live @playwright/test → playwright edge fails the gate (splitting the chromium revision); renaming the snapshot key — the shape that passed green pre-fix — now fails with has no snapshot for [email protected]. Vacuity check: reverting the key === undefined hunk 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 rmSync removes 10 live channel-base symlinks on the pnpm tree; @qwen-code/channel-base still resolves via the root file: link. Unchanged risk: becomes a release-breaker if those root file: devDependencies are ever trimmed.

4. Mutation arm: root zod declaration removed

zod mutation arm

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:scripts and 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/cli review 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.

@wenshao

wenshao commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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
@yiliang114
yiliang114 dismissed a stale review September 20, 2026 02:09

fixed

@yiliang114
yiliang114 added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 64dd058 Sep 20, 2026
70 checks passed
yiliang114 added a commit that referenced this pull request Sep 20, 2026
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
qwen-code-dev-bot pushed a commit that referenced this pull request Sep 20, 2026
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]>
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.

5 participants