Repository navigation
fix(ci): fail yamllint and shellcheck lanes loudly on an empty git file list - #12650
Conversation
…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.
Autofix E2E Report — #12647 (Main CI failed: Run yamllint on a064952)What failedThe main-branch CI run for Root causeEvidence assembled from the public Actions API and local reproduction:
A second latent defect made the first one unrecoverable: even when setup does install the pinned yamllint via Fix
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 Regression coverageThree new tests in
Mutation probes (each reverted hunk was confirmed to fail the suite, then restored):
Environment-specific limitationThe failing runner image itself cannot be inspected from here, so the exact pathology on Verification
中文说明Autofix E2E 报告 —— #12647(main 分支 CI 在 a064952 的 Run yamllint 步骤失败)失败现象
根本原因通过公开的 Actions API 与本地复现收集到的证据:
第二个潜在缺陷使第一个缺陷无法自愈:即使 setup 确实通过 修复
两半缺一不可:只有探测而没有 PATH 调整,钉定副本装上了却仍会先解析到镜像里的坏副本;只有 PATH 调整而没有探测,则因为 回归覆盖
变异探针(每个被还原的代码块都先确认会让测试套件失败,随后恢复):
环境限制无法从此处检查故障 runner 镜像本身,因此 验证
🧠 Handled by Qwen Code · model/模型 |
|
🤖 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:
Not implemented (needs a maintainer): the review's workflow-side alternatives — a durable Deferred (verified, out of scope): the same trailing Observation for maintainers (not feedback, not changed): the CI step Changes
Test witnesses and mutation probes
Conflict notesNone — Verification
中文说明Autofix 本轮总结 — PR #12650(issue #12647)已处理的反馈[rv:5305678922] CHANGES_REQUESTED @qwen-code-ci-bot —— 本轮唯一反馈。其中包含四个可检验的论断;在改动任何代码之前均已逐一探测验证:
未实现(需要维护者决定): 评审提到的工作流侧替代方案 —— 为 checkout 之后的步骤配置持久的 暂缓(已验证属实,超出本 PR 范围): 管道末尾的 给维护者的观察(非反馈、未改动): CI 步骤 改动
测试见证与变异探测
冲突说明无 —— 验证
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/模型 |
|
🔀 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 将重新运行。 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no action taken (PR #12650, issue #12647)Feedback this roundOne issue-level comment and one failed check, both reporting the same event:
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 Why no code changeThe failed check is the LLM review pipeline failing at the agent-process level, not a defect in this PR:
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 ( State of the PR
VerificationNo code was changed, so no build/test re-runs were required this round. Evidence actually gathered:
中文说明Autofix 本轮 —— 未做改动(PR #12650,issue #12647)本轮反馈一条 issue 级评论和一个失败的检查,两者报告的是同一事件:
本轮没有任何行内评论、没有 review 正文,也没有其他失败的检查。此前的 review 发现([rv:5305678922],CHANGES_REQUESTED)早于上次评估水位线,已在第 1 轮(提交 为什么不做代码改动失败的检查是 LLM 审查流水线在代理进程层面的失败,而不是本 PR 的缺陷:
没有任何假设能把这段约 300 行的 diff(shell lane 守卫及其测试)与审查代理的确定性崩溃联系起来;本沙箱内 PR 当前状态
验证本轮未改动代码,因此无需重跑构建/测试。实际收集的证据:
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🔀 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 将重新运行。 |
|
@qwen-code /triage |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no action taken (PR #12650, issue #12647)Feedback this roundExactly one item is newer than the last evaluation watermark (2026-09-26T21:10:14Z):
There were no inline comments ( Why no code change
State of the PR
VerificationNo code was changed, so no build/test re-runs were required. Evidence actually gathered:
中文说明Autofix 本轮 —— 未做改动(PR #12650,issue #12647)本轮反馈晚于上次评估水位线(2026-09-26T21:10:14Z)的反馈仅有一条:
本轮没有任何行内评论( 为什么不做代码改动
PR 当前状态
验证本轮未改动代码,因此无需重跑构建/测试。实际收集的证据:
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no code change (PR #12650, issue #12647)Feedback this roundExactly 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 verificationThe review is source-blind-treated: every checkable claim was verified against the live diff and tree before accepting it. All of them hold.
Why no code changeThe 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:
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 descriptionProposed title (matches the surviving delta; drop the Proposed body (drop ## 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 nextThe 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 Verification
中文说明Autofix 本轮 —— 无代码改动(PR #12650,issue #12647)本轮反馈距上次评估水位线(2026-09-27T02:27:55Z)之后恰有一条反馈;无行内评论、无失败检查、无持续红色检查:
逐条验证评审按来源中立处理:每一条可检验论断都先对照实时 diff 与代码树核实,再予以接受。全部成立。
为什么没有代码改动评审只要求 PR 元数据改动 —— 标题、描述、关闭性关联 —— 并明确表示不需要代码改动。本轮无法代为执行,有两个相互独立的原因:
因此该发现是被接受,而非被拒绝 —— 框架确实已过期 —— 但落地只差一个人工(或 dev-bot PAT)操作。为使该操作机械化,上文已给出可直接套用的替换文本。 可直接套用的标题与描述建议标题(与留存 delta 相符;去掉 后续CHANGES_REQUESTED 评审被有意保持未解决状态:其诉求真实、已被接受,只差一次本通道无法执行的元数据编辑。一旦维护者(或 bot 运营方)套用上述标题/正文 —— 或指示本循环停止 —— 就没有遗留事项了:代码已获评审认可,本地与 CI 均为绿( 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Maintainer verification, real environment: PR #12650 @
|
| 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
| 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
mainstep 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
- Nothing changes on a healthy checkout. With
safe.directoryset (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 thatexecthe real binaries. - Real violations still surface. A duplicate-key YAML file makes yamllint exit 1 with the same
::errorannotation on both trees. ASC2086script 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.jspasses 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 thexargs -rshortcut, break each file filter, move the pip dir ahead of the inherited PATH, and revert bothrunstrings while keeping the exports. Each is killed by the test named for it. Cross-check: the PR tests againstmain'slint.jswith 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 forexecSync's/bin/sh.
3. Residual silent passes (pre-existing, not introduced here, non-blocking)
-
The shellcheck lane still cannot fail, because its status is the trailing
sed's. I measured this on both trees:case mainPR 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
mainred on the 2324 existing findings. A narrower candidate fails only whenxargsreports that shellcheck did not run (status ≥ 124) or when the output haserror-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: {
-
The
Run sensitive keyword linterstep inci.ymlis a no-op. It runsnode scripts/lint.js --sensitive-keywords, butlint.jshas no handler for that flag, andmain()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. -
Minor: the shellcheck candidate regex
^([^.]+|.*\.(sh|zsh|bash))has no$anchor, so it matches 9485 of 9740 tracked paths, andfile --mime-typedoes 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.. Thegit ls-filesandfile(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.aarch64release, which the arm64 kernel runs natively. PIP_BREAK_SYSTEM_PACKAGES=1was set so lint.js's ownpip3 install --userworks on Ubuntu 24.04 (PEP 668). The runner images already ship a yamllint (the failed job printed noInstalling yamllint...), so CI does not reach that installer.
中文版
维护者真实环境验证:PR #12650 @ de0f9143
合并参考结论: 代码可以合入,PR 元数据还不行。
- 代码:✅ 已验证。 我用 root 作业、runner 属主的工作区和真实
gitdubious 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 引入,不阻塞)
-
shellcheck 通道仍然无法失败,因为它的退出码取自末尾的
sed。两棵树上实测:场景 mainPR 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 见英文部分。 -
ci.yml里的Run sensitive keyword linter步骤是空操作。 它运行node scripts/lint.js --sensitive-keywords,但lint.js没有处理这个参数的代码,main()会静默忽略未知参数。两棵树上都是 0.5 s、无任何输出、exit 0。这是同一类静默通过,值得单独开 issue。 -
小问题: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 不会走到这个安装器。
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round — no code change (PR #12650, issue #12647)Feedback this roundExactly one item is newer than the last evaluation watermark, and there are no inline comments, no failed checks, and no still-red checks:
Dispositions1. 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 2. Stale PR metadata — accepted, not actionable from this lane (one human action remains)The title, body, and
The ready-to-apply replacement was drafted in this comment. Proposed title: 3. Residual pre-existing gaps — deferred to the follow-up queueThe 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
Verification
中文说明Autofix 本轮——无需改动代码(PR #12650,issue #12647)本轮反馈距离上次评估水位线之后只有一条新反馈,且没有行内评论、没有失败的检查、也没有持续飘红的检查:
逐条处置1. 代码已验证——无需改动(同意)维护者复现了真实的 #12647 触发条件(root 作业、runner 属主的工作区、真实的 git dubious-ownership),确认本 PR 把 shellcheck 的假绿和 yamllint 的误导性 usage 页失败变成两条亚秒级、点名真实原因的明确失败;在健康检出下 stdout 与 2. PR 元数据过期——接受,但本通道无法执行(还差一步人工操作)标题、正文和
可直接套用的替换文本已在这条评论中拟好。建议标题: 3. 残留的既有缺口——已转入后续跟进队列维护者明确把这三项全部划为后续跟进(「既有问题、不阻塞……更适合作为后续跟进处理」)。每一条都已对照代码树核实,并记录进
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /triage |
|
🕐 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]>
|
🤖 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 Still open, and not code: the PR metadata edit (rv:5330189532, Critical)The one Critical — retitle the PR, drop 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 Resolved in code this round (5 inline findings)
Deferred to follow-up (recorded in deferred-findings.json)
Acknowledged, no action
Mutation probes (each applied, measured, then reverted; suite green after every revert)
Verification
中文说明Autofix 轮次 —— 代码改动(PR #12650,issue #12647)提交 仍未解决、且与代码无关:PR 元数据编辑(rv:5330189532,Critical)唯一一条 Critical —— 重命名 PR 标题、移除 验证报告的 F1/F2/F3 是同一发现的三种形态(标题/描述/测试计划 vs 代码树),由同一笔元数据编辑覆盖 —— 不应改代码:交付的测试是有意钉住 pip 本轮在代码中解决的发现(5 条行内评论)
暂缓到后续跟进(已记入 deferred-findings.json)
已知晓,无需动作
变异探测(每次施加、测量、然后还原;每次还原后套件全绿)
验证
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/模型 |
|
🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind 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 performOne 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 Feedback disposition this round
中文说明🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 停止前我了解到的情况: 无法为本轮反馈产出可通过的修复 —— 唯一在范围内的发现需要一次 agent 无权执行的 PR 元数据编辑只差一个维护者动作即可解除本 PR 的阻塞。 本轮唯一可执行的发现(Critical R1-6,由评审 rv:5351827294、rv:5353379604、rv:5356359554 重申)要求修改 PR 标题、重写正文,并将 本轮反馈处置
在头提交
|
|
🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind What I found before stopping: Cannot produce a passing fix: the only in-scope finding is a PR-metadata edit this lane cannot performRequested 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 ( The one actionable findingR1-6 (Critical, re-asserted by rv:5351827294, rv:5353379604, rv:5356359554, rv:5359601706): the PR title, body, Reviewer Test Plan and
中文说明🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 停止前我了解到的情况: 无法产出可通过验证的修复:唯一在范围内的发现是 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)逐条重新核实了所有可检验的论断:
为什么 agent 无法落地
如果评论 5868718857 无法使用,需要的修改是
Run log: https://github.com/QwenLM/qwen-code/actions/runs/36645485063 🧠 Handled by Qwen Code · model/模型 |
|
⏸️ 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 中文说明⏸️ AutoFix 已暂停:本 PR 达到自动轮次上限(10/10),循环不再管理——新反馈与 base 冲突将无人处理。评论 |
Maintainer verification, round 2 (delta only): PR #12650 @
|
| 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
| 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
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: 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 betweenmainand the PR. A duplicate-key YAML file fails yamllint with the same::errorlines on both arms. - The rig matches the real runner. This head's Lint & Static job ran on
ecs-qwen-hk3-13and logged 2324 warnings, 0 errors, over 58 files. The rig's findings are identical line for line. - Refactor delta,
de0f9143→0f8f1756: I diffed the generatedshellcheckandyamllintlane strings,checkandinstallerincluded. 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.archforced to an unsupported value forlint.jsonly: 8 passed and 8 skipped. The lane cases go throughctx.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.
- Linux x86_64: 16/16. CI
4. Mutation matrix at this head
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 theenvmutant.- An assertion on the pip
--userdir forprocess.platformwould kill theplatformmutant.
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 thegetLinterPathdefault case. - Windows was not executed. The 8 lane cases skip through
ctx.skip(); the unsupported-arch simulation above exercises that path. The twogetLinterPathcases normalise paths withtoPosix, and I only reasoned through them.Test (macos-latest)andTest (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合入。- CI 实际执行的 shell 与第 1 轮验证的版本逐字节一致,只有一处 stderr 文案不同。
getLinterPath()在 linux、darwin、win32 和无参调用下输出完全相同。- Main CI failed: Qwen Code CI on a064952e33dc #12647 触发条件下,本 PR 仍会把 shellcheck 的假绿变成响亮失败。
- 本轮新增: 如果 fix(ci): trust the job workspace as a git safe.directory before checkout #12648 自己的
safe.directory步骤软失败,本 PR 就是兜底。这正是 Main CI failed: Qwen Code CI on a064952e33dc #12647 关闭之后本 PR 仍保有的价值。 - 健康代码树上,装置的 2324 条 shellcheck finding 与当前 head 真实 CI 作业逐行一致。
- 测试套件 16/16 通过。变异测试杀掉 14/18。4 个存活体我都在真实通道里实测过,均不阻塞合入。
-
元数据:❌ 仍未更新。 标题、正文和
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 凭证,所以需要有写权限的人来套用。 -
建议路径:
- 套用这份标题和正文,其中已去掉
Fixes #12647。 - 驳回只针对元数据的
CHANGES_REQUESTED(最新一条)。 - 合入。
第 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 个通过。
- Linux x86_64:16/16。CI 的
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
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
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 checkcommand -v yamllintis 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
--userbin dir is appended after the inherited PATH. That matchesgetLinterPath, which builds${nodeBin}:${tempDir}/actionlint:${tempDir}/shellcheck:${env.PATH}and appends${env.HOME}/.local/binafterwards. - (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 threelinter directoriescases, theskipIfcase at:110and the twoprettier lanecases, and the 10 new ones are the lane andgetLinterPathcases. Nopip3/--setupcase is claimed. - (d) misnamed Windows mechanism — now says the 8 lane cases gate at runtime through
ctx.skip()and that the twogetLinterPathcases execute ungated including on the Windows leg, which is what the file does. - (e) false root cause —
## Why it's needednow attributes the 2026-09-24 failure tofatal: detected dubious ownership in repositoryand credits #12648 with removing the trigger, agreeing with the issue and with the shipped code comment. - (f) false close —
Fixes #12647is nowReferences #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 andgetLinterPath.
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.
refuseEmptyListwrites$${variable}, which interpolates to a literal$followed by the shell variable name — the guard tests$candidates,$scriptsand$files, not the JS parameter.printf '%s\\n'andgrep -E '^([^.]+|.*\\.(sh|zsh|bash))'emit\nand\.to the shell exactly as the pre-change pipeline had them, andstageGitFileList'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 trailingsedpreviously swallowed. The two-zrefusals then stop an empty list reachingfile --mime-typeandshellcheck. 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. getLinterPathextraction. Behaviour-preserving: the defaults areprocess.env,process.platform,process.cwd()and the moduleTEMP_DIR, the same four values the inline code read, andrunCommandnow assignsenv.PATH = getLinterPath(). The darwin and linux branches still append to the already-assembled path, andenv.HOMEequalsprocess.env.HOMEon the default path. The comment recording whytempDirdefaults toTEMP_DIRis 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.
getLintersandgetLinterPathbecome module exports so tests can pin them.scripts/lint.jsis 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.
Maintainer verification, round 3 (delta + macOS gap closure): PR #12650 @
|
| # | 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). mainmoved27a4485d→9915c7ff8f; two commits touchedci.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.directorysoft-fail backstop, line-for-line CI match) are carried forward on this proven-identical closure: samelint.jsbytes, 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)
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 |
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.
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.
6. Not covered
- Windows: not executed (this rig is macOS arm64). The 8 lane cases skip through
ctx.skip()on Windows; the 2getLinterPathcases 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.
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











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-filesmade 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 insedand 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-filesdied, 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; expectTests 16 passed (16)on Linux (6 pre-existing + 10 new). The newgit-sourced lint lanesdescribe stubsgit,file,yamllintandshellcheckon a fake PATH and pins: whengit ls-filesfails, each lane aborts on git's own error and never invokes its linter (:304yamllint,:383shellcheck); 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--userbin 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 throughctx.skip()on platforms/architectures the installers do not support; the twogetLinterPathcases (: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.jslanes 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. Swappingmain'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:
Tested on
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
:366and thegetLinterPathdefault case:524) have not run on macOS. Windows: not run; the suite is not win32-excluded, the 8 lane cases skip at runtime viactx.skip()(exercised on Linux by simulating an unsupported architecture: 8 passed, 8 skipped), and the twogetLinterPathcases 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
sedstill 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.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 lanesdescribe 在伪造的 PATH 上桩化git、file、yamllint与shellcheck,并钉住:git ls-files失败时,每个通道都在 git 自身错误上中止且从不调用其 linter(:304yamllint、:383shellcheck);每个通道都拒绝在空的暂存列表上运行(:327、:406、:424);正常路径下每个通道恰好把检测到的文件传给其 linter(:343、:445);yamllint 自身的非零退出会使通道失败(:366);pip--userbin 目录在 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)。风险与范围
sed仍会吞掉 shellcheck 自身的退出码 —— 无论是有发现、二进制缺失还是崩溃 —— 这是脚本引入时就存在的问题,留给后续处理,因为让每条发现都生效会让 main 因现有约 2300 条 warning 变红;linter 版本与 linter 集合不变。关联 Issue
引用 #12647(暴露此失效模式的事故报告;触发条件本身已由 #12648 消除)。