-
Notifications
You must be signed in to change notification settings - Fork 3k
perf(autofix): build the review CLI bundle once per scan and fan it out to legs #8548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ceaac7
dd4ab70
8c4b525
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -747,6 +747,9 @@ jobs: | |
| exit 1 | ||
| fi | ||
|
|
||
| # The npm-ci retry recipe and the 'Prepare Qwen Code CLI' shim below | ||
| # are duplicated in build-cli and review-address; the workflow | ||
| # contract tests pin every copy in lockstep — edit them together. | ||
| - name: 'Install dependencies and build' | ||
| env: | ||
| QWEN_SKIP_PREPARE: '1' | ||
|
|
@@ -2586,12 +2589,107 @@ jobs: | |
| echo "targets=${TARGETS}" >> "${GITHUB_OUTPUT}" | ||
| echo "has_targets=$([[ "${COUNT}" -gt 0 ]] && echo true || echo false)" >> "${GITHUB_OUTPUT}" | ||
|
|
||
| # =========================================================================== | ||
| # REVIEW PHASE (build) — compile the trusted-base CLI bundle ONCE per scan | ||
| # and fan it out to the address legs as an artifact. Each leg otherwise | ||
| # repeated the same base build: measured 3.5-5 minutes of npm ci + build + | ||
| # bundle per leg (6 legs ≈ 25 runner-minutes on one 2026-08 scan) before | ||
| # the agent could start. Gated on has_targets so an idle tick builds | ||
| # nothing — the issue phase runs only when there are no review targets, so | ||
| # gating on do_issue too would rebuild on every quiet scheduled tick. | ||
| # fetch-depth 1 suffices: nothing here needs history, and the legs (full | ||
| # fetch) always contain this commit as an ancestor of their base fetch. | ||
| # =========================================================================== | ||
| build-cli: | ||
| needs: ['route', 'review-scan'] | ||
| if: |- | ||
| ${{ needs.review-scan.outputs.has_targets == 'true' }} | ||
| runs-on: 'ubuntu-latest' | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: 'read' | ||
| outputs: | ||
| base_sha: '${{ steps.meta.outputs.base_sha }}' | ||
| steps: | ||
| - name: 'Checkout trusted base' | ||
| uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 | ||
| with: | ||
| ref: '${{ github.event.repository.default_branch }}' | ||
| fetch-depth: 1 | ||
| persist-credentials: false | ||
|
|
||
| - name: 'Set up Node.js (hosted)' | ||
| if: |- | ||
| ${{ runner.environment == 'github-hosted' }} | ||
| uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 | ||
| with: | ||
| node-version: '22.x' | ||
| cache: 'npm' | ||
| cache-dependency-path: 'package-lock.json' | ||
|
|
||
| # The npm-ci retry recipe and the 'Prepare Qwen Code CLI' shim below | ||
| # are duplicated in issue-autofix and review-address; the workflow | ||
| # contract tests pin every copy in lockstep — edit them together. | ||
| - name: 'Install dependencies and build' | ||
| env: | ||
| QWEN_SKIP_PREPARE: '1' | ||
| run: |- | ||
| for attempt in 1 2 3; do | ||
| if npm ci --prefer-offline --no-audit --progress=false; then | ||
| break | ||
| fi | ||
| if [[ "${attempt}" == "3" ]]; then | ||
| exit 1 | ||
| fi | ||
| sleep $((attempt * 15)) | ||
| done | ||
| npm run build | ||
| npm run bundle | ||
|
|
||
| # Producer-side sanity: a bundle that cannot even answer --version | ||
| # fails THIS job once, instead of every leg failing its restore or | ||
| # first qwen invocation minutes later and N times over. | ||
| - name: 'Prepare Qwen Code CLI' | ||
| run: |- | ||
| qwen_version="$(node -p "require('./package.json').version")" | ||
| echo "Using checked-out Qwen Code bundle ${qwen_version}" | ||
| qwen_bin="${RUNNER_TEMP}/qwen-bin" | ||
| mkdir -p "${qwen_bin}" | ||
| cat > "${qwen_bin}/qwen" <<'EOF' | ||
| #!/usr/bin/env bash | ||
| exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@" | ||
| EOF | ||
| chmod +x "${qwen_bin}/qwen" | ||
| echo "${qwen_bin}" >> "${GITHUB_PATH}" | ||
| PATH="${qwen_bin}:${PATH}" | ||
| qwen --version | ||
|
|
||
| # Only the repo-root dist/ is shipped: copy_bundle_assets.js already | ||
| # gathers every runtime asset (chunks, vendor, web-shell, locales) | ||
| # under it. packages/*/dist would triple the artifact size and is | ||
| # never read by the legs — the verify gate's full `npm run build` | ||
| # wipes and rebuilds each package's dist from branch sources | ||
| # (build_package.js rms it first, so no staleness leaks through). | ||
| - name: 'Upload CLI bundle' | ||
| id: 'meta' | ||
| run: |- | ||
| echo "base_sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" | ||
| tar -czf "${RUNNER_TEMP}/qwen-cli-dist.tar.gz" dist | ||
|
|
||
| - name: 'Publish CLI bundle artifact' | ||
| uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 | ||
| with: | ||
| name: 'qwen-autofix-cli-dist' | ||
| path: '${{ runner.temp }}/qwen-cli-dist.tar.gz' | ||
| retention-days: 1 | ||
| if-no-files-found: 'error' | ||
|
|
||
| # =========================================================================== | ||
| # REVIEW PHASE (address) — one job per PR: triage, address (incl. conflict | ||
| # resolution), verify, push, and report. | ||
| # =========================================================================== | ||
| review-address: | ||
| needs: ['route', 'review-scan'] | ||
| needs: ['route', 'review-scan', 'build-cli'] | ||
| if: |- | ||
| ${{ needs.review-scan.outputs.has_targets == 'true' }} | ||
| runs-on: 'ubuntu-latest' | ||
|
|
@@ -2660,14 +2758,30 @@ jobs: | |
| HEAD_REPO: '${{ matrix.target.head_repo }}' | ||
| steps: | ||
| # SECURITY: checkout trusted base code first. The PR branch is checked | ||
| # out later in "Prepare branch and feedback" after the trusted CLI bundle | ||
| # is built from this base. Without this pin, pull_request_review events | ||
| # would check out the PR merge ref by default, letting PR-controlled | ||
| # code influence the secret-bearing address run. | ||
| # out later in "Prepare branch and feedback" after the trusted CLI | ||
| # bundle (built once from this base in build-cli) is in place. Without | ||
| # this pin, pull_request_review events would check out the PR merge ref | ||
| # by default, letting PR-controlled code influence the secret-bearing | ||
| # address run. The ref is pinned to the SHA build-cli compiled — not | ||
| # the live default branch — so a mid-run base push can never leave a | ||
| # leg running a bundle built from DIFFERENT sources than its checkout. | ||
| # The SHA is validated fail-loud FIRST: actions/checkout resolves an | ||
| # empty ref to the event default — on pull_request_review triggers the | ||
| # PR merge ref — so a broken build-cli output must fail this leg | ||
| # instead of silently unpinning it. | ||
| - name: 'Validate bundle SHA' | ||
| env: | ||
| BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}' | ||
| run: |- | ||
| if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then | ||
| echo "::error::build-cli published no usable base_sha ('${BASE_SHA:-<empty>}') — refusing to fall back to the event-default ref." | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: 'Checkout trusted base' | ||
| uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 | ||
| with: | ||
| ref: '${{ github.event.repository.default_branch }}' | ||
| ref: '${{ needs.build-cli.outputs.base_sha }}' | ||
|
Comment on lines
2783
to
+2784
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This ref pin degrades fail-open: if Failure scenario: a future rename of the step id or a typo in the output name → Suggested fix — a fail-loud validation step before this checkout (a job-level - name: 'Validate bundle SHA'
env:
BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}'
run: |-
if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::build-cli published no usable base_sha ('${BASE_SHA:-<empty>}') — refusing to fall back to the event-default ref."
exit 1
fi中文说明此 ref 固定以 fail-open 方式退化:一旦 失败场景:未来某次 step id 改名或输出名拼写错误 → 建议修复——在此 checkout 之前增加一个 fail-loud 校验步骤(job 级 — qwen3.8-max via Qwen Code /review (v0.21.5) |
||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
|
|
@@ -2736,7 +2850,14 @@ jobs: | |
| exit 1 | ||
| fi | ||
|
|
||
| - name: 'Install dependencies and build' | ||
| # npm ci only — the build itself ran ONCE in build-cli. The legs still | ||
| # need node_modules: the agent builds/tests the PR branch, and the | ||
| # verify gate rebuilds from branch sources (wiping the restored | ||
| # packages/*/dist via build_package.js, so no base staleness leaks | ||
| # into branch verification). The retry recipe (and the shim below) is | ||
| # duplicated in issue-autofix and build-cli; the workflow contract | ||
| # tests pin every copy in lockstep — edit them together. | ||
| - name: 'Install dependencies' | ||
| env: | ||
| QWEN_SKIP_PREPARE: '1' | ||
| run: |- | ||
|
|
@@ -2750,8 +2871,17 @@ jobs: | |
| sleep $((attempt * 15)) | ||
| done | ||
| git config core.hooksPath .husky | ||
| npm run build | ||
| npm run bundle | ||
|
|
||
| - name: 'Download CLI bundle' | ||
| uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 | ||
| with: | ||
| name: 'qwen-autofix-cli-dist' | ||
| path: '${{ runner.temp }}/cli-dist' | ||
|
|
||
| - name: 'Restore CLI bundle' | ||
| run: |- | ||
| tar -xzf "${RUNNER_TEMP}/cli-dist/qwen-cli-dist.tar.gz" | ||
| test -f dist/cli.js | ||
|
|
||
| - name: 'Prepare Qwen Code CLI' | ||
| run: |- | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -147,6 +147,10 @@ const installAndBuildSteps = | |||||||||||||||
| workflow.match( | ||||||||||||||||
| /- name: 'Install dependencies and build'[\s\S]*?(?=\n[ ]{6}- name: ')/g, | ||||||||||||||||
| ) ?? []; | ||||||||||||||||
| const nodeSetupSteps = | ||||||||||||||||
| workflow.match( | ||||||||||||||||
| /- name: 'Set up Node.js \(hosted\)'[\s\S]*?(?=\n[ ]{6}- name: ')/g, | ||||||||||||||||
| ) ?? []; | ||||||||||||||||
|
|
||||||||||||||||
| function readAutofixSkill() { | ||||||||||||||||
| return readFileSync('.qwen/skills/autofix/SKILL.md', 'utf8'); | ||||||||||||||||
|
|
@@ -5205,6 +5209,7 @@ describe('qwen-autofix workflow', () => { | |||||||||||||||
|
|
||||||||||||||||
| expect(workflow).toMatch(/issue-autofix:[\s\S]*?runs-on: 'ubuntu-latest'/); | ||||||||||||||||
| expect(workflow).toMatch(/review-address:[\s\S]*?runs-on: 'ubuntu-latest'/); | ||||||||||||||||
| expect(workflow).toMatch(/build-cli:[\s\S]*?runs-on: 'ubuntu-latest'/); | ||||||||||||||||
| expect(workflow).not.toContain( | ||||||||||||||||
| '["self-hosted", "linux", "x64", "autofix"]', | ||||||||||||||||
| ); | ||||||||||||||||
|
|
@@ -5214,7 +5219,9 @@ describe('qwen-autofix workflow', () => { | |||||||||||||||
| expect(workflow).toContain( | ||||||||||||||||
| "RUNNER_ENVIRONMENT: '${{ runner.environment }}'", | ||||||||||||||||
| ); | ||||||||||||||||
| expect(prepareQwenCliSteps).toHaveLength(2); | ||||||||||||||||
| // issue-autofix, build-cli, and review-address each stage the qwen shim | ||||||||||||||||
| // against the workspace bundle. | ||||||||||||||||
| expect(prepareQwenCliSteps).toHaveLength(3); | ||||||||||||||||
| for (const step of prepareQwenCliSteps) { | ||||||||||||||||
| expect(step).toContain( | ||||||||||||||||
| 'qwen_version="$(node -p "require(\'./package.json\').version")"', | ||||||||||||||||
|
|
@@ -5223,6 +5230,9 @@ describe('qwen-autofix workflow', () => { | |||||||||||||||
| 'exec node "${GITHUB_WORKSPACE}/dist/cli.js" "$@"', | ||||||||||||||||
| ); | ||||||||||||||||
| expect(step).toContain('qwen-bin'); | ||||||||||||||||
| expect(step).toContain('chmod +x "${qwen_bin}/qwen"'); | ||||||||||||||||
| expect(step).toContain('echo "${qwen_bin}" >> "${GITHUB_PATH}"'); | ||||||||||||||||
| expect(step).toContain('qwen --version'); | ||||||||||||||||
| expect(step).not.toContain('current_version="$(qwen --version'); | ||||||||||||||||
| expect(step).not.toContain('Using pre-installed Qwen Code'); | ||||||||||||||||
| expect(step).not.toContain('npm install -g'); | ||||||||||||||||
|
|
@@ -5279,6 +5289,8 @@ describe('qwen-autofix workflow', () => { | |||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| it('retries dependency installation before building', () => { | ||||||||||||||||
| // issue-autofix and build-cli build from sources; review-address restores | ||||||||||||||||
| // the shared bundle instead (see 'builds the review CLI bundle once...'). | ||||||||||||||||
| expect(installAndBuildSteps).toHaveLength(2); | ||||||||||||||||
| for (const step of installAndBuildSteps) { | ||||||||||||||||
| expect(step).toContain('for attempt in 1 2 3; do'); | ||||||||||||||||
|
|
@@ -5291,6 +5303,111 @@ describe('qwen-autofix workflow', () => { | |||||||||||||||
| } | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| it('keeps the Node setup recipe identical across the autofix jobs', () => { | ||||||||||||||||
| // The three setup steps are lockstep copies; a partial recipe edit (a | ||||||||||||||||
| // Node bump applied to two of the three jobs) must not ship green. | ||||||||||||||||
| expect(nodeSetupSteps).toHaveLength(3); | ||||||||||||||||
| for (const step of nodeSetupSteps) { | ||||||||||||||||
| expect(step).toContain( | ||||||||||||||||
| 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e', | ||||||||||||||||
| ); | ||||||||||||||||
| expect(step).toContain("node-version: '22.x'"); | ||||||||||||||||
| expect(step).toContain("cache: 'npm'"); | ||||||||||||||||
| expect(step).toContain("cache-dependency-path: 'package-lock.json'"); | ||||||||||||||||
| } | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| it('builds the review CLI bundle once and fans it out to the address legs', () => { | ||||||||||||||||
| // Measured driver: 6 review-address legs each spent 3.5-5 minutes on | ||||||||||||||||
| // npm ci + build + bundle of the SAME trusted base (~25 runner-minutes | ||||||||||||||||
| // per scan) before the agent could start. The legs download the shared | ||||||||||||||||
| // artifact instead; only their npm ci remains (the agent and the verify | ||||||||||||||||
| // gate still need node_modules against the PR branch). | ||||||||||||||||
| const buildCliJob = | ||||||||||||||||
| workflow.match( | ||||||||||||||||
| /\n {2}build-cli:[\s\S]*?(?=\n {2}review-address:)/, | ||||||||||||||||
| )?.[0] ?? ''; | ||||||||||||||||
| const addressJob = | ||||||||||||||||
| workflow.match(/\n {2}review-address:[\s\S]*$/)?.[0] ?? ''; | ||||||||||||||||
| const stepOf = (job, name) => | ||||||||||||||||
| job.match( | ||||||||||||||||
| new RegExp(`- name: '${name}'[\\s\\S]*?(?=\\n[ ]{6}- name: |$)`), | ||||||||||||||||
| )?.[0] ?? ''; | ||||||||||||||||
| expect(buildCliJob).toBeTruthy(); | ||||||||||||||||
| expect(addressJob).toBeTruthy(); | ||||||||||||||||
|
|
||||||||||||||||
| expect(buildCliJob).toContain("needs: ['route', 'review-scan']"); | ||||||||||||||||
| expect(addressJob).toContain( | ||||||||||||||||
| "needs: ['route', 'review-scan', 'build-cli']", | ||||||||||||||||
| ); | ||||||||||||||||
| // An idle tick (no review targets) must not spend a build; the issue | ||||||||||||||||
| // phase only runs when there are none, so gating on do_issue too would | ||||||||||||||||
| // rebuild on every quiet scheduled tick. | ||||||||||||||||
| expect(buildCliJob).toContain( | ||||||||||||||||
| "needs.review-scan.outputs.has_targets == 'true'", | ||||||||||||||||
| ); | ||||||||||||||||
| expect(buildCliJob).not.toContain('do_issue'); | ||||||||||||||||
|
|
||||||||||||||||
| // The leg checks out the SHA the bundle was compiled from — a mid-run | ||||||||||||||||
| // base push can never leave a leg running a bundle built from different | ||||||||||||||||
| // sources than its checkout. | ||||||||||||||||
| expect(buildCliJob).toContain( | ||||||||||||||||
| "base_sha: '${{ steps.meta.outputs.base_sha }}'", | ||||||||||||||||
| ); | ||||||||||||||||
|
Comment on lines
+5354
to
+5356
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The new fan-out test pins each side of the SHA chain but not the link itself: nothing asserts the step writing Failure scenario: a future rename of the step id ships with a green suite → every leg silently checks out the PR merge ref in a secret-bearing run.
Suggested change
中文说明新增的 fan-out 测试固定了 SHA 链的两端,但没有固定链本身:没有任何断言保证写出 失败场景:未来某次 step id 改名会在测试全绿的情况下上线 → 每条 leg 在携带密钥的运行中静默 checkout PR 合并 ref。 — qwen3.8-max via Qwen Code /review (v0.21.5) |
||||||||||||||||
| expect(buildCliJob).toContain( | ||||||||||||||||
| 'echo "base_sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"', | ||||||||||||||||
| ); | ||||||||||||||||
| // The step id is the LINK between those two pins: without id: 'meta', | ||||||||||||||||
| // steps.meta.outputs.base_sha resolves to '' at runtime and every leg | ||||||||||||||||
| // silently checks out the event-default ref. | ||||||||||||||||
| expect(stepOf(buildCliJob, 'Upload CLI bundle')).toContain("id: 'meta'"); | ||||||||||||||||
| expect(stepOf(addressJob, 'Checkout trusted base')).toContain( | ||||||||||||||||
| "ref: '${{ needs.build-cli.outputs.base_sha }}'", | ||||||||||||||||
| ); | ||||||||||||||||
| // The guard must fail the leg LOUD before the checkout: an empty ref | ||||||||||||||||
| // makes actions/checkout fall back to the event default — on | ||||||||||||||||
| // pull_request_review triggers the PR merge ref. | ||||||||||||||||
| const validateShaStep = stepOf(addressJob, 'Validate bundle SHA'); | ||||||||||||||||
| expect(validateShaStep).toContain( | ||||||||||||||||
| "BASE_SHA: '${{ needs.build-cli.outputs.base_sha }}'", | ||||||||||||||||
| ); | ||||||||||||||||
| expect(validateShaStep).toContain( | ||||||||||||||||
| 'if [[ ! "${BASE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then', | ||||||||||||||||
| ); | ||||||||||||||||
| expect(validateShaStep).toContain('exit 1'); | ||||||||||||||||
| expect(addressJob.indexOf("- name: 'Validate bundle SHA'")).toBeLessThan( | ||||||||||||||||
| addressJob.indexOf("- name: 'Checkout trusted base'"), | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| // The artifact is the repo-root dist/ only — copy_bundle_assets.js | ||||||||||||||||
| // already gathers every runtime asset under it; packages/*/dist would | ||||||||||||||||
| // triple the size and is rebuilt from branch sources by the verify gate. | ||||||||||||||||
| expect(buildCliJob).toContain( | ||||||||||||||||
| 'tar -czf "${RUNNER_TEMP}/qwen-cli-dist.tar.gz" dist', | ||||||||||||||||
| ); | ||||||||||||||||
| expect(buildCliJob).toContain("name: 'qwen-autofix-cli-dist'"); | ||||||||||||||||
| expect(buildCliJob).toContain('retention-days: 1'); | ||||||||||||||||
| expect(buildCliJob).toContain("if-no-files-found: 'error'"); | ||||||||||||||||
|
|
||||||||||||||||
| const restoreStep = stepOf(addressJob, 'Restore CLI bundle'); | ||||||||||||||||
| expect(restoreStep).toContain( | ||||||||||||||||
| 'tar -xzf "${RUNNER_TEMP}/cli-dist/qwen-cli-dist.tar.gz"', | ||||||||||||||||
| ); | ||||||||||||||||
| expect(restoreStep).toContain('test -f dist/cli.js'); | ||||||||||||||||
| const downloadStep = stepOf(addressJob, 'Download CLI bundle'); | ||||||||||||||||
| expect(downloadStep).toContain("name: 'qwen-autofix-cli-dist'"); | ||||||||||||||||
| // Download directory and restore extract path are one contract — pin | ||||||||||||||||
| // both sides so a rename of either fails this suite. | ||||||||||||||||
| expect(downloadStep).toContain("path: '${{ runner.temp }}/cli-dist'"); | ||||||||||||||||
|
|
||||||||||||||||
| // The leg itself never rebuilds the base bundle — that is the entire | ||||||||||||||||
| // point of the fan-out. | ||||||||||||||||
| const legInstall = stepOf(addressJob, 'Install dependencies'); | ||||||||||||||||
| expect(legInstall).toContain('npm ci --prefer-offline'); | ||||||||||||||||
| expect(legInstall).not.toContain('npm run build'); | ||||||||||||||||
| expect(legInstall).not.toContain('npm run bundle'); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| it('uses the standard checkout action for autonomous runner jobs', () => { | ||||||||||||||||
| expect(workflow).toContain('actions/checkout@'); | ||||||||||||||||
| expect(workflow).not.toContain('Checkout with retry'); | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] This diff adds a third copy of the npm-ci retry + build + bundle recipe and a third byte-identical
qwenshim heredoc, leaving the build procedure triplicated acrossissue-autofix,build-cli, andreview-address— all three sites are touched by this change. Cross-site agreement onnode-version: '22.x', theGITHUB_PATHwiring,chmod, and the--versionsmoke is pinned by no test.Failure scenario: a partial recipe edit (Node version bump applied to two of the three jobs, npm flag, backoff tweak) passes the whole suite and silently diverges
build-cli's bundle from what the legs' shims point at — the exact class of mismatch this PR's SHA-pinning was written to prevent, reintroduced at the recipe level.Suggested fix: extract the shared install/build/bundle + shim-staging block into a composite action (e.g.
.github/actions/prepare-qwen-cli) invoked by all three jobs; at minimum add cross-reference comments at each copy and extend the contract tests to pinnode-versionand theGITHUB_PATHwiring across all three sites.中文说明
本 diff 新增了第三份 npm-ci 重试 + build + bundle 配方和第三份逐字节相同的
qwenshim heredoc,使构建配方在issue-autofix、build-cli、review-address三处重复——三处都在本次改动中被触及。三处之间node-version: '22.x'、GITHUB_PATH接线、chmod和--version冒烟的一致性没有任何测试固定。失败场景:一次局部的配方修改(对三个 job 中的两个升级 Node 版本、npm 参数、退避调整)会通过整套测试,并悄悄让
build-cli的 bundle 与各 leg 的 shim 指向产生偏差——正是本 PR 的 SHA 固定要防止的那类不匹配,在配方层面被重新引入。建议修复:把共享的安装/构建/打包 + shim 部署代码块提取为复合 action(如
.github/actions/prepare-qwen-cli)供三个 job 调用;至少在每个副本处加交叉引用注释,并扩展契约测试以固定三处的node-version与GITHUB_PATH接线。— qwen3.8-max via Qwen Code /review (v0.21.5)