Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4ecf3d
perf(dev): add pnpm worktree bootstrap foundation
yiliang114 Aug 29, 2026
74bad44
fix(dev): harden worktree bootstrap process handling
yiliang114 Aug 29, 2026
fcb27a4
perf(dev): prefer cache-only worktree installs
yiliang114 Aug 29, 2026
605d205
fix(dev): preserve cached install cancellation
yiliang114 Aug 29, 2026
f7161c7
fix(build): harden pnpm worktree bootstrap
yiliang114 Aug 29, 2026
f2ab686
merge: sync main and narrow pnpm bootstrap stage
yiliang114 Aug 29, 2026
90cbf2d
chore: record merge attribution
yiliang114 Aug 29, 2026
e7f7a87
fix(dev): keep worktree bootstrap clean
yiliang114 Aug 29, 2026
a51bcac
ci: track pnpm smoke workflow size
yiliang114 Aug 29, 2026
b2ec83d
Merge branch 'main' into codex/pnpm-worktree-bootstrap
yiliang114 Aug 30, 2026
5535f7b
fix(ci): drop stale importers from the new pnpm lockfile
yiliang114 Aug 30, 2026
b01d415
Merge remote-tracking branch 'origin/main' into codex/pnpm-worktree-b…
yiliang114 Sep 1, 2026
9fe920a
Merge branch 'main' into codex/pnpm-worktree-bootstrap
yiliang114 Sep 1, 2026
9300088
fix(ci): refresh pnpm lockfile after merging main
yiliang114 Sep 1, 2026
b2dbbe7
Merge remote-tracking branch 'origin/codex/pnpm-worktree-bootstrap' i…
yiliang114 Sep 1, 2026
0cb3aec
fix(scripts): resolve the path variable case-insensitively on Windows
yiliang114 Sep 1, 2026
fd43387
test(scripts): pin the worktree bootstrap guards against mutation
yiliang114 Sep 1, 2026
eaf0da0
Merge branch 'main' into codex/pnpm-worktree-bootstrap
yiliang114 Sep 2, 2026
5ef1884
Merge branch 'main' into codex/pnpm-worktree-bootstrap
yiliang114 Sep 2, 2026
0b1de75
chore(deps): sync pnpm-lock.yaml with qwen-live ACP backend deps
yiliang114 Sep 2, 2026
def9ec3
Merge remote-tracking branch 'origin/codex/pnpm-worktree-bootstrap' i…
yiliang114 Sep 2, 2026
eb6d0c2
fix(dev): stop patch-package from corrupting the pnpm store
Sep 3, 2026
154570d
Merge branch 'main' into codex/pnpm-worktree-bootstrap
yiliang114 Sep 3, 2026
4191008
Merge remote-tracking branch 'origin/main' into prmerge-10449
yiliang114 Sep 3, 2026
b106462
fix(ci): refresh pnpm-lock.yaml for qwen-live prompts deps
yiliang114 Sep 3, 2026
35171c8
Merge branch 'codex/pnpm-worktree-bootstrap' of https://github.com/Qw…
yiliang114 Sep 3, 2026
6ed4ea3
Merge origin/main into codex/pnpm-worktree-bootstrap
yiliang114 Sep 3, 2026
d401674
Merge upstream codex/pnpm-worktree-bootstrap updates
yiliang114 Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions .github/workflows/pnpm-worktree-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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:
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
cancel-in-progress: true

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: 'Ensure bootstrap keeps the worktree clean'
run: 'git diff --exit-code'
47 changes: 47 additions & 0 deletions .pnpmfile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

const workspacePackages = 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-qqbot',
'@qwen-code/channel-telegram',
'@qwen-code/channel-wecom',
'@qwen-code/channel-weixin',
'@qwen-code/qwen-code-core',
'@qwen-code/sdk',
'@qwen-code/web-templates',
'@qwen-code/webui',
]);

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 (workspacePackages.has(name)) {
dependencies[name] = 'workspace:*';
}
}
}

return packageJson;
},
};
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .yamllint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ rules:
check-keys: true

ignore:
- 'pnpm-lock.yaml'
- 'thirdparty/'
- 'third_party/'
- 'vendor/'
Expand Down
74 changes: 74 additions & 0 deletions docs/design/2026-08-29-pnpm-worktree-bootstrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 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.
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 prefers Corepack
so an existing pnpm cache can stay fully offline, and falls back to npm's
bundled `npx` on Node versions that no longer include Corepack. Both paths use
the exact pnpm package declared by `packageManager`. 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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.23.0",
"packageManager": "pnpm@11.24.0",
"engines": {
"node": ">=22.0.0"
},
Expand Down
17 changes: 16 additions & 1 deletion packages/vscode-ide-companion/scripts/generate-notices.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
59 changes: 58 additions & 1 deletion packages/vscode-ide-companion/scripts/generate-notices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Comment thread
yiliang114 marked this conversation as resolved.
});

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;

Expand Down
Loading