perf(dev): add pnpm worktree bootstrap foundation (#10449)

* perf(dev): add pnpm worktree bootstrap foundation

Co-authored-by: Qwen-Coder <[email protected]>

* fix(dev): harden worktree bootstrap process handling

Co-authored-by: Qwen-Coder <[email protected]>

* perf(dev): prefer cache-only worktree installs

Co-authored-by: Qwen-Coder <[email protected]>

* fix(dev): preserve cached install cancellation

Co-authored-by: Qwen-Coder <[email protected]>

* fix(build): harden pnpm worktree bootstrap

Validate pnpm lock updates in releases and exercise real installs and builds across supported hosts. Preserve npm release compatibility and keep dependency-only setup from rewriting npm-layout notices.

Co-authored-by: Qwen-Coder <[email protected]>

* chore: record merge attribution

Record the required attribution for the Stage 1 merge without rewriting published history.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(dev): keep worktree bootstrap clean

Use a bootstrap-scoped notice guard because nested npm lifecycle commands replace npm_lifecycle_event. This preserves explicit notice generation while preventing dependency setup from rewriting tracked output.

Co-authored-by: Qwen-Coder <[email protected]>

* ci: track pnpm smoke workflow size

Register the new workflow in the repository size ratchet as required by the main CI gate.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): drop stale importers from the new pnpm lockfile

The lockfile was generated before the WebShell cutover (#9811) removed
@qwen-code/webui from web-shell and the tailwind tooling plus
@qwen-code/webui from vscode-ide-companion, so frozen-lockfile
installs fail on all three smoke platforms with
ERR_PNPM_OUTDATED_LOCKFILE. Drop the five stale importer entries so
the lockfile matches the current package.json manifests; verified with
pnpm 11.24.0 install --frozen-lockfile --lockfile-only.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): refresh pnpm lockfile after merging main

The merge of origin/main added remend@^1.3.1 to packages/cli/package.json
without updating pnpm-lock.yaml, breaking the pnpm Worktree Smoke workflow
frozen-lockfile install. Regenerated with pnpm install.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmtid3rse9h

* fix(scripts): resolve the path variable case-insensitively on Windows

A spread of process.env is an ordinary object, and native Windows shells
expose the path variable as `Path`, so `env.PATH` was undefined there and
findOnPath never located corepack — the offline-first Corepack bootstrap
silently degraded to npx on exactly the hosts it exists for.

Co-authored-by: Qwen-Coder <[email protected]>

* test(scripts): pin the worktree bootstrap guards against mutation

- Split the fallback log on /\r?\n/ like the sibling cmd.exe mocks, so the
  assertion holds on the Windows lane's CRLF output.
- Add a win32 variant bootstrapping with the native `Path` casing to pin
  the case-insensitive lookup.
- Assert the smoke workflow's fail-fast flag, the install-before-clean
  step order, and the no-build guard via a substring on the raw job text.
- Extend the pnpmfile rewrite fixture to devDependencies and
  optionalDependencies, which the committed lockfile already uses.
- Assert the notice-skip guard by effect (writeFile never called) and add
  the flag-absent companion test with I/O stubbed.

Co-authored-by: Qwen-Coder <[email protected]>

* chore(deps): sync pnpm-lock.yaml with qwen-live ACP backend deps

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmtjyz3tdco

* fix(dev): stop patch-package from corrupting the pnpm store

The root postinstall runs patch-package, which rewrites files under
node_modules in place. With pnpm's default import method those files are
hard links into the content-addressable store, so the patch rewrites the
store entry too and its contents stop matching the sha512 it is filed
under. Every subsequent fresh worktree then fails its --offline stage
with ERR_PNPM_NO_OFFLINE_TARBALL, silently falls back to the registry
(exit 0), and the fallback's postinstall corrupts the store again, so the
offline path never hits. The advertised offline timing was only reachable
on a same-tree reinstall, which needs no relinking.

Set packageImportMethod to clone-or-copy: a copy-on-write clone where the
filesystem supports it, a plain copy elsewhere. Either way patch-package
edits only the worktree's own copy and the store entry stays intact, so
later worktrees resolve entirely from the store.

Verified on an isolated fixture store: under the default method the
node_modules file and its store entry share an inode, and an in-place
edit moves the store file's sha512 off its address, after which a fresh
offline install fails with ERR_PNPM_NO_OFFLINE_TARBALL; under
clone-or-copy the same edit leaves the store hash unchanged and the fresh
offline install reports reused 1, downloaded 0.

Also add pnpm-lock.yaml to .prettierignore. prettier reflows it into a
shape pnpm does not emit, so `npm run format` and `pnpm install` fight
over the file; .yamllint.yml already ignores it for the same reason.

Both facts are pinned by tests in scripts/tests/package-scripts.test.js.

Claude-Session: https://claude.ai/code/session_012797rgiteWJxLT9TLkKq8G

* fix(ci): refresh pnpm-lock.yaml for qwen-live prompts deps

Main added prompts/@types/prompts (and the ACP sdk) to
packages/qwen-live/package.json after this branch generated its
lockfile, so the pnpm Worktree Smoke frozen-lockfile install failed
with ERR_PNPM_OUTDATED_LOCKFILE. Regenerated with pnpm 11.24.0
(--lockfile-only); frozen-lockfile and supply-chain policy checks pass.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmtlc9sozfg

* fix(pnpm): close review findings on worktree bootstrap

- Declare @qwen-code/qwen-code-core in vscode-ide-companion so the pnpm
  linker materializes it; regenerate both lockfiles and assert the link in
  the smoke workflow (R5-1)
- Pin @types/node 20.19.1 in packages/core so the pnpm layout compiles
  against the same types npm hoists (D6-1)
- Accept and pin the corepack +sha512 integrity suffix on packageManager;
  strip it for the npx fallback spec (D6-2)
- Cover every workspace member in the pnpmfile rewrite set, mirror the npm
  channel list in pnpm-workspace.yaml, and cross-check both in tests
- Run setup-worktree.js installs with cwd pinned to the checkout, gate the
  smoke clean-check on git status --porcelain, stop post-merge runs from
  cancelling each other, and extend check-lockfile.js to pnpm-lock.yaml

* merge(main): realign pnpm workspace set and lockfiles

Main removed the webui and cua-driver packages; drop both from the pnpm
rewrite set and regenerate the pnpm lockfile against the merged manifests
(picks up playwright, react-markdown, and the other main-side dependency
changes) so the frozen bootstrap matches what the PR merge ref will run.

* style(ci): quote smoke workflow scalars per yamllint

* fix(dev): require Corepack for pnpm bootstrap

* fix(dev): close pnpm bootstrap review blockers

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-08 03:07:40 +00:00
1 parent 663e55bd29
commit 3ecfaffdf8
20 files changed
+21617 -2

No files matched your search

+1
View File
@@ -31,6 +31,7 @@
5950 live-host.yml
7642 main-ci-failure-issue.yml
1686 npm-cache.yml
2489 pnpm-worktree-smoke.yml
7299 pr-force-push-reminder.yml
6495 pr-self-report-label.yml
9646 qwen-autofix-fork-bridge.yml
+100
View File
@@ -0,0 +1,100 @@
name: 'pnpm Worktree Smoke'
on:
pull_request:
branches:
- 'main'
- 'release/**'
paths:
- '.github/workflows/pnpm-worktree-smoke.yml'
- '.npmrc'
- '.pnpmfile.mjs'
- 'package.json'
- 'packages/*/package.json'
- '!packages/desktop-shell/package.json'
- '!packages/live-host/package.json'
- 'packages/channels/*/package.json'
- 'integrations/*/package.json'
- 'patches/**'
- 'packages/audio-capture/install.js'
- 'packages/core/scripts/postinstall.js'
- 'packages/vscode-ide-companion/scripts/generate-notices.js'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'scripts/generate-git-commit-info.js'
- 'scripts/prepare.js'
- 'scripts/pnpm-package.js'
- 'scripts/setup-worktree.js'
push:
branches:
- 'main'
paths:
- '.github/workflows/pnpm-worktree-smoke.yml'
- '.npmrc'
- '.pnpmfile.mjs'
- 'package.json'
- 'packages/*/package.json'
- '!packages/desktop-shell/package.json'
- '!packages/live-host/package.json'
- 'packages/channels/*/package.json'
- 'integrations/*/package.json'
- 'patches/**'
- 'packages/audio-capture/install.js'
- 'packages/core/scripts/postinstall.js'
- 'packages/vscode-ide-companion/scripts/generate-notices.js'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'scripts/generate-git-commit-info.js'
- 'scripts/prepare.js'
- 'scripts/pnpm-package.js'
- 'scripts/setup-worktree.js'
workflow_dispatch:
concurrency:
# Post-merge runs must not cancel each other: a cancelled push run would
# silently erase the only post-merge witness of a regression.
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
permissions:
contents: 'read'
jobs:
install:
name: 'Install (${{ matrix.os }})'
runs-on: '${{ matrix.os }}'
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
- name: 'Set up Node.js 22.x'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '22.x'
- name: 'Install frozen pnpm worktree dependencies'
run: 'node scripts/setup-worktree.js'
- name: 'Verify workspace links resolve'
shell: 'bash'
run: >-
node -e "require.resolve('@qwen-code/qwen-code-core/package.json',
{ paths: ['packages/vscode-ide-companion'] })"
- name: 'Ensure bootstrap keeps the worktree clean'
shell: 'bash'
run: |
status="$(git status --porcelain)"
if [ -n "$status" ]; then
echo 'Bootstrap left the worktree dirty:'
echo "$status"
exit 1
fi
+61
View File
@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
// Every pnpm workspace member name, so any spelling of an internal
// dependency (file:, an exact release version, or *) rewrites to
// workspace:*. scripts/tests/package-scripts.test.js pins this set against
// the workspace manifests, so a newly added package fails that test until
// it is listed here.
export const workspacePackageNames = new Set([
'@qwen-code/acp-bridge',
'@qwen-code/audio-capture',
'@qwen-code/channel-base',
'@qwen-code/channel-dingtalk',
'@qwen-code/channel-dws',
'@qwen-code/channel-feishu',
'@qwen-code/channel-github',
'@qwen-code/channel-gitlab',
'@qwen-code/channel-plugin-example',
'@qwen-code/channel-qqbot',
'@qwen-code/channel-telegram',
'@qwen-code/channel-wecom',
'@qwen-code/channel-weixin',
'@qwen-code/chrome-bridge',
'@qwen-code/external-context',
'@qwen-code/external-context-mem0',
'@qwen-code/mobile-mcp',
'@qwen-code/node-repl-mcp',
'@qwen-code/qwen-code',
'@qwen-code/qwen-code-core',
'@qwen-code/qwen-live',
'@qwen-code/sdk',
'@qwen-code/web-shell',
'@qwen-code/web-templates',
'qwen-code-vscode-ide-companion',
]);
const dependencyFields = [
'dependencies',
'devDependencies',
'optionalDependencies',
];
export const hooks = {
readPackage(packageJson) {
for (const field of dependencyFields) {
const dependencies = packageJson[field];
if (!dependencies) continue;
for (const name of Object.keys(dependencies)) {
if (workspacePackageNames.has(name)) {
dependencies[name] = 'workspace:*';
}
}
}
return packageJson;
},
};
+4
View File
@@ -19,6 +19,10 @@ eslint.config.js
**/generated
gha-creds-*.json
junit.xml
# Generated by pnpm; prettier reflows it into a shape pnpm does not emit,
# so formatting and `pnpm install` fight over the file. .yamllint.yml
# already ignores it for the same reason.
pnpm-lock.yaml
Thumbs.db
packages/vscode-ide-companion/schemas/settings.schema.json
packages/cli/src/services/insight/templates/insightTemplate.ts
+1
View File
@@ -83,6 +83,7 @@ rules:
check-keys: true
ignore:
- 'pnpm-lock.yaml'
- 'thirdparty/'
- 'third_party/'
- 'vendor/'
+10
View File
@@ -87,6 +87,16 @@ the run and names the fix; build once from the repository root:
npm run build
```
**pnpm worktree bootstrap (opt-in):** an additional Git worktree can install
dependencies with `node scripts/setup-worktree.js`, which runs the pinned
pnpm with `--frozen-lockfile` (warm store ≈ 99 MiB on copy-on-write
filesystems such as APFS, btrfs, and XFS with reflink; without reflink it is
≈ 1.2 GiB). npm remains the authoritative path for build, CI, packaging, and
release; the pnpm layout is install-only for now. When dependencies change,
regenerate the pnpm lockfile with
`corepack pnpm install --lockfile-only --no-frozen-lockfile` and commit both
lockfiles together.
**Run individual test files** (always preferred):
```bash
@@ -0,0 +1,80 @@
# pnpm worktree bootstrap
## Problem
Every npm-backed Git worktree materializes another full dependency tree. The
root `prepare` lifecycle also builds and bundles the repository unless the
caller knows to set `QWEN_SKIP_PREPARE`, so a worktree pays for generated
artifacts before source-based development needs them.
On the same commit and APFS volume, a warm-cache npm install added about 1.44
GiB while a warm-store pnpm install added about 99 MiB. The dependency-only
installs took 27 and 22 seconds respectively; a separate full build took about
129 seconds.
## Design
The repository declares pnpm as its package manager and mirrors the existing
npm workspace boundaries in `pnpm-workspace.yaml`. A hoisted linker is used
for the initial migration because current build and packaging scripts still
contain assumptions inherited from npm's layout.
The committed pnpm lockfile is the source of dependency resolution for this
bootstrap only. pnpm-specific overrides preserve the dependency versions used
by the current npm installation without changing npm's manifest or lockfile.
Two versions cannot be preserved by overrides alone and are pinned in
manifests instead: `packages/core` declares the exact `@types/node` release
it compiles against under npm (20.19.1, previously supplied only by
hoisting), and every package `vscode-ide-companion` imports at runtime is
declared in its manifest so the pnpm linker materializes it.
Dependency install scripts are denied unless they are explicitly listed in
`allowBuilds`; the allowlist contains only packages whose scripts run in the
current npm installation.
During the dual-lock transition, `.pnpmfile.mjs` rewrites known internal
dependencies to pnpm's `workspace:*` protocol in memory. This covers both the
existing `file:` dependencies and the exact channel dependency versions that
the release script updates. As a result, release version bumps do not stale the
pnpm lockfile, while the checked-in manifests and npm lockfile remain untouched.
The compatibility hook can be removed when the manifests adopt `workspace:`
during the final cutover.
New worktrees use `node scripts/setup-worktree.js`. The script uses Corepack so
an existing pnpm cache can stay fully offline and the integrity-pinned pnpm
package declared by `packageManager` is verified before execution. It fails
closed when Corepack is unavailable; Node versions that no longer bundle it
must install it separately. The script freezes the lockfile and first
attempts an offline install from the shared local store. It retries with
registry access only when that cache-only attempt is incomplete.
This avoids waiting for pnpm to prefetch optional binaries for other platforms
on the common warm-store path. The script sets `QWEN_SKIP_PREPARE=1` plus a
bootstrap-private notice-generation guard, keeping dependency install scripts
enabled while skipping repository build, bundle, Husky setup, and npm-layout
notice generation. Script execution does not
implicitly install stale dependencies; the bootstrap command is the explicit
installation boundary. Building from this pnpm layout is deferred to Stage 2.
## Migration boundary
Stage 1 applies only to dependency installation in additional Git worktrees.
It does not change repository build commands, CI build orchestration, release
versioning, packaging, or publishing. `package-lock.json` remains authoritative
for every existing npm path. A path-filtered workflow exercises the real frozen
bootstrap on Linux, macOS, and Windows whenever a pnpm installation input
changes. The generated pnpm lockfile is excluded from the repository's
human-authored YAML style rules.
Stage 2 can separately make the pnpm layout a supported build and development
path, then migrate CI installation and caches. Stage 3 can address release
installation and build orchestration. npm remains the product boundary for
package creation, registry publication, and clean artifact installation until
the scripts that intentionally read `package-lock.json` are migrated.
## Verification
The Stage 1 pnpm path must complete a frozen install without modifying tracked
files. Script tests cover the bootstrap command, its prepare-skip environment,
the version-independent workspace rewrite, and process failure behavior. The
cross-platform workflow provides the real install gate. Building from the pnpm
layout is intentionally deferred to Stage 2; existing npm build and release
validation remain unchanged.
+3
View File
@@ -27241,6 +27241,7 @@
"@qwen-code/qwen-code-core": "file:../core"
},
"devDependencies": {
"@types/node": "20.19.1",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
@@ -27950,6 +27951,7 @@
"@types/dotenv": "^6.1.1",
"@types/fast-levenshtein": "^0.0.4",
"@types/minimatch": "^5.1.2",
"@types/node": "20.19.1",
"@types/picomatch": "^4.0.1",
"@types/prompts": "^2.4.9",
"@types/tar": "^6.1.13",
@@ -30949,6 +30951,7 @@
"@agentclientprotocol/sdk": "^0.14.1",
"@modelcontextprotocol/sdk": "^1.30.0",
"@qwen-code/acp-bridge": "*",
"@qwen-code/qwen-code-core": "*",
"@qwen-code/sdk": "*",
"@qwen-code/web-shell": "*",
"cors": "^2.8.5",
+1
View File
@@ -1,6 +1,7 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.23.0",
"packageManager": "[email protected]+sha512.bd27e345e976dcb0be0b7a1228217b049a817e21b1f355c90dbe7dc46671895a8bc1e6d06c24554505ea93ea0b45f489a27ec1bfbc8de6a9659fca0f16fa0000",
"engines": {
"node": ">=22.0.0"
},
+1
View File
@@ -159,6 +159,7 @@
"@qwen-code/qwen-code-core": "file:../core"
},
"devDependencies": {
"@types/node": "20.19.1",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
+1
View File
@@ -149,6 +149,7 @@
"@types/dotenv": "^6.1.1",
"@types/fast-levenshtein": "^0.0.4",
"@types/minimatch": "^5.1.2",
"@types/node": "20.19.1",
"@types/picomatch": "^4.0.1",
"@types/prompts": "^2.4.9",
"@types/tar": "^6.1.13",
@@ -288,6 +288,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.14.1",
"@qwen-code/acp-bridge": "*",
"@qwen-code/qwen-code-core": "*",
"@qwen-code/sdk": "*",
"@qwen-code/web-shell": "*",
"@modelcontextprotocol/sdk": "^1.30.0",
@@ -506,11 +506,26 @@ async function main() {
}
}
export async function runNoticeGeneration(env = process.env) {
const skipGeneration = ['1', 'true'].includes(
(env.QWEN_SKIP_NOTICE_GENERATION ?? '').toLowerCase(),
);
if (skipGeneration) {
console.log(
'Skipping VS Code notice generation during worktree bootstrap.',
);
return;
}
await main();
}
// Only run when executed directly (e.g. `npm run generate:notices`), not when
// imported by tests.
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
main().catch(console.error);
runNoticeGeneration().catch(console.error);
}
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
@@ -15,8 +15,65 @@ import {
findSupplementaryLicenseFiles,
getFallbackLicenseText,
normalizeRepositoryUrl,
runNoticeGeneration,
} from './generate-notices.js';
describe('runNoticeGeneration', () => {
it('skips generation during dependency-only worktree setup', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
// Stubbed no-op so a regression that drops the early return rewrites the
// tracked NOTICES.txt instead of merely failing this assertion.
const write = vi.spyOn(fs, 'writeFile').mockImplementation(async () => {});
try {
await runNoticeGeneration({
npm_lifecycle_event: 'generate:notices',
QWEN_SKIP_NOTICE_GENERATION: '1',
});
expect(log).toHaveBeenCalledWith(
'Skipping VS Code notice generation during worktree bootstrap.',
);
expect(write).not.toHaveBeenCalled();
} finally {
log.mockRestore();
write.mockRestore();
}
});
it('still generates notices when the skip flag is absent', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
// Empty dependency graph: main() reaches the write without touching the
// real node_modules, and a failing main() cannot process.exit the worker.
const read = vi.spyOn(fs, 'readFile').mockImplementation(async (file) => {
const name = String(file);
if (name.endsWith('package.json')) {
return JSON.stringify({ dependencies: {} });
}
if (name.endsWith('package-lock.json')) {
return JSON.stringify({ packages: {} });
}
throw Object.assign(new Error(`ENOENT: ${name}`), { code: 'ENOENT' });
});
const write = vi.spyOn(fs, 'writeFile').mockImplementation(async () => {});
try {
await runNoticeGeneration({ npm_lifecycle_event: 'generate:notices' });
expect(write).toHaveBeenCalledTimes(1);
expect(String(write.mock.calls[0]?.[1])).toContain(
'third-party software notices and license terms',
);
} finally {
log.mockRestore();
error.mockRestore();
read.mockRestore();
write.mockRestore();
}
});
});
describe('findLicenseFile', () => {
let packageDir;
+20603
View File
File diff suppressed because it is too large. Load diff
+61
View File
@@ -0,0 +1,61 @@
# Mirrors the workspaces list in package.json exactly; a new channel must be
# added to both files, the same as under npm. A glob here would let a
# channel join the pnpm layout silently while npm still ignores it.
packages:
- 'packages/*'
- 'packages/channels/base'
- 'packages/channels/dingtalk'
- 'packages/channels/dws'
- 'packages/channels/feishu'
- 'packages/channels/github'
- 'packages/channels/gitlab'
- 'packages/channels/plugin-example'
- 'packages/channels/qqbot'
- 'packages/channels/telegram'
- 'packages/channels/wecom'
- 'packages/channels/weixin'
- 'integrations/external-context'
- 'integrations/external-context-mem0'
- '!packages/desktop-shell'
- '!packages/live-host'
nodeLinker: 'hoisted'
# The root postinstall runs patch-package, which rewrites files inside
# node_modules in place. Under the default import method those files are
# hard links into the content-addressable store, so the patch also rewrites
# the store entry -- its contents stop matching the sha512 it is filed
# under. Every later worktree then fails its --offline stage with
# ERR_PNPM_NO_OFFLINE_TARBALL, falls back to the registry, and re-patches
# the store, so the offline path never hits again. Cloning (copy-on-write
# where the filesystem supports it, a plain copy elsewhere) keeps the store
# entry untouched while patch-package edits only the worktree's own copy.
packageImportMethod: 'clone-or-copy'
linkWorkspacePackages: true
verifyDepsBeforeRun: false
overrides:
typescript: '5.8.3'
'@microsoft/api-extractor>typescript': '5.9.3'
'@qwen-code/mobile-mcp>typescript': '5.8.2'
ansi-regex: '6.2.2'
'cliui>wrap-ansi': '7.0.0'
baseline-browser-mapping: '^2.9.19'
normalize-package-data: '^7.0.1'
react: '^19.2.4'
react-dom: '^19.2.4'
'@types/react': '^19.2.0'
'@types/react-dom': '^19.2.0'
axios: '^1.19.0'
react-markdown: '^9.0.0'
allowBuilds:
'@google/genai': true
'@qwen-code/qwen-code-core@file:packages/core': true
'@vscode/vsce-sign': true
esbuild: true
keytar: true
msw: true
protobufjs: true
minimumReleaseAgeExclude:
- '@qwen-code/channel-base'
+39
View File
@@ -8,6 +8,8 @@ import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse as parseYaml } from 'yaml';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const lockfilePath = join(root, 'package-lock.json');
@@ -72,3 +74,40 @@ if (invalidPackages.length > 0) {
console.log('Lockfile check passed.');
process.exitCode = 0;
}
console.log('Checking pnpm lockfile...');
const pnpmLockfilePath = join(root, 'pnpm-lock.yaml');
let pnpmLockfile;
try {
pnpmLockfile = parseYaml(fs.readFileSync(pnpmLockfilePath, 'utf-8'));
} catch (error) {
console.error(`Error reading or parsing ${pnpmLockfilePath}:`, error);
process.exit(1);
}
const invalidPnpmPackages = [];
for (const [key, details] of Object.entries(pnpmLockfile?.packages ?? {})) {
const resolution = details?.resolution ?? {};
// Registry packages carry a sha512 integrity hash; git and tarball
// resolutions identify their source directly, mirroring the npm rules
// above.
const hasIntegrity =
typeof resolution.integrity === 'string' &&
resolution.integrity.startsWith('sha512-');
const isGitOrTarball =
resolution.type === 'git' || typeof resolution.tarball === 'string';
if (!hasIntegrity && !isGitOrTarball) {
invalidPnpmPackages.push(key);
}
}
if (invalidPnpmPackages.length > 0) {
console.error(
'\nError: The following dependencies in pnpm-lock.yaml are missing "resolution.integrity":',
);
invalidPnpmPackages.forEach((pkg) => console.error(`- ${pkg}`));
process.exitCode = 1;
} else {
console.log('pnpm lockfile check passed.');
}
+19
View File
@@ -0,0 +1,19 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export function getPinnedPnpmPackage(packageJson) {
const packageManager = packageJson.packageManager;
// `corepack use [email protected]` appends an integrity suffix
// (+sha512.<128 hex chars>); accept it so the command corepack itself
// writes cannot break the bootstrap, but reject everything else.
if (
!/^pnpm@\d+\.\d+\.\d+(\+sha512\.[0-9a-f]{128})?$/.test(packageManager ?? '')
) {
throw new Error('packageManager must pin an exact pnpm version');
}
return packageManager;
}
+97
View File
@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { constants as osConstants } from 'node:os';
import { delimiter, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { getPinnedPnpmPackage } from './pnpm-package.js';
const corepack = process.platform === 'win32' ? 'corepack.cmd' : 'corepack';
// The script lives in <repo>/scripts, so it bootstraps the checkout it
// belongs to no matter which directory the caller runs it from.
const rootDir = fileURLToPath(new URL('..', import.meta.url));
getPinnedPnpmPackage(
JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')),
);
const env = {
...process.env,
QWEN_SKIP_PREPARE: '1',
QWEN_SKIP_NOTICE_GENERATION: '1',
};
// A spread of process.env is an ordinary object: on Windows the path
// variable canonically arrives as `Path`, so a case-sensitive `env.PATH`
// read misses it and corepack is never found.
function pathValue() {
if (process.platform !== 'win32') return env.PATH ?? '';
const key = Object.keys(env).find((name) => name.toUpperCase() === 'PATH');
return (key !== undefined ? env[key] : env.PATH) ?? '';
}
function findOnPath(command) {
for (const entry of pathValue().split(delimiter)) {
const directory = entry.replace(/^"(.*)"$/, '$1');
const candidate = resolve(directory || '.', command);
if (existsSync(candidate)) return candidate;
}
return undefined;
}
const corepackPath = findOnPath(corepack);
if (!corepackPath) {
console.error(
'worktree setup failed: Corepack is required to verify the pinned pnpm package',
);
process.exit(1);
}
function runPnpm(args) {
return spawnSync(corepack, ['pnpm', ...args], {
cwd: rootDir,
env,
shell: process.platform === 'win32',
stdio: 'inherit',
});
}
function install(cacheMode) {
return runPnpm(['install', '--frozen-lockfile', cacheMode]);
}
function exitWithResult(result) {
if (result.error) {
console.error(`worktree setup failed: ${result.error.message}`);
process.exit(1);
}
if (result.signal) {
console.error(`worktree setup killed by signal ${result.signal}`);
const signalNumber = osConstants.signals[result.signal];
process.exit(signalNumber ? 128 + signalNumber : 1);
}
process.exit(result.status ?? 1);
}
const cachedInstall = install('--offline');
if (cachedInstall.status === 0) {
process.exit(0);
}
if (
cachedInstall.error ||
cachedInstall.signal ||
(cachedInstall.status !== null && cachedInstall.status >= 128)
) {
exitWithResult(cachedInstall);
}
console.warn('Cached install unavailable; retrying with registry access.');
exitWithResult(install('--prefer-offline'));
+460
View File
@@ -6,7 +6,10 @@
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
@@ -16,6 +19,10 @@ import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { parse } from 'yaml';
import { hooks as pnpmHooks, workspacePackageNames } from '../../.pnpmfile.mjs';
import { getPinnedPnpmPackage } from '../pnpm-package.js';
import { getWorkflowJob, getWorkflowStep } from './workflow-helpers.js';
@@ -31,6 +38,365 @@ function readWorkflow(relativePath) {
}
describe('package scripts', () => {
it('accepts only an exact pnpm package-manager version', () => {
expect(getPinnedPnpmPackage({ packageManager: '[email protected]' })).toBe(
'[email protected]',
);
// `corepack use [email protected]` appends the tarball's integrity hash; the
// bootstrap must accept the string corepack itself writes.
const hashed =
'[email protected]+sha512.bd27e345e976dcb0be0b7a1228217b049a817e21b1f355c90dbe7dc46671895a8bc1e6d06c24554505ea93ea0b45f489a27ec1bfbc8de6a9659fca0f16fa0000';
expect(getPinnedPnpmPackage({ packageManager: hashed })).toBe(hashed);
expect(() =>
getPinnedPnpmPackage({ packageManager: 'pnpm@latest' }),
).toThrow('packageManager must pin an exact pnpm version');
expect(() =>
getPinnedPnpmPackage({
packageManager: '[email protected]+sha512.not-hex',
}),
).toThrow('packageManager must pin an exact pnpm version');
});
it('pins pnpm with the corepack integrity hash', () => {
expect(readPackageJson().packageManager).toMatch(
/^pnpm@\d+\.\d+\.\d+\+sha512\.[0-9a-f]{128}$/,
);
});
it('keeps internal pnpm workspaces independent of manifest versions', () => {
const packageJson = {
dependencies: {
'@qwen-code/qwen-code-core': 'file:../core',
'@qwen-code/channel-base': '0.22.4',
fixture: 'file:../fixture',
},
devDependencies: {
'@qwen-code/acp-bridge': 'file:../acp-bridge',
},
optionalDependencies: {
'@qwen-code/sdk': '0.22.4',
},
};
expect(pnpmHooks.readPackage(packageJson)).toEqual({
dependencies: {
'@qwen-code/qwen-code-core': 'workspace:*',
'@qwen-code/channel-base': 'workspace:*',
fixture: 'file:../fixture',
},
devDependencies: {
'@qwen-code/acp-bridge': 'workspace:*',
},
optionalDependencies: {
'@qwen-code/sdk': 'workspace:*',
},
});
});
it('keeps the pnpm rewrite set in sync with the workspace manifests', () => {
const { workspaces } = readPackageJson();
const negated = workspaces
.filter((entry) => entry.startsWith('!'))
.map((entry) => entry.slice(1));
const directories = [];
for (const pattern of workspaces) {
if (pattern.startsWith('!')) continue;
if (pattern.endsWith('/*')) {
const parent = pattern.slice(0, -2);
for (const entry of readdirSync(path.join(root, parent))) {
directories.push(`${parent}/${entry}`);
}
} else {
directories.push(pattern);
}
}
const names = directories
.filter((directory) => !negated.includes(directory))
.map((directory) => {
const manifestPath = path.join(root, directory, 'package.json');
if (!existsSync(manifestPath)) return undefined;
return JSON.parse(readFileSync(manifestPath, 'utf8')).name;
})
.filter((name) => name !== undefined);
// A member missing from the set keeps its release version or file:
// specifier under pnpm, which is exactly the lockfile staleness the
// rewrite exists to prevent.
expect([...workspacePackageNames].sort()).toEqual(
[...new Set(names)].sort(),
);
});
it('mirrors the npm workspace boundaries in pnpm-workspace.yaml', () => {
const workspace = parse(readWorkflow('pnpm-workspace.yaml'));
expect([...workspace.packages].sort()).toEqual(
[...readPackageJson().workspaces].sort(),
);
});
it('mirrors npm overrides in pnpm-workspace.yaml', () => {
const workspace = parse(readWorkflow('pnpm-workspace.yaml'));
const { cliui, ...npmOverrides } = readPackageJson().overrides;
expect(workspace.overrides).toMatchObject({
...npmOverrides,
'cliui>wrap-ansi': cliui['wrap-ansi'],
});
});
it('checks both lockfiles for integrity', () => {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/check-lockfile.js')],
{ cwd: root, encoding: 'utf8' },
);
expect(result.status).toBe(0);
expect(result.stdout).toContain('Lockfile check passed.');
expect(result.stdout).toContain('pnpm lockfile check passed.');
});
it('keeps the internal release-age exception independent of the version', () => {
const workspace = parse(
readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8'),
);
expect(workspace.minimumReleaseAgeExclude).toEqual([
'@qwen-code/channel-base',
]);
});
it('imports packages without hard links so patch-package cannot corrupt the store', () => {
const workspace = parse(
readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8'),
);
// The root postinstall rewrites node_modules files in place. Under
// 'auto' or 'hardlink' those files are hard links into the
// content-addressable store, so the rewrite corrupts the store entry and
// every later worktree misses its --offline stage.
expect(readPackageJson().scripts.postinstall).toBe('patch-package');
expect(['clone-or-copy', 'copy', 'clone']).toContain(
workspace.packageImportMethod,
);
});
it('keeps the pnpm lockfile out of prettier so formatting cannot fight pnpm', () => {
const ignored = readFileSync(path.join(root, '.prettierignore'), 'utf8')
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'));
expect(ignored).toContain('pnpm-lock.yaml');
});
it('keeps channel workspace lock entries independent of release versions', () => {
const lockfile = parse(
readFileSync(path.join(root, 'pnpm-lock.yaml'), 'utf8'),
);
const specifiers = Object.values(lockfile.importers).flatMap((importer) =>
['dependencies', 'devDependencies', 'optionalDependencies'].flatMap(
(field) => {
const entry = importer[field]?.['@qwen-code/channel-base'];
return entry ? [entry.specifier] : [];
},
),
);
expect(specifiers.length).toBeGreaterThan(0);
expect(new Set(specifiers)).toEqual(new Set(['workspace:*']));
});
it('bootstraps worktrees with frozen pnpm dependencies and skips prepare', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-setup-'));
const commandDir = path.join(binDir, 'runner bin');
const logFile = path.join(binDir, 'corepack.log');
mkdirSync(commandDir);
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(commandDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n',
);
} else {
writeFileSync(
path.join(commandDir, 'corepack'),
'#!/bin/sh\necho "$QWEN_SKIP_PREPARE $QWEN_SKIP_NOTICE_GENERATION $*" >> "$WORKTREE_SETUP_LOG"\n',
);
chmodSync(path.join(commandDir, 'corepack'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${commandDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
},
},
);
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim()).toBe(
'1 1 pnpm install --frozen-lockfile --offline',
);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it.skipIf(process.platform !== 'win32')(
'resolves the path variable under its native Windows casing',
() => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-path-'));
const commandDir = path.join(binDir, 'runner bin');
const logFile = path.join(binDir, 'corepack.log');
mkdirSync(commandDir);
try {
writeFileSync(
path.join(commandDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n',
);
// Native shells expose the path variable as `Path`; a case-sensitive
// `env.PATH` read on the spread object would miss it and report
// Corepack unavailable.
const env = { ...process.env, WORKTREE_SETUP_LOG: logFile };
delete env.PATH;
delete env.Path;
env.Path = `${commandDir}${path.delimiter}${process.env.Path ?? process.env.PATH ?? ''}`;
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env,
},
);
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim()).toBe(
'1 1 pnpm install --frozen-lockfile --offline',
);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
},
);
it('fails closed when Corepack is unavailable', () => {
const binDir = mkdtempSync(
path.join(tmpdir(), 'qwen-worktree-no-corepack-'),
);
const env = { ...process.env, PATH: binDir };
for (const name of Object.keys(env)) {
if (name !== 'PATH' && name.toUpperCase() === 'PATH') delete env[name];
}
try {
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{ cwd: root, encoding: 'utf8', env },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain('Corepack is required');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('falls back to registry access when the pnpm store is incomplete', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-fallback-'));
const logFile = path.join(binDir, 'corepack.log');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'corepack.cmd'),
'@echo %QWEN_SKIP_PREPARE% %QWEN_SKIP_NOTICE_GENERATION% %*>>"%WORKTREE_SETUP_LOG%"\r\n@if "%4"=="--offline" exit /b 1\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'corepack'),
'#!/bin/sh\necho "$QWEN_SKIP_PREPARE $QWEN_SKIP_NOTICE_GENERATION $*" >> "$WORKTREE_SETUP_LOG"\n[ "$4" != "--offline" ]\n',
);
chmodSync(path.join(binDir, 'corepack'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: binDir,
WORKTREE_SETUP_LOG: logFile,
},
},
);
expect(result.status).toBe(0);
expect(readFileSync(logFile, 'utf8').trim().split(/\r?\n/)).toEqual([
'1 1 pnpm install --frozen-lockfile --offline',
'1 1 pnpm install --frozen-lockfile --prefer-offline',
]);
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('preserves a worktree bootstrap interrupt status', () => {
const binDir = mkdtempSync(path.join(tmpdir(), 'qwen-worktree-signal-'));
const logFile = path.join(binDir, 'corepack.log');
try {
if (process.platform === 'win32') {
writeFileSync(
path.join(binDir, 'corepack.cmd'),
'@echo called>>"%WORKTREE_SETUP_LOG%"\r\n@exit /b 130\r\n',
);
} else {
writeFileSync(
path.join(binDir, 'corepack'),
'#!/bin/sh\necho called >> "$WORKTREE_SETUP_LOG"\nkill -INT $$\n',
);
chmodSync(path.join(binDir, 'corepack'), 0o755);
}
const result = spawnSync(
process.execPath,
[path.join(root, 'scripts/setup-worktree.js')],
{
cwd: root,
encoding: 'utf8',
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ''}`,
WORKTREE_SETUP_LOG: logFile,
},
},
);
expect(result.status).toBe(130);
expect(readFileSync(logFile, 'utf8').trim()).toBe('called');
} finally {
rmSync(binDir, { recursive: true, force: true });
}
});
it('does not couple Node REPL to Qwen release versions', () => {
const versionScript = readFileSync(
path.join(root, 'scripts/version.js'),
@@ -47,6 +413,100 @@ describe('package scripts', () => {
);
});
it('smoke-tests the real worktree bootstrap on every supported host', () => {
const workflow = parse(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
);
const job = workflow.jobs.install;
expect(job.strategy.matrix.os).toEqual([
'ubuntu-latest',
'macos-latest',
'windows-latest',
]);
expect(job.strategy['fail-fast']).toBe(false);
expect(
job.steps.find(
(step) => step.name === 'Install frozen pnpm worktree dependencies',
)?.run,
).toBe('node scripts/setup-worktree.js');
// The pnpm linker only materializes declared dependencies, so a
// workspace import that npm's hoisting hides breaks typecheck and tests
// in the bootstrapped tree; resolve one through the worst offender.
const resolveCheck = job.steps.find(
(step) => step.name === 'Verify workspace links resolve',
);
expect(resolveCheck?.run).toContain(
'@qwen-code/qwen-code-core/package.json',
);
expect(resolveCheck?.run).toContain('packages/vscode-ide-companion');
// `git diff --exit-code` misses untracked files; the install must leave
// the porcelain output empty on every host, including Windows (hence
// the explicit POSIX shell).
const cleanCheck = job.steps.find(
(step) => step.name === 'Ensure bootstrap keeps the worktree clean',
);
expect(cleanCheck?.shell).toBe('bash');
expect(cleanCheck?.run).toContain('git status --porcelain');
// The checks only mean anything after the install; pin the order.
const stepNames = job.steps.map((step) => step.name);
expect(
stepNames.indexOf('Install frozen pnpm worktree dependencies'),
).toBeLessThan(stepNames.indexOf('Verify workspace links resolve'));
expect(stepNames.indexOf('Verify workspace links resolve')).toBeLessThan(
stepNames.indexOf('Ensure bootstrap keeps the worktree clean'),
);
// A post-merge run is the only witness of a regression on main, so
// consecutive merges must not cancel each other.
expect(workflow.concurrency['cancel-in-progress']).toBe(
"${{ github.event_name == 'pull_request' }}",
);
// Substring check on the raw job text: an exact `step.run` match is
// bypassed by any other spelling of a build step (block scalar,
// compound command).
const installJob = getWorkflowJob(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
'install',
);
expect(installJob).not.toContain('npm run build');
expect(installJob).not.toContain('node scripts/build.js');
});
it('runs the pnpm smoke workflow when a dependency input changes', () => {
const workflow = parse(
readWorkflow('.github/workflows/pnpm-worktree-smoke.yml'),
);
const expectedPaths = [
'.github/workflows/pnpm-worktree-smoke.yml',
'.npmrc',
'.pnpmfile.mjs',
'package.json',
'packages/*/package.json',
'!packages/desktop-shell/package.json',
'!packages/live-host/package.json',
'packages/channels/*/package.json',
'integrations/*/package.json',
'patches/**',
'packages/audio-capture/install.js',
'packages/core/scripts/postinstall.js',
'packages/vscode-ide-companion/scripts/generate-notices.js',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'scripts/generate-git-commit-info.js',
'scripts/prepare.js',
'scripts/pnpm-package.js',
'scripts/setup-worktree.js',
];
expect(workflow.on.pull_request.paths).toEqual(expectedPaths);
expect(workflow.on.push.paths).toEqual(expectedPaths);
});
it('builds the standalone qwen-live daemon in the root build order', () => {
const buildScript = readFileSync(
path.join(root, 'scripts/build.js'),