Skip to content

fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list - #12650

Merged
wenshao merged 26 commits into
mainfrom
autofix/issue-12647
Oct 5, 2026
Merged

wenshao merged 26 commits into
mainfrom
autofix/issue-12647

Conversation

@qwen-code-dev-bot

@qwen-code-dev-bot qwen-code-dev-bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

The two lint lanes that build their file lists with git — shellcheck and yamllint — now stage the list first and refuse to run when it comes up empty, instead of letting an empty list reach the linter. Previously, a failed or empty git ls-files made the yamllint lane invoke yamllint with zero file arguments — failing loudly but misleadingly on the tool's usage screen — and let the shellcheck lane report success having checked nothing, because its pipeline ends in sed and swallowed every upstream exit status. The list-staging and empty-list-refusal fragments both lanes share are built by two small helpers, and the linter map plus the lanes' PATH assembly are exported so tests can pin the behavior. Ten new tests cover each lane's git-failure abort, its empty-list refusals, its exact file selection and yamllint's own exit status, plus the PATH ordering and the installer temp-dir default. Linter versions, the yamllint availability check (command -v yamllint) and the PATH order are unchanged.

Why it's needed

On 2026-09-24, two Lint & Static runs on main failed with fatal: detected dubious ownership in repository: git ls-files died, the yamllint lane failed on yamllint's usage screen rather than on git's error, and the shellcheck lane vacuously passed on an empty file list. #12648 removed that trigger by trusting the job workspace as a git safe.directory before checkout, but its step ends in || echo "::warning::…", so when that write fails the job continues and the lanes meet the same dubious ownership. This PR removes the failure mode itself — however the list comes up empty (git error, a soft-failed safe.directory step, a filter regression, a layout change), both lanes now fail loudly at the real cause instead of passing vacuously or dying on a usage screen.

Reviewer Test Plan

How to verify

Run npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js; expect Tests 16 passed (16) on Linux (6 pre-existing + 10 new). The new git-sourced lint lanes describe stubs git, file, yamllint and shellcheck on a fake PATH and pins: when git ls-files fails, each lane aborts on git's own error and never invokes its linter (:304 yamllint, :383 shellcheck); each lane refuses to run on an empty staged list (:327, :406, :424); on the happy path each lane passes exactly the detected files to its linter (:343, :445); a non-zero exit from yamllint itself fails the lane (:366); the pip --user bin dir is appended after the inherited PATH on Linux and macOS and omitted on Windows (:484); and the lane PATH defaults to the temp dir the installers actually extract into (:524). The 8 lane cases gate at runtime through ctx.skip() on platforms/architectures the installers do not support; the two getLinterPath cases (:484, :524) execute ungated, including on the Windows leg.

Real-environment verification by a maintainer is in round 1 and round 2. Both drive the real node scripts/lint.js lanes under real git dubious ownership, with a root job and a runner-owned workspace. Under that trigger, main's shellcheck lane exits 0 having linted 0 files and its yamllint lane fails on the usage screen; with this PR, both lanes exit 1 naming git's error and never invoke a linter. On a healthy checkout both trees lint the same 66 scripts and 78 YAML files with byte-identical output, and the shellcheck findings match the real CI job line for line. Swapping main's lane strings back in fails exactly the 5 guard tests.

Evidence (Before & After)

N/A — CI-lane behavior only; no user-visible surface. The before/after of the lanes under the #12647 trigger is captured in the maintainer verification:

#12647 trigger, main vs PR

Tested on

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

Linux: suite green (16/16) locally and in CI, and the real lanes were exercised on a native x86_64 Ubuntu 24.04 rig (dash, git 2.55). macOS: partially tested — 14 of the 16 cases passed on macOS arm64 in maintainer round 1; the two cases added since (the yamllint exit-status lane case :366 and the getLinterPath default case :524) have not run on macOS. Windows: not run; the suite is not win32-excluded, the 8 lane cases skip at runtime via ctx.skip() (exercised on Linux by simulating an unsupported architecture: 8 passed, 8 skipped), and the two getLinterPath cases execute ungated.

Environment (optional)

Unit tests on Linux. The real-lane runs used the maintainer's Docker rig (Ubuntu 24.04.4 amd64, /bin/sh = dash, git 2.55.0, GNU xargs 4.9.0, pinned actionlint 1.7.12 / shellcheck 0.11.0 via the script's own SHA-256-verified --setup, yamllint 1.35.1).

Risk & Scope

  • Main risk or tradeoff: a lane now hard-fails where it could previously pass vacuously; on a healthy checkout the candidate lists are never empty (66 scripts, 78 YAML files today), so the change only converts silent greens into loud failures.
  • Not validated / out of scope: the original dubious-ownership trigger (removed by fix(ci): trust the job workspace as a git safe.directory before checkout #12648); the trailing sed still swallowing shellcheck's own exit status — on findings, a missing binary or a crash — which is pre-existing since the script's introduction and left for a follow-up, because surfacing every finding would turn main red on its existing ~2300 warnings; linter versions and the set of linters are unchanged.
  • Breaking changes / migration notes: none.

Linked Issues

References #12647 (the incident report that exposed this failure mode; the trigger itself was removed by #12648).

中文说明

本 PR 做了什么

两个基于 git 构建文件列表的 lint 通道(shellcheck 与 yamllint)现在先暂存文件列表,并在列表为空时拒绝运行,而不是让空列表到达 linter。此前,git ls-files 失败或为空会让 yamllint 通道以零个文件参数调用 yamllint —— 在工具的 usage 屏幕上大声但误导地失败 —— 并让 shellcheck 通道在一个文件都没检查的情况下报告成功,因为其管道以 sed 结尾,吞掉了所有上游退出状态。两个通道共用的列表暂存与空列表拒绝片段由两个小 helper 构建;linter 映射表与通道的 PATH 拼装被导出,以便测试钉住这些行为。新增十个测试,覆盖每个通道的 git 失败中止、空列表拒绝、精确文件选择和 yamllint 自身的退出码,以及 PATH 顺序与安装器临时目录默认值。linter 版本、yamllint 可用性检查(command -v yamllint)和 PATH 顺序均未改变。

为什么需要

2026-09-24,两个 main 分支的 Lint & Static 运行因 fatal: detected dubious ownership in repository 失败:git ls-files 崩溃,yamllint 通道在 yamllint 的 usage 屏幕而非 git 错误上失败,而 shellcheck 通道在空文件列表上空转通过。#12648 通过在 checkout 前将作业工作区信任为 git safe.directory 消除了这个触发条件,但它的步骤以 || echo "::warning::…" 结尾,写入失败时作业会继续,两个通道仍会撞上同样的 dubious ownership。本 PR 消除的是失效模式本身 —— 无论列表因何变空(git 错误、safe.directory 步骤软失败、过滤器回归、目录结构变化),两个通道都会在真实原因处大声失败,而不是空转通过或死在 usage 屏幕上。

评审者测试计划

如何验证

运行 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js;Linux 上预期 Tests 16 passed (16)(6 个原有 + 10 个新增)。新的 git-sourced lint lanes describe 在伪造的 PATH 上桩化 git、file、yamllint 与 shellcheck,并钉住:git ls-files 失败时,每个通道都在 git 自身错误上中止且从不调用其 linter(:304 yamllint、:383 shellcheck);每个通道都拒绝在空的暂存列表上运行(:327、:406、:424);正常路径下每个通道恰好把检测到的文件传给其 linter(:343、:445);yamllint 自身的非零退出会使通道失败(:366);pip --user bin 目录在 Linux 与 macOS 上追加在继承 PATH 之后、在 Windows 上省略(:484);通道 PATH 默认指向安装器实际解压的临时目录(:524)。8 个通道用例在安装器不支持的平台/架构上于运行时经 ctx.skip() 跳过;两个 getLinterPath 用例(:484、:524)无门槛执行,包括 Windows 腿。

维护者的真实环境验证见第 1 轮和第 2 轮。两轮都在真实的 git dubious ownership 下驱动真实的 node scripts/lint.js 通道,作业以 root 运行、工作区属于 runner 用户。在该触发条件下,main 的 shellcheck 通道检查 0 个文件却 exit 0,yamllint 通道失败在 usage 屏幕上;本 PR 下两个通道都 exit 1、点名 git 的报错,且从不调用 linter。健康检出下两棵树检查同样的 66 个脚本和 78 个 YAML 文件,输出逐字节一致,shellcheck 的结果与真实 CI 作业逐行一致。把 main 的通道字符串换回去,恰好挂掉 5 个守卫测试。

证据(前后对比)

N/A —— 仅 CI 通道行为;无用户可见界面。#12647 触发条件下通道的前后对比见上方英文部分维护者验证中的截图。

测试平台

见上方表格。Linux:本地与 CI 中套件全绿(16/16),并在原生 x86_64 的 Ubuntu 24.04 装置(dash、git 2.55)上实际运行了真实通道。macOS:部分测试 —— 维护者第 1 轮在 macOS arm64 上跑过 16 个用例中的 14 个;之后新增的两个用例(yamllint 退出码通道用例 :366 和 getLinterPath 默认值用例 :524)还没有在 macOS 上运行过。Windows:未运行;该套件不在 win32 排除列表中,8 个通道用例在运行时经 ctx.skip() 跳过(已在 Linux 上通过模拟不支持的架构验证:8 个通过、8 个跳过),两个 getLinterPath 用例无门槛执行。

环境(可选)

单元测试在 Linux 上运行。真实通道运行使用维护者的 Docker 装置(Ubuntu 24.04.4 amd64,/bin/sh = dash,git 2.55.0,GNU xargs 4.9.0,通过脚本自带、带 SHA-256 校验的 --setup 安装钉定的 actionlint 1.7.12 / shellcheck 0.11.0,yamllint 1.35.1)。

风险与范围

  • 主要风险或权衡:通道现在在以往可能空转通过的地方硬失败;在健康的检出中候选列表从不为空(目前是 66 个脚本、78 个 YAML 文件),因此该改动只是把静默的假绿变成大声失败。
  • 未验证 / 超出范围:原始的 dubious-ownership 触发条件(已由 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 消除);管道末尾的 sed 仍会吞掉 shellcheck 自身的退出码 —— 无论是有发现、二进制缺失还是崩溃 —— 这是脚本引入时就存在的问题,留给后续处理,因为让每条发现都生效会让 main 因现有约 2300 条 warning 变红;linter 版本与 linter 集合不变。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

引用 #12647(暴露此失效模式的事故报告;触发条件本身已由 #12648 消除)。

…is stale or broken (#12647)

Two main-branch CI runs died at `Run yamllint` on the same self-hosted
runner (ecs-qwen-hk4-19), each step completing in the same second it
started — yamllint never ran. `Install linters` was green because setup
trusted `command -v yamllint`: the image ships a copy that is stale or
crashes at launch, which satisfied the check, suppressed the pinned
install, then died the moment the lane invoked it. A second latent gap
made recovery impossible even when the install did run: runCommand
appended the pip --user bin dir last in PATH, so the image copy still
shadowed the pinned one.

Probe the version instead of the name: `yamllint --version` must print
exactly the pinned version, or the pinned install runs. And put the pip
--user bin dir ahead of the inherited PATH so the pinned install is what
the lane resolves. The identical tree lints clean with yamllint 1.35.1,
and a same-day run on a healthy runner passed the step in 3s.

Coverage: a probe unit test (absent/pinned/stale/crashing copies), a
PATH-order unit test, and an end-to-end surrogate replaying the incident
— a fake runner image whose yamllint dies on launch, a fake pip3 that
installs a healthy stub into the pip --user bin dir, and the real
`node scripts/lint.js --setup --yamllint` against the real checkout.
Mutation-probed: reverting either half fails the suite.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

Autofix E2E Report — #12647 (Main CI failed: Run yamllint on a064952)

What failed

The main-branch CI run for a064952e33 failed in the Lint & Static job at the Run yamllint step, before any test result was reported. The failing commit touched only TypeScript sources under packages/core — no YAML files — so the step failure could not be a lint violation in the tree. This report traces the failure to its root cause and verifies the fix with a surrogate that reproduces the failing environment.

Root cause

Evidence assembled from the public Actions API and local reproduction:

  • The failed run (35999787358) and an earlier failed main run (35992528956, commit 00080e0d23) both ran Lint & Static on the same self-hosted runner, ecs-qwen-hk4-19. In both, the Run yamllint step started and completed within the same second — yamllint never actually linted anything (a real run over this repo's ~200 YAML files takes ~3s, measured on the passing run 35994576202 which ran on ecs-qwen-hk5-9).
  • In the failed jobs, Install linters succeeded. That step only installs yamllint when command -v yamllint finds nothing, so the runner image must already provide something named yamllint — a copy that is stale (too old for this repo's .yamllint.yml, dying instantly on an unknown config key) or broken (crashing at launch). Setup trusted it, and the lint step died the moment it invoked it.
  • Running the exact CI command (git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github) locally with the pinned yamllint 1.35.1 against the same tree passes with exit 0; no YAML file changed between the failed commit and current main. The tree was never the problem.

A second latent defect made the first one unrecoverable: even when setup does install the pinned yamllint via pip3 install --user, scripts/lint.js appended ~/.local/bin last in the child PATH, so a copy shipped earlier in the runner image's PATH would still shadow the pinned install at run time.

Fix

scripts/lint.js:

  1. The yamllint availability check is now a version probe — test "$(yamllint --version 2>/dev/null)" = 'yamllint <pin>' — instead of command -v yamllint. A missing, stale, or launch-crashing copy all fail the probe and fall through to the pinned pip3 install --user install.
  2. The pip --user bin directory now leads the inherited PATH in the linter child environment (was: appended last), so the pinned install is the binary the lint step actually resolves.

Both halves are required: the probe alone would install the pinned copy but still resolve the broken image copy first; the PATH order alone would never install anything because command -v already found the broken copy.

Regression coverage

Three new tests in scripts/tests/lint.test.js:

  • accepts only the pinned yamllint version — behavioral probe test: absent / pinned / stale / crash-on-launch yamllint fakes produce fail / pass / fail / fail.
  • prefers the pip --user bin dir over the inherited PATH — pure PATH-construction test for linux, darwin, and win32.
  • replaces a broken runner-image yamllint with the pinned install — end-to-end surrogate of the incident: a fake runner image whose yamllint dies on launch and whose pip3 installs a healthy stub into the pip --user bin dir, then the real node scripts/lint.js --setup --yamllint runs against the real repository checkout. Asserts the probe met the broken copy (--version), the lint run went to the pinned install (--format), and the lane exits 0.

Mutation probes (each reverted hunk was confirmed to fail the suite, then restored):

  • Reverting the check to command -v yamllint → 2 tests fail (probe test, incident surrogate).
  • Reverting the PATH order to append-last → 2 tests fail (PATH-order test, incident surrogate).

Environment-specific limitation

The failing runner image itself cannot be inspected from here, so the exact pathology on ecs-qwen-hk4-19 (stale version vs. broken install) is inferred from step timing and the green Install linters step, not read from its logs. Both pathologies take the same code path through the fix and are both covered by the surrogate test. The repository's own CI remains the final verification gate: this PR changes scripts/lint.js, which routes the PR to the full CI profile, exercising the repaired lane on the shared runner pool.

Verification

  • git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github (yamllint 1.35.1, exact CI command, current tree) — passed, exit 0
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js — 9 passed (3 new)
  • Mutation probe 1 (check reverted to command -v yamllint) — 2 tests failed as required; restored and green
  • Mutation probe 2 (pip user bin appended last again) — 2 tests failed as required; restored and green
  • node scripts/lint.js --setup --yamllint with a genuine pinned yamllint on PATH — probe passed without installing, lane exit 0
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js — passed
  • npm run test:scripts (full scripts suite) — 92/94 files passed; the 12 failures in verify-capture.test.js (1) and web-shell-publish-artifacts.test.js (11) reproduce identically with this change stashed (base-tree run), i.e. pre-existing sandbox limitations unrelated to this change; lint.test.js passes within the suite
中文说明

Autofix E2E 报告 —— #12647(main 分支 CI 在 a064952 的 Run yamllint 步骤失败)

失败现象

a064952e33 的 main 分支 CI 运行在 Lint & Static 作业的 Run yamllint 步骤失败,且失败发生在任何测试结果产出之前。该提交只改动了 packages/core 下的 TypeScript 源码——没有改动任何 YAML 文件——因此步骤失败不可能是仓库树内的 lint 违规。本报告追溯了失败的根本原因,并用一个复现故障环境的替代测试验证了修复。

根本原因

通过公开的 Actions API 与本地复现收集到的证据:

  • 失败的运行(35999787358)与此前一次失败的 main 运行(35992528956,提交 00080e0d23)都调度到了同一台自托管 runner ecs-qwen-hk4-19。两次运行中,Run yamllint 步骤都在同一秒内开始并结束——yamllint 根本没有真正执行 lint(对本仓库约 200 个 YAML 文件的真实运行约需 3 秒,该数据来自在 ecs-qwen-hk5-9 上通过的运行 35994576202)。
  • 失败的作业中 Install linters 步骤是成功的。该步骤只有在 command -v yamllint 找不到 yamllint 时才会安装,因此 runner 镜像里必然已经存在某个名为 yamllint 的东西——一个过旧的副本(对本仓库的 .yamllint.yml 来说版本太老,遇到未知配置项立即报错退出)或已损坏的副本(启动即崩溃)。setup 信任了它,lint 步骤一调用它就失败了。
  • 在本地用钉定的 yamllint 1.35.1 对同一棵树运行与 CI 完全相同的命令(git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github)以退出码 0 通过;且失败提交与当前 main 之间没有任何 YAML 文件变化。仓库树从来不是问题所在。

第二个潜在缺陷使第一个缺陷无法自愈:即使 setup 确实通过 pip3 install --user 安装了钉定版本,scripts/lint.js 此前把 ~/.local/bin 追加在子进程 PATH 的末尾,因此 runner 镜像 PATH 中位置靠前的副本在运行时仍然会遮蔽钉定安装。

修复

scripts/lint.js:

  1. yamllint 可用性检查改为版本探测——test "$(yamllint --version 2>/dev/null)" = 'yamllint <钉定版本>'——取代 command -v yamllint。缺失、过旧、启动即崩溃的副本都会使探测失败,从而落入钉定版本的 pip3 install --user 安装。
  2. pip --user bin 目录在 linter 子进程环境中现在置于继承 PATH 之前(此前为追加在末尾),因此 lint 步骤实际解析到的就是钉定安装的二进制。

两半缺一不可:只有探测而没有 PATH 调整,钉定副本装上了却仍会先解析到镜像里的坏副本;只有 PATH 调整而没有探测,则因为 command -v 已经找到了坏副本而永远不会触发安装。

回归覆盖

scripts/tests/lint.test.js 新增三个测试:

  • accepts only the pinned yamllint version——行为化探测测试:缺失 / 钉定版本 / 过旧 / 启动即崩溃四种 yamllint 假副本分别产生 失败 / 通过 / 失败 / 失败。
  • prefers the pip --user bin dir over the inherited PATH——针对 linux、darwin、win32 三个平台的纯 PATH 构造测试。
  • replaces a broken runner-image yamllint with the pinned install——事故的端到端替代复现:构造一个假 runner 镜像,其 yamllint 启动即崩溃、其 pip3 会把健康桩安装进 pip --user bin 目录,然后对真实仓库检出运行真实的 node scripts/lint.js --setup --yamllint。断言探测遇到的是坏副本(--version),lint 运行走的是钉定安装(--format),且整条通道退出码为 0。

变异探针(每个被还原的代码块都先确认会让测试套件失败,随后恢复):

  • 把检查还原为 command -v yamllint → 2 个测试如期失败(探测测试、事故替代测试)。
  • 把 PATH 顺序还原为追加在末尾 → 2 个测试如期失败(PATH 顺序测试、事故替代测试)。

环境限制

无法从此处检查故障 runner 镜像本身,因此 ecs-qwen-hk4-19 上的确切病理(版本过旧还是安装损坏)是从步骤耗时和绿色的 Install linters 步骤推断的,而非直接读取其日志。两种病理经过修复后走同一代码路径,且都被替代测试覆盖。仓库自身 CI 仍是最终验证关口:本 PR 改动了 scripts/lint.js,会把 PR 路由到完整 CI 配置,从而在共享 runner 池上实际运行修复后的通道。

验证

  • git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github(yamllint 1.35.1,与 CI 完全相同的命令,当前代码树)——通过,退出码 0
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js——9 个通过(3 个新增)
  • 变异探针 1(检查还原为 command -v yamllint)——2 个测试如期失败;已恢复并恢复为绿
  • 变异探针 2(pip user bin 恢复为追加在末尾)——2 个测试如期失败;已恢复并恢复为绿
  • node scripts/lint.js --setup --yamllint(PATH 上放真实的钉定版本 yamllint)——探测通过且未触发安装,通道退出码 0
  • npm run build——通过
  • npm run typecheck——通过
  • npm run lint——通过
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js——通过
  • npm run test:scripts(完整 scripts 套件)——92/94 个文件通过;verify-capture.test.js(1 个)和 web-shell-publish-artifacts.test.js(11 个)中的 12 个失败在 stash 掉本次改动后的基线树上同样复现,即与本改动无关的沙箱环境预置限制;lint.test.js 在套件中通过

🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Sep 24, 2026
@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

⚠️ AutoFix round 10 ended without publishing a report — view run.

中文说明

⚠️ AutoFix 第 10 轮结束但未发布报告 —— 查看运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 1/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/10 轮)。改动内容与我反驳保留之处如下:

Autofix round summary — PR #12650 (issue #12647)

Feedback addressed

[rv:5305678922] CHANGES_REQUESTED @qwen-code-ci-bot — the only feedback item this round. It made four checkable claims; each was probed before any code change:

  1. "The diff leaves the yamllint run command unchanged, so the same failure recurs verbatim" — reproduced and fixed. Probe on the pre-round code (non-git dir, GNU xargs 4.9.0, zero-file yamllint stub): git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github invoked yamllint with zero file operands and exited 123 with only a usage screen, exactly the Main CI failed: Qwen Code CI on a064952e33dc #12647 symptom. The lane now stages the list first: git ls-files failure aborts the lane with git's own error plus yamllint: git ls-files failed; refusing to lint an empty file list, and an empty match aborts with yamllint: git ls-files matched no yaml files; …. No xargs -r — per the review, that would only convert the loud failure into a false green.

  2. "Run shellcheck concluded success while linting zero files, because its pipeline ends in sed and swallows the status" — reproduced and fixed. Probe on the pre-round code: with git ls-files failing, the full shellcheck pipeline exited 0 (silent pass). The lane now stages git ls-files (fail on git error), asserts the candidate list is non-empty, and asserts the file --mime-type-detected shell-script list is non-empty before invoking shellcheck.

  3. "The getLinterPath extraction is good and worth keeping" — kept (function, injectable env/platform/cwd).

  4. "Split out the version probe / pin enforcement … and drop the global PATH reorder" — implemented the drop. check is back to command -v yamllint; getLinterPath restores the original append order (pip --user bin dir after the inherited PATH). The version probe and the reorder stood or fell together — the probe without the reorder would reinstall the pin yet still resolve the image copy — and the review reproduced that the stale/broken-image scenario was not the Main CI failed: Qwen Code CI on a064952e33dc #12647 cause. If pin enforcement is still wanted, it can land as its own PR with its own justification, as the review suggested.

Not implemented (needs a maintainer): the review's workflow-side alternatives — a durable safe.directory for post-checkout steps, and handling uid == 0 in the ownership-restore step — live in .github/workflows/*.yml, which this PR has never touched and which the autofix CI-machinery boundary bars this loop from editing. With the lane-side loud failure landed, those are hardening options, not prerequisites; they remain open for a maintainer who wants defense in depth.

Deferred (verified, out of scope): the same trailing sed also swallows shellcheck's own exit status, so shellcheck findings have never been able to fail the lane (swallowed since the initial import in eb95c13). Verified with the pinned shellcheck 0.11.0 (SHA-256 checked against scripts/lint.js): it reports 2348 lines of findings on this repo and exits non-zero, while the lane exits 0. Enforcing findings requires a repo-wide cleanup or an exclude-list decision — far outside #12647 — so it is recorded in deferred-findings.json instead of implemented here.

Observation for maintainers (not feedback, not changed): the CI step Run sensitive keyword linter invokes node scripts/lint.js --sensitive-keywords, but main() has no such flag branch — the step is a silent no-op. Same false-green class as this issue; flagging for triage.

Changes

  • scripts/lint.js — yamllint and shellcheck run commands stage the git-sourced file list and refuse to run/pass on git failure or an empty list; yamllint check reverted to command -v yamllint; getLinterPath restored to the original PATH append order (extraction kept).
  • scripts/tests/lint.test.js — replaced the yamllint lane describe (which pinned the refuted stale-image scenario) with git-sourced lint lanes: six lane tests (git-failure, empty-list, and happy-path selection for each linter) plus the PATH-order test flipped to pin the append order.

Test witnesses and mutation probes

  • The six new/updated tests were first run against the pre-round code: exactly those 6 failed, 8 passed — the new tests fail without the fix.
  • Mutation probes on the committed code, each followed by a restore and a green re-run: removing (a) the yamllint git-failure guard, (b) the yamllint empty-list assertion, (c) the shellcheck git-failure guard, (d) the shellcheck candidate assertion, or (e) the shellcheck detected-scripts assertion each turned its matching witness test red.

Conflict notes

None — --conflict false; no merge performed.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js — 14 passed (pre-round red witness run of the same file: 6 failed, 8 passed). Run with COREPACK_HOME/XDG_CACHE_HOME pointed at a writable temp dir because this sandbox's default corepack cache path is read-only; the tests themselves are unaffected.
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/ci-platform-lanes.test.js — 38 passed.
  • Committed-state re-run of both suites — 52 passed.
  • npm run build — passed.
  • npm run typecheck — passed.
  • npm run lint — passed.
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js — passed.
  • Real-lane smoke: node scripts/lint.js --yamllint with a logging yamllint stub on PATH — exit 0, yamllint invoked with --format github plus exactly the 76 repo yaml files.
  • Real-lane smoke: node scripts/lint.js --shellcheck with a file --mime-type stub and the pinned shellcheck 0.11.0 — exit 0, findings printed with the expected sed severity rewrite.
  • Not run: a real yamllint end-to-end (no pip in this sandbox; lane mechanics covered by the stub-based tests and smoke), and integration tests (these lanes are not exercised through the bundled CLI).
中文说明

Autofix 本轮总结 — PR #12650(issue #12647)

已处理的反馈

[rv:5305678922] CHANGES_REQUESTED @qwen-code-ci-bot —— 本轮唯一反馈。其中包含四个可检验的论断;在改动任何代码之前均已逐一探测验证:

  1. “diff 没有改动 yamllint 的 run 命令,同样的失败会原样复现” —— 已复现并修复。在改动前的代码上探测(非 git 目录、GNU xargs 4.9.0、零文件 yamllint 桩):git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github 以零个文件参数调用了 yamllint 并以 123 退出,输出只有 usage 信息,正是 Main CI failed: Qwen Code CI on a064952e33dc #12647 的症状。该 lane 现在先暂存文件列表:git ls-files 失败时 lane 以 git 自身的错误加上 yamllint: git ls-files failed; refusing to lint an empty file list 中止;匹配为空时以 yamllint: git ls-files matched no yaml files; … 中止。未使用 xargs -r —— 按评审意见,那只会把大声失败变成假绿。

  2. “Run shellcheck 在检查零个文件的情况下得出 success,因为它的管道以 sed 结尾并吞掉了状态” —— 已复现并修复。在改动前的代码上探测:git ls-files 失败时,整条 shellcheck 管道退出码为 0(静默通过)。该 lane 现在先暂存 git ls-files(git 出错即失败),并在调用 shellcheck 之前断言候选列表非空、经 file --mime-type 检测出的 shell 脚本列表非空。

  3. “getLinterPath 的抽取是好的,值得保留” —— 已保留(函数及可注入的 env/platform/cwd 参数)。

  4. “把版本探测/钉定强制拆出去……并去掉全局 PATH 顺序调整” —— 已按建议移除。check 恢复为 command -v yamllint;getLinterPath 恢复原有的追加顺序(pip --user bin 目录排在大于继承 PATH 之后)。版本探测与 PATH 重排是耦合的 —— 只留探测而不重排时,钉定版本会被重新安装却仍解析到镜像里的副本 —— 且评审已复现:Main CI failed: Qwen Code CI on a064952e33dc #12647 的根因并非镜像中 yamllint 过旧/损坏这一场景。若仍需要钉定强制,可按评审建议单独成 PR 并附各自立论。

未实现(需要维护者决定): 评审提到的工作流侧替代方案 —— 为 checkout 之后的步骤配置持久的 safe.directory,以及在 ownership 恢复步骤中处理 uid == 0 —— 位于 .github/workflows/*.yml,本 PR 从未触碰该目录,且 autofix 的 CI 设施边界禁止本循环编辑它。lane 侧的大声失败落地后,这些属于加固选项而非前置条件,留给需要纵深防御的维护者。

暂缓(已验证属实,超出本 PR 范围): 管道末尾的 sed 同时吞掉了 shellcheck 自身的退出状态,因此 shellcheck 的发现从未能让该 lane 失败(自最初引入 eb95c13 起即如此)。已用钉定的 shellcheck 0.11.0 验证(SHA-256 与 scripts/lint.js 中的钉值核对一致):它在本仓库上报告 2348 行发现并以非零退出,而 lane 退出码为 0。让发现生效需要全仓库清理或调整排除清单 —— 远超 #12647 的范围 —— 因此记录进 deferred-findings.json,不在此处实现。

给维护者的观察(非反馈、未改动): CI 步骤 Run sensitive keyword linter 调用 node scripts/lint.js --sensitive-keywords,但 main() 没有该标志分支 —— 该步骤是静默空操作。与本 issue 同属假绿类别,提请分类处理。

改动

  • scripts/lint.js —— yamllint 与 shellcheck 的 run 命令改为先暂存基于 git 的文件列表,在 git 失败或列表为空时拒绝运行/通过;yamllint 的 check 恢复为 command -v yamllint;getLinterPath 恢复原有的 PATH 追加顺序(保留抽取)。
  • scripts/tests/lint.test.js —— 用 git-sourced lint lanes 替换原 yamllint lane 描述块(原块钉住的是已被证伪的镜像过旧场景):六个 lane 测试(两个 linter 各自的 git 失败、空列表与正常路径选择),以及改为钉住追加顺序的 PATH 测试。

测试见证与变异探测

  • 六个新增/更新的测试先在改动前的代码上运行:恰好这 6 个失败、8 个通过 —— 没有修复时新测试确实变红。
  • 对提交后的代码做变异探测,每次变异后均恢复并重新跑绿:分别移除 (a) yamllint 的 git 失败守卫、(b) yamllint 的空列表断言、(c) shellcheck 的 git 失败守卫、(d) shellcheck 的候选断言、(e) shellcheck 的检测结果断言,每一次都使其对应的见证测试变红。

冲突说明

无 —— --conflict false;未执行合并。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js —— 14 通过(同一文件在改动前的红灯见证运行:6 失败、8 通过)。运行时把 COREPACK_HOME/XDG_CACHE_HOME 指向可写临时目录,因为本沙箱默认的 corepack 缓存路径只读;测试本身不受影响。
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/ci-platform-lanes.test.js —— 38 通过。
  • 提交后状态对两个套件的重跑 —— 52 通过。
  • npm run build —— 通过。
  • npm run typecheck —— 通过。
  • npm run lint —— 通过。
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js —— 通过。
  • 真实 lane 冒烟:node scripts/lint.js --yamllint(PATH 上放记录参数的 yamllint 桩)—— 退出码 0,yamllint 以 --format github 加恰好 76 个仓库 yaml 文件被调用。
  • 真实 lane 冒烟:node scripts/lint.js --shellcheck(file --mime-type 桩 + 钉定的真实 shellcheck 0.11.0)—— 退出码 0,发现按预期经 sed 重写严重级别后打印。
  • 未运行:真实 yamllint 端到端(本沙箱无 pip;lane 机制已由桩测试与冒烟覆盖),以及集成测试(这些 lane 不经过打包后的 CLI 执行)。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.4

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [Lint & Static (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Lint & Static (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no action taken (PR #12650, issue #12647)

Feedback this round

One issue-level comment and one failed check, both reporting the same event:

  • [ic:5849911712] @qwen-code-ci-bot (review-fallback) — "Qwen Code review did not complete successfully. Qwen review exited with status 1. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review."
  • Failed check: review-pr FAILURE (run 36259959805, job 108456197970, 2026-09-26 17:57:24Z → 21:10:14Z).

There were no inline comments, no review bodies, and no other failed checks. The previous review findings ([rv:5305678922], CHANGES_REQUESTED) predate the last evaluation watermark and were addressed in round 1 (commit b83301c7).

Why no code change

The failed check is the LLM review pipeline failing at the agent-process level, not a defect in this PR:

  1. The failing job never executes this PR's code. qwen-code-pr-review.yml contains no invocation of scripts/lint.js or npm run lint (verified by grep — zero matches). The PR touches only scripts/lint.js and scripts/tests/lint.test.js.
  2. The failure signature is an agent crash, not a review verdict. "Qwen review exited with status 1" is emitted by the workflow (line 1832) when the qwen review CLI process exits non-zero — a path distinct from the timeout branch (exit 124/137, which reports "timed out" instead) and from log-write failures. The job ran ~193 minutes against a 360-minute job cap, consistent with the run consuming its shared retry budget on agent-level errors.
  3. Every check that does exercise this PR's changes passed on this exact head (a3f82a57ba): Lint & Static (runs node scripts/lint.js --setup/--shellcheck/--yamllint, ci.yml:1192–1209) succeeded at 17:55:03Z; Test (ubuntu-latest, Node 22.x) (runs npm run test:scripts, ci.yml:790, covering the new scripts/tests/lint.test.js lane tests) succeeded at 18:04:16Z; Integration Tests (no-AK, No Sandbox) succeeded at 17:49:13Z.
  4. The prescribed remediation is a review re-trigger, not a code change. The fallback comment itself states transient errors are retried automatically and otherwise to retry with @qwen-code /review. Posting that comment requires GitHub credentials this round does not have — the workflow owns all GitHub writes, and the autofix workflow already classifies review-pr as a non-blocking check.

No hypothesis connects the ~300-line diff (shell lane guards plus their tests) to a deterministic review-agent crash, and the review logs are not readable from this sandbox (gh is unauthenticated) to pursue one further. If the failure recurs on re-review, it belongs to the review infrastructure owners, not to this PR's diff.

State of the PR

  • Head: a3f82a57ba (includes the round-1 fix and a clean merge of main from 2026-09-24). Working tree clean; no merge performed (--conflict false).
  • All deterministic CI checks green on this head; the only red check is the review pipeline failure described above.
  • The PR awaits a successful re-review of the round-1 changes. Nothing in this round's feedback asked for a code change, and none was made.

Verification

No code was changed, so no build/test re-runs were required this round. Evidence actually gathered:

  • git diff origin/main...HEAD — read in full; the diff matches the round-1 state (lint lane guards + tests) and nothing else.
  • git status / git rev-parse HEAD — clean tree at a3f82a57ba, up to date with origin/autofix/issue-12647.
  • Grep of .github/workflows/qwen-code-pr-review.yml for scripts/lint.js / npm run lint — zero matches (the failing job does not run the changed code).
  • Read the review-result handling in qwen-code-pr-review.yml (lines 1741–1870) — confirmed exit-status-1 is the agent-process failure path, distinct from timeout and log-write paths.
  • Grep of .github/workflows/ci.yml — confirmed Lint & Static runs the changed lanes (lines 1192–1217) and Test runs npm run test:scripts (line 790); both checks are green on this head per the check-run rollup.
  • gh auth status — unauthenticated; the failed run's logs are not accessible from this sandbox.
中文说明

Autofix 本轮 —— 未做改动(PR #12650,issue #12647)

本轮反馈

一条 issue 级评论和一个失败的检查,两者报告的是同一事件:

  • [ic:5849911712] @qwen-code-ci-bot(review 回退评论) —— "Qwen Code 审查未能成功完成。 Qwen 审查以状态码 1 退出。瞬时错误会自动重试;如果您看到此消息,请用 @qwen-code /review 重试。"
  • 失败检查:review-pr FAILURE(运行 36259959805,作业 108456197970,2026-09-26 17:57:24Z → 21:10:14Z)。

本轮没有任何行内评论、没有 review 正文,也没有其他失败的检查。此前的 review 发现([rv:5305678922],CHANGES_REQUESTED)早于上次评估水位线,已在第 1 轮(提交 b83301c7)处理完毕。

为什么不做代码改动

失败的检查是 LLM 审查流水线在代理进程层面的失败,而不是本 PR 的缺陷:

  1. 失败的作业根本不执行本 PR 的代码。 经 grep 验证,qwen-code-pr-review.yml 中没有任何对 scripts/lint.js 或 npm run lint 的调用(零匹配)。本 PR 只改动了 scripts/lint.js 和 scripts/tests/lint.test.js。
  2. 失败特征是代理进程崩溃,而不是审查结论。 "Qwen review exited with status 1" 由工作流(第 1832 行)在 qwen 审查 CLI 进程以非零状态退出时发出 —— 这条路径与超时分支(退出码 124/137,会报告 "timed out")和日志写入失败都不同。该作业在 360 分钟的作业上限内运行了约 193 分钟,与运行在代理级错误上耗尽共享重试预算的表现一致。
  3. 所有真正执行本 PR 改动的检查都在当前 head(a3f82a57ba)上通过: Lint & Static(运行 node scripts/lint.js --setup/--shellcheck/--yamllint,ci.yml:1192–1209)于 17:55:03Z 成功;Test (ubuntu-latest, Node 22.x)(运行 npm run test:scripts,ci.yml:790,覆盖新增的 scripts/tests/lint.test.js lane 测试)于 18:04:16Z 成功;Integration Tests (no-AK, No Sandbox) 于 17:49:13Z 成功。
  4. 既定的补救方式是重新触发审查,而不是改代码。 回退评论本身就说明瞬时错误会自动重试,否则用 @qwen-code /review 重试。发该评论需要本轮不具备的 GitHub 凭据 —— 所有 GitHub 写操作均由工作流负责,且 autofix 工作流本身已把 review-pr 归为非阻塞检查。

没有任何假设能把这段约 300 行的 diff(shell lane 守卫及其测试)与审查代理的确定性崩溃联系起来;本沙箱内 gh 未认证,无法读取审查日志进一步追查。如果重审后该失败复发,应归审查基础设施的维护者处理,而不是本 PR 的 diff。

PR 当前状态

  • Head:a3f82a57ba(包含第 1 轮修复及 2026-09-24 对 main 的干净合并)。工作区干净;未执行合并(--conflict false)。
  • 该 head 上所有确定性 CI 检查均为绿色;唯一红色检查即上述审查流水线失败。
  • PR 等待对第 1 轮改动的一次成功复审。本轮反馈中没有要求改动代码的内容,本轮也未做任何改动。

验证

本轮未改动代码,因此无需重跑构建/测试。实际收集的证据:

  • git diff origin/main...HEAD —— 已完整阅读;diff 与第 1 轮状态一致(lint lane 守卫 + 测试),无其他内容。
  • git status / git rev-parse HEAD —— 工作区干净,位于 a3f82a57ba,与 origin/autofix/issue-12647 同步。
  • 对 .github/workflows/qwen-code-pr-review.yml grep scripts/lint.js / npm run lint —— 零匹配(失败的作业不运行被改动的代码)。
  • 阅读 qwen-code-pr-review.yml 中审查结果处理逻辑(第 1741–1870 行)—— 确认退出码 1 是代理进程失败路径,与超时及日志写入路径均不同。
  • 对 .github/workflows/ci.yml grep —— 确认 Lint & Static 运行被改动的 lane(第 1192–1217 行),Test 运行 npm run test:scripts(第 790 行);按检查汇总,两者在该 head 上均为绿色。
  • gh auth status —— 未认证;本沙箱无法读取失败运行的日志。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no action taken (PR #12650, issue #12647)

Feedback this round

Exactly one item is newer than the last evaluation watermark (2026-09-26T21:10:14Z):

  • [ic:5851963839] @qwen-code-ci-bot (verify status) — "🔬 Sandboxed verification is running — the report will be posted here when the run completes." This is a status marker posted by the triage/verify workflow (run 36286871134) after @wenshao triggered @qwen-code /triage ([ic:5851767635]). It announces that an asynchronous sandboxed verification of this PR is in progress; it contains no finding, request, or defect claim.

There were no inline comments (rc.json is empty), no new review bodies, no failed checks, and no still-red checks. The earlier CHANGES_REQUESTED review ([rv:5305678922], 2026-09-24) predates the watermark and was addressed in round 1 (commit b83301c7); the round-2 transient review-pr pipeline failure re-ran green after the base update (check-run rollup on head de0f9143 shows review-pr SUCCESS and zero non-passing checks overall).

Why no code change

  1. A status comment is not actionable feedback. The verify run is owned by a separate workflow with its own credentials; its report, if it surfaces anything, will arrive as ordinary new feedback for a future round to classify and address. There is nothing in this item that maps to a code edit.
  2. Nothing else regressed. All deterministic checks that exercise this PR's diff are green on the current head: Lint & Static (runs node scripts/lint.js --setup/--shellcheck/--yamllint, the lanes this PR changes), Test (ubuntu-latest, Node 22.x) (runs npm run test:scripts, covering the new scripts/tests/lint.test.js lane tests), and Integration Tests (no-AK, No Sandbox).
  3. Diff-growth budget is intact (source 0 / test 0 net this window), and --conflict false correctly required no merge — the branch already contains a clean merge of current main (de0f9143, from the 2026-09-26 22:19 base update).

State of the PR

  • Head: de0f9143ce1fe71df3d1946e75f11d26c5134032 (round-1 fix b83301c7 plus clean merges of main). Working tree clean and in sync with origin/autofix/issue-12647; no commit made this round.
  • The diff remains 53 source lines in scripts/lint.js (fail-loudly guards on the git-sourced file lists for the yamllint and shellcheck lanes, plus the getLinterPath extraction the reviewer asked to keep) and 252 lines of lane tests in scripts/tests/lint.test.js.
  • The PR awaits the outcome of the in-flight sandboxed verification and/or a fresh human/automated review. Nothing actionable exists for this round.

Verification

No code was changed, so no build/test re-runs were required. Evidence actually gathered:

  • git diff origin/main...HEAD — read in full; matches the round-1 state (lint lane guards + tests) and nothing else.
  • git status / git rev-parse HEAD — clean tree at de0f9143ce1fe71df3d1946e75f11d26c5134032, up to date with origin/autofix/issue-12647.
  • feedback.md — parsed; the sole new item is [ic:5851963839], the verify-status comment triaged above.
  • checks.json — enumerated all check runs; 0 non-passing conclusions, including a green review-pr.
  • rc.json / rv.json — confirmed zero inline comments and no reviews newer than the watermark.
中文说明

Autofix 本轮 —— 未做改动(PR #12650,issue #12647)

本轮反馈

晚于上次评估水位线(2026-09-26T21:10:14Z)的反馈仅有一条:

  • [ic:5851963839] @qwen-code-ci-bot(验证状态评论) —— "🔬 沙箱验证正在运行 —— 运行结束后验证报告会发布在这里。" 这是 @wenshao 触发 @qwen-code /triage([ic:5851767635])后,由 triage/verify 工作流(运行 36286871134)发布的状态标记。它只是告知对本 PR 的异步沙箱验证正在进行中,不包含任何发现、请求或缺陷主张。

本轮没有任何行内评论(rc.json 为空)、没有新的 review 正文、没有失败的检查,也没有持续红色的检查。此前的 CHANGES_REQUESTED review([rv:5305678922],2026-09-24)早于水位线,已在第 1 轮(提交 b83301c7)处理完毕;第 2 轮出现的 review-pr 流水线瞬时失败在 base 更新后已重跑转绿(head de0f9143 上的检查汇总显示 review-pr 为 SUCCESS,且整体无任何未通过检查)。

为什么不做代码改动

  1. 状态评论不是可执行的反馈。 该验证运行由另一个独立工作流持有凭据并负责;其报告若有任何发现,会作为普通的新反馈到达,由后续轮次分类处理。该条评论没有任何可映射到代码修改的内容。
  2. 没有其他任何回归。 在当前 head 上,所有真正执行本 PR diff 的确定性检查均为绿色:Lint & Static(运行 node scripts/lint.js --setup/--shellcheck/--yamllint,即本 PR 改动的 lane)、Test (ubuntu-latest, Node 22.x)(运行 npm run test:scripts,覆盖新增的 scripts/tests/lint.test.js lane 测试)、以及 Integration Tests (no-AK, No Sandbox)。
  3. diff 增长预算完好(本窗口净增 source 0 / test 0),且 --conflict false 正确地无需合并 —— 分支已包含对当前 main 的干净合并(de0f9143,来自 2026-09-26 22:19 的 base 更新)。

PR 当前状态

  • Head:de0f9143ce1fe71df3d1946e75f11d26c5134032(第 1 轮修复 b83301c7 加上对 main 的干净合并)。工作区干净并与 origin/autofix/issue-12647 同步;本轮未产生提交。
  • diff 仍为 scripts/lint.js 中 53 行源码改动(对 yamllint 与 shellcheck lane 的 git 文件列表做"空列表即失败"守卫,外加评审要求保留的 getLinterPath 抽取)以及 scripts/tests/lint.test.js 中 252 行 lane 测试。
  • PR 正在等待进行中的沙箱验证结果,和/或一次新的人工/自动复审。本轮不存在可处理的事项。

验证

本轮未改动代码,因此无需重跑构建/测试。实际收集的证据:

  • git diff origin/main...HEAD —— 已完整阅读;与第 1 轮状态一致(lint lane 守卫 + 测试),无其他内容。
  • git status / git rev-parse HEAD —— 工作区干净,位于 de0f9143ce1fe71df3d1946e75f11d26c5134032,与 origin/autofix/issue-12647 同步。
  • feedback.md —— 已解析;唯一的新条目是 [ic:5851963839],即上述已分类的验证状态评论。
  • checks.json —— 已枚举全部检查运行;0 个未通过结论,其中 review-pr 为绿色。
  • rc.json / rv.json —— 已确认零行内评论,且无晚于水位线的 review。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no code change (PR #12650, issue #12647)

Feedback this round

Exactly one item is newer than the last evaluation watermark (2026-09-27T02:27:55Z), and there are no inline comments, no failed checks, and no still-red checks:

Claim-by-claim verification

The review is source-blind-treated: every checkable claim was verified against the live diff and tree before accepting it. All of them hold.

  1. "Main CI failed: Qwen Code CI on a064952e33dc #12647 is closed by fix(ci): trust the job workspace as a git safe.directory before checkout #12648" — confirmed locally: 97cd5636c9 fix(ci): trust the job workspace as a git safe.directory before checkout (#12648) is on origin/main, and this branch merged main on 2026-09-26 (de0f9143ce), so the PR diff is exactly the remaining delta (scripts/lint.js +53/−18, scripts/tests/lint.test.js +252).
  2. "The diff leaves check: 'command -v yamllint' untouched" — confirmed: the availability check is unchanged in the diff; the version probe existed only in the superseded round-0 framing and was removed in round 1 (b83301c7).
  3. "The pip --user bin dir is still appended last, and the new test asserts that" — confirmed: getLinterPath appends ${HOME}/.local/bin (Linux) / Library/Python/3.12/bin (macOS) after the inherited PATH, and the test appends the pip --user bin dir after the inherited PATH pins exactly that order.
  4. "The fake-runner-image test-plan case is not in the diff" — confirmed: the 8 new tests are stub-based lane tests (git failure, empty-list refusal, happy-path file selection for each linter, PATH ordering); no such image-level test exists.
  5. "The correct Linux test count is 14 (6 on main + 8 new), not 9" — confirmed by counting (main has 6 it blocks; HEAD has 14) and by running the suite: 14 passed (see Verification).
  6. "The diff rewrites the shellcheck lane while the description calls that out of scope" — confirmed: the shellcheck run is rewritten to stage the list and refuse an empty one, so the old Risk & Scope text is stale.

Why no code change

The review asks for PR-metadata changes only — title, description, closing linkage — and explicitly asks for no code changes. This round cannot apply them, for two independent reasons:

  1. No credentials. This lane runs without GitHub credentials (gh has no token here), and the autofix rules forbid GitHub writes from the agent; the workflow owns all network writes.
  2. No channel. The address-review lane's contract carries only the round report (address-summary.md / no-action.md) plus thread-resolution sidecars; pr-title.txt / pr-body.md are consumed exclusively by the issue-fix lane's gh pr create, and no step in the review lane edits PR title or body.

So the finding is accepted, not declined — the framing genuinely is stale — but applying it is one human (or dev-bot-PAT) action away. To make that action mechanical, the exact replacement text is provided below.

Ready-to-apply title and description

Proposed title (matches the surviving delta; drop the (#12647) suffix framing):

fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list

Proposed body (drop Fixes #12647; reference without a closing keyword; claims corrected per the verification above):

## What this PR does

The two lint lanes that build their file lists from git (shellcheck and yamllint) now stage the list before piping it onward and hard-fail with a clear message when that list is empty, instead of letting an empty list reach the linter. Previously, a failing or empty `git ls-files` made the yamllint lane invoke yamllint with zero file operands — a loud but misleading failure on yamllint's usage screen — and made the shellcheck lane report success while linting nothing, because its pipeline ends in a `sed` that swallows every upstream exit status. The linter map and the lane PATH assembly are now exported so tests can pin them. Eight new tests cover git failure, empty-list refusal, and exact file selection for each lane, plus the PATH ordering.

## Why it's needed

On 2026-09-24 two main-branch Lint & Static runs failed with `fatal: detected dubious ownership in repository`: `git ls-files` died, the yamllint lane failed misleadingly on a tool usage screen, and the shellcheck lane passed vacuously on an empty file list. #12648 removed that trigger by trusting the job workspace as a git safe.directory before checkout; this PR removes the failure mode itself, so that whatever empties the list — a git error, a filter regression, a layout change — both lanes fail loudly at the real cause instead of passing vacuously or dying on a usage screen.

## Reviewer Test Plan

### How to verify

Run `npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js`; expect 14 passed on Linux (6 pre-existing + 8 new). The new tests stub `git`, `file`, `yamllint`, and `shellcheck` on a fake PATH and pin: each lane aborts with a clear error when `git ls-files` fails and never invokes its linter; each lane refuses to run or pass on an empty staged list; each lane passes exactly the detected files to its linter on the happy path; the pip `--user` bin dir is appended after the inherited PATH on Linux and macOS.

### Evidence (Before & After)

N/A — CI-lane behavior only; no user-visible surface.

### Tested on

|     OS     | Status |
| :--------: | :----: |
|  🍏 macOS  |  N/A   |
| 🪟 Windows |  N/A   |
|  🐧 Linux  |   ✅   |

The lanes are POSIX-only and the new tests skip on Windows; CI runs the real lanes on ubuntu-latest.

### Environment (optional)

N/A — unit tests only.

## Risk & Scope

- Main risk or tradeoff: a lane now hard-fails where it could previously pass vacuously; on a healthy checkout the candidate lists are never empty, so the change only converts silent greens into loud failures.
- Not validated / out of scope: the original dubious-ownership trigger (fixed by #12648); the trailing `sed` also swallowing shellcheck's own non-zero exit on real findings (pre-existing since the script's introduction, verified with the pinned shellcheck 0.11.0, and tracked as a deferred finding — enforcing findings needs a repo-wide cleanup or an exclude-list decision); linter versions and the set of linters are unchanged.
- Breaking changes / migration notes: none.

## Linked Issues

References #12647 (the failure report that exposed this failure mode; the trigger itself was fixed by #12648).

<details>
<summary>中文说明</summary>

## 本 PR 做了什么

两个基于 git 构建文件列表的 lint 通道(shellcheck 与 yamllint)现在在继续管道之前先暂存文件列表,并在列表为空时以清晰的错误信息硬失败,而不是让空列表到达 linter。此前,`git ls-files` 失败或为空会让 yamllint 通道以零个文件参数调用 yamllint —— 在 yamllint 的 usage 屏幕上大声但误导地失败 —— 并让 shellcheck 通道在一个文件都没检查的情况下报告成功,因为其管道以 `sed` 结尾,吞掉了所有上游退出状态。linter 映射表与通道的 PATH 拼装现在被导出,以便测试钉住这些行为。新增八个测试,覆盖每个通道的 git 失败、空列表拒绝与精确文件选择,以及 PATH 顺序。

## 为什么需要

2026-09-24,两个 main 分支的 Lint & Static 运行因 `fatal: detected dubious ownership in repository` 失败:`git ls-files` 崩溃,yamllint 通道在工具 usage 屏幕上误导性地失败,而 shellcheck 通道在空文件列表上空转通过。#12648 通过在 checkout 前将作业工作区信任为 git safe.directory 消除了该触发条件;本 PR 消除的是失效模式本身 —— 无论列表因何变空(git 错误、过滤器回归、目录结构变化),两个通道都会在真实原因处大声失败,而不是空转通过或死在 usage 屏幕上。

## 评审者测试计划

### 如何验证

运行 `npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js`;Linux 上预期 14 个通过(6 个原有 + 8 个新增)。新测试在伪造的 PATH 上桩化 `git`、`file`、`yamllint` 与 `shellcheck`,并钉住:`git ls-files` 失败时每个通道都以清晰错误中止且从不调用其 linter;暂存列表为空时每个通道都拒绝运行或通过;正常路径下每个通道恰好把检测到的文件传给其 linter;pip `--user` bin 目录在 Linux 与 macOS 上追加在继承 PATH 之后。

### 证据(前后对比)

N/A —— 仅 CI 通道行为;无用户可见界面。

### 测试平台

见上方表格。这些通道仅限 POSIX,新测试在 Windows 上跳过;CI 在 ubuntu-latest 上运行真实通道。

## 风险与范围

- 主要风险或权衡:通道现在在以往可能空转通过的地方硬失败;在健康的检出中候选列表从不为空,因此该改动只是把静默的假绿变成大声失败。
- 未验证 / 超出范围:原始的 dubious-ownership 触发条件(已由 #12648 修复);管道末尾的 `sed` 同样吞掉 shellcheck 自身在真实发现上的非零退出(自该脚本引入起即存在,已用钉定的 shellcheck 0.11.0 验证,并作为暂缓发现跟踪 —— 让发现生效需要全仓库清理或排除清单决策);linter 版本与 linter 集合不变。
- 破坏性变更 / 迁移说明:无。

## 关联 Issue

引用 #12647(暴露此失效模式的故障报告;触发条件本身已由 #12648 修复)。

</details>

Applying it is a single command for anyone with write access to the PR:

gh pr edit 12650 --repo QwenLM/qwen-code --title "fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list" --body-file <file-with-the-body-above>

What happens next

The CHANGES_REQUESTED review is intentionally left unresolved: its request is real, accepted, and pending exactly one metadata edit that this lane cannot perform. Once a maintainer (or the bot operator) applies the title/body above — or tells this loop to stand down — there is nothing left to address; the code is endorsed by the review and green locally and in CI (22 checks on de0f9143, including Lint & Static (ubuntu-latest, Node 22.x)).

Verification

  • git diff origin/main...HEAD — inspected in full; every factual claim above checked against it.
  • git log origin/main --grep=12648 — confirmed 97cd5636c9 (the safe.directory fix) is on main.
  • Test-count check: git show origin/main:scripts/tests/lint.test.js has 6 it blocks; HEAD has 14 (6 pre-existing + 8 new), matching the review's corrected count.
  • COREPACK_HOME=/tmp/corepack-home npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js — 14 passed (the COREPACK_HOME override works around this sandbox's read-only default corepack cache path; the tests themselves are unaffected).
  • No build/typecheck/lint runs this round: no file was changed.
  • git status — clean; nothing committed, as intended.
中文说明

Autofix 本轮 —— 无代码改动(PR #12650,issue #12647)

本轮反馈

距上次评估水位线(2026-09-27T02:27:55Z)之后恰有一条反馈;无行内评论、无失败检查、无持续红色检查:

逐条验证

评审按来源中立处理:每一条可检验论断都先对照实时 diff 与代码树核实,再予以接受。全部成立。

  1. "Main CI failed: Qwen Code CI on a064952e33dc #12647 已由 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 关闭" —— 本地确认:97cd5636c9 fix(ci): trust the job workspace as a git safe.directory before checkout (#12648) 已在 origin/main 上;本分支已于 2026-09-26 合入 main(de0f9143ce),故 PR diff 恰好就是剩余 delta(scripts/lint.js +53/−18,scripts/tests/lint.test.js +252)。
  2. "diff 未改动 check: 'command -v yamllint'" —— 确认:可用性检查在 diff 中原样未动;版本探测只存在于已被取代的第 0 轮框架中,第 1 轮(b83301c7)已移除。
  3. "pip --user bin 目录仍追加在末尾,且新测试正是如此断言" —— 确认:getLinterPath 把 ${HOME}/.local/bin(Linux)/ Library/Python/3.12/bin(macOS)追加在继承 PATH 之后,测试 appends the pip --user bin dir after the inherited PATH 钉住的正是该顺序。
  4. "假 runner 镜像的测试计划用例不在 diff 中" —— 确认:8 个新测试均为基于桩的通道测试(git 失败、空列表拒绝、两个 linter 各自的正常路径文件选择、PATH 顺序);不存在此类镜像级测试。
  5. "Linux 上正确的测试计数是 14(main 6 个 + 新增 8 个),不是 9" —— 通过计数确认(main 有 6 个 it 块;HEAD 有 14 个),并通过运行套件确认:14 个通过(见"验证"一节)。
  6. "diff 重写了 shellcheck 通道,而描述却称其在范围之外" —— 确认:shellcheck 的 run 已改为先暂存列表并在为空时拒绝,旧的"风险与范围"文本已过期。

为什么没有代码改动

评审只要求 PR 元数据改动 —— 标题、描述、关闭性关联 —— 并明确表示不需要代码改动。本轮无法代为执行,有两个相互独立的原因:

  1. 无凭据。 本通道在没有 GitHub 凭据的情况下运行(此处 gh 无 token),且 autofix 规则禁止 agent 进行 GitHub 写操作;所有网络写操作由工作流负责。
  2. 无通道。 address-review 通道的契约只承载本轮报告(address-summary.md / no-action.md)及线程解析附属文件;pr-title.txt / pr-body.md 仅由 issue-fix 通道的 gh pr create 消费,评审通道中没有任何步骤编辑 PR 标题或正文。

因此该发现是被接受,而非被拒绝 —— 框架确实已过期 —— 但落地只差一个人工(或 dev-bot PAT)操作。为使该操作机械化,上文已给出可直接套用的替换文本。

可直接套用的标题与描述

建议标题(与留存 delta 相符;去掉 (#12647) 后缀框架):fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list。建议正文完整见上方英文部分(去掉 Fixes #12647,改为不带关闭关键字的引用;各项论断已按上述验证修正;正文自带中文折叠段)。对任何有 PR 写权限的人,应用只需一条 gh pr edit 12650 命令(见英文部分)。

后续

CHANGES_REQUESTED 评审被有意保持未解决状态:其诉求真实、已被接受,只差一次本通道无法执行的元数据编辑。一旦维护者(或 bot 运营方)套用上述标题/正文 —— 或指示本循环停止 —— 就没有遗留事项了:代码已获评审认可,本地与 CI 均为绿(de0f9143 上 22 项检查,含 Lint & Static (ubuntu-latest, Node 22.x))。

验证

  • git diff origin/main...HEAD —— 全文审阅;上述每项事实论断均对照核实。
  • git log origin/main --grep=12648 —— 确认 97cd5636c9(safe.directory 修复)在 main 上。
  • 测试计数核对:git show origin/main:scripts/tests/lint.test.js 有 6 个 it 块;HEAD 有 14 个(6 原有 + 8 新增),与评审修正后的计数一致。
  • COREPACK_HOME=/tmp/corepack-home npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js —— 14 个通过(COREPACK_HOME 覆盖是为了绕过本沙箱只读的默认 corepack 缓存路径;测试本身不受影响)。
  • 本轮未运行 build/typecheck/lint:未改动任何文件。
  • git status —— 干净;按计划未提交任何内容。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

wenshao added a commit to wenshao/qwen-code that referenced this pull request Sep 27, 2026
@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, real environment: PR #12650 @ de0f9143

For the merge decision: the code is ready to merge. The PR metadata is not.

  • Code: ✅ verified. I reproduced the real Main CI failed: Qwen Code CI on a064952e33dc #12647 trigger with a root job, a runner-owned workspace and real git dubious ownership. The rig's main output matches the failed CI job's log line for line. Under that trigger, the PR turns shellcheck's false green and yamllint's misleading usage-screen failure into two clear, sub-second failures that name the real cause. On a healthy checkout, both lanes lint the same 72 scripts and 79 YAML files as main, and the stdout is byte-identical. The PR suite passes 14/14 on macOS and on Linux x86_64 (dash). The PR's tests kill all 10 mutants.
  • Metadata: ❌ still stale. The title, the body and Fixes #12647 still describe the round-0 design (a yamllint version probe plus a PATH reorder). That design is not in the tree: check is still command -v yamllint, and the pip --user dir is still appended last, which the new test pins. Main CI failed: Qwen Code CI on a064952e33dc #12647 was already closed by fix(ci): trust the job workspace as a git safe.directory before checkout #12648. The bot's CHANGES_REQUESTED (review) is about metadata only. The autofix round already drafted a ready-to-apply title and body (comment). It could not apply them itself because that lane has no GitHub credentials.
  • Recommended path: apply that title and body (it drops Fixes #12647), then clear the metadata-only CHANGES_REQUESTED and merge. The residual gaps below are pre-existing and non-blocking. They are better handled as follow-ups.

Environment

Rig ubuntu:24.04 amd64, /bin/sh = dash 0.5.12, git 2.43.0, GNU xargs 4.9.0, file 5.45, Node 22.23.3 (colima, qemu-user on an arm64 host)
Trigger Job runs as root with $HOME=/root. Workspace /home/github-runner/actions-runner-19/_work/... is owned by github-runner (uid 1001). There is no durable safe.directory, matching the failed job.
Trees main = 6a459e6e (the PR's merge base; head − base is exactly the PR diff) vs PR head de0f9143. Both come from git archive and are committed in the container. de0f9143 still merges cleanly into today's main (692a5d74), and scripts/lint.js is unchanged on main since the base.
Lanes The real entry point, node scripts/lint.js --setup / --shellcheck / --yamllint. --setup installs the SHA-256-pinned actionlint 1.7.12 and shellcheck 0.11.0 archives, and installs yamllint 1.35.1 with its own pip3 install --user.
Tests macOS arm64 (Node 24.18.1) and the Linux container above, with vitest 3.2.4

1. #12647 trigger: main vs PR

dubious ownership A/B

lane real CI (job 107633391206, ecs-qwen-hk4-19) rig main rig PR
Run shellcheck success, linted 0 files exit 0, false green exit 1, shellcheck: git ls-files failed; refusing to lint an empty file list
Run yamllint failure on the usage screen exit 1, FILE_OR_DIR - is required exit 1, yamllint: git ls-files failed; refusing to lint an empty file list
  • The rig's main step output equals the CI log's steps 24 and 25 line for line (31 + 10 lines). The only differences are the workspace path and one trailing blank line. That confirms the rig recreates the actual failure and not an approximation of it.
  • On the PR, git's own fatal: detected dubious ownership … line is still printed right above the new message, so the log names the real cause. The PR lanes fail in 0.88 s and 0.63 s, and neither linter is invoked.

2. Healthy path, real findings, tests and mutation

parity, tests, mutation

  • Nothing changes on a healthy checkout. With safe.directory set (the state fix(ci): trust the job workspace as a git safe.directory before checkout #12648 restores), both trees make one shellcheck call over 72 scripts and one yamllint call over 79 files. Both lanes exit 0 and produce byte-identical stdout (2326 and 2 lines). I counted with argv-logging shims placed first on the lane PATH that exec the real binaries.
  • Real violations still surface. A duplicate-key YAML file makes yamllint exit 1 with the same ::error annotation on both trees. A SC2086 script is reported by shellcheck on both trees. The shellcheck lane stays exit 0, which is unchanged and advisory; see §3.
  • Suite: scripts/tests/lint.test.js passes 14/14 on macOS (the PR lists macOS as ⚠️ untested) and 14/14 on Linux x86_64 with dash. That is 6 existing tests plus 8 new ones. The body's "9 passed" is the round-0 count.
  • Mutation matrix: 10/10 killed. The mutants drop each || exit, drop each empty-list refusal, swap in the xargs -r shortcut, break each file filter, move the pip dir ahead of the inherited PATH, and revert both run strings while keeping the exports. Each is killed by the test named for it. Cross-check: the PR tests against main's lint.js with exports kept fail exactly the 5 guard tests; the happy-path tests pass on both, as expected.
  • Design check: dash has no set -o pipefail (set: Illegal option -o pipefail), so the PR's approach of staging the list in a variable, then checking -z, is the right portable shape for execSync's /bin/sh.

3. Residual silent passes (pre-existing, not introduced here, non-blocking)

residual

  1. The shellcheck lane still cannot fail, because its status is the trailing sed's. I measured this on both trees:

    case main PR
    shellcheck binary missing exit 0 exit 0
    shellcheck binary crashes exit 0 exit 0
    unparseable script (4 error: findings) exit 0 exit 0

    The crash is a real one: the pinned x86_64 GHC binary is SIGKILLed under qemu here. The earlier sandboxed verification (F4) proposed propagating shellcheck's full status. As it noted, that would turn main red on the 2324 existing findings. A narrower candidate fails only when xargs reports that shellcheck did not run (status ≥ 124) or when the output has error-severity findings. Warnings and notes stay advisory. I measured it. Missing binary: exit 1 (xargs 127). Crash: exit 1 (xargs 125). Unparseable script: exit 1. One extra SC2086 script: exit 0. Healthy tree: exit 0 with byte-identical stdout. This belongs in a follow-up, not in this PR.

    Candidate diff (measured against de0f9143)
    --- a/scripts/lint.js
    +++ b/scripts/lint.js
    @@ -224,13 +224,29 @@
             echo "shellcheck: file --mime-type detected no shell scripts; refusing to pass on an empty file list" >&2
             exit 1
           fi
    +      sc_out="$(mktemp)"
           printf '%s\\n' "$scripts" | xargs shellcheck \\
             --check-sourced \\
             --enable=all \\
             --exclude=SC2002,SC2129,SC2310 \\
             --severity=style \\
             --format=gcc \\
    -        --color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
    +        --color=never > "$sc_out"
    +      sc_status=$?
    +      sed -e 's/note:/warning:/g' -e 's/style:/warning:/g' < "$sc_out"
    +      grep -q ': error: ' "$sc_out"; sc_has_error=$?
    +      rm -f "$sc_out"
    +      # xargs 123 = findings: warnings/notes stay advisory, error-severity
    +      # findings (e.g. unparseable scripts) fail; 124-127 = shellcheck crashed,
    +      # was killed, or could not be run at all.
    +      if [ "$sc_status" -ge 124 ]; then
    +        echo "shellcheck: xargs exited $sc_status; shellcheck did not run to completion" >&2
    +        exit 1
    +      fi
    +      if [ "$sc_has_error" -eq 0 ]; then
    +        echo "shellcheck: error-severity findings above" >&2
    +        exit 1
    +      fi
         `,
           },
           yamllint: {
  2. The Run sensitive keyword linter step in ci.yml is a no-op. It runs node scripts/lint.js --sensitive-keywords, but lint.js has no handler for that flag, and main() silently ignores unknown flags. It exits 0 in 0.5 s with no output on both trees. It is the same class of silent pass and is worth its own issue.

  3. Minor: the shellcheck candidate regex ^([^.]+|.*\.(sh|zsh|bash)) has no $ anchor, so it matches 9485 of 9740 tracked paths, and file --mime-type does the real filtering. The result is still correct (72 scripts). The practical effect is that the PR's "no shell-script candidates" guard only fires when every tracked path starts with .. The git ls-files and file(1) guards carry the protection.

Not covered / rig caveats

  • Real ECS runners and Windows. The lanes are POSIX-only and the new tests skipIf(win32).
  • The amd64 userland runs under qemu-user. The pinned x86_64 shellcheck crashes there, so after lint.js's own SHA-verified install, the runs that need shellcheck to execute use the same-version 0.11.0 linux.aarch64 release, which the arm64 kernel runs natively.
  • PIP_BREAK_SYSTEM_PACKAGES=1 was set so lint.js's own pip3 install --user works on Ubuntu 24.04 (PEP 668). The runner images already ship a yamllint (the failed job printed no Installing yamllint...), so CI does not reach that installer.
中文版

维护者真实环境验证:PR #12650 @ de0f9143

合并参考结论: 代码可以合入,PR 元数据还不行。

  • 代码:✅ 已验证。 我用 root 作业、runner 属主的工作区和真实 git dubious ownership 复现了 Main CI failed: Qwen Code CI on a064952e33dc #12647 的触发条件。装置里 main 的输出与失败 CI 作业日志逐行一致。在这个触发条件下,本 PR 把 shellcheck 的假绿和 yamllint 的误导性 usage 失败变成两条亚秒级、点名真实原因的明确失败。健康检出下,两条通道与 main 检查的是同样的 72 个脚本和 79 个 YAML 文件,stdout 逐字节一致。PR 测试套件在 macOS 和 Linux x86_64(dash)上都是 14/14。PR 的测试杀掉了全部 10 个变异体。
  • 元数据:❌ 仍过期。 标题、正文和 Fixes #12647 仍在描述第 0 轮的设计(yamllint 版本探测加 PATH 重排)。这套设计不在代码树里:check 仍是 command -v yamllint,pip --user 目录仍追加在末尾,新测试恰好钉住了这个顺序。Main CI failed: Qwen Code CI on a064952e33dc #12647 已被 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 关闭。bot 的 CHANGES_REQUESTED(review)只针对元数据。autofix 这一轮已经起草好可直接套用的标题和正文(评论),但该通道没有 GitHub 凭证,没法自己改。
  • 建议路径: 套用这份标题和正文(其中已去掉 Fixes #12647),再清掉这条只针对元数据的 CHANGES_REQUESTED,然后合入。下文的残留缺口都是既有问题、不阻塞,更适合作为后续跟进处理。

环境

装置 ubuntu:24.04 amd64,/bin/sh = dash 0.5.12,git 2.43.0,GNU xargs 4.9.0,file 5.45,Node 22.23.3(colima,arm64 宿主上的 qemu-user)
触发条件 作业以 root 身份运行,$HOME=/root。工作区 /home/github-runner/actions-runner-19/_work/... 的属主是 github-runner(uid 1001)。没有持久的 safe.directory,与失败作业一致。
代码树 main = 6a459e6e(PR 的 merge base;head 减 base 正好是 PR diff),对比 PR head de0f9143。两者都用 git archive 导出,并在容器内提交。de0f9143 与今天的 main(692a5d74)仍可干净合并,自 base 以来 main 上的 scripts/lint.js 没有变化。
通道 真实入口 node scripts/lint.js --setup / --shellcheck / --yamllint。--setup 安装 SHA-256 钉定的 actionlint 1.7.12 和 shellcheck 0.11.0 归档包,并用脚本自带的 pip3 install --user 安装 yamllint 1.35.1。
测试 macOS arm64(Node 24.18.1)和上面的 Linux 容器,vitest 3.2.4

1. #12647 触发条件:main 与 PR 对比

通道 真实 CI(ecs-qwen-hk4-19 上的作业 107633391206) 装置 main 装置 PR
Run shellcheck success,检查了 0 个文件 exit 0,假绿 exit 1,git ls-files failed; refusing to lint an empty file list
Run yamllint 在 usage 页面上失败 exit 1,FILE_OR_DIR - is required exit 1,git ls-files failed; refusing to lint an empty file list
  • 装置里 main 的步骤输出与 CI 日志第 24、25 步逐行一致(31 + 10 行),差别只有工作区路径和末尾一个空行。这说明装置重建的是真实故障本身,而不是近似。
  • PR 下,git 自己的 fatal: detected dubious ownership … 仍会打印在新提示的上方,日志直接点名真实原因。PR 的两条通道分别在 0.88 s 和 0.63 s 内失败,两个 linter 都没有被调用。

2. 健康路径、真实问题、测试与变异

  • 健康检出下没有任何变化。 设置 safe.directory 后(即 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 恢复的状态),两棵树都是一次 shellcheck 调用检查 72 个脚本、一次 yamllint 调用检查 79 个文件。两条通道都 exit 0,stdout 逐字节一致(2326 行和 2 行)。计数来自放在通道 PATH 最前面的 argv 记录垫片,垫片会 exec 真实二进制。
  • 真实违规照常暴露。 一个重复键的 YAML 文件让 yamllint 在两棵树上都 exit 1,给出同样的 ::error 注解。一个 SC2086 脚本在两棵树上都被 shellcheck 报出。shellcheck 通道仍是 exit 0,行为未变,属于建议性;见第 3 节。
  • 测试套件: scripts/tests/lint.test.js 在 macOS 上 14/14(PR 把 macOS 标为 ⚠️ 未测)、在 Linux x86_64 + dash 上 14/14,即原有 6 个加新增 8 个。正文里的「9 passed」是第 0 轮的计数。
  • 变异矩阵:10/10 全部被杀。 变异体包括:去掉各处 || exit、去掉各处空列表拒绝、换成 xargs -r 捷径、破坏各自的文件过滤、把 pip 目录挪到继承 PATH 之前、在保留导出的前提下还原两条 run 字符串。每个变异体都被对应命名的测试杀掉。交叉校验:保留导出、换上 main 的 lint.js 后,PR 测试恰好只挂 5 个守卫测试;正常路径的测试两边都通过,符合预期。
  • 设计核对: dash 不支持 set -o pipefail(报 set: Illegal option -o pipefail)。所以 PR 先把列表存进变量、再判断 -z 的做法,正是适配 execSync 所用 /bin/sh 的可移植写法。

3. 残留的静默通过(既有问题,不是本 PR 引入,不阻塞)

  1. shellcheck 通道仍然无法失败,因为它的退出码取自末尾的 sed。两棵树上实测:

    场景 main PR
    shellcheck 二进制缺失 exit 0 exit 0
    shellcheck 二进制崩溃 exit 0 exit 0
    无法解析的脚本(4 条 error:) exit 0 exit 0

    这里的崩溃是真实发生的:钉定的 x86_64 GHC 二进制在 qemu 下被 SIGKILL。此前的沙箱验证(F4)建议透传 shellcheck 的完整退出码。它自己也指出,那样做会因为现有 2324 条 finding 让 main 变红。一个更窄的候选修法只在 xargs 报告 shellcheck 没跑起来(状态 ≥ 124)或输出里有 error 级 finding 时才失败,warning 和 note 仍是建议性的。我实测了它。缺失:exit 1(xargs 127)。崩溃:exit 1(xargs 125)。无法解析的脚本:exit 1。额外加一个 SC2086 脚本:exit 0。健康树:exit 0,stdout 逐字节一致。这适合作为后续跟进,不必放进本 PR;diff 见英文部分。

  2. ci.yml 里的 Run sensitive keyword linter 步骤是空操作。 它运行 node scripts/lint.js --sensitive-keywords,但 lint.js 没有处理这个参数的代码,main() 会静默忽略未知参数。两棵树上都是 0.5 s、无任何输出、exit 0。这是同一类静默通过,值得单独开 issue。

  3. 小问题:shellcheck 的候选正则 ^([^.]+|.*\.(sh|zsh|bash)) 没有 $ 锚点,在 9740 个受跟踪路径里匹配了 9485 个,真正起筛选作用的是 file --mime-type。结果仍然正确(72 个脚本)。实际影响是:PR 的「no shell-script candidates」守卫只有在所有受跟踪路径都以 . 开头时才会触发,真正起保护作用的是 git ls-files 和 file(1) 这两道守卫。

未覆盖 / 装置说明

  • 真实 ECS runner 和 Windows 未覆盖。这两条通道只支持 POSIX,新测试对 win32 做了 skipIf。
  • amd64 用户态跑在 qemu-user 下,钉定的 x86_64 shellcheck 在这里会崩溃。因此在 lint.js 自己完成 SHA 校验安装之后,需要真正执行 shellcheck 的场景改用同版本 0.11.0 的 linux.aarch64 官方包,由 arm64 内核原生运行。
  • 设置了 PIP_BREAK_SYSTEM_PACKAGES=1,让 lint.js 自带的 pip3 install --user 能在 Ubuntu 24.04(PEP 668)上运行。runner 镜像本身已带 yamllint(失败作业没有打印 Installing yamllint...),所以 CI 不会走到这个安装器。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no code change (PR #12650, issue #12647)

Feedback this round

Exactly one item is newer than the last evaluation watermark, and there are no inline comments, no failed checks, and no still-red checks:

  • [ic:5853242337] @wenshao — maintainer real-environment verification at de0f9143. Verdict: the code is ready to merge; the PR metadata (title, body, Fixes #12647) is still stale; three residual pre-existing gaps are named as non-blocking follow-ups.

Dispositions

1. Code verified — no change (agree)

The maintainer reproduced the real #12647 trigger (root job, runner-owned workspace, real git dubious-ownership) and confirmed the PR turns shellcheck's false green and yamllint's misleading usage-screen failure into two clear sub-second failures that name the real cause, with byte-identical stdout to main on a healthy checkout (72 scripts, 79 YAML files), 14/14 tests on macOS and Linux/dash, and 10/10 mutants killed. I independently re-ran the PR suite at this exact head: 14 passed (6 pre-existing + 8 new). Nothing in the comment asks for a code change, and none is warranted.

2. Stale PR metadata — accepted, not actionable from this lane (one human action remains)

The title, body, and Fixes #12647 still describe the superseded round-0 design (yamllint version probe + PATH reorder). This round cannot apply the correction, for two independent reasons:

  1. No credentials. This lane runs without GitHub credentials and the autofix rules forbid GitHub writes from the agent; the workflow owns all network writes.
  2. No channel. The review lane's contract carries only the round report and thread sidecars; pr-title.txt / pr-body.md are consumed exclusively by the issue-fix lane's gh pr create, and no review-lane step edits PR title or body.

The ready-to-apply replacement was drafted in this comment. Proposed title: fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list (the draft body drops Fixes #12647, corrects the test count to 14, and removes the stale scope text). The remaining path, per the maintainer: apply the drafted title and body, dismiss the metadata-only CHANGES_REQUESTED, and merge.

3. Residual pre-existing gaps — deferred to the follow-up queue

The maintainer explicitly scoped all three to follow-ups ("pre-existing and non-blocking… better handled as follow-ups"). Each is verified against the tree and recorded in deferred-findings.json so it survives the merge:

  • shellcheck lane cannot fail — its pipeline ends in sed, so the step's exit status is sed's; a missing/crashing shellcheck or error-severity findings all exit 0 (maintainer-measured on both trees; a measured narrow candidate — fail only on xargs status ≥ 124 or error-severity output — is posted on the PR).
  • Run sensitive keyword linter step in ci.yml is a no-op — node scripts/lint.js --sensitive-keywords has no handler in lint.js, and main() silently ignores unknown flags (verified by inspection: no such handler exists under scripts/).
  • Minor: shellcheck candidate regex lacks a $ anchor — matches ~9485 of 9740 tracked paths so file --mime-type does the real filtering; the result is still correct (72 scripts), and the git ls-files / file(1) guards carry the protection.

Verification

  • COREPACK_HOME=/tmp/corepack-home npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js — 14 passed at de0f9143 (the COREPACK_HOME override only works around this sandbox's read-only default corepack cache; it does not affect what is tested).
  • npm run build / npm run typecheck / npm run lint — not run: this round changes no code, so there is nothing new to verify; the tree is the exact head the maintainer verified.
  • git status — clean; no commit made.
中文说明

Autofix 本轮——无需改动代码(PR #12650,issue #12647)

本轮反馈

距离上次评估水位线之后只有一条新反馈,且没有行内评论、没有失败的检查、也没有持续飘红的检查:

  • [ic:5853242337] @wenshao——维护者在 de0f9143 上做的真实环境验证。 结论:代码可以合入;PR 元数据(标题、正文、Fixes #12647)仍然过期;三个残留的既有缺口被点名为不阻塞的后续跟进项。

逐条处置

1. 代码已验证——无需改动(同意)

维护者复现了真实的 #12647 触发条件(root 作业、runner 属主的工作区、真实的 git dubious-ownership),确认本 PR 把 shellcheck 的假绿和 yamllint 的误导性 usage 页失败变成两条亚秒级、点名真实原因的明确失败;在健康检出下 stdout 与 main 逐字节一致(72 个脚本、79 个 YAML 文件),测试在 macOS 和 Linux/dash 上 14/14 通过,10 个变异体全部被杀。我在同一个 head 上独立重跑了本 PR 的测试套件:14 个全部通过(6 个既有 + 8 个新增)。该评论没有要求任何代码改动,也确实不需要。

2. PR 元数据过期——接受,但本通道无法执行(还差一步人工操作)

标题、正文和 Fixes #12647 仍在描述已被取代的第 0 轮设计(yamllint 版本探测加 PATH 重排)。本轮无法自行修正,有两个相互独立的原因:

  1. 没有凭证。 本通道在没有 GitHub 凭证的环境下运行,且 autofix 规则禁止 agent 进行 GitHub 写操作;所有网络写入都由 workflow 负责。
  2. 没有通道。 评审通道的契约只携带本轮报告和讨论串的附属文件;pr-title.txt / pr-body.md 只被 issue 修复通道的 gh pr create 消费,评审通道没有任何步骤能修改 PR 标题或正文。

可直接套用的替换文本已在这条评论中拟好。建议标题:fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list(草拟的正文已去掉 Fixes #12647,把测试计数更正为 14,并删除了过期的范围说明)。按维护者给出的路径,剩下要做的是:套用这份草拟的标题和正文,撤销那条只针对元数据的 CHANGES_REQUESTED,然后合入。

3. 残留的既有缺口——已转入后续跟进队列

维护者明确把这三项全部划为后续跟进(「既有问题、不阻塞……更适合作为后续跟进处理」)。每一条都已对照代码树核实,并记录进 deferred-findings.json,保证合入后不会丢失:

  • shellcheck 通道仍然无法失败——它的管道以 sed 结尾,步骤的退出码取自 sed;shellcheck 缺失/崩溃或存在 error 级 finding 时都会 exit 0(维护者在两棵树上实测;一个更窄的候选修法——只在 xargs 状态 ≥ 124 或输出含 error 级 finding 时才失败——已贴在 PR 上)。
  • ci.yml 里的 Run sensitive keyword linter 步骤是空操作——node scripts/lint.js --sensitive-keywords 在 lint.js 中没有对应处理逻辑,main() 会静默忽略未知参数(已通过代码检查核实:scripts/ 下不存在该参数的处理)。
  • 小问题:shellcheck 候选正则缺少 $ 锚点——在 9740 个受跟踪路径中匹配了约 9485 个,真正起筛选作用的是 file --mime-type;结果仍然正确(72 个脚本),真正的保护由 git ls-files 和 file(1) 两道守卫承担。

验证

  • COREPACK_HOME=/tmp/corepack-home npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js——在 de0f9143 上 14 个全部通过(COREPACK_HOME 覆盖只是为了绕过本沙箱默认 corepack 缓存目录只读的问题,不影响被测内容)。
  • npm run build / npm run typecheck / npm run lint——未运行:本轮不改任何代码,没有新内容需要验证;代码树正是维护者验证过的那个 head。
  • git status——干净;未产生提交。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

wenshao
wenshao previously approved these changes Sep 27, 2026
@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge September 27, 2026 10:08
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🕐 Review received — an automatic review of the current head is still running, so this round is held until it lands (a push now would cancel it and discard its work, #8888). Your feedback stays queued for the next eligible round.

中文说明

🕐 已收到评审 —— 当前 head 上仍有一轮自动 review 在运行,本轮暂缓(现在推送会取消该 review 并丢弃其工作,#8888)。反馈保持排队,等待下一次可运行的轮次处理。

…12647)

Address review on #12650: factor the five git-sourced guard fragments into
two parameterized builders (unifying the drifted refusal wording), make
getLinterPath's temp dir injectable, probe getLinters() capability instead
of gating the seven lane tests on win32, pin both lanes' exact argv and the
shellcheck sed severity rewrite, and add a yamllint exit-status propagation
test.

Co-authored-by: Qwen-Coder <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 Addressed the latest review feedback (round 2/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/10 轮)。改动内容与我反驳保留之处如下:

Autofix round — code changes (PR #12650, issue #12647)

Commit ad4ceab8a4 — refactor(ci): share lint-lane guards and pin lane argv, status, PATH (#12647), touching only scripts/lint.js and scripts/tests/lint.test.js.

Still open, and not code: the PR metadata edit (rv:5330189532, Critical)

The one Critical — retitle the PR, drop Fixes #12647, rewrite the description around the shipped delta — is accepted and verified, but it is a GitHub metadata edit this lane cannot perform. Verified again this round: the review-address lane's contract carries only the round report and its sidecars; pr-title.txt/pr-body.md are consumed exclusively by the issue-fix lane's gh pr create (qwen-autofix.yml), and no step in the review lane edits PR title or body. The previous round's ready-to-apply title and body in comment 5852271537 remain accurate against this tree with exactly two count updates from this round's added test: the suite is now 15 passed (6 pre-existing + 9 new) — "expect 14 passed" → "expect 15 passed", "Eight new tests" → "Nine new tests" — and the test list gains fails the lane when yamllint itself exits non-zero. Applying it stays one gh pr edit for anyone with write access. The review thread is deliberately left unresolved so this pending action stays visible; the code itself was measured ready by both the reviewer and the maintainer verification, and this round does not change that.

The verification report's F1/F2/F3 are the same finding in three forms (title/description/test-plan vs. tree) and are covered by the same metadata edit — no code change is warranted: the shipped test deliberately pins the trailing pip --user PATH order (F2's M8 evidence), and the version-probe design the stale description describes was superseded when #12648 closed the incident's trigger.

Resolved in code this round (5 inline findings)

  • rc:4115242595 (R1-4, guard duplication) — factored the five guard fragments into two parameterized builders, stageGitFileList(label) and refuseEmptyList(label, variable, reason), following the file's own getCachedArchiveInstaller pattern, and unified the drifted wording (yamllint's empty-list refusal now says "pass on" like the other two). Measured: the generated actionlint and shellcheck lane scripts are byte-identical to before, the yamllint script differs on exactly one line (the wording unification), and getLinterPath() output is byte-identical for linux/darwin/win32.
  • rc:4115242599 (R1-3, yamllint exit status unpinned) — added fails the lane when yamllint itself exits non-zero: a stub that logs argv and exits 1, asserting the lane fails AND the linter ran (log exists), which distinguishes a linter failure from a guard firing.
  • rc:4115242602 (R1-1, uninjected temp dir) — getLinterPath now takes tempDir = TEMP_DIR (default stays the module constant, since installers and extract paths are built from it), and the PATH-order test passes tempDir: '/tmp/linters' and asserts both linter-dir segments.
  • rc:4115242603 (R1-5, platform-gated tests) — replaced all seven it.skipIf(process.platform === 'win32') gates with a getLintersOrSkip(ctx) helper that probes the capability: getLinters() throws on hosts with no pinned linter build (win32, linux/arm64, …) and the test skips instead of failing. The ungated PATH-order test (the file's Windows-side coverage) is untouched, and no platform triple is re-derived in the test file.
  • rc:4115242606 (R1-2, unasserted argv/flags) — both positive lane tests now assert the recorded argv exactly: --format github ci.yml deploy.yaml (yamllint) and --check-sourced --enable=all --exclude=SC2002,SC2129,SC2310 --severity=style --format=gcc --color=never tool.sh (shellcheck). The shellcheck stub additionally emits a note:-level finding and the test asserts stdout shows the sed-rewritten warning: line with no note: remaining, pinning the severity rewrite end to end.

Deferred to follow-up (recorded in deferred-findings.json)

  • F4 — shellcheck lane exit status swallowed by the trailing sed (verified, pre-existing): fixing it turns main CI red until the 2324 existing findings are triaged or the flag set is relaxed; the verifier explicitly scoped it to a follow-up with its own baseline decision.
  • F9 — Run sensitive keyword linter is a dead step (verified, pre-existing): main() never handles --sensitive-keywords and no such linter exists; the fix (implement the flag, delete the step, make main() reject unknown flags) touches ci.yml and the dispatcher, outside this PR's footprint.
  • F6 — the PATH test pins a colon-joined PATH on win32 (verified, pre-existing): getLinterPath joins with : on every platform, which is not the Windows separator; fixing it changes runCommand behavior for the eslint/prettier lanes and needs Windows validation.

Acknowledged, no action

  • F5 (guards convert silent passes into loud failures on awkward filenames): the verifier measured this as not-a-regression with zero blast radius in this repo (0 of 9808 tracked paths affected); the guards surfacing the pre-existing xargs limitation loudly is the intended behavior.
  • F7/F8: process notes (shallow-clone attribution limits, stale PR framing) — the framing is the escalated metadata item above.

Mutation probes (each applied, measured, then reverted; suite green after every revert)

Probe (mutation to scripts/lint.js) Expected to fail Actual
Remove the yamllint refuseEmptyList call yamllint refuses to lint an empty file list failed as expected
Remove the shellcheck refuseEmptyList (scripts) call shellcheck refuses to pass when no shell scripts are detected failed as expected
Remove the yamllint stageGitFileList call yamllint fails on the git error and never runs yamllint when git ls-files fails failed as expected
Append | cat to the yamllint invocation (status swallow) fails the lane when yamllint itself exits non-zero failed as expected
Delete the ${tempDir} segments from getLinterPath appends the pip --user bin dir after the inherited PATH failed as expected
Delete --format github yamllint lints exactly the yaml files git lists failed as expected
Delete the shellcheck sed severity rewrite shellcheck lints exactly the files file(1) detects as shell scripts failed as expected

Verification

  • npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/lint.test.js (COREPACK_HOME=/tmp/corepack-home) — 15 passed (6 pre-existing + 9 new), before and after the commit.
  • Lane-fidelity harness (imports HEAD's and this commit's scripts/lint.js, compares generated lane scripts and getLinterPath output) — actionlint/shellcheck byte-identical, yamllint differs only in the unified guard wording, PATH byte-identical on all three platforms, tempDir injection verified.
  • npm run build — passed.
  • npm run typecheck — passed.
  • npm run lint — passed.
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js — passed (the files were already prettier-clean; --write changed nothing).
  • npm run test:scripts (full scripts suite) — 2625 passed, 20 skipped, 13 failed in 3 files (install-script, verify-capture, web-shell-publish-artifacts) that share no code with this diff: reproduced byte-for-byte with the pre-change versions of this PR's two files in place (same 13 failures), i.e. pre-existing environment failures on this runner (npm-pack/docker/network-dependent), not caused by this round. The focused suite for the touched files is green 15/15.
  • No settings source changed, so npm run generate:settings-schema was not needed; the diff touches no bundled-CLI or integration-harness behavior (CI script + its unit tests only), so no integration run applies.
中文说明

Autofix 轮次 —— 代码改动(PR #12650,issue #12647)

提交 ad4ceab8a4 —— refactor(ci): share lint-lane guards and pin lane argv, status, PATH (#12647),仅触及 scripts/lint.js 与 scripts/tests/lint.test.js。

仍未解决、且与代码无关:PR 元数据编辑(rv:5330189532,Critical)

唯一一条 Critical —— 重命名 PR 标题、移除 Fixes #12647、按实际交付内容重写描述 —— 已接受并经核实,但它是本通道无法执行的 GitHub 元数据编辑。本轮再次核实:review-address 通道的契约只承载轮次报告及其附属文件;pr-title.txt/pr-body.md 仅被 issue-fix 通道的 gh pr create 消费(qwen-autofix.yml),review 通道中没有任何步骤编辑 PR 标题或描述。上一轮在 comment 5852271537 给出的可直接套用的标题与正文,在当前代码树下仍然准确,只需因本轮新增的一个测试更新两处计数:套件现为 15 个通过(6 个原有 + 9 个新增)—— "expect 14 passed" 改为 "expect 15 passed","Eight new tests" 改为 "Nine new tests" —— 测试列表新增 fails the lane when yamllint itself exits non-zero。应用它仍只需有写权限者执行一条 gh pr edit。该评审线程被有意保持未解决状态,以使这项待办操作保持可见;代码本身已被评审者与维护者验证双双实测为可合并,本轮并未改变这一点。

验证报告的 F1/F2/F3 是同一发现的三种形态(标题/描述/测试计划 vs 代码树),由同一笔元数据编辑覆盖 —— 不应改代码:交付的测试是有意钉住 pip --user 目录在 PATH 末尾的顺序(F2 的 M8 证据),而过时描述所写的版本探测设计,已在 #12648 关闭事故触发条件时被取代。

本轮在代码中解决的发现(5 条行内评论)

  • rc:4115242595(R1-4,守卫重复) —— 把五段守卫片段抽成两个参数化构建器 stageGitFileList(label) 与 refuseEmptyList(label, variable, reason),沿用该文件自身的 getCachedArchiveInstaller 模式,并统一了漂移的措辞(yamllint 的空列表拒绝现在与其他两处一样说 "pass on")。实测:生成的 actionlint 与 shellcheck 通道脚本与之前逐字节一致,yamllint 脚本只差一行(措辞统一),getLinterPath() 输出在 linux/darwin/win32 下逐字节一致。
  • rc:4115242599(R1-3,yamllint 退出码未被钉住) —— 新增 fails the lane when yamllint itself exits non-zero:桩程序记录参数后 exit 1,断言通道失败且 linter 确实运行过(日志存在),以此区分「linter 运行并失败」与「守卫提前触发」。
  • rc:4115242602(R1-1,临时目录不可注入) —— getLinterPath 新增 tempDir = TEMP_DIR 参数(默认值仍是模块常量,因为安装器与解压路径都由它构建),PATH 顺序测试传入 tempDir: '/tmp/linters' 并断言两个 linter 目录段。
  • rc:4115242603(R1-5,按平台门控的测试) —— 用 getLintersOrSkip(ctx) helper 替换了全部七处 it.skipIf(process.platform === 'win32') 门控,改为探测能力:getLinters() 在没有钉定 linter 构建的主机(win32、linux/arm64 等)上抛错,测试随之跳过而非失败。未加门控的 PATH 顺序测试(本文件在 Windows 侧的覆盖)保持不动,测试文件中没有重新推导平台三元组。
  • rc:4115242606(R1-2,argv/flag 未被断言) —— 两个正常路径通道测试现在精确断言记录的 argv:yamllint 为 --format github ci.yml deploy.yaml,shellcheck 为 --check-sourced --enable=all --exclude=SC2002,SC2129,SC2310 --severity=style --format=gcc --color=never tool.sh。shellcheck 桩还会额外输出一条 note: 级发现,测试断言 stdout 中出现经 sed 改写的 warning: 行且无残留 note:,端到端钉住严重级别改写。

暂缓到后续跟进(已记入 deferred-findings.json)

  • F4 —— shellcheck 通道退出码被末尾 sed 吞掉(已核实,既有问题):修复会让 main CI 立即变红,直到既有 2324 条发现被清理或 flag 集合放宽;验证者明确将其划入需要单独基线决策的后续跟进。
  • F9 —— Run sensitive keyword linter 是空转步骤(已核实,既有问题):main() 从不处理 --sensitive-keywords,仓库中也不存在该 linter;修复(实现该 flag、删除该步骤、让 main() 拒绝未知 flag)触及 ci.yml 与分发器,超出本 PR 的范围。
  • F6 —— PATH 测试在 win32 上钉住冒号拼接的 PATH(已核实,既有问题):getLinterPath 在所有平台用 : 拼接,这不是 Windows 分隔符;修复会改变 runCommand 对 eslint/prettier 通道的行为,需要 Windows 验证。

已知晓,无需动作

  • F5(守卫把静默假绿变成大声失败,针对怪异文件名):验证者实测这不是回归,且本仓库影响面为零(9808 个受跟踪路径中 0 个受影响);守卫把既有 xargs 限制大声暴露出来正是预期行为。
  • F7/F8:流程性备注(浅克隆归因受限、PR 文案过时)—— 文案问题即上方已升级的元数据事项。

变异探测(每次施加、测量、然后还原;每次还原后套件全绿)

探测(对 scripts/lint.js 的变异) 预期失败 实际
删除 yamllint 的 refuseEmptyList 调用 yamllint refuses to lint an empty file list 按预期失败
删除 shellcheck 的 refuseEmptyList(scripts)调用 shellcheck refuses to pass when no shell scripts are detected 按预期失败
删除 yamllint 的 stageGitFileList 调用 yamllint fails on the git error and never runs yamllint when git ls-files fails 按预期失败
给 yamllint 调用追加 | cat(吞掉退出码) fails the lane when yamllint itself exits non-zero 按预期失败
从 getLinterPath 删除 ${tempDir} 段 appends the pip --user bin dir after the inherited PATH 按预期失败
删除 --format github yamllint lints exactly the yaml files git lists 按预期失败
删除 shellcheck 的 sed 严重级别改写 shellcheck lints exactly the files file(1) detects as shell scripts 按预期失败

验证

  • npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/lint.test.js(COREPACK_HOME=/tmp/corepack-home)—— 15 个通过(6 个原有 + 9 个新增),提交前后均如此。
  • 通道保真 harness(分别 import HEAD 与本提交的 scripts/lint.js,比较生成的通道脚本与 getLinterPath 输出)—— actionlint/shellcheck 逐字节一致,yamllint 仅差统一后的守卫措辞,三个平台 PATH 逐字节一致,tempDir 注入已验证。
  • npm run build —— 通过。
  • npm run typecheck —— 通过。
  • npm run lint —— 通过。
  • npx prettier --experimental-cli --check scripts/lint.js scripts/tests/lint.test.js —— 通过(两文件本就符合 prettier 风格;--write 未改动任何内容)。
  • npm run test:scripts(完整 scripts 套件)—— 2625 通过、20 跳过、13 个失败,分布于 3 个与本 diff 无代码关联的文件(install-script、verify-capture、web-shell-publish-artifacts):将本 PR 两个文件还原为改动前版本后逐字节复现了完全相同的 13 个失败,即该 runner 上的既有环境性失败(依赖 npm-pack/docker/网络),非本轮引入。触及文件的聚焦套件 15/15 全绿。
  • 未改动任何 settings 源,故无需 npm run generate:settings-schema;本 diff 不触及打包 CLI 或集成测试 harness 行为(仅 CI 脚本及其单元测试),故不适用集成测试。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

🧵 Resolved all 5 selected review thread(s). · 已关闭全部选中的 5 条评审线程。

Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:

Could not produce a passing fix for this feedback — the only in-scope finding requires a PR-metadata edit the agent cannot perform

One maintainer action unblocks this PR. The single actionable finding this round (Critical R1-6, re-asserted by reviews rv:5351827294, rv:5353379604 and rv:5356359554) asks to retitle the PR, rewrite its body, and replace the Fixes #12647 closing keyword with a non-closing reference. The fix touches no code — the automated reviewer explicitly does not block the shipped code, and the maintainer's verification agrees ("the code is ready to merge. The PR metadata is not.", comment 5853242337). This lane has neither GitHub credentials nor a contract channel to edit a PR's title or body, so it cannot land the one edit that resolves the finding. To make the maintainer's action mechanical, a ready-to-apply title and body are included verbatim in the "Ready-to-apply title and body" section below (and were previously posted as comment 5868718857). Apply them with the single gh pr edit command shown there, then clear the metadata-only CHANGES_REQUESTED and merge — or merge while accepting the stale metadata, which is the land-with-residual-risk exit the review loop itself now recommends. This is the third consecutive round stopped on exactly this wall (rounds 7, 8, and this one); no further agent round can change the outcome, because the blocker is permission scope, not code.

Feedback disposition this round

  • **[rv:5351827294], [
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

停止前我了解到的情况:

无法为本轮反馈产出可通过的修复 —— 唯一在范围内的发现需要一次 agent 无权执行的 PR 元数据编辑

只差一个维护者动作即可解除本 PR 的阻塞。 本轮唯一可执行的发现(Critical R1-6,由评审 rv:5351827294、rv:5353379604、rv:5356359554 重申)要求修改 PR 标题、重写正文,并将 Fixes #12647 关闭关键字替换为不触发关闭的引用。该修复不涉及任何代码 —— 自动评审明确不阻塞已交付代码,维护者验证意见亦同("代码可以合并,PR 元数据不行",评论 5853242337)。本通道既无 GitHub 凭据,也没有编辑 PR 标题或正文的契约通道,因此无法落地这个唯一能解决该发现的编辑。为使维护者的操作机械化,可直接套用的标题与正文已逐字收录在下方「可直接套用的标题与正文」一节(此前也已作为评论 5868718857 发布)。用该节给出的单条 gh pr edit 命令套用,然后清除仅针对元数据的 CHANGES_REQUESTED 并合并 —— 或者在接受元数据过时这一残余风险的前提下合并,即评审循环自身现在建议的 land-with-residual-risk 出口。这已是连续第三轮停在同一面墙上(第 7、8 轮及本轮);再增加任何 agent 轮次都无法改变结果,因为阻塞点是权限范围,而非代码。

本轮反馈处置

  • [rv:5351827294]、[rv:5353379604]、[rv:5356359554] —— CHANGES_REQUESTED @qwen-code-ci-bot —— R1-6(PR 标题/正文/Fixes #12647 描述的设计不在代码树中,并冒认了 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 已完成的关闭):已接受,阻塞于维护者。 接受前已对照头提交 84da158ff0 逐条重新验证每项可检验论断(见下方「在头提交处的验证」)。所请求的修复仅涉及 PR 元数据;该发现自身亦注明「修复见证 —— 不适用:修复仅涉及 PR 标题、正文与关联,不改变任何代码行为」。
  • 「已延后的非 Critical 反馈」区:按要求未触碰。 当前处于仅处理 Critical 的模式;该区是审计记录而非工作项 —— 本轮未对那些条目做代码、线程或回复操作。
  • 本轮可执行区域未列出行内评论、issue 级评论、失败检查或持续红色检查,因此没有其他需要处理、解决或回复的事项。resolved-comments.txt 与 comment-replies.json 有意缺省:不存在可解决或可回复的行内发现。

在头提交 84da158ff0 处的验证(逐条)

  1. 「版本探测不存在」 —— 确认:grep -c -- "--version" scripts/lint.js 为 0,且 scripts/lint.js:249 仍为 check: 'command -v yamllint'。正文中的版本探测段落与标题中的「回退到钉定的 yamllint」描述的代码并不存在。
  2. 「pip --user bin 目录仍追加在继承 PATH 之后,且本 PR 自己的测试正是如此钉住」 —— 确认:getLinterPath() 在 scripts/lint.js:281 构建 ${nodeBin}:${tempDir}/actionlint:${tempDir}/shellcheck:${env.PATH},然后才在 :285 追

Run log: https://github.com/QwenLM/qwen-code/actions/runs/36612167309


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.7

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:

Cannot produce a passing fix: the only in-scope finding is a PR-metadata edit this lane cannot perform

Requested human action (mechanical): retitle PR #12650 and replace its body with the ready-to-apply title+body already posted as PR comment 5868718857 — re-verified still accurate at head b1ebf73 this round — or apply the equivalent edits listed at the bottom. That single edit retires the only standing Critical (R1-6) and unblocks the maintainer decision the reviewer recommends (land-with-residual-risk; maintainer verification comment 5853242337: "the code is ready to merge. The PR metadata is not.").

The one actionable finding

R1-6 (Critical, re-asserted by rv:5351827294, rv:5353379604, rv:5356359554, rv:5359601706): the PR title, body, Reviewer Test Plan and Fixes #12647 linkage certify a design that is not in the tree — a yamllint version probe, a PATH-leads reorder, a fake-runner-image test case — and claim a close PR #12648 already made on 2026-09-24. Every checkable clause re-verified against this head (b1ebf73):

  • scripts/lint.js:249 is still check: 'command -v yamllint', and --version appears nowhere in the file: no version probe exists.
  • getLinterPath (scripts/lint.js:273-288) builds ${nodeBin}:${tempDir}/actionlint:${tempDir}/shellcheck:${env.PATH} and appends ${env.HOME}/.local/bin AFTER it; this PR's own test appends the pip --user bin dir after the inherited PATH (scripts/tests/lint.test.js:484) pins that order.
中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

停止前我了解到的情况:

无法产出可通过验证的修复:唯一在范围内的发现是 PR 元数据编辑,本通道无法执行

请求的人工操作(纯机械步骤): 将 PR #12650 的标题改为、正文替换为已发布在 PR 评论 5868718857 中的可直接套用的标题与正文 —— 本轮已在 head b1ebf73 上重新核实其仍然准确 —— 或按文末列出的等价修改逐项执行。这一次编辑即可清除唯一未决的 Critical(R1-6),并解除评审者建议的维护者决策的阻塞(land-with-residual-risk;维护者核实评论 5853242337:「代码可以合入,PR 元数据不行」)。

唯一可执行的发现

R1-6(Critical,由 rv:5351827294、rv:5353379604、rv:5356359554、rv:5359601706 反复重申):PR 的标题、正文、评审者测试计划以及 Fixes #12647 关联,证明了一个不存在于代码树中的设计 —— yamllint 版本探测、PATH 置顶重排、伪 runner 镜像测试用例 —— 并且认领了 PR #12648 已于 2026-09-24 完成的关闭。本轮已对照当前 head(b1ebf73067)逐条重新核实了所有可检验的论断:

  • scripts/lint.js:249 仍是 check: 'command -v yamllint',且文件中不存在 --version:没有版本探测。
  • getLinterPath(scripts/lint.js:273-288)构造的 PATH 是 ${nodeBin}:${tempDir}/actionlint:${tempDir}/shellcheck:${env.PATH},然后才把 ${env.HOME}/.local/bin 追加在其后;本 PR 自己的测试 appends the pip --user bin dir after the inherited PATH(scripts/tests/lint.test.js:484)钉住的正是这个顺序。
  • 文件中唯一的 skipIf 是 scripts/tests/lint.test.js:110 处的既有代码;新增 describe 在运行时通过 ctx.skip()(:300)门控。
  • 该发现自己的结论是:「修复仅涉及 PR 标题、正文与关联,不改变任何代码行为」。评审并未要求、也不宜做出任何代码改动 —— 同一批评审已对合入的 diff(空列表守卫、stageGitFileList/refuseEmptyList、getLinterPath 提取)端到端重新测量并予以确认。

为什么 agent 无法落地

  1. 没有凭据。 autofix 规则禁止 agent 进行任何 GitHub 写操作;所有网络写入由 workflow 负责。
  2. 本模式没有通道。 pr-title.txt / pr-body.md 仅被 issue-fix 通道的 gh pr create 消费(qwen-autofix.yml:1584);address-review 的报告步骤只读取 address-summary.md / no-action.md / failure.md / handoff.md。本通道没有任何步骤能编辑 PR 的标题或正文。第 7、8、9 轮正是停在这同一堵墙上(评论 5879812494、5891877334、5896702019)。

如果评论 5868718857 无法使用,需要的修改是

Run log: https://github.com/QwenLM/qwen-code/actions/runs/36645485063


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.7

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/needs-human The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it label Sep 30, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator Author

⏸️ AutoFix paused: this PR reached its automatic round cap (10/10) and the loop will not manage it further — new feedback and base conflicts stay unhandled. Comment @qwen-code /retry to re-arm a fresh window under the same cap, or @qwen-code /takeover to take it over with the raised cap.

中文说明

⏸️ AutoFix 已暂停:本 PR 达到自动轮次上限(10/10),循环不再管理——新反馈与 base 冲突将无人处理。评论 @qwen-code /retry 可在同一上限下重置计数窗口,或评论 @qwen-code /takeover 以更高上限接管。

@wenshao

wenshao commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, round 2 (delta only): PR #12650 @ 0f8f1756

For the merge decision: same split as round 1 at de0f9143. The code is ready to merge. The PR metadata is not. This round re-verifies the current head on a native x86_64 rig. Round 1 ran under qemu, where the pinned shellcheck could not execute. This comment covers only what round 1 did not.

  • Code: ✅ re-verified at this head. Since de0f9143 the PR's own commits are one refactor (ad4ceab8) and two test-only commits (bd9bcfc3, 83807551). Everything else is merges from main.

  • Metadata: ❌ unchanged. The title, the body and Fixes #12647 still describe the round-0 design: a yamllint version probe plus a PATH reorder. That design is not in the tree. The ready-to-apply title and body in autofix comment 5868718857 hold at this head. I checked every testable claim in it: 16 tests (6 existing + 10 new), all ten line references, the ctx.skip() gate, and the two ungated getLinterPath cases. One correction is needed before applying it. Its macOS line says the cases added since de0f9143 are getLinterPath tests. In fact they are one getLinterPath default case and one yamllint exit-status lane case (:366). So 2 of the 16 cases have never run on macOS, and that cell should be ⚠️, or the sentence should say so. The autofix loop is paused at 10/10 and has no GitHub credentials, so someone with write access has to apply it.

  • Recommended path:

    1. Apply that title and body. This drops Fixes #12647.
    2. Dismiss the metadata-only CHANGES_REQUESTED (latest).
    3. Merge.

    The shellcheck-status residual in §5 is pre-existing and is better handled as a follow-up.

Environment

Rig catthehacker/ubuntu:act-latest (Ubuntu 24.04.4), native amd64 on an x86_64 host. /bin/sh = dash, git 2.55.0, GNU xargs 4.9.0, file 5.45, Node 22.22.2. yamllint 1.35.1 is preinstalled in the image, because this head's CI printed no Installing yamllint.... I removed the image's system-wide safe.directory=*.
Trigger The job runs as root with HOME=/root. The workspace is /home/github-runner/actions-runner-hk3-13/_work/qwen-code/qwen-code (the runner that ran this head's CI), owned by github-runner (uid 1001).
Trees The PR head 0f8f1756 tree has 10252 tracked files in a real git index. The main arm is the same tree with merge-base afb911a3's scripts/lint.js; head − base is exactly the 2-file PR diff. lint.js, lint.test.js and ci.yml are unchanged on today's main (27a4485d), and the PR merges into it cleanly.
Lanes The real node scripts/lint.js --setup / --actionlint / --shellcheck / --yamllint, each run with the workflow's bash -eo pipefail. --setup installs actionlint 1.7.12 and shellcheck 0.11.0 linux.x86_64 through lint.js's own SHA-256-verified cache path. I pre-seeded the archives because downloads through this host's proxy are very slow; their SHA-256 matches the pins. Argv-logging shims, placed first on the lane PATH, exec the real binaries and count linter calls.
Host suite Debian 13 x86_64 (dash), vitest 3.2.7

1. #12647 trigger at this head

trigger A/B

lane main PR
Run shellcheck exit 0, false green: shellcheck invoked with 0 files exit 1 in 0.03 s, shellcheck never invoked
Run yamllint exit 1, but on FILE_OR_DIR - is required exit 1 in 0.03 s, yamllint never invoked

On the PR, git's own fatal: detected dubious ownership … stays directly above the lane's git ls-files failed; refusing to lint an empty file list.

2. New: what happens when #12648's step soft-fails

safe.directory soft-fail A/B

The safe.directory line that #12648 added to Restore workspace ownership ends in || echo "::warning::…", so the job continues when it fails. I ran that step verbatim from ci.yml with a read-only $HOME. It printed could not lock config file … Read-only file system and the ::warning::, then exited 0. The two lanes then hit dubious ownership:

  • main: the same false green from shellcheck and the same usage-screen failure from yamllint.
  • PR: two loud failures that name git's error.

So the PR backstops #12648's own failure mode.

3. Healthy path, real CI match, refactor delta, tests

parity, refactor, tests

  • Parity: I ran the verbatim step first; it added safe.directory. Each arm then made one shellcheck call over 66 scripts and one yamllint call over 78 files. Both exited 0, and stdout and stderr were byte-identical between main and the PR. A duplicate-key YAML file fails yamllint with the same ::error lines on both arms.
  • The rig matches the real runner. This head's Lint & Static job ran on ecs-qwen-hk3-13 and logged 2324 warnings, 0 errors, over 58 files. The rig's findings are identical line for line.
  • Refactor delta, de0f9143 → 0f8f1756: I diffed the generated shellcheck and yamllint lane strings, check and installer included. The only change is the empty-YAML-list message, refusing to lint → refusing to pass. getLinterPath() is identical for linux, darwin, win32 and with no arguments.
  • Suite:
    • Linux x86_64: 16/16. CI Test (ubuntu-latest): ✓ scripts/tests/lint.test.js (16 tests).
    • With process.arch forced to an unsupported value for lint.js only: 8 passed and 8 skipped. The lane cases go through ctx.skip() and do not fail.
    • Negative control: I swapped main's two lane strings into the PR file and kept the exports. Exactly the 5 guard tests fail; 11 pass.

4. Mutation matrix at this head

mutation and residual

I made 18 single-edit mutants of scripts/lint.js and ran each against the PR's own suite. 14 are killed. These include each guard, $${variable}, -z→-n, || true on yamllint, the --format github and --exclude flags, the awk and sed rewrites, the pip-dir order, and the tempDir and cwd defaults. I drove each of the 4 survivors through the real lanes:

survivor measured effect in the real lanes weight
drop the terminal-bench exclusion (lint.js:229) 66 → 70 scripts, +22 advisory warnings, exit 0 Matches the bot's deferred lint.js:229 finding (D12-2). Harmless today.
platform default → 'darwin' Output identical to the PR on this runner, because the image's yamllint is on the system PATH Test gap only
env default → {} setup, actionlint, shellcheck and yamllint all exit 1. The PR's own git guard fires in both lanes. Test gap only; fails loudly
runCommand drops env.PATH = getLinterPath() Run actionlint exits 1, so CI goes red. The shellcheck lane alone exits 0 (xargs: shellcheck: No such file or directory). This is the §5 residual again

None of these blocks the merge. Optional hardening is two assertions in the defaults to the module temp dir… case:

  • toContain(process.env.PATH) would kill the env mutant.
  • An assertion on the pip --user dir for process.platform would kill the platform mutant.

5. Pre-existing residual, re-measured natively (not introduced here)

The shellcheck lane still takes its status from the trailing sed. With the real pinned x86_64 binary, the PR lane exits 0 in three cases: the binary is missing, the binary is replaced by a stand-in that dies with SIGSEGV, or a script is unparseable (SC1073 error:). I re-anchored round 1's narrower candidate to the refactored source (harness/cand.diff); the hunk itself is unchanged. With it, the PR suite stays 16/16. Measured natively:

case PR candidate
missing binary exit 0 exit 1 (xargs 127)
SIGSEGV stand-in exit 0 exit 1 (xargs 125)
unparseable script exit 0 exit 1
one extra SC2086 warning exit 0 exit 0
healthy tree exit 0 exit 0, stdout byte-identical

This belongs in a follow-up. Round 1's other residual also still holds on the real runner at this head: the Run sensitive keyword linter step finished in 34 ms with no output, because lint.js has no --sensitive-keywords handler.

Not covered / rig caveats

  • macOS was not re-run this round (Linux host). Round 1 ran 14/14 on macOS arm64 at de0f9143. The lane shell is byte-identical since then. The two cases added since then have not run on macOS: the yamllint exit-status lane case (stub exits 1; any non-zero xargs status passes it) and the getLinterPath default case.
  • Windows was not executed. The 8 lane cases skip through ctx.skip(); the unsupported-arch simulation above exercises that path. The two getLinterPath cases normalise paths with toPosix, and I only reasoned through them. Test (macos-latest) and Test (windows-latest) were skipped in this PR's CI.
  • This is a rig, not the ECS runner. Its healthy-path output does match the real runner line for line. The real runner image's yamllint version is unknown; the rig uses the pinned 1.35.1.

Evidence (figures, REPORT.md, rig Dockerfile and scenario script, mutation driver, figure generators, raw lane outputs): wenshao/qwen-code@3315be93/pr-12650-round2

中文版

维护者验证第 2 轮(仅增量):PR #12650 @ 0f8f1756

合并参考结论: 与 de0f9143 上的第 1 轮相同。代码可以合入,PR 元数据还不行。本轮在原生 x86_64 装置上重新验证当前 head。第 1 轮跑在 qemu 下,钉定的 shellcheck 在那里无法执行。本评论只覆盖第 1 轮没有覆盖的内容。

  • 代码:✅ 已在当前 head 重新验证。 自 de0f9143 以来,PR 自身的提交只有一个重构(ad4ceab8)和两个纯测试提交(bd9bcfc3、83807551),其余都是从 main 合入。

  • 元数据:❌ 仍未更新。 标题、正文和 Fixes #12647 仍在描述第 0 轮的设计:yamllint 版本探测加 PATH 重排。这套设计不在代码树里。autofix 评论 5868718857 中可直接套用的标题和正文在当前 head 下依然成立。我逐条核对了其中可检验的说法:16 个测试(原有 6 个 + 新增 10 个)、全部十处行号引用、ctx.skip() 门槛,以及两个无门槛的 getLinterPath 用例。套用前需要改一处:它的 macOS 那句说 de0f9143 之后新增的用例都是 getLinterPath 测试,实际上是一个 getLinterPath 默认值用例加一个 yamllint 退出码通道用例(:366)。所以 16 个用例里有 2 个从未在 macOS 上跑过,那一格应为 ⚠️,或者在句子里写明。autofix 循环已在 10/10 暂停,且没有 GitHub 凭证,所以需要有写权限的人来套用。

  • 建议路径:

    1. 套用这份标题和正文,其中已去掉 Fixes #12647。
    2. 驳回只针对元数据的 CHANGES_REQUESTED(最新一条)。
    3. 合入。

    第 5 节的 shellcheck 退出码残留是既有问题,更适合作为后续跟进处理。

环境

装置 catthehacker/ubuntu:act-latest(Ubuntu 24.04.4),在 x86_64 宿主上原生 amd64 运行。/bin/sh = dash,git 2.55.0,GNU xargs 4.9.0,file 5.45,Node 22.22.2。镜像预装了 yamllint 1.35.1,因为当前 head 的 CI 没有打印 Installing yamllint...。我去掉了镜像自带的系统级 safe.directory=*。
触发条件 作业以 root 身份运行,HOME=/root。工作区是 /home/github-runner/actions-runner-hk3-13/_work/qwen-code/qwen-code(即跑当前 head CI 的那台 runner),属主 github-runner(uid 1001)。
代码树 PR head 0f8f1756 的代码树,真实 git index 中有 10252 个受跟踪文件。main 臂用的是同一棵树,只把 scripts/lint.js 换成 merge-base afb911a3 的版本;head 减 base 正好是 PR 的两个文件。lint.js、lint.test.js、ci.yml 在今天的 main(27a4485d)上都没有变化,PR 可以干净地合入。
通道 真实的 node scripts/lint.js --setup / --actionlint / --shellcheck / --yamllint,每条都用工作流的 bash -eo pipefail 运行。--setup 通过 lint.js 自己带 SHA-256 校验的缓存路径安装 actionlint 1.7.12 和 shellcheck 0.11.0 linux.x86_64。因为本机代理下载极慢,我预置了归档,其 SHA-256 与钉定值一致。argv 记录垫片放在通道 PATH 最前面,exec 真实二进制,并统计 linter 调用次数。
宿主测试 Debian 13 x86_64(dash),vitest 3.2.7

1. 当前 head 下的 #12647 触发条件

通道 main PR
Run shellcheck exit 0,假绿:shellcheck 以 0 个文件被调用 0.03 s 内 exit 1,shellcheck 从未被调用
Run yamllint exit 1,但失败在 FILE_OR_DIR - is required 上 0.03 s 内 exit 1,yamllint 从未被调用

PR 下,git 自己的 fatal: detected dubious ownership … 紧挨在通道的 git ls-files failed; refusing to lint an empty file list 上方。

2. 新增:#12648 的步骤软失败时会怎样

#12648 在 Restore workspace ownership 里加的 safe.directory 那一行以 || echo "::warning::…" 结尾,所以它失败时作业会继续。我把这个步骤从 ci.yml 原样拿来,在只读 $HOME 下运行。它打印了 could not lock config file … Read-only file system 和 ::warning::,然后 exit 0。随后两条通道都撞上 dubious ownership:

  • main:shellcheck 同样假绿,yamllint 同样失败在 usage 页面上。
  • PR:两条响亮失败,都点名 git 的报错。

所以本 PR 为 #12648 自身的失效模式兜底。

3. 健康路径、与真实 CI 对照、重构增量、测试

  • 一致性: 我先运行上述原样步骤,它添加了 safe.directory。之后每个臂都是一次 shellcheck 调用检查 66 个脚本、一次 yamllint 调用检查 78 个文件。两臂都 exit 0,main 与 PR 的 stdout 和 stderr 逐字节一致。一个重复键的 YAML 文件在两臂上都让 yamllint 失败,::error 行相同。
  • 装置与真实 runner 一致。 当前 head 的 Lint & Static 作业跑在 ecs-qwen-hk3-13 上,日志里有 2324 条 warning、0 条 error,涉及 58 个文件。装置的 finding 与之逐行一致。
  • 重构增量 de0f9143 → 0f8f1756: 我对生成的 shellcheck 和 yamllint 通道字符串做了 diff,check 和 installer 也包括在内。唯一的变化是 YAML 空列表的提示,refusing to lint → refusing to pass。getLinterPath() 在 linux、darwin、win32 和无参调用下完全相同。
  • 测试套件:
    • Linux x86_64:16/16。CI 的 Test (ubuntu-latest):✓ scripts/tests/lint.test.js (16 tests)。
    • 只对 lint.js 把 process.arch 改成不受支持的值:8 个通过、8 个跳过。通道用例走 ctx.skip(),不会失败。
    • 负对照:把 main 的两条通道字符串换进 PR 文件,并保留导出。恰好是 5 个守卫测试失败,其余 11 个通过。

4. 当前 head 的变异矩阵

我对 scripts/lint.js 做了 18 个单点变异,每个都用 PR 自己的测试套件跑。14 个被杀掉,包括:每道守卫、$${variable}、-z→-n、yamllint 后加 || true、--format github 和 --exclude 两个参数、awk 和 sed 改写、pip 目录顺序、tempDir 和 cwd 默认值。4 个存活体我都放进真实通道实测:

存活体 真实通道中的实测影响 定性
去掉 terminal-bench 排除(lint.js:229) 66 → 70 个脚本,多 22 条建议性 warning,exit 0 对应 bot 已暂缓的 lint.js:229 发现(D12-2),目前无害
platform 默认值 → 'darwin' 在这台 runner 上输出与 PR 相同,因为镜像的 yamllint 在系统 PATH 上 仅为测试缺口
env 默认值 → {} setup、actionlint、shellcheck、yamllint 全部 exit 1;PR 自己的 git 守卫在两条通道里都会触发 仅为测试缺口,失败是响亮的
runCommand 去掉 env.PATH = getLinterPath() Run actionlint exit 1,所以 CI 变红;但只有 shellcheck 通道 exit 0(xargs: shellcheck: No such file or directory) 就是第 5 节的那个残留

这些都不阻塞合入。可选的加固是在 defaults to the module temp dir… 用例里加两条断言:

  • toContain(process.env.PATH),可以杀掉 env 变异体。
  • 断言 process.platform 对应的 pip --user 目录,可以杀掉 platform 变异体。

5. 既有残留,原生环境复测(不是本 PR 引入)

shellcheck 通道的退出码仍然取自末尾的 sed。用真实钉定的 x86_64 二进制,PR 通道在三种情况下都 exit 0:二进制缺失、二进制被换成以 SIGSEGV 退出的替身、脚本无法解析(SC1073 error:)。我把第 1 轮提出的更窄候选修法重新锚定到重构后的源码上(harness/cand.diff),改动内容本身不变。套用后 PR 测试套件仍是 16/16。原生实测结果:

场景 PR 候选修法
二进制缺失 exit 0 exit 1(xargs 127)
SIGSEGV 替身 exit 0 exit 1(xargs 125)
无法解析的脚本 exit 0 exit 1
额外一条 SC2086 warning exit 0 exit 0
健康代码树 exit 0 exit 0,stdout 逐字节一致

这适合放在后续跟进里处理。第 1 轮提到的另一处残留在当前 head 的真实 runner 上也仍然存在:Run sensitive keyword linter 步骤 34 ms 结束、没有任何输出,因为 lint.js 没有处理 --sensitive-keywords 的代码。

未覆盖 / 装置说明

  • 本轮没有重跑 macOS(宿主是 Linux)。第 1 轮在 de0f9143 上用 macOS arm64 跑过,14/14。此后通道 shell 逐字节未变。之后新增的两个用例都没有在 macOS 上跑过:yamllint 退出码通道用例(桩 exit 1,xargs 返回任何非零状态都能通过)和 getLinterPath 默认值用例。
  • 没有在 Windows 上实际执行。 8 个通道用例会经 ctx.skip() 跳过,上面的不支持架构模拟走到了这条路径。两个 getLinterPath 用例用 toPosix 归一化路径,我只做了推理。本 PR 的 CI 里 Test (macos-latest) 和 Test (windows-latest) 都被跳过了。
  • 这是装置,不是 ECS runner 本身。不过它在健康路径上的输出与真实 runner 逐行一致。真实 runner 镜像里的 yamllint 版本未知,装置用的是钉定的 1.35.1。

证据(截图、REPORT.zh-CN.md、装置 Dockerfile 和场景脚本、变异驱动、出图脚本、通道原始输出):wenshao/qwen-code@3315be93/pr-12650-round2


🤖 Generated with Claude Code — Claude Opus 5.5

@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao wenshao changed the title fix(ci): fall back to the pinned yamllint when the runner image copy is stale or broken (#12647) fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list Oct 2, 2026
@github-actions github-actions Bot removed the review/self-reported The linked issue was opened by the PR author (self-reported) label Oct 2, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical-only review at head 9f2b5eb53369924bc978e377f628d45b74cc7040.

The one standing Critical is resolved

This PR carried a single Critical, R1-6, re-asserted across every CHANGES_REQUESTED round from review 5328635753 onward. It was explicitly a metadata finding — "The code is NOT what this blocks" — charging that the title, description, Test Plan and Fixes #12647 linkage certified a design absent from the tree, recorded a root cause the linked issue contradicted, and claimed a close another PR had already made. I re-checked each of its seven limbs against the live body at this head rather than relying on the resolved flag:

  • (a) unshipped version probe — the title is now fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list, the retitle the finding prescribed verbatim, and the body states the yamllint availability check command -v yamllint is unchanged. check: 'command -v yamllint' is indeed still the shipped line, so the body now agrees with the tree.
  • (b) inverted PATH claim — the body now says the PATH order is unchanged, and the Test Plan says the pip --user bin dir is appended after the inherited PATH. That matches getLinterPath, which builds ${nodeBin}:${tempDir}/actionlint:${tempDir}/shellcheck:${env.PATH} and appends ${env.HOME}/.local/bin afterwards.
  • (c) nonexistent Test Plan — now names the real command and Tests 16 passed (16), 6 pre-existing plus 10 new. I verified the inventory independently at this head: 16 declarations, of which the 6 pre-existing are the three linter directories cases, the skipIf case at :110 and the two prettier lane cases, and the 10 new ones are the lane and getLinterPath cases. No pip3 / --setup case is claimed.
  • (d) misnamed Windows mechanism — now says the 8 lane cases gate at runtime through ctx.skip() and that the two getLinterPath cases execute ungated including on the Windows leg, which is what the file does.
  • (e) false root cause — ## Why it's needed now attributes the 2026-09-24 failure to fatal: detected dubious ownership in repository and credits #12648 with removing the trigger, agreeing with the issue and with the shipped code comment.
  • (f) false close — Fixes #12647 is now References #12647, a non-closing reference that names #12648 as the repair, so the linkage no longer claims a close another branch made.
  • (g) omission of what shipped — the body now leads with git ls-files, the empty-list refusals and getLinterPath.

All eight review threads are resolved, and the bot's own round 16 at this head reports zero findings at the Critical floor.

Current scan — no provable Critical

scripts/lint.js is the only production file; the other is new tests.

  • Generated shell text. The two fragment builders are JS template literals emitting shell, so escaping is the first thing to check. refuseEmptyList writes $${variable}, which interpolates to a literal $ followed by the shell variable name — the guard tests $candidates, $scripts and $files, not the JS parameter. printf '%s\\n' and grep -E '^([^.]+|.*\\.(sh|zsh|bash))' emit \n and \. to the shell exactly as the pre-change pipeline had them, and stageGitFileList's $(git ls-files) passes through because only ${ interpolates. The shellcheck candidate and detection filters are carried over unchanged, so file selection cannot widen or narrow.
  • The guards themselves. files="$(git ls-files)" || { …; exit 1; } catches git's failure directly, which is the case the trailing sed previously swallowed. The two -z refusals then stop an empty list reaching file --mime-type and shellcheck. On the yamllint side the refusal replaces a zero-argument invocation that failed on the tool's usage screen instead of on git's error. Both lanes now fail at the real cause, which is the stated intent.
  • getLinterPath extraction. Behaviour-preserving: the defaults are process.env, process.platform, process.cwd() and the module TEMP_DIR, the same four values the inline code read, and runCommand now assigns env.PATH = getLinterPath(). The darwin and linux branches still append to the already-assembled path, and env.HOME equals process.env.HOME on the default path. The comment recording why tempDir defaults to TEMP_DIR is one of the few places a why is genuinely non-obvious, and it is accurate — the installers and extract paths are built from that same constant.
  • Newly exported surface. getLinters and getLinterPath become module exports so tests can pin them. scripts/lint.js is a repository build script rather than a published package entry, and neither export reveals a secret or accepts untrusted input, so widening visibility adds no attack surface.

I did not re-run the suite. The Linux Test leg executed it and is green. For the guards' discrimination I am relying on the two maintainer verification rounds the body links rather than on my own execution: they report driving the real lane strings under a genuine dubious-ownership trigger, with main's shellcheck lane exiting 0 having linted nothing and its yamllint lane failing on the usage screen, while both post-change lanes exit 1 naming git's error and invoking no linter, and that swapping main's strings back in fails exactly the five guard tests. That count is internally consistent with the file — the five are the two git-error aborts and the three empty-list refusals, distinct from the five selection, exit-status and PATH cases.

CI

Green at this head, with no failing, errored or timed-out check. Test (ubuntu-latest, Node 22.x), Lint & Static, review-pr, Integration Tests (no-AK, No Sandbox), both Desktop Shell legs and web-shell E2E Smoke all pass. The macOS and Windows Test legs are skipped by a pre-existing workflow gate rather than by this PR, and the body honestly records Windows as not run and macOS as partial, so this is a coverage disclosure and not a PR-introduced blocker.

Note

Two Suggestion-level items remain on record and do not gate this approval: the unwitnessed integration-tests/terminal-bench/ exclusion in the shellcheck candidate filter, and the argv-stub fixture re-declared in the second describe. The trailing sed still swallowing shellcheck's own exit status is declared out of scope with a reason — surfacing every finding would turn main red on its existing warning backlog — which is a defensible split rather than a gap in this change.

No Critical found. Approving.

@wenshao

wenshao commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, round 3 (delta + macOS gap closure): PR #12650 @ 9f2b5eb5

Verdict: merge-ready. 65/65 scripted assertions passed, 0 failed. Verified head 9f2b5eb53369924bc978e377f628d45b74cc7040.

For the merge decision: the code delta since round 2 (0f8f1756) is zero — every commit since is a merge from main, and scripts/lint.js / scripts/tests/lint.test.js are byte-identical blob-for-blob. The round-2 blocker (stale title/body/Fixes #12647) is fixed. Round 2's one uncovered platform gap — the two cases that had never run on macOS — is closed: the full suite now has a 16/16 macOS arm64 run at this head. This round also re-proved the central claim on macOS with real lanes, where the base failure mode turns out to be worse than on Linux (see §3).

中文摘要

第 3 轮维护者验证(增量 + 补 macOS 缺口):PR #12650 @ 9f2b5eb5

结论:可以合入(merge-ready)。 65/65 条脚本断言全部通过,0 失败。验证 head:9f2b5eb53369924bc978e377f628d45b74cc7040。

  • 代码增量为零。 自第 2 轮(0f8f1756)以来的 118 个提交全部是从 main 合入的 merge;scripts/lint.js 与 scripts/tests/lint.test.js 两个文件的 blob 哈希逐字节一致;ci.yml 里两条 lint 通道的调用段也无一行改动。第 2 轮在原生 x86_64 Linux 装置上的通道实测结论因此原样成立。
  • 第 2 轮的元数据阻塞已修复。 标题、正文已按建议重写(Fixes #12647 改为 References #12647,macOS 那句的更正也已采纳);旧的 CHANGES_REQUESTED 已处理,现有两个 APPROVED(wenshao 10-03 02:34 北京、qqqys 10-05 07:48 北京),head 的 CI 全绿(含 Lint & Static 11m59s)。
  • macOS 缺口关闭。 第 2 轮从没在 macOS 上跑过的两个用例(yamllint 退出码 :366、getLinterPath 默认值 :524)本轮在 macOS arm64 上随完整套件 16/16 通过。
  • 真实通道 A/B 在 macOS 重新实测。 在 git 失败(索引损坏,exit 128)触发下:base 两臂 静默假绿(BSD xargs 对空输入不执行工具,比 Linux 的 usage 屏报错更糟——连误导性红都没有),head 两臂 exit 1、git 的 fatal 紧贴在守卫信息上方、linter 零调用。健康路径两臂 exit 0、stdout/stderr 逐字节一致(shellcheck 一次调用 67 个脚本、yamllint 一次调用 78 个文件)。空列表守卫(git 正常但无匹配文件)同样实测通过。
  • 变异矩阵在新 head 重跑:19 个变异体,NC + 14/18 被杀,存活体与第 2 轮完全相同(M11/M16/M17/M18),均不阻塞。
  • 两个既有残留复测仍存在(均为 PR 引入前的问题,适合后续跟进):shellcheck 通道退出码仍被末尾 sed 吞掉(二进制缺失、linter 退出 1 都 exit 0);--sensitive-keywords 无处理器(84ms 静默 exit 0)。
  • 一个小瑕疵(不阻塞):正文里 "66 scripts" 已过时,当前 base 是 67 个(YAML 仍是 78)。

证据图见文末四张;原始日志与装置脚本在 tmp/pr12650-verify-20261005/(本地)。

0. Previous-finding status (round 2 → round 3)

# Round-2 finding Status at 9f2b5eb5 Evidence
1 Metadata stale: title/body/Fixes #12647 describe the round-0 design Fixed. Title and body rewritten as recommended; References #12647; the macOS-correction sentence was applied verbatim. CHANGES_REQUESTED resolved; two APPROVEDs on file gh pr view, reviews API
2 shellcheck lane status taken from trailing sed (pre-existing, deferred) Stands (re-measured at head): binary missing → lane exit 0; shellcheck exits 1 with a finding → lane exit 0, finding printed §4 C2/C3
3 --sensitive-keywords step is a no-op (pre-existing) Stands (re-measured): exit 0, 0 bytes out/err, 84 ms; lint.js still has no handler; ci.yml still calls it §4 C1
4 4 mutation survivors (M11, M16, M17, M18), all non-blocking Stand unchanged: identical 4 survivors at this head §5
5 macOS gap: cases :366 and :524 never run on macOS Fixed: full suite 16/16 on macOS arm64 at this head §2
6 Windows never executed Not covered (this rig is macOS; the 8 lane cases skip via ctx.skip() there by design) §6

1. Delta since round 2 (the input closure)

  • git log 0f8f1756..9f2b5eb5 -- scripts/lint.js scripts/tests/lint.test.js → empty. Blob hashes for both files are identical between the two heads.
  • Effective PR diff vs the current base 9915c7ff8f: still exactly the two files (+384/−19).
  • main moved 27a4485d → 9915c7ff8f; two commits touched ci.yml, but the diff contains no line touching the lint-lane invocations (lint.js, yamllint, shellcheck, safe.directory, the restore step — grep-verified). The corpus grew: 67 shell scripts (was 66) and 78 YAML files on the current base.
  • Round-2's native-x86_64 Linux measurements (dubious-ownership trigger, safe.directory soft-fail backstop, line-for-line CI match) are carried forward on this proven-identical closure: same lint.js bytes, same lane invocation, same pinned tool versions, and the head's own CI Lint & Static job is green (11m59s).

2. macOS gap closure — full suite at this head

vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/lint.test.js on macOS arm64 (the machine this round ran on), vitest 3.2.7, node 24.18.1:

Test Files  1 passed (1)
     Tests  16 passed (16)

16/16 on macOS arm64

This includes the two cases round 2 flagged as never-run-on-macOS (fails the lane when yamllint itself exits non-zero and the getLinterPath temp-dir default). (The SHA-256 mismatch lines in the capture are the archive-cache test's own negative fixtures printing to the console — the suite is green.)

3. Central claim re-proven on macOS with the real lanes

Real node scripts/lint.js --shellcheck / --yamllint on the real worktrees, argv-recording shims in front of the real shellcheck 0.11.0 (pinned version, via Homebrew) and yamllint 1.35.1 (pinned, pip --user). Trigger: the worktree git index replaced with garbage, so git ls-files exits 128 (fatal: index file smaller than expected) — the same guard branch #12647's dubious-ownership failure takes.

lane base (9915c7ff8f) head (9f2b5eb5)
Run shellcheck exit 0, silent false green — shellcheck never invoked exit 1, fatal: index file smaller than expected directly above shellcheck: git ls-files failed; refusing to lint an empty file list; shellcheck never invoked
Run yamllint exit 0, silent false green — yamllint never invoked exit 1, same shape; yamllint never invoked

trigger A/B

New observation (widens the PR's value): on macOS the base failure mode is worse than the Linux one round 2 measured. BSD xargs does not execute the utility at all on completely empty stdin, so both base lanes pass having run nothing — no misleading usage-screen red, just a silent green. The head's behaviour is identical on both platforms: loud exit 1 naming git's error. (The empty-list guard was also exercised separately — index removed, git exits 0 with an empty list: base exits 0 on both lanes; head exits 1 with refusing to pass on an empty file list.)

Healthy path: all four cells exit 0; exactly one shellcheck invocation over 67 scripts (argc 73 = 67 files + 6 flags) and one yamllint invocation over 78 files (argc 80); base vs head stdout+stderr byte-identical per lane.

parity + residuals

4. Residuals re-measured at head (pre-existing, not introduced here)

  • C1 node scripts/lint.js --sensitive-keywords: exit 0, zero bytes out/err, 84 ms — the ci.yml step still calls a handler that does not exist.
  • C2 shellcheck missing from the lane PATH: xargs: shellcheck: No such file or directory, lane exit 0.
  • C3 shellcheck stand-in printing one finding and exiting 1: finding visible in lane stdout, lane exit 0.

Both remain follow-up material, as round 2 concluded.

5. Mutation matrix re-run at this head (macOS arm64)

19 mutants (round-2 driver, self-verifying single-site edits): negative control killed (exactly the 5 guard tests fail), 14/18 mutants killed, and the survivors are the same four as round 2 — M11 (terminal-bench exclusion), M16 (platform default), M17 (env default), M18 (runCommand PATH wiring) — all adjudicated non-blocking in round 2 and unchanged here.

mutation matrix

6. Not covered

  • Windows: not executed (this rig is macOS arm64). The 8 lane cases skip through ctx.skip() on Windows; the 2 getLinterPath cases run ungated in CI's Windows leg when it runs.
  • The native-Linux trigger/parity cells were not re-run this round — they are carried from round 2 on the proven-identical closure in §1 (same file blobs, same ci.yml lanes, same pins). The macOS cells here are new evidence, not a substitute measurement of the Linux runner.
  • The head's healthy-path shellcheck output was compared base-vs-head on this machine (byte-identical); it was not compared line-for-line against the real CI job this round (round 2 did that on the Linux rig; macOS path prefixes differ by construction).
  • Body nit, not blocking: "66 scripts and 78 YAML files today" is now 67/78 on the current base.

7. Methodology

One macOS arm64 machine (the maintainer's): node 24.18.1, git 2.55.0, BSD xargs, file 5.41, shellcheck 0.11.0 (Homebrew, same version as the pin), yamllint 1.35.1 (pip --user). Detached worktrees ~/pr12650-verify/{base,head} at 9915c7ff8f and 9f2b5eb533; lanes driven by the real node scripts/lint.js with argv-recording shims that exec the real binaries; git-failure trigger = corrupted worktree index (restored from a byte copy after each cell; both worktrees verified clean afterwards); mutation driver = round-2's mutate.mjs re-pathed (its once() helper asserts exactly one match per edit, so pattern drift would have failed loudly — none did). Raw per-cell stdout/stderr/exit-code/argv logs, the harness scripts, and mutation-matrix.json are in tmp/pr12650-verify-20261005/ of the working repo. Captures produced with scripts/verify-capture.mjs.

@wenshao
wenshao dismissed a stale review October 5, 2026 12:43

fixed

@wenshao
wenshao added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 6b878e1 Oct 5, 2026
80 checks passed
yiliang114 added a commit that referenced this pull request Oct 5, 2026
The Lint & Static lane failed on 2ce7b0c at its gate-freshness
pre-flight, not on any lint rule. Its own output: "The lint gate changed
on 'main' after this branch last incorporated it: scripts/lint.js:
6b878e1 (#12650) (2026-10-05)". That commit landed on main at
12:44:27Z, seven seconds before this job started at 12:44:34Z, and the
lane checks out the branch head alone, so it requires the branch to carry
the current gate definition. Merging main re-validates it.

No source conflicts: main has not touched xml-tool-call-fallback.ts or its
test since this branch last merged it.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmuv5xnpc3q
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/needs-human The autofix loop stopped on this PR — a human must re-arm, split, merge, or close it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants