Skip to content

fix(ci): keep a fallback comment when the PR review runner dies - #9255

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/pr-review-runner-resilience
Aug 16, 2026
Merged

fix(ci): keep a fallback comment when the PR review runner dies#9255
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:fix/pr-review-runner-resilience

Conversation

@wenshao

@wenshao wenshao commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes the PR-review workflow resilient to a class of failures where the review job dies abnormally and the PR is left with no review and no explanation. It adds two defenses: a preflight health probe at job start verifies that the runner user can still create files in its own directories, attempts a narrowly scoped ownership repair using the same passwordless-sudo pattern already established in the workflow, and fails fast with a clear diagnostic when repair is impossible; and a new lightweight dependent job on an ephemeral hosted runner posts the usual retry-guidance fallback comment whenever the review job fails, deriving the PR number from the event payload (a dead job's outputs do not survive a crash) and skipping when a fallback comment for the same run already exists. To make that dedup reliable, fallback comments now carry a hidden HTML marker, following the convention the acknowledgement comment already uses.

Why it's needed

The review run for PR #8894 (job 95028756485) died when the self-hosted runner worker crashed in its job-finalization phase with a permission-denied error creating files under the runner user's home directory. Because the crash happened outside the normal step flow, none of the remaining steps ever ran: no fallback comment was posted, no worktree cleanup happened, and the PR sat silently with no review and no indication of what went wrong. The known trigger on the shared self-hosted pool is a prior containerised job running as root leaving root-owned or mode-broken directories behind — the workflow already documents and defends against this pattern for the workspace, but not for the runner's own directories. Even when the root cause cannot be prevented (for example, corruption that happens mid-run), the PR should still get an explanatory comment, and a job that starts on an already-broken runner should fail in seconds with a clear message instead of burning hours of review budget.

Reviewer Test Plan

How to verify

The two new pieces of shell were extracted from the workflow and exercised locally on macOS. The health probe was run against a directory made read-only: it detected the missing write access, attempted repair, and exited non-zero with a clear error when repair was unavailable. The fallback job's script was run against a mock gh across four comment scenarios: an acknowledgement comment carrying the run URL but not the marker (must not suppress posting), a fallback comment with the marker and the same run URL (must suppress), a fallback comment with the marker and a different run URL (must not suppress), and no comments at all (must post). All four behaved as designed. The full workflow passes yamllint at the same version CI uses, actionlint, and a plain YAML parse. End to end, the fallback job fires on any failed review run once this merges; the marker plus run-URL dedup is what keeps ordinary failures (where the in-job fallback step already posted) and re-runs quiet. A natural first live check is the next review failure: the PR should receive exactly one explanatory comment.

Evidence (Before & After)

N/A (workflow change, no user-visible UI)

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux ⚠️

Environment (optional)

Local simulation of the embedded scripts with mocked gh; yamllint 1.35.1 (same as CI), actionlint, PyYAML parse. No live end-to-end run yet — the real path activates on the next failed review run after merge.

Risk & Scope

  • Main risk or tradeoff: the preflight probe adds a few seconds to every review job, and a probe failure now fails the whole job early (deliberate — the job could not have succeeded anyway). In a crash → re-run → normal-failure sequence the PR may carry one extra fallback-style comment versus today, matching existing re-run posting behavior.
  • Not validated / out of scope: repairing corruption that appears mid-run (covered only by the fallback comment, not by repair), and fixing the poisoned directory on the affected self-hosted runner host, which is an ops task. Other workflows on the same runner pool are untouched.
  • Breaking changes / migration notes: none; the marker is a hidden HTML comment and existing fallback messages are unchanged apart from gaining it.

Linked Issues

Incident reference: PR #8894 review run 31891414261 (no auto-close).

中文说明

这个 PR 做了什么

本 PR 让 PR review workflow 对一类故障具备容忍能力:review job 异常死亡时,PR 不再落得"既没有 review、也没有任何解释"的状态。具体加了两种防御:一是在 job 启动时做前置健康探测,验证 runner 用户仍能在自己的目录中创建文件,发现不可写时先用 workflow 中既有的免密 sudo 模式做范围受限的单目录属主修复,修不好就带着清晰的诊断信息快速失败;二是新增一个跑在临时托管 runner 上的轻量依赖 job,只要 review job 失败就由它兜底发布常规的重试指引评论——PR 号从事件载荷推导(死掉的 job 的 outputs 无法幸存),并且当同一 run 的兜底评论已存在时跳过。为了让去重可靠,兜底评论现在带一个隐藏的 HTML 标记,沿用确认评论已在使用的惯例。

为什么需要

PR #8894 的 review 运行(job 95028756485)死于 self-hosted runner worker 在 job 收尾阶段的崩溃:在 runner 用户 home 目录下创建文件时权限被拒。由于崩溃发生在正常步骤流之外,后续步骤一个都没跑:兜底评论没发、worktree 清理没做,PR 悄无声息地搁置着,没有 review 也没有任何说明。共享 self-hosted 池上的已知诱因是先前以 root 运行的容器化 job 留下 root 属主或权限损坏的目录——workflow 对 workspace 已有文档和防御,但对 runner 自身目录没有。即便根因无法预防(比如运行中途发生的损坏),PR 也应该收到一条解释性评论;而落在已损坏 runner 上的 job 应该在几秒内带着清晰信息失败,而不是烧掉几小时的 review 预算后才死掉。

Reviewer 测试计划

如何验证

两段新增 shell 已从 workflow 中提取出来,在 macOS 上本地演练:健康探测对着一个只读目录运行——检测到不可写、尝试修复、修复不可用时以清晰报错非零退出。兜底 job 脚本用 mock gh 跑了四种评论场景:只带 run URL 但不带标记的确认评论(不得抑制发布)、带标记且同 run URL 的兜底评论(必须抑制)、带标记但 run URL 不同的兜底评论(不得抑制)、完全没有评论(必须发布),四种行为均符合设计。整个 workflow 通过与 CI 相同版本的 yamllint、actionlint 以及 YAML 解析检查。端到端地,合并后任何一次失败的 review 运行都会触发兜底 job;"标记 + run URL"去重保证普通失败(job 内兜底步骤已发过)和重跑保持安静。第一个自然的线上验证是下一次 review 失败:PR 应当恰好收到一条解释性评论。

证据(Before & After)

N/A(workflow 改动,无用户可见 UI)

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux ⚠️

环境(可选)

用 mock gh 对内嵌脚本做本地模拟;yamllint 1.35.1(与 CI 相同)、actionlint、PyYAML 解析。尚无真实端到端运行——真实路径在合并后的下一次 review 失败时激活。

风险与范围

  • 主要风险或权衡:前置探测给每个 review job 增加几秒;探测失败会让整个 job 提前失败(刻意为之——该 job 本来也不可能成功)。在"崩溃 → 重跑 → 正常失败"的序列中,PR 可能比现在多一条兜底类评论,与既有的重跑发布行为一致。
  • 未验证 / 超出范围:运行中途出现的权限损坏(只由兜底评论覆盖,不做修复);修复受影响 self-hosted runner 主机上已被污染的目录属于运维操作。同池上的其他 workflow 未改动。
  • 破坏性变更 / 迁移说明:无;标记是隐藏 HTML 注释,现有兜底消息除新增该标记外不变。

关联 Issue

事故引用:PR #8894 的 review 运行 31891414261(不自动关闭)。

A review job that dies abnormally never reaches its in-job fallback
comment step: the runner worker crash in FinalizeJob on the PR QwenLM#8894
run (EACCES creating under the runner home directory) left the PR with
no review and no explanation.

- Probe write access to $HOME, $RUNNER_TEMP and the runner root at job
  start, repair single-directory ownership with the existing sudo
  pattern, and fail fast with a clear message when repair is impossible
  instead of burning the review budget to die at finalize.
- Add a fallback-comment job on an ephemeral hosted runner that posts
  the retry guidance whenever review-pr fails. It derives the PR number
  from the event payload (dead job outputs do not survive a crash) and
  dedupes on a qwen-review-fallback comment marker plus this run's URL,
  so the in-job step, the ack comment, and re-runs never double-post.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Re-run of the gate at the round-3 head (3cad099). Nothing changed my read from the earlier pass:

Template looks good ✓

Problem: observed, not theoretical — the review run for PR #8894 (job 95028756485) died in the runner worker's finalization phase with permission-denied on the runner user's own home directory. Because the crash happened outside the normal step flow, no fallback comment was posted and no cleanup ran; the PR sat silently with no review and no explanation. The linked run/job is concrete evidence.

Direction: aligned — this hardens the repo's own review pipeline against a failure class it demonstrably has. Pure CI-infrastructure change, no product surface.

Size: not applicable — no core (packages/) paths touched. 203 lines in the review workflow plus 504 lines of tests.

Approach: scope feels right. Two defenses for two distinct failure modes — a preflight health probe for corruption already present at job start, and a dependent fallback job on an ephemeral hosted runner for mid-run deaths — plus marker/run-URL dedup reusing the ack-comment convention. The gate enumerates exactly the jobs whose failure leaves review-pr skipped; I re-verified that against the live job graph at this head (review-pr needs review-config/delay-automatic-review/authorize, authorize needs precheck-pr). The diff carries no unrelated changes.

Risk: no elevated risk signals — neither changed file matches the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

在第 3 轮 autofix 后的 head(3cad099)上重新跑 gate,结论与之前一致:

模板完整 ✓

问题:已观测到的事故,不是理论假设——PR #8894 的 review 运行(job 95028756485)死于 runner worker 收尾阶段:在 runner 用户自己的 home 目录下创建文件时权限被拒。崩溃发生在正常步骤流之外,兜底评论没发、清理没跑,PR 停留在无 review、无解释的状态。PR 描述附了具体 run/job 链接作为证据。

方向:对齐——加固的是仓库自己的 review 流水线,针对的是已被证实存在的故障类别。纯 CI 基础设施改动,不涉及产品面。

规模:不适用——未触及核心(packages/)路径。review workflow 203 行 + 测试 504 行。

方案:范围合理。两种防御对应两种不同的故障模式——前置健康探测处理 job 启动时已存在的损坏;跑在临时托管 runner 上的依赖 job 兜底运行中途的死亡——外加复用 ack 评论惯例的 marker/run-URL 去重。gate 恰好枚举了"失败会导致 review-pr 被跳过"的全部 job,我在当前 head 上对照实际 job 图重新核对过(review-pr 依赖 review-config/delay-automatic-review/authorizeauthorize 依赖 precheck-pr)。diff 中没有无关改动。

风险:无升级风险信号——两个改动文件均不匹配与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 3cad099256a97036ee3c894f3e09b1582f451dbc · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Code review

Fresh pass at the round-3 head. The earlier blocking findings from prior rounds stay resolved, and I found no new blockers — a few notes on what I checked and what remains as follow-ups:

  • The gate enumerates exactly the right jobs. I compared the fallback-comment gate against the live job graph at this head rather than taking the comment's word for it: review-pr needs review-config/delay-automatic-review/authorize, authorize needs precheck-pr — so the five enumerated jobs are precisely the failure closure that leaves a review skipped or dead. I also checked the safety precheck (qwen-pr-safety-precheck.yml): it never fails by design — it always exits 0 with an allow_triage/manual_required decision — so a manual_required hold or a permission skip produces a skipped chain, not a failed one, and the gate correctly stays shut there. No false-alarm fallbacks on designed rejections.
  • The secret-holding job never touches PR code. fallback-comment carries CI_BOT_PAT but has no checkout and no actions/* step that runs repo content — it's inline bash plus the official gh CLI on an ephemeral ubuntu-latest runner. Every payload-derived value (PR_NUMBER, run URL) is quoted; the dedup --jq filter interpolates only the bot's own login (resolved via gh api user) and the static marker, so a participant pasting the marker cannot capture the lookup. This is the right shape for a job that posts with a write token on pull_request_target events.
  • Fail-closed discipline holds end to end. A failed dedup lookup is never treated as an empty result: the in-job step defers to the fallback job (exit 0, job stays failed so the dependent job still runs), and the fallback job retries 3× then exits 1 without posting. PR-state lookup failure exits 1; closed/merged PRs skip quietly; on pull_request_target a head that moved mid-run skips quietly, and when the comparison is unavailable, posting wins over silence. Run-id anchoring (actions/runs/<id>), pinned to the markdown link's closing paren) keeps a later run's superstring id from suppressing this run's comment — and the test suite pins the RUN_URL shape that the anchor depends on.
  • The health probe interacts correctly with the rest of the job. It reuses the established passwordless-sudo pattern (same as Restore workspace ownership), repairs single directories only (never recursive), probes _diag only when it exists, and fail-fasts with a clear diagnostic when repair is impossible. When the probe fails, the context step never runs, so the in-job fallback step's own if skips it and the dependent job posts the generic body — one comment, right source.
  • Live signal at this head already: two runs of the review workflow evaluated the new gate — both fallback-comment checks landed skipped, including the review_requested sibling run where review-pr was skipped by design. The gate stayed silent on non-failures, which is the quiet-path behavior this PR must not break.
sequenceDiagram
    participant P1 as Upstream gate jobs
    participant P2 as review-pr on self-hosted
    participant P3 as fallback-comment on hosted
    participant P4 as PR comments
    P2->>P2: health probe, repair or fail fast
    P2-->>P3: failure or timeout-cancel opens gate
    P1-->>P3: any upstream failure opens gate too
    P3->>P4: list bot comments, marker-filtered
    alt same-run fallback exists
        P3->>P3: skip on marker plus run URL
    else none yet for this run
        P3->>P4: post marker-headed retry guidance
    end
Loading

Non-blocking follow-ups (none of these block this PR):

  1. The verify's F1 is still open on main: qwen-autofix.yml's actionable-feedback census (the BOT_COMMENT_FILTER alternation and the NEWEST watermark computation) does not exclude the new qwen-review-fallback marker — I grepped the current workflows and nothing outside this PR references it yet. Once this merges, a posted fallback comment would count as actionable feedback and could spin up an autofix round over a dead review. The sandboxed verify measured this and recommended a follow-up PR (different file); noting it here so it isn't dropped.
  2. R3-8 deferred: the PR's own stub gh pre-applies the dedup filter's semantics instead of executing the real --jq program — a test-fidelity gap, not a live bug (the verify's independent harness executes the real filter through all 16 scenarios green). The follow-up test shape is already agreed on the thread.
  3. F2 (one-time): during the roll-out window, a same-run comment posted by the pre-marker code is invisible to the new dedup and one duplicate can slip through. Inherent, documented, accepted.

Testing evidence (this PR's own CI, read via the API)

The Test (ubuntu-latest, Node 22.x) job is the load-bearing one here: it runs npm run test:ci, which ends in test:scriptsvitest run --config ./scripts/tests/vitest.config.ts, the exact suite this PR extends. It landed success on the reviewed head, so the new tests (the round-3 report counts 144, up from 135 — author's count; the job's success is the verified fact) ran green in CI. The macOS/Windows test legs and the CLI integration tests are skipped by the CI profile for a change with no package edits. The only in-flight check is the bot's own review-pr orchestration run (a pull_request_target run, not this PR's CI); both pull_request-event workflow runs at this head completed green, so nothing is pending.

Additionally, the sandboxed /verify run at the earlier head 95ada48 (round 2, before the round-3 commit) passed 236/236 scripted assertions with an executed A/B proving the fallback job load-bearing, an 8-mutant vacuity matrix, and the scenario harness above; it is where findings F1/F2 came from. It has not been re-run at the round-3 head.

Check Conclusion
Qwen Code CI · Test (ubuntu-latest, Node 22.x) ✅ success
Qwen Code CI · Test (macos-latest / windows-latest) ⏭️ skipped (CI profile, no package edits)
Qwen Code CI · Integration Tests (CLI, No Sandbox) ⏭️ skipped (CI profile)
Qwen Code CI · Classify PR ✅ success
Security Checks · Dependency CVE audit ✅ success
Security Checks · Secret scan (TruffleHog) ✅ success
Qwen Code CI · Desktop Shell (ubuntu-22.04) ✅ success
Qwen Code CI · Desktop Shell (windows-2022) ✅ success
Qwen Code CI · web-shell E2E Smoke (ubuntu-latest) ✅ success
Fork precheck · precheck-pr / precheck, authorize, delay-automatic-review ✅ success
Bot orchestration · review-pr (pull_request_target run) 🔄 in progress — not PR CI

Sandboxed verification would settle the one remaining gap: @qwen-code /verify — the executed A/B and scenario matrix above were run at the round-2 head 95ada48, and the round-3 delta (the in-job cross-dedup guard, the cancelled gate arm, the executed health-probe harness) is currently pinned only by the CI suite with its stub gh; a re-run at 3cad099 would re-prove the live behavior at exactly the commit under review.

中文说明

代码审查

在第 3 轮 head 上重新过了一遍。此前各轮的阻塞性发现保持已修复状态,本轮没有发现新的阻塞问题——以下是我核对过的点和遗留的跟进项:

  • gate 的 job 枚举是精确的。 我把 fallback-comment 的 gate 与当前 head 的实际 job 图做了对照,而不是只看注释怎么说:review-pr 依赖 review-config/delay-automatic-review/authorizeauthorize 依赖 precheck-pr——所以枚举的五个 job 恰好是"失败会导致 review 被跳过或死亡"的闭包。还核对了安全预检(qwen-pr-safety-precheck.yml):它从不以失败作为设计结果——总是以 0 退出并给出 allow_triage/manual_required 决定——所以 manual_required 拦截或权限跳过产生的是被跳过的链条而非失败的链条,gate 在这些设计内的拒绝上正确保持关闭。不会误发兜底评论。
  • 持密钥的 job 从不触碰 PR 代码。 fallback-comment 携带 CI_BOT_PAT,但没有 checkout、也没有任何执行仓库内容的步骤——只有内联 bash 加官方 gh CLI,跑在临时 ubuntu-latest runner 上。所有来自事件载荷的值(PR_NUMBER、run URL)都加了引号;去重的 --jq 过滤器只内插 bot 自己的登录名(经 gh api user 动态解析)和静态 marker,因此参与者在评论里粘贴 marker 无法劫持查找。对于在 pull_request_target 事件上以写权限 token 发评论的 job,这是正确的形态。
  • fail-closed 纪律贯穿到底。 去重查找失败永远不会被当作空结果:job 内步骤让位给兜底 job(exit 0,job 仍为失败态所以依赖 job 仍会运行),兜底 job 重试 3 次后 exit 1 且不发帖。PR 状态查找失败 exit 1;已关闭/已合并的 PR 安静跳过;pull_request_target 下运行中途 head 移动时安静跳过,比较不可用时则以发帖优先于沉默。run-id 锚定(actions/runs/<id>),锚在 markdown 链接的右括号上)防止更长的 run id 超串误抑——测试套件同时钉住了锚所依赖的 RUN_URL 形状。
  • 健康探测与 job 其余部分的交互正确。 复用既有的免密 sudo 模式(与 Restore workspace ownership 相同),只修复单个目录(绝不递归),_diag 仅在存在时探测,修复无望时带清晰诊断快速失败。探测失败时 context 步骤不会运行,job 内兜底步骤自身的 if 因此跳过它,由依赖 job 发通用文案——一条评论,来源正确。
  • 当前 head 上已有实时信号: review workflow 已有两次运行评估了新 gate——两次 fallback-comment 检查都是 skipped 落地,包括 review-pr 被设计性跳过的 review_requested 兄弟运行。gate 在非失败场景保持安静——这正是本 PR 不能破坏的安静路径。

非阻塞跟进项(均不阻塞本 PR):

  1. verify 的 F1 在 main 上仍未闭环: qwen-autofix.yml 的"可操作反馈"统计(BOT_COMMENT_FILTER 交替式与 NEWEST 水位线计算)没有排除新的 qwen-review-fallback marker——我 grep 了当前 workflows,除本 PR 外尚无任何引用。合并后,已发出的兜底评论会被计为可操作反馈,可能为死掉的 review 触发一整轮 autofix。沙箱 verify 实测过这一点并建议跟进 PR(不同文件);在此记录以免遗漏。
  2. R3-8 延期: PR 自己的 stub gh 预先套用了去重过滤器的语义,而不是执行真实的 --jq 程序——这是测试保真度缺口,不是线上 bug(verify 的独立 harness 已用真实过滤器跑通全部 16 个场景)。跟进的测试形态已在评审线程中达成一致。
  3. F2(一次性): 合并过渡窗口内,旧代码(无 marker)发出的同 run 评论对新去重不可见,可能漏过一条重复评论。属固有性质,已记录在案,可接受。

测试证据(通过 API 读取本 PR 自己的 CI)

Test (ubuntu-latest, Node 22.x) 是这里的关键 job:它运行 npm run test:ci,最后执行 test:scripts——即 vitest run --config ./scripts/tests/vitest.config.ts,正是本 PR 扩展的套件。该 job 在被审 head 上为 success,所以新测试(第 3 轮报告称 144 个,从 135 增加——这是作者方计数;job 成功才是已验证事实)在 CI 中跑绿。macOS/Windows 测试与 CLI 集成测试因 CI profile 对无包改动变更而跳过。唯一在跑的检查是 bot 自己的 review-pr 编排运行(pull_request_target 运行,不是本 PR 的 CI);该 head 上两个 pull_request 事件的 workflow 运行均已绿色完成,无待决项。

另外,针对较早 head 95ada48(第 2 轮,在第 3 轮提交之前)的沙箱 /verify 运行以 236/236 断言通过,执行级 A/B 证明了兜底 job 的承重性,外加 8 突变体空转矩阵与上述场景 harness;F1/F2 两个发现即出自该运行。尚未在第 3 轮 head 上重跑——如需在当前被审提交上重新执行级验证,可再触发 @qwen-code /verify

Qwen Code · qwen3.8-max

Reviewed at 3cad099256a97036ee3c894f3e09b1582f451dbc · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — an incident-driven fix that survived three review rounds, a sandboxed verification (236/236 assertions, executed A/B), and a fresh static pass at the round-3 head; what remains is follow-up work, not doubt about this diff.

Stepping back: this PR answers a real, evidenced failure — a review run that died in the runner's finalization phase and left a PR with no review and no explanation — and it answers it with the two defenses that failure actually calls for. A probe that catches an already-broken runner in seconds instead of burning hours of review budget, and a dependent job on an ephemeral runner that survives whatever killed the review job and posts the explanation itself. I tried to talk myself out of it and the objections didn't hold: the gate is scoped to real failures (designed skips and rejections stay quiet — verified against the job graph and the precheck's exit semantics, and already observed live at this head), the token-carrying job executes no PR code, and every lookup failure fails toward silence-or-retry rather than toward a duplicate. Six months from now this reads as careful ops hygiene, not cleverness.

The 4 rather than 5 is honest bookkeeping, not hesitation: the qwen-autofix.yml census gap (verify's F1) lands as a real — if rare and self-contained — consequence of this merge and should get its follow-up PR promptly, and the executed end-to-end verification exists one commit behind the head under review (the round-3 delta is pinned by the CI suite, which is green). Neither belongs in this diff.

Approving, pinned to the reviewed commit. @wenshao the F1 follow-up (excluding the qwen-review-fallback marker from the autofix census) is the one worth filing before the first dead review lands.

中文说明

置信度:4/5 —— 一个由真实事故驱动的修复,经历了三轮评审、一次沙箱验证(236/236 断言、执行级 A/B),以及在第 3 轮 head 上的全新静态审查;剩下的是跟进工作,而不是对这个 diff 的疑虑。

退一步看:这个 PR 回应的是一个有证据的真实故障——review 运行死于 runner 收尾阶段,PR 落得既无 review 也无解释——而它给出的正是该故障真正需要的两种防御:一个探测,在几秒钟内发现已经损坏的 runner,而不是烧掉数小时的 review 预算;一个跑在临时 runner 上的依赖 job,能在杀死 review job 的任何故障中幸存,并自己发出解释。我试着说服自己否决它,但反对意见都站不住:gate 只针对真实失败打开(设计内的跳过与拒绝保持安静——已对照 job 图和预检的退出语义核实,并且在当前 head 上已有实时观察),携带 token 的 job 不执行任何 PR 代码,每次查找失败都倒向沉默或重试而非重复评论。六个月后再看,这读起来是审慎的运维卫生,而不是炫技。

给 4 而不是 5 是如实记账,不是犹豫:qwen-autofix.yml 的统计缺口(verify 的 F1)会作为本次合并的一个真实后果落地——虽然罕见且自限——应尽快补上跟进 PR;执行级端到端验证停留在被审 head 的前一个提交上(第 3 轮的增量由 CI 套件钉住,且为绿色)。这两项都不属于这个 diff。

批准,钉在被审提交上。@wenshao F1 的跟进(把 qwen-review-fallback marker 从 autofix 统计中排除)值得在第一次死掉的 review 出现之前建好。

Qwen Code · qwen3.8-max

Reviewed at 3cad099256a97036ee3c894f3e09b1582f451dbc · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot 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.

Reviewed — no blockers. Suggestions are inline.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查——无阻断问题。 建议见行内评论。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +431 to +433
# Two levels above the workspace (_work/owner/repo) is the runner
# root, whose _diag/pages dir is what FinalizeJob creates in.
RUNNER_ROOT="$(cd "$GITHUB_WORKSPACE/../.." && pwd)"

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.

[Suggestion] The RUNNER_ROOT derivation is off by one level on the self-hosted pool: $GITHUB_WORKSPACE/../.. resolves to the runner's _work directory, not the runner root — the workspace sits three levels below it (<runner-root>/_work/<owner>/<repo>). The probe therefore never checks the directory this comment names ("the runner root, whose _diag/pages dir is what FinalizeJob creates in"), and the repair arm chowns _work instead of the runner root.

Failure scenario: a prior containerised job running as root leaves the runner root or _diag root-owned/mode-broken while _work stays writable (the same corruption family the comment cites) → all probes pass green → the job burns its full review budget and the worker still dies in FinalizeJob with EACCES — the exact mode this step was added to fail-fast on. Witness (probed on this pool): workspace/../.._work (lists _actions _PipelineMapping qwen-code _temp _tool); one level further up → the runner root (lists _diag bin config.sh externals _work).

The observed incident class stays covered ($HOME and $RUNNER_TEMP are probed), so this is a coverage gap in the defense, not the PR's core promise — but the fix is one line.

Suggested change
# Two levels above the workspace (_work/owner/repo) is the runner
# root, whose _diag/pages dir is what FinalizeJob creates in.
RUNNER_ROOT="$(cd "$GITHUB_WORKSPACE/../.." && pwd)"
# Three levels above the workspace (_work/owner/repo) is the runner
# root, whose _diag/pages dir is what FinalizeJob creates in.
RUNNER_ROOT="$(cd "$GITHUB_WORKSPACE/../../.." && pwd)"
中文说明

RUNNER_ROOT 的推导在 self-hosted 池上差了一层:$GITHUB_WORKSPACE/../.. 解析到的是 runner 的 _work 目录,而不是 runner 根目录——workspace 位于根目录下方三层(<runner-root>/_work/<owner>/<repo>)。因此探测从未真正检查注释声称的目录("runner 根目录,其 _diag/pages 目录是 FinalizeJob 写入的位置"),修复分支 chown 的也是 _work 而非 runner 根目录。

失败场景:先前以 root 运行的容器化 job 把 runner 根目录或 _diag 留成 root 属主/权限损坏,而 _work 仍可写(正是注释引用的损坏家族)→ 所有探测通过 → job 烧完整个 review 预算,worker 仍在 FinalizeJob 阶段以 EACCES 死亡——正是本步骤要快速失败的故障模式。实测(就在本池):workspace/../.. 列出 _actions _PipelineMapping qwen-code _temp _tool(即 _work 目录);再上一层才列出 _diag bin config.sh externals _work

已观测到的事故类别仍有覆盖($HOME$RUNNER_TEMP 已被探测),所以这是防御上的覆盖缺口,而非本 PR 的核心承诺——但修复只需一行。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# root, whose _diag/pages dir is what FinalizeJob creates in.
RUNNER_ROOT="$(cd "$GITHUB_WORKSPACE/../.." && pwd)"
status=0
for dir in "$HOME" "${RUNNER_TEMP:?}" "$RUNNER_ROOT"; do

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.

[Suggestion] The probe list covers $RUNNER_ROOT but not the _diag subdirectory this step's own comment names as the FinalizeJob write site — and a writable parent does not prove an existing subdirectory writable (ownership is per-directory). Demonstrated on this pool: with _diag mode-broken under a writable parent, the parent probe reported green while a FinalizeJob-shaped write inside _diag failed with EACCES. Combined with the off-by-one derivation above, _diag is doubly unreached; and the repair is single-directory (no recursion), so a correlated corruption event gets the probed dirs repaired while a still-broken _diag keeps the job dying.

Probe "$RUNNER_ROOT/_diag" as well when it exists (if it doesn't, FinalizeJob creates it, which only needs the already-probed runner root).

中文说明

探测列表覆盖了 $RUNNER_ROOT,但没有覆盖本步骤注释明确指出的 FinalizeJob 写入位置 _diag 子目录——且父目录可写并不能证明已存在的子目录可写(属主是逐目录的)。已在本池演示:父目录可写而 _diag 权限损坏时,父目录探测为绿色,而模拟 FinalizeJob 的 _diag 内写入以 EACCES 失败。叠加上面的层级推导偏差,_diag 双重不可达;且修复是单目录的(不递归),关联性损坏事件下被探测的目录修好了,仍然损坏的 _diag 却让 job 继续死亡。

建议在 _diag 存在时一并探测 "$RUNNER_ROOT/_diag"(若不存在,FinalizeJob 会创建它,只需要已被探测的 runner 根目录可写)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

exit 0
fi
fallback_bodies="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json comments \
--jq '.comments[] | select(.body | contains("<!-- qwen-review-fallback -->")) | .body' 2>/dev/null)" || fallback_bodies=""

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.

[Suggestion] The dedup matches the marker + run URL in comments by ANY author. The sibling ack dedup in this same file filters on the posting account as well, and upsert-bot-comment.sh documents author-scoping as the property that keeps "a participant posting the marker" from capturing the lookup.

Failure scenario: anyone who can comment on this public PR posts a comment containing <!-- qwen-review-fallback --> plus actions/runs/<run_id> (the run id is published by the ack comment at run start and is sequential). When that run's review job then dies abnormally — the exact case this job exists for — the case match hits the planted body and the job exits 0 without posting: the retry guidance is silently suppressed, and the HTML-comment marker renders invisibly, so maintainers won't see why.

Verified live: gh pr view --json comments is GraphQL (comment keys: author, authorAssociation, body, … — no .user field, so the fix uses .author.login), and the in-job fallback comment is posted as qwen-code-ci-bot via CI_BOT_PAT, so an author filter still matches it. The bot login is the bot_login constant review-config already exports — keep them in sync rather than hardcoding a second copy.

Suggested change
--jq '.comments[] | select(.body | contains("<!-- qwen-review-fallback -->")) | .body' 2>/dev/null)" || fallback_bodies=""
--jq '.comments[] | select(.body | contains("<!-- qwen-review-fallback -->")) | select(.author.login == "qwen-code-ci-bot") | .body' 2>/dev/null)" || fallback_bodies=""
中文说明

去重匹配的是任意作者评论中的标记 + run URL。同文件中姊妹的 ack 去重还额外过滤了发布账号;upsert-bot-comment.sh 也明确记载作者限定是保证"参与者发标记无法劫持查找"的关键属性。

失败场景:任何能在这个公开 PR 上评论的人,都可以发一条包含 <!-- qwen-review-fallback -->actions/runs/<run_id> 的评论(run id 在运行开始时由 ack 评论公布,且顺序递增)。当该 run 的 review job 随后异常死亡——正是本 job 存在的场景——case 匹配命中伪造内容,job 直接 exit 0 不再发布:重试指引被静默抑制,而 HTML 注释标记渲染不可见,维护者看不出原因。

已实测验证:gh pr view --json comments 是 GraphQL(评论字段为 authorauthorAssociationbody 等——没有 .user 字段,所以修复用 .author.login);job 内兜底评论通过 CI_BOT_PATqwen-code-ci-bot 身份发布,因此作者过滤仍然能匹配到它。bot 登录名就是 review-config 已导出的 bot_login 常量——保持同步,不要再硬编码第二份。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1776 to +1777
fallback_bodies="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json comments \
--jq '.comments[] | select(.body | contains("<!-- qwen-review-fallback -->")) | .body' 2>/dev/null)" || fallback_bodies=""

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.

[Suggestion] A dedup-lookup failure is swallowed into an empty result and the step proceeds to post — fail-open toward duplicates. A transient GitHub API 5xx or a secondary rate limit on the shared CI_BOT_PAT makes fallback_bodies="", the case misses the comment the in-job step already posted for this run, and the PR gets a second identical fallback comment — the exact duplicate this marker dedup was added to prevent. The house norm says the opposite: upsert-bot-comment.sh's header — "A FAILED lookup is never treated as an EMPTY result: posting on a failed listing is how a transient 5xx mints a permanent duplicate" — with bounded retry as the established remedy.

Fail closed on a persistent lookup error (bounded retry, then skip with a summary line): fail-open buys nothing here, because when no comment exists both choices post — the only branch where they differ is the one that mints a duplicate.

中文说明

去重查找失败被吞成空结果,步骤继续发布——失败时偏向重复。共享 CI_BOT_PAT 上一次瞬时的 GitHub API 5xx 或次级限流就会让 fallback_bodies="",case 错过 job 内步骤为本 run 已发的评论,PR 上出现第二条一模一样的兜底评论——正是这个标记去重要防止的重复。仓库既有规范恰好相反:upsert-bot-comment.sh 头部写明"失败的查找绝不能当作空结果:在失败的列表上发布,就是瞬时 5xx 铸成永久重复的方式",并以有限重试作为既定补救。

建议在查找持续失败时失败关闭(有限重试,然后写一行 summary 跳过):失败开放在此没有任何收益——没有评论时两种选择都会发布,唯一有分歧的分支恰好是铸成重复的那个。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1784 to +1787
pr_state="$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state --jq '.state')" || {
echo "Could not verify PR #${PR_NUMBER}; skipping fallback comment." >> "$GITHUB_STEP_SUMMARY"
exit 0
}

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.

[Suggestion] The twin failure mode of the lookup above: if this gh pr view call fails (expired/revoked CI_BOT_PAT, API outage), the job exits 0 GREEN with no comment posted and one ambiguous step-summary line — "Could not verify PR #N" reads identically for a deleted PR and for dead credentials. The run page shows fallback-comment ✓ for the one job whose sole purpose went unachieved, and the repo's flaky-rerun automation keys on failure, so green hides from it too: the failure-notifier fails silently.

Red-on-own-failure is safe here: the gate already requires review-pr.result == 'failure', so the run is red anyway. Emit a ::error:: annotation (annotations surface at run level; step summaries don't) and exit non-zero when the state check itself cannot complete; reserve exit 0 for a positively-determined non-OPEN PR.

中文说明

上面查找的对称失效模式:如果这个 gh pr view 调用失败(CI_BOT_PAT 过期/被吊销、API 故障),job 以 0 绿色退出,评论没发,只在 step summary 留下一句含糊的话——"Could not verify PR #N" 对"PR 已删除"和"凭证已失效"读起来一模一样。运行页面上,唯一职责没达成的 job 却显示 fallback-comment ✓;仓库的 flaky 重跑自动化以 failure 为键,绿色也同样躲过它:失败通知者自己静默失败了。

自身失败时变红在这里是安全的:门控已要求 review-pr.result == 'failure',整个 run 本来就是红的。状态检查本身无法完成时,发一条 ::error:: 注解(注解在 run 级别可见,step summary 不可见)并以非零退出;exit 0 保留给确认为非 OPEN 的 PR。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1754 to +1756
if: |-
always() &&
needs.review-pr.result == 'failure' &&

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.

[Suggestion] The gate is keyed only to review-pr's result, but the incident's own trigger can kill the chain's EARLIER self-hosted jobs first: authorize runs on self-hosted ecs-qwen for same-repo PRs (verified live: run 31899604037 → ecs-qwen-runner-64c-7), and review-config likewise for bot review_requested. A dependent of a failed job is marked skipped, so needs.review-pr.result is 'skipped', this gate never opens, and the PR is left with no review and no explanation — reached through the PR #8894 trigger itself. (delay-automatic-review is hosted, so authorize/review-config complete the exposure list.)

Widen the gate by keying on the upstream jobs DIRECTLY:

needs: ['review-config', 'authorize', 'review-pr']
if: |-
  always() &&
  (needs.review-pr.result == 'failure' ||
   needs.authorize.result == 'failure' ||
   needs.review-config.result == 'failure') &&
  …keep the existing repository / review_mode gates…

Do not key on review-pr.result != 'success' instead — 4 failed workflow_dispatch resolve runs in recent history (e.g. 31915250131) skip review-pr BY DESIGN and would have received false-alarm fallback comments.

中文说明

门控只以 review-pr 的结果为键,但事故自身的诱因可以先杀死链上更靠前的 self-hosted job:同仓库 PR 的 authorize 跑在 self-hosted ecs-qwen 上(实测:run 31899604037 → ecs-qwen-runner-64c-7),bot review_requestedreview-config 同理。依赖 job 失败时,下游被标记为 skipped,于是 needs.review-pr.result'skipped',这个门控永远不会打开,PR 再次落入"没有 review 也没有解释"——正是从 PR #8894 的诱因走进来的。(delay-automatic-review 是托管 runner,所以 authorize/review-config 就是暴露面的全部。)

建议直接以上游 job 为键放宽门控(见上方代码)。不要改用 review-pr.result != 'success'——近期历史里有 4 个失败的 workflow_dispatch resolve 运行(如 31915250131)按设计跳过 review-pr,那样会收到误报的兜底评论。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1788 to +1791
if [ "$pr_state" != "OPEN" ]; then
echo "Skipping fallback comment: PR #${PR_NUMBER} is ${pr_state}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

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.

[Suggestion] This job drops the in-job step's moved-head guard (~line 1665 compares EXPECTED_HEAD_SHA with the current head): comment/review-event runs use per-run concurrency groups that a new push does NOT cancel (cancel-in-progress is only set for pull_request_target synchronize/closed). So a commit landing mid-review, followed by an abnormal review death, makes this job post stale "retry with @qwen-code /review" pointing at a dead run while a fresh lifecycle review of the new head is already queued — noise, and potentially provoking a redundant manual /review.

⚠️ Fix carefully: for issue_comment runs, gh run view --json headSha returns the tip of main, not the PR head (verified: 8 recent issue_comment runs all show head_branch=main) — a naive headSha vs headRefOid comparison would mismatch on essentially every comment-triggered run and silently suppress the fallback on the very path this job exists for. Apply the guard only where a comparable head exists (pull_request_target events), and degrade to POSTING, never suppressing, when the comparison cannot be made.

中文说明

本 job 丢掉了 job 内步骤的"head 已移动"防护(约 1665 行会比较 EXPECTED_HEAD_SHA 与当前 head):评论/review 事件的运行使用按 run 分组的并发组,新 push 不会取消它们(cancel-in-progress 只对 pull_request_target 的 synchronize/closed 生效)。因此 review 进行中有新提交落入、随后 review 异常死亡时,本 job 会发出一条指向已死 run 的过期"重试 @qwen-code /review",而新 head 的自动 review 已经排队——噪音,还可能诱发多余的手动 /review。

⚠️ 修复需谨慎:对 issue_comment 运行,gh run view --json headSha 返回的是 main 的尖端而不是 PR 的 head(已验证:最近 8 个 issue_comment 运行全部 head_branch=main)——朴素的 headShaheadRefOid 比较几乎在每个评论触发的 run 上都会失配,从而在本 job 存在的核心路径上静默抑制兜底评论。只在 head 可比较的事件(pull_request_target)上加该防护;无法比较时,退化为发布,绝不抑制。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1685 to +1686
# dedupes on this marker plus this run's URL.
body="$(printf '<!-- qwen-review-fallback -->\n\n%s' "$body")"

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.

[Suggestion] The marker literal is now hardcoded at three byte-identity-load-bearing sites across two jobs (this printf, the dedup filter at ~1777, and the fallback job's printf at ~1793). This same file states the norm for marker contracts (~line 1500): "The marker is defined ONCE and used for both the body and the upsert lookup — the two must be byte-identical or the upsert posts duplicates." A future edit renaming the marker in one or two of the three sites silently breaks cross-job dedup — every failed run that reached its in-job step then gets two fallback comments, and no test would fail (nothing outside this workflow references the marker).

Define it once — e.g. a workflow-level env: block (this file currently has none) — and reference $FALLBACK_MARKER in both printfs and via --arg marker "$FALLBACK_MARKER" / contains($marker) in the jq filter.

中文说明

标记字面量现在硬编码在两个 job 的三处"字节必须一致"的位置(这个 printf、约 1777 行的去重过滤器、约 1793 行兜底 job 的 printf)。同文件对标记契约已有规范(约 1500 行):"标记只定义一次,正文和 upsert 查找都用它——两者必须字节一致,否则 upsert 会发重复评论"。未来若只改三处中的一两处,跨 job 去重会静默失效——每个跑过 job 内步骤的失败 run 都会收到两条兜底评论,而且不会有任何测试失败(workflow 之外没有任何地方引用这个标记)。

建议只定义一次——例如 workflow 级 env: 块(本文件目前还没有)——两个 printf 都引用 $FALLBACK_MARKER,jq 过滤器用 --arg marker "$FALLBACK_MARKER" / contains($marker)

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1752 to +1753
fallback-comment:
needs: ['review-pr']

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.

[Suggestion] The behavior this PR adds — the health-probe fail-fast, this job, and the cross-job marker + run-URL dedup contract — is pinned by zero tests; reverting any hunk leaves every suite green (verified: grep finds no test reference to qwen-review-fallback / fallback-comment / Verify runner directory health; the efficacy probe had nothing it could probe). The repo already pins this exact workflow's invariants elsewhere: scripts/tests/qwen-pr-review-workflow.test.js and .github/scripts/qwen-triage-workflow.test.mjs parse this file and assert step invariants (assertUnconditional over the adjacent "Restore workspace ownership" step), and deadline.test.ts pins this workflow's env exports for the same drift reason.

Failure scenario: a future edit renames the marker on one side only, drops the See [workflow logs](…) link from a body variant, inverts the if: gate, or softens the probe to warn-only (drops status=1 / exit "$status") — each silently breaks dedup (duplicate comments) or restores the exact incident class (hours burned to die at finalize), and nothing fails.

Extend one of those suites: assert the in-job step prepends the marker and every FAILURE_KIND body includes ${RUN_URL}; this job selects the same literal marker, dedupes on actions/runs/${GITHUB_RUN_ID}, keeps needs.review-pr.result == 'failure' plus the repository gate, and exits 0 on each skip path; and the health step probes $HOME/$RUNNER_TEMP/the runner root, sets status=1 on unrepaired dirs, and ends with exit "$status".

中文说明

本 PR 新增的行为——健康探测快速失败、这个 job、跨 job 的"标记 + run URL"去重契约——没有任何测试固定;回退任何一个 hunk,所有测试套件仍然是绿的(已验证:全树 grep 没有任何测试引用 qwen-review-fallback / fallback-comment / Verify runner directory health;有效性探针也无可探)。仓库已在别处固定了同一 workflow 的不变量:scripts/tests/qwen-pr-review-workflow.test.js.github/scripts/qwen-triage-workflow.test.mjs 解析本文件并断言步骤不变量(对相邻的 "Restore workspace ownership" 步骤有 assertUnconditional),deadline.test.ts 也出于同样的漂移原因固定了本 workflow 的 env 导出。

失败场景:未来某次编辑只改一侧的标记名、从某个 body 分支丢掉 See [workflow logs](…) 链接、反转 if: 门控、或把探测软化成只告警(去掉 status=1 / exit "$status")——每一种都会静默破坏去重(重复评论)或复活事故类别(烧几小时然后在 finalize 死亡),而不会有任何测试失败。

建议在上面任一测试套件中补充断言:job 内步骤前置标记且每个 FAILURE_KIND 正文都含 ${RUN_URL};本 job 选择同一标记字面量、按 actions/runs/${GITHUB_RUN_ID} 去重、保留 needs.review-pr.result == 'failure' 与仓库门控、每条跳过路径都 exit 0;健康步骤探测 $HOME/$RUNNER_TEMP/runner 根目录,修复失败置 status=1,并以 exit "$status" 结束。

— qwen3.8-max via Qwen Code /review (v0.21.12)

@wenshao

wenshao commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 16, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 3 finishedview run. See this round's report below.

中文说明

AutoFix 第 3 轮已完成 —— 查看运行。本轮报告见下方。

Review-round fixes for the fallback-comment defenses:

- Probe the actual runner root (three levels above the workspace, not
  two) and the _diag subdirectory FinalizeJob writes in; a writable
  parent does not prove an existing subdirectory writable.
- Open the fallback gate on authorize/review-config failures too — the
  incident's trigger can kill those earlier self-hosted jobs first, and
  a failed dependency marks review-pr 'skipped', which the old gate
  never matched. Guarded against resolve dispatch runs, which skip
  review-pr by design.
- Author-scope the dedup lookup (resolved dynamically like
  upsert-bot-comment.sh) so a planted marker cannot suppress the
  fallback, and fail closed with bounded retry when the lookup or the
  state check fails instead of fail-open toward duplicates or a green
  job that never posted.
- Skip the stale fallback when the PR head moved, but only on
  pull_request_target events where the run head is comparable —
  comment/review runs report main's tip, and posting wins over silence
  when the comparison is unavailable.
- Define the marker once in a workflow-level env and pin all of the
  above in the workflow test suite, executing the fallback step's real
  bash against a stubbed gh.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review feedback addressed — PR #9255 (round 1)

All 9 Suggestion findings from the automated reviewer were verified against the
code and resolved in one commit (fix(ci): harden the PR review fallback comment (#9255)).
No conflicts (--conflict false); no base merge performed.

Findings and dispositions

  • R1-1 — RUNNER_ROOT derivation off by one levelResolved. Reproduced on this pool: $GITHUB_WORKSPACE/../.. resolves to _work (lists _temp qwen-code …), one more level up is the runner root (lists _work …). Changed the derivation to $GITHUB_WORKSPACE/../../.. and corrected the comment. Pinned by a new test that also rejects the old two-level shape.
  • R1-2 — _diag not probedResolved. The probe list now also covers $RUNNER_ROOT/_diag when it exists (same single-directory probe+repair). When absent, FinalizeJob creates it, which only needs the already-probed runner root — matching the finding's suggestion.
  • R1-3 — dedup matches comments by ANY authorResolved. The dedup lookup now filters on the authenticated bot login, resolved dynamically with gh api user --jq '.login' — the exact protocol of upsert-bot-comment.sh (which already runs under CI_BOT_PAT in this workflow via the docs-only supersede step), so the filter can never drift from the account the comment is posted as, and no second hardcoded login copy exists. Verified the expanded jq filter with real jq against a fixture: a bot-authored marker comment is selected; a planted marker comment by another user and a deleted-account (author: null) comment are both rejected.
  • R1-4 — dedup-lookup failure swallowed (fail-open)Resolved. The lookup runs in a bounded retry loop (3 attempts, 10s apart — same shape as upsert-bot-comment.sh), re-resolving both prerequisites per attempt. A persistent failure now emits ::error::, writes a step-summary line, and exits 1 without posting. One deliberate deviation from the suggested wording: the failure exits 1, not 0 — the same red-on-own-failure rationale R1-5 states (the run is red anyway, annotations surface at run level, and the repo's flaky-rerun automation keys on failure; a green skip would hide the very outage this job exists for).
  • R1-5 — state-check failure exits 0 greenResolved. A failed gh pr view --json state now emits ::error:: and exits 1; exit 0 is reserved for a positively-determined non-OPEN PR, as suggested.
  • R1-6 — gate keyed only to review-pr.resultResolved. The job now needs ['review-config', 'authorize', 'review-pr'] and opens on any of the three result == 'failure'. Verified the exposure claim: authorize runs on the self-hosted ecs-qwen pool for same-repo PR events and review-config for bot review_requested (both runs-on expressions checked); precheck-pr is a hosted reusable workflow, so the list is complete. One addition beyond the suggested snippet: a github.event.inputs.command != 'resolve' guard — on dispatch runs authorize only executes for command == 'resolve' (where review-pr is skipped BY DESIGN), so without the guard the widened OR would mint false-alarm fallbacks on exactly the runs the finding warned about. The finding's warning against != 'success' keying is also pinned by a test.
  • R1-7 — moved-head guard droppedResolved, per the ⚠️ caveat. The guard compares the run's headSha with the live headRefOid only when GITHUB_EVENT_NAME == pull_request_target (the one trigger where the run head is the PR head), short-circuits before the dedup lookup, and degrades to POSTING whenever either lookup fails or the event is a comment/review trigger (whose headSha is main's tip). All three directions are execution-tested.
  • R1-8 — marker hardcoded at three byte-identity sitesResolved. The marker is now defined once in a workflow-level env: block (FALLBACK_MARKER) referenced by both printfs and interpolated into the dedup jq filter. One deviation from the suggested mechanism: gh has no --arg passthrough for --jq (verified on gh 2.23, and no gh release adds one — go-jq receives only the expression), so the filter interpolates the shell variables; the values are workflow constants, not attacker-controlled input. A test asserts no hardcoded copy of the literal survives in any run block.
  • R1-9 — behavior pinned by zero testsResolved. Added a 13-test suite block to scripts/tests/qwen-pr-review-workflow.test.js (the suite the finding named): text pins for the marker contract, the author-scoped dynamic-login filter, the widened failure-keyed gate (including the resolve guard and the != 'success' prohibition), and the health step (three-level derivation, _diag, status=1, exit "$status"); plus executed tests that run the fallback step's real bash with a stubbed gh and cover: post, dedup skip, fail-closed lookup (3 retries, ::error::, exit 1), fail-closed state check, non-OPEN skip, moved-head skip on pull_request_target (before the dedup lookup), posting on issue_comment despite head drift, degrading to POSTING when head lookups fail, and the marker+blank-line+run-URL body shape. Falsifiability check: against the pre-round workflow 9 of the 13 tests fail (the other 4 pin deliberately preserved behavior).

Changed files

  • .github/workflows/qwen-code-pr-review.yml — workflow-level marker env; health-step runner-root/_diag fix; in-job marker printf; fallback-comment gate, moved-head guard, author-scoped fail-closed dedup, red-on-own-failure.
  • scripts/tests/qwen-pr-review-workflow.test.js — new fallback comment resilience (PR #8894 incident class) suite block (13 tests).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed (re-run after a Prettier reformat of the test file)
  • npx prettier --check scripts/tests/qwen-pr-review-workflow.test.js — passed (after --write)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js — 131 passed (118 existing + 13 new)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js scripts/tests/review-worktree-cleanup-workflow.test.js scripts/tests/qwen-resolve-workflow.test.js — 207 passed (other suites that parse this workflow)
  • cd packages/cli && npx vitest run src/commands/review/lib/deadline.test.ts — 49 passed (pins this workflow's env exports)
  • yamllint 1.35.1 (the exact version CI's scripts/lint.js --yamllint pins) on the modified workflow — 0 findings
  • bash -n on all three modified run blocks extracted from the parsed YAML — passed
  • Expanded dedup jq filter executed with real jq against a GraphQL-shaped fixture — selects only the bot-authored marker comment
  • Fallback step bash executed against a stubbed gh — 10 scenarios passed (see R1-9 above)
  • Pre-round falsifiability check — 9/13 new tests fail against the pre-round workflow, then restored
  • Integration tests — not applicable (touched behavior is CI workflow YAML and scripts/tests, not the bundled CLI); npm run generate:settings-schema — not applicable (no settings source changed)
中文说明

已处理的评审反馈 — PR #9255(第 1 轮)

自动评审者的 9 条 Suggestion 发现全部经过代码核实,并在一个提交中解决(fix(ci): harden the PR review fallback comment (#9255))。无冲突(--conflict false),未做 base 合并。

发现与处置

  • R1-1 — RUNNER_ROOT 推导差一层已解决。 在本池实测复现:$GITHUB_WORKSPACE/../.. 解析到 _work(列出 _temp qwen-code …),再上一层才是 runner 根目录(列出 _work …)。推导改为 $GITHUB_WORKSPACE/../../..,注释同步更正。新增测试固定该形状并显式拒绝旧的两层写法。
  • R1-2 — 未探测 _diag已解决。 探测列表在 $RUNNER_ROOT/_diag 存在时一并覆盖(同样的单目录探测+修复)。不存在时由 FinalizeJob 创建,只需要已被探测的 runner 根目录——与发现建议一致。
  • R1-3 — 去重匹配任意作者的评论已解决。 去重查找现在按已认证的 bot 登录名过滤,登录名通过 gh api user --jq '.login' 动态解析——与 upsert-bot-comment.sh 的协议完全一致(该脚本在本 workflow 的 docs-only 徽章步骤中已经在 CI_BOT_PAT 下运行),因此过滤器永远不会偏离评论实际发布的账号,也不存在第二份硬编码登录名。用真实 jq 对 GraphQL 形状夹具验证了展开后的过滤器:bot 发布的标记评论被选中;他人伪造的标记评论与已注销账号(author: null)的评论均被拒绝。
  • R1-4 — 去重查找失败被吞掉(失败开放)已解决。 查找现在运行在有限重试循环中(3 次尝试、间隔 10 秒——与 upsert-bot-comment.sh 同形),每次尝试都重新解析两个前置条件。持续失败时发 ::error::、写一行 step summary,并以 exit 1 退出且不发布。相对建议措辞有一处刻意偏离:失败时以 1 退出而非 0——理由与 R1-5 陈述的"自身失败时变红"一致(run 本来就是红的、注解在 run 级别可见、仓库的 flaky 重跑自动化以 failure 为键;绿色跳过会把本 job 要覆盖的故障藏起来)。
  • R1-5 — 状态检查失败以 0 绿色退出已解决。 gh pr view --json state 失败时现在发 ::error:: 并以 exit 1 退出;exit 0 仅保留给确认为非 OPEN 的 PR,与建议一致。
  • R1-6 — 门控只以 review-pr.result 为键已解决。 该 job 现在 needs ['review-config', 'authorize', 'review-pr'],三者任一 result == 'failure' 即开门。暴露面声明已核实:authorize 在同仓库 PR 事件上跑在 self-hosted ecs-qwen 池、review-config 在 bot review_requested 时同理(两处 runs-on 表达式均已核对);precheck-pr 是托管的可复用 workflow,因此暴露列表完整。在建议片段之外增加了一处:github.event.inputs.command != 'resolve' 守卫——dispatch 运行中 authorize 只在 command == 'resolve' 时执行(此时 review-pr 按设计被跳过),没有该守卫,放宽后的 OR 会在恰恰是发现警告的那类运行上铸成误报兜底。发现对 != 'success' 键控的警告也由测试固定。
  • R1-7 — 丢失"head 已移动"防护已解决,遵循 ⚠️ 告诫。 防护仅在 GITHUB_EVENT_NAME == pull_request_target(run head 即 PR head 的唯一触发类别)时比较 run 的 headSha 与实时的 headRefOid,并在去重查找之前短路;任一查找失败或事件为评论/review 触发(其 headSha 是 main 的尖端)时,退化为发布。三个方向均有执行级测试。
  • R1-8 — 标记硬编码在三处字节一致的位置已解决。 标记现在只定义在 workflow 级 env: 块(FALLBACK_MARKER)中,两处 printf 引用它,去重 jq 过滤器通过插值使用它。相对建议机制有一处偏离:gh--jq 没有 --arg 透传(在 gh 2.23 上核实,且 gh 各版本均未提供——go-jq 只接收表达式本身),因此过滤器用 shell 变量插值;这些值是 workflow 常量,不是攻击者可控输入。测试断言任何 run 块中不再存留硬编码副本。
  • R1-9 — 行为没有任何测试固定已解决。scripts/tests/qwen-pr-review-workflow.test.js(发现点名的套件)新增 13 个测试:标记契约、作者限定的动态登录名过滤器、放宽后以 failure 为键的门控(含 resolve 守卫与对 != 'success' 的禁止)、健康步骤(三层推导、_diagstatus=1exit "$status")的文本固定;以及用打桩的 gh 执行兜底步骤真实 bash 的执行级测试,覆盖:发布、去重跳过、查找失败关闭(3 次重试、::error::、exit 1)、状态检查失败关闭、非 OPEN 跳过、pull_request_target 上 head 移动跳过(发生在去重查找之前)、issue_comment 上尽管 head 漂移仍发布、head 查找失败时退化为发布、以及"标记+空行+run URL"的正文形状。可证伪性检查:对第 1 轮前的 workflow,13 个测试中 9 个失败(其余 4 个固定的是刻意保留的行为)。

变更文件

  • .github/workflows/qwen-code-pr-review.yml — workflow 级标记 env;健康步骤的 runner 根目录/_diag 修复;job 内标记 printf;兜底 job 的门控、head 移动防护、作者限定的失败关闭去重、自身失败时变红。
  • scripts/tests/qwen-pr-review-workflow.test.js — 新增 fallback comment resilience (PR #8894 incident class) 套件块(13 个测试)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过(测试文件 Prettier 重排后重跑)
  • npx prettier --check scripts/tests/qwen-pr-review-workflow.test.js — 通过(--write 之后)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js — 131 通过(118 个既有 + 13 个新增)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js scripts/tests/review-worktree-cleanup-workflow.test.js scripts/tests/qwen-resolve-workflow.test.js — 207 通过(其他解析本 workflow 的套件)
  • cd packages/cli && npx vitest run src/commands/review/lib/deadline.test.ts — 49 通过(固定本 workflow 的 env 导出)
  • yamllint 1.35.1(CI 的 scripts/lint.js --yamllint 所固定的确切版本)检查修改后的 workflow — 0 个问题
  • 对从解析后 YAML 提取的三个修改过的 run 块执行 bash -n — 通过
  • 展开后的去重 jq 过滤器用真实 jq 对 GraphQL 形状夹具执行 — 仅选中 bot 发布的标记评论
  • 兜底步骤 bash 对打桩 gh 执行 — 10 个场景全部通过(见上文 R1-9)
  • 第 1 轮前可证伪性检查 — 13 个新测试中 9 个对旧 workflow 失败,随后恢复现场
  • 集成测试 — 不适用(触及的行为是 CI workflow YAML 与 scripts/tests,不经捆绑 CLI 验证);npm run generate:settings-schema — 不适用(未改动 settings 源)

🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):

  • scripts
    Review the expansion deliberately; the footprint gate is in advisory mode. · 本轮改动了 PR 足迹之外的区域(门自动测量,非 agent 文本),当前足迹门为 advisory 模式,请有意识地审阅该扩张。

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

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


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.12)

(needs.review-pr.result == 'failure' ||
needs.authorize.result == 'failure' ||
needs.review-config.result == 'failure') &&
github.event.inputs.command != 'resolve' &&

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] The /resolve exclusion only covers workflow_dispatch runs (github.event.inputs.command != 'resolve'), but /resolve is also a first-class issue_comment command — authorize.if matches startsWith(github.event.comment.body, '@qwen-code /resolve') — and on issue_comment events github.event.inputs is empty, so this exclusion never fires there. — Failure scenario: a maintainer comments @qwen-code /resolve on a same-repo PR; authorize runs on the self-hosted ECS pool and fails (a permission-API 5xx under set -euo pipefail, or the incident-class runner corruption this PR hardens against). review-pr is skipped by design (its if has no /resolve branch), the gate opens via needs.authorize.result == 'failure', and the job posts "Qwen Code review did not complete successfully … retry with @qwen-code /review" on a run where no review was ever attempted — misdiagnosing a resolve run and recommending the wrong command. Suggested fix: mirror authorize's /resolve predicate for comment events, e.g. add !(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@qwen-code /resolve')) && to the gate, and extend the gate test's resolve case to the comment-driven path.

中文说明

/resolve 排除项只覆盖了 workflow_dispatch 运行(github.event.inputs.command != 'resolve'),但 /resolve 也是一等的 issue_comment 命令——authorize.if 匹配 startsWith(github.event.comment.body, '@qwen-code /resolve')——而在 issue_comment 事件中 github.event.inputs 为空,该排除项在那里永远不会生效。— 故障场景:维护者在同仓库 PR 上评论 @qwen-code /resolveauthorize 在自托管 ECS 池上运行并失败(set -euo pipefail 下的权限 API 5xx,或本 PR 所要防御的事故类 runner 损坏)。review-pr 按设计被跳过(其 if 没有 /resolve 分支),gate 经 needs.authorize.result == 'failure' 打开,在一个从未尝试过 review 的运行上发布 "Qwen Code review did not complete successfully … retry with @qwen-code /review"——把 resolve 运行误诊为 review 失败,并推荐了错误的命令。建议修复:在 gate 中为评论事件镜像 authorize 的 /resolve 谓词,例如添加 !(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@qwen-code /resolve')) &&,并在 gate 测试中把 resolve 用例扩展到评论驱动路径。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# the same run id. The PR number comes from the event payload, not the dead
# job's outputs, which do not survive a crash.
fallback-comment:
needs: ['review-config', 'authorize', 'review-pr']

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.

[Suggestion] The gate hand-enumerates upstream failures but omits delay-automatic-review (review-pr's third direct dependency, line 365) and precheck-pr (the fork-PR chain root): their failures mark review-pr skipped without opening the gate, against this job's own comment — "a skipped review is just as unexplained as a dead one". — Failure scenario: (a) delay-automatic-review's 'Re-check PR state' step runs gh pr view under set -euo pipefail with no retry; a transient GitHub API 5xx fails the job, review-pr is skipped (failed dependency + missing should_review), the gate sees only skipped/success results and stays closed — the automatic review is silently lost. (b) For fork PRs, precheck-pr fails before posting anything (its comment step is gated on decision == 'manual_required'): the chain skips and the gate never opens — silence. Note: round-1 R1-6 scoped this gate to the incident class and explicitly excluded hosted jobs; the trigger here is transient API failure, a different mechanism — if hosted-job omission is deliberate, say so in the job comment. Suggested fix: add delay-automatic-review and precheck-pr to needs: and needs.delay-automatic-review.result == 'failure' || needs.precheck-pr.result == 'failure' to the disjunction (both are skipped where they don't apply, so the gate stays correctly closed there).

中文说明

gate 手工枚举上游失败,但遗漏了 delay-automatic-review(review-pr 的第三个直接依赖,第 365 行)和 precheck-pr(fork PR 链路的根):它们的失败会把 review-pr 标记为 skipped 却不打开 gate,与本 job 自己的注释——"a skipped review is just as unexplained as a dead one"(被跳过的 review 和死掉的同样缺乏解释)——相矛盾。— 故障场景:(a) delay-automatic-review 的 'Re-check PR state' 步骤在 set -euo pipefail 下无重试地运行 gh pr view;一次瞬时的 GitHub API 5xx 就会使该 job 失败,review-pr 被跳过(依赖失败 + 缺少 should_review),gate 只看到 skipped/success 结果而保持关闭——自动 review 悄无声息地丢失。(b) 对 fork PR,precheck-pr 在发布任何内容之前失败(其评论步骤以 decision == 'manual_required' 为条件):整条链路被跳过且 gate 永不打开——一片沉默。注:round-1 的 R1-6 把该 gate 限定在事故类(incident class)并明确排除了托管 job;此处的触发机制是瞬时 API 失败,属不同机制——如果有意排除托管 job,请在 job 注释中说明。建议修复:把 delay-automatic-reviewprecheck-pr 加入 needs:,并在析取式中加入 needs.delay-automatic-review.result == 'failure' || needs.precheck-pr.result == 'failure'(两者在不适用时均为 skipped,gate 在这些路径上仍正确保持关闭)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# from the account CI_BOT_PAT posts as.
bot_login=""
fallback_bodies=""
for _attempt in 1 2 3; do

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.

[Suggestion] This dedup lookup re-inlines the marker+author upsert protocol that .github/scripts/upsert-bot-comment.sh was created to consolidate — the 3-attempt retry with sleep 10, dynamic gh api user --jq '.login' resolution, author-scoped marker filter, and fail-closed-on-failed-listing. The script's header states it exists because "the previous per-step copies had already drifted", and the in-code comment here even says "same norm as upsert-bot-comment.sh". The ~10% that differs is deliberate (dedup on marker + this run's URL, skip instead of PATCH), but the prerequisite machinery is now maintained in two places, and the new tests pin the inline form textually, cementing the second implementation. — Concrete cost: when the shared script's protocol is next hardened (retry count, backoff, fail-closed semantics), nothing references it from this job — the inline copy silently stays stale and the two comment paths drift, the exact failure mode the shared script was extracted to prevent. Suggested fix: add a base-ref actions/checkout step to this job (it currently checks out no code) and route the lookup/post through upsert-bot-comment.sh — either accepting upsert semantics or extending the script with a post-if-absent mode plus a caller-supplied body predicate.

中文说明

该去重查找把 .github/scripts/upsert-bot-comment.sh 被抽出来统一的 marker+author upsert 协议重新内联了一遍——3 次重试 + sleep 10、动态 gh api user --jq '.login' 解析、按作者过滤的 marker 筛选、查询失败即拒绝发布(fail-closed)。脚本头部写明它存在的原因正是"先前各步骤的拷贝已经发生漂移",而这里的代码注释甚至写着 "same norm as upsert-bot-comment.sh"。约 10% 的差异是有意为之(以 marker + 本 run URL 去重、跳过而非 PATCH),但前置机制现在要在两处手工维护,且新测试以文本方式钉住了内联形式,把第二份实现固化了下来。— 具体代价:当共享脚本的协议下次被加固(重试次数、退避、fail-closed 语义)时,本 job 没有任何东西引用它——内联拷贝会悄悄停留在旧版本,两条评论路径随之漂移,正是共享脚本被抽取出来要防止的失败模式。建议修复:给该 job 增加一个 base-ref 的 actions/checkout 步骤(目前不 checkout 任何代码),并把查找/发布改走 upsert-bot-comment.sh——要么接受 upsert 语义,要么给脚本扩展一个"不存在才发布"的模式并支持调用方提供的正文谓词。

— qwen3.8-max via Qwen Code /review (v0.21.12)

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.

Declined for this PR (not on the merits of consolidation). The suggested fix extends .github/scripts/upsert-bot-comment.sh with a post-if-absent mode plus a caller-supplied body predicate and adds a base-ref checkout to this job — that script is shared machinery used by the docs-only relay and the stale-badge supersede step, so a protocol change risks regressing both, and .github/scripts/ is outside this PR's footprint. The ~10% divergence here is deliberate (dedup on marker plus this run's URL, skip instead of PATCH), and the inline copy is pinned textually and executed by the suite, so it cannot silently rot within this PR. Recommended follow-up: a dedicated PR consolidating all three comment-protocol sites (the upsert script, this dedup, and the R2-9 in-job lookup) behind one script mode.

中文说明

对本 PR 予以拒绝(并非不认可合并本身)。建议的修复要给 .github/scripts/upsert-bot-comment.sh 扩展"不存在才发布"模式并支持调用方正文谓词,还要给该 job 增加 base-ref checkout——该脚本是 docs-only relay 与 stale-badge supersede 步骤共用的机制,改动其协议有让两者回归的风险,且 .github/scripts/ 不在本 PR 的足迹之内。此处约 10% 的差异是有意为之(以 marker 加本 run URL 去重、跳过而非 PATCH),且内联拷贝被套件以文本钉住并实际执行,在本 PR 生命周期内不会悄悄腐化。建议后续:用一个专门的 PR 把三处评论协议站点(upsert 脚本、此去重、以及 R2-9 的 job 内查找)统一到同一脚本模式之下。

expect(run).not.toMatch(/GITHUB_WORKSPACE\/\.\.\/\.\."/);
expect(run).toContain('"$HOME"');
expect(run).toContain('"${RUNNER_TEMP:?}"');
expect(run).toContain('"$RUNNER_ROOT/_diag"');

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.

[Suggestion] The health probe's [ -d "$RUNNER_ROOT/_diag" ] guard polarity is unpinned — toContain('"$RUNNER_ROOT/_diag"') matches the dirs+= line regardless of the guard, and nothing executes or asserts the if [ -d … ] condition. — Failure scenario: verified surviving mutant — inverting the guard to if [ ! -d "$RUNNER_ROOT/_diag" ] leaves the suite 131/131 green. On a runner where _diag does not yet exist (the case the workflow comment addresses), the mutant probes a nonexistent directory: touch fails, repair fails, and the review job fails fast on a healthy runner — while a corrupt existing _diag goes unprobed. Suggested fix: pin the guard text (expect(run).toContain('if [ -d "$RUNNER_ROOT/_diag" ]; then')) or add an executed-shape probe test with a sandboxed directory tree containing/omitting _diag.

中文说明

健康探测的 [ -d "$RUNNER_ROOT/_diag" ] 守卫极性未被钉住——toContain('"$RUNNER_ROOT/_diag"') 无论守卫如何都会匹配到 dirs+= 那一行,且没有任何测试执行或断言 if [ -d … ] 条件本身。— 故障场景:经验证存活的变异体——把守卫反转为 if [ ! -d "$RUNNER_ROOT/_diag" ] 后测试套件仍为 131/131 全绿。在 _diag 尚不存在的 runner 上(正是 workflow 注释所说明的情况),变异体会探测一个不存在的目录:touch 失败、修复失败,review job 在一台健康 runner 上快速失败——而一个已损坏的 _diag 反而不会被探测到。建议修复:钉住守卫文本(expect(run).toContain('if [ -d "$RUNNER_ROOT/_diag" ]; then')),或增加一个执行形态的探测测试,用包含/不包含 _diag 的沙箱目录树来验证。

— qwen3.8-max via Qwen Code /review (v0.21.12)

' [ "${SCENARIO:-}" = "lookup_fail" ] && exit 1',
' cat "$COMMENTS_FILE"; exit 0 ;;',
' *headRefOid*)',
' [ "${SCENARIO:-}" = "prview_fail" ] && exit 1',

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.

[Suggestion] The head-moved guard's "posting wins over silence" degradation is only tested when BOTH head lookups come back empty; the partial-failure edges are untested, and the stub's prview_fail scenario here is dead infrastructure no test invokes. — Failure scenario: verified surviving mutant — dropping [ -n "$run_head" ] && from the guard leaves the suite 131/131 green (the only partial-failure test runs with PR_HEAD unset, so the surviving check short-circuits). With the mutant, a transient gh run view 5xx while gh pr view … headRefOid succeeds makes [ "" != "<sha>" ] true: the fallback is suppressed on a dead run — silence instead of the retry comment, on the pull_request_target path the job exists for. Suggested fix: add runFallbackStep('runview_fail', { eventName: 'pull_request_target', prHead: 'newsha' }) expecting a post, and a prview_fail scenario expecting a post — pinning "comparison unavailable → post" for each lookup independently.

中文说明

head 已移动守卫的"发布优先于沉默"降级只在两次 head 查询都返回空时被测试;部分失败的边界未被测试,且此处 stub 的 prview_fail 场景是没有任何测试调用的死代码。— 故障场景:经验证存活的变异体——从守卫中删去 [ -n "$run_head" ] && 后套件仍为 131/131 全绿(唯一的部分失败测试在 PR_HEAD 未设置时运行,存活的检查会短路)。带上该变异体后,当 gh run view 瞬时 5xx 而 gh pr view … headRefOid 成功时,[ "" != "<sha>" ] 为真:兜底评论在一个已死的 run 上被抑制——在本 job 存在的 pull_request_target 路径上,换来沉默而非重试指引。建议修复:增加 runFallbackStep('runview_fail', { eventName: 'pull_request_target', prHead: 'newsha' }) 并断言会发布,再增加一个 prview_fail 场景断言会发布——分别为每次查询钉住"比较不可用 → 发布"。

— qwen3.8-max via Qwen Code /review (v0.21.12)

// while review-pr is skipped by design.
expect(job.if).toContain("github.event.inputs.command != 'resolve'");
expect(job.if).toContain("github.repository == 'QwenLM/qwen-code'");
expect(job.if).toContain("github.event.inputs.review_mode == '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.

[Suggestion] The gate test pins review_mode == 'comment' but not the disjunction guarding it — github.event_name != 'workflow_dispatch' || appears in no assertion. — Failure scenario: verified surviving mutant — collapsing (github.event_name != 'workflow_dispatch' || github.event.inputs.review_mode == 'comment') to the bare github.event.inputs.review_mode == 'comment' leaves the suite 131/131 green. On pull_request_target and issue_comment events review_mode is null, so the mutant evaluates false and the fallback-comment job is skipped on exactly the events that produce dead review runs — the entire round-2 defense disabled while the test certifies the gate. Suggested fix: add expect(job.if).toContain("github.event_name != 'workflow_dispatch'") alongside the existing gate assertions.

中文说明

gate 测试钉住了 review_mode == 'comment',却没有钉住保护它的析取式——github.event_name != 'workflow_dispatch' || 没有出现在任何断言中。— 故障场景:经验证存活的变异体——把 (github.event_name != 'workflow_dispatch' || github.event.inputs.review_mode == 'comment') 坍缩为裸的 github.event.inputs.review_mode == 'comment' 后套件仍为 131/131 全绿。在 pull_request_targetissue_comment 事件上 review_mode 为 null,变异体求值为 false,fallback-comment job 恰恰在会产生死 review run 的事件上被跳过——整个 round-2 防御被废除,而测试还为 gate 背书。建议修复:在现有 gate 断言旁增加 expect(job.if).toContain("github.event_name != 'workflow_dispatch'")

— qwen3.8-max via Qwen Code /review (v0.21.12)

exit 1
fi
case "$fallback_bodies" in
*"actions/runs/${GITHUB_RUN_ID}"*)

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.

[Suggestion] The dedup match is an unanchored substring test, broader than its stated contract ("marker plus this run's URL"): a fallback comment from any run whose numeric id merely CONTAINS this run's id suppresses this run's comment. — Failure scenario: run C dies without posting its fallback. Later run E dies and posts; E's id contains C's id as a digit substring (e.g. C=17123456789, E=171234567890 — as run ids grow a digit longer, each older id gains superstring successors). When the old failed run C is re-run (re-runs keep the same run id, which the design relies on) and dies again, the lookup finds E's marker comment, the pattern matches E's longer URL, and the job exits 0 "already exists" — C's re-run death is never announced. Probe-verified: reproduced on the unmodified PR; the anchored fix flips it, and legit same-id dedup still works (all five marker bodies render the URL as [workflow logs](${RUN_URL}), so the id is always immediately followed by )).

Suggested change
*"actions/runs/${GITHUB_RUN_ID}"*)
*"actions/runs/${GITHUB_RUN_ID})"*)
中文说明

去重匹配是一个未加界定的子串测试,比其声明的契约("marker 加本 run 的 URL")更宽:任何数字 id 包含本 run id 作为子串的 run 的兜底评论,都会抑制本 run 的评论。— 故障场景:run C 死亡且未及发布兜底评论。之后的 run E 死亡并发布了评论;E 的 id 以数字子串方式包含 C 的 id(例如 C=17123456789,E=171234567890——当 run id 增长一位数,每个较早的 id 都会获得若干超串后继)。当旧的失败 run C 被重跑(重跑保留同一 run id,设计依赖这一点)并再次死亡时,查找会找到 E 的 marker 评论,模式匹配上 E 更长的 URL,job 以 0 退出"already exists"——C 的重跑死亡永远不会被通告。已用探针验证:在未修改的 PR 上复现;加界修复可使其翻转,且合法的同 id 去重仍然有效(五处 marker 正文都把 URL 渲染为 [workflow logs](${RUN_URL}),id 后总是紧跟 ))。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# Blank line after the marker or the prose renders as raw source —
# same HTML-block quirk as the ack marker. The fallback-comment job
# dedupes on this marker plus this run's URL.
body="$(printf '%s\n\n%s' "$FALLBACK_MARKER" "$body")"

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.

[Suggestion] The cross-job dedup is one-directional: the new fallback-comment job lists existing marker comments before posting, but this in-job step has no dedup check — so a re-run after a fallback post mints a duplicate failure comment for the same run, contrary to the job comment's claim that "the same check dedupes re-runs, which keep the same run id". (In-job→in-job re-run duplicates pre-date this PR; the fallback→in-job pair is newly introduced by this diff.) — Failure scenario: attempt 1 — the self-hosted runner dies mid-review (the incident class this PR targets); this step never runs; the fallback-comment job posts the marker-headed comment linking actions/runs/<id>. The maintainer re-runs failed jobs (same run id). Attempt 2 completes but the review step fails for an ordinary reason (timeout/quota/generic): this step's if: failure() fires and posts a second marker-headed comment linking the identical run URL, because nothing on its path lists existing comments. Probe-verified via extract-step (the step posts without any comment listing; a fail-open lookup flips it). Suggested fix: apply the same author-scoped marker+run-URL lookup before posting here (fail-open — posting wins over silence, the fallback job still catches a true miss), or correct the job comment claiming re-run dedup.

中文说明

跨 job 的去重是单向的:新的 fallback-comment job 在发布前会列出现有 marker 评论,但 job 内的这个步骤没有去重检查——于是在兜底评论发布之后重跑,会为同一个 run 制造重复的失败评论,与 job 注释中"the same check dedupes re-runs, which keep the same run id"(同一检查会为保留相同 run id 的重跑去重)的说法矛盾。(job 内→job 内的重跑重复在本 PR 之前就存在;fallback→job 内的这一对是本 diff 新引入的。)— 故障场景:第 1 次尝试——自托管 runner 在 review 中途死亡(本 PR 针对的事故类);此步骤从未运行;fallback-comment job 发布了带 marker、链接 actions/runs/<id> 的评论。维护者重跑失败的 jobs(run id 不变)。第 2 次尝试完成但 review 步骤因普通原因失败(超时/配额/通用):此步骤的 if: failure() 触发,由于其路径上没有任何东西列出已有评论,又发布了第二条带 marker、链接同一 run URL 的评论。已用 extract-step 探针验证(该步骤发布时不做任何评论列出;加上 fail-open 查找后可翻转)。建议修复:在此处发布前应用同样的按作者过滤的 marker+run-URL 查找(fail-open——发布优先于沉默,fallback job 仍会兜住真正的遗漏),或修正声称重跑去重的 job 注释。

— qwen3.8-max via Qwen Code /review (v0.21.12)

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.

Acknowledged — this is a real duplication path. Deferred to the next round by this round's batch bound (eight findings implemented, Critical first). Planned fix: a fail-open, author-scoped marker + run-URL lookup before the in-job gh pr comment, evaluated together with the R2-3 consolidation question — adding a third inline copy of the lookup protocol this round would deepen exactly the drift R2-3 flags. The job comment's re-run-dedup claim is accurate for the fallback job's own check; the gap is the fallback→in-job pair, which the next round addresses.

中文说明

已确认——这是一条真实的重复路径。受本轮批次上限(实现了 8 个发现、Critical 优先)推迟到下一轮。计划中的修复:在 job 内 gh pr comment 之前加 fail-open、按作者过滤的 marker+run-URL 查找,并与 R2-3 的合并问题一并评估——本轮再加第三份内联查找拷贝会加深 R2-3 所指出的漂移。job 注释中关于重跑去重的说法对 fallback job 自身的检查是准确的;缺口在 fallback→job 内这一对,下一轮处理。

github.repository == 'QwenLM/qwen-code' &&
(github.event_name != 'workflow_dispatch' ||
github.event.inputs.review_mode == 'comment')
runs-on: 'ubuntu-latest'

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.

[Suggestion] This job's load-bearing placement invariant — it must run on an ephemeral hosted runner, never the self-hosted ECS pool whose failure mode it exists to survive — is pinned by no test; the suite pins only needs and if fragments. The job comment ("runs on an ephemeral hosted runner, so it survives whatever killed the review job") is the sole enforcement. — Failure scenario: a future edit routes fallback-comment onto the ECS pool (a routine-looking consolidation — the pool every other compute job in this file uses); all 131 tests stay green (verified mutant). On the next PR #8894-class incident the fallback job dies the same death as review-pr, and the PR is again left with no review and no comment — the exact state this PR exists to eliminate. Suggested fix: expect(job['runs-on']).toBe('ubuntu-latest') in the resilience suite, with a comment naming the survival requirement.

Suggested change
runs-on: 'ubuntu-latest'
runs-on: 'ubuntu-latest'
中文说明

该 job 承重的部署不变量——必须运行在临时托管 runner 上、绝不能运行在它所要幸存其故障模式的自托管 ECS 池上——没有任何测试钉住;套件只钉了 needsif 的片段。job 注释("runs on an ephemeral hosted runner, so it survives whatever killed the review job")是唯一的约束。— 故障场景:未来某次编辑把 fallback-comment 改到 ECS 池上(看似例行整合——该文件里其他所有计算 job 都用这个池);全部 131 个测试保持绿色(变异体已验证)。下一次 PR #8894 类事故发生时,fallback job 会与 review-pr 同样死亡,PR 再次落入既无 review 也无评论的境地——正是本 PR 要消灭的状态。建议修复:在 resilience 套件中加入 expect(job['runs-on']).toBe('ubuntu-latest'),并用注释写明存活要求。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# Blank line after the marker or the prose renders as raw source —
# same HTML-block quirk as the ack marker. The fallback-comment job
# dedupes on this marker plus this run's URL.
body="$(printf '%s\n\n%s' "$FALLBACK_MARKER" "$body")"

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.

[Suggestion] The ordering invariant that this step prepends FALLBACK_MARKER to the body BEFORE gh pr comment is unpinned — the tests assert the printf line exists (toContain) but not that it precedes the post, so a regression that strips the marker from the posted in-job comment ships green. — Failure scenario: a future edit moves the printf line below the gh pr comment block (mutating a dead variable); the suite passes 131/131 (verified mutant). At runtime the in-job comment then posts without the marker, so the fallback job's author+marker dedup filter never sees it — and every run where review-pr fails after this step already posted mints a duplicate "review did not complete" comment, the duplication the marker was introduced to prevent. Suggested fix: pin the order (expect(inJobStep.run.indexOf(printf)).toBeLessThan(inJobStep.run.indexOf('gh pr comment')) — an established pattern elsewhere in this file), or execute this step's script the way runFallbackStep executes the fallback job's and assert the posted body starts with the marker.

中文说明

该步骤在 gh pr comment 之前把 FALLBACK_MARKER 前置到正文的这一顺序不变量未被钉住——测试只断言 printf 行存在(toContain),未断言它先于发布,因此一个把 marker 从所发布的 job 内评论中去掉的回归可以绿着上线。— 故障场景:未来某次编辑把 printf 行移到 gh pr comment 块之下(变异一个死变量);套件 131/131 通过(变异体已验证)。运行时 job 内评论随后会不带 marker 发布,fallback job 的按作者+marker 去重筛选永远看不到它——于是每个 review-pr 在此步骤已发布之后才失败的 run 都会制造重复的 "review did not complete" 评论,正是 marker 被引入要防止的重复。建议修复:钉住顺序(expect(inJobStep.run.indexOf(printf)).toBeLessThan(inJobStep.run.indexOf('gh pr comment'))——本文件其他地方已有的既定模式),或像 runFallbackStep 执行 fallback job 脚本那样执行此步骤的脚本,并断言发布的正文以 marker 开头。

— qwen3.8-max via Qwen Code /review (v0.21.12)

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.

Acknowledged — deferred to the next round by this round's batch bound. The fix is the suggested one-line ordering assertion (indexOf(printf) < indexOf('gh pr comment') on the in-job step, an established pattern in this suite) and ships with the R2-9 follow-up.

中文说明

已确认——受本轮批次上限推迟到下一轮。修复即所建议的一行顺序断言(对 job 内步骤断言 indexOf(printf) < indexOf('gh pr comment'),本套件中已有的既定模式),随 R2-9 的后续一并提交。

…enLM#9255)

- Exclude comment-driven /resolve runs from the fallback gate:
  authorize runs on `@qwen-code /resolve` issue comments where
  github.event.inputs is empty, so the dispatch-only exclusion never
  fired there and a failed resolve run was misdiagnosed as a dead
  review recommending the wrong command.
- Enumerate precheck-pr and delay-automatic-review failures in the
  gate: either failure marks review-pr 'skipped' (a transient API 5xx
  in delay's re-check step, or the fork-PR chain root dying before it
  posts anything), which the old gate never matched — silence,
  against its own "a skipped review is as unexplained as a dead one"
  norm. Both are 'skipped' where they do not apply, so the gate stays
  closed there.
- Anchor the cross-job dedup on the run URL's closing paren: run ids
  grow digits over time, so the unanchored substring let a later
  run's fallback comment (id 123450) suppress an earlier run's (id
  12345) re-run comment; every marker body renders the URL as
  [workflow logs](...runs/<id>), so the id is always followed by ')'.
- Pin the previously surviving mutants in the workflow suite: the
  _diag probe guard polarity, the workflow_dispatch disjunction, the
  ephemeral-hosted-runner placement, and the fallback body's
  marker-link shape the anchor relies on; add executed coverage for
  each head-lookup partial failure and for a distinct run's fallback
  not suppressing this run's comment.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

PR #9255 — round-2 review feedback addressed

Findings and decisions

Finding Severity Decision
R2-1 — /resolve exclusion misses comment-driven resolve runs Critical Fixed
R2-2 — gate omits delay-automatic-review and precheck-pr failures Suggestion Fixed
R2-3 — dedup lookup re-inlines the upsert-bot-comment.sh protocol Suggestion Declined (recorded below)
R2-4 — _diag health-probe guard polarity unpinned Suggestion Fixed
R2-5 — head-moved partial-failure edges untested (prview_fail stub dead) Suggestion Fixed
R2-6 — dedup test only exercises the same run id Suggestion Fixed
R2-7 — gate test does not pin the workflow_dispatch disjunction Suggestion Fixed
R2-8 — dedup match is an unanchored substring test Suggestion Fixed
R2-9 — cross-job dedup is one-directional (in-job step has no dedup) Suggestion Deferred to next round (recorded below)
R2-10 — ephemeral-hosted-runner placement invariant unpinned Suggestion Fixed
R2-11 — marker-prepend-before-post ordering unpinned Suggestion Deferred to next round (recorded below)

Eight findings were implemented this round (the Critical plus seven
suggestions); R2-9 and R2-11 are deferred under the per-round batch bound and
tracked via comment replies; R2-3 is declined with reason.

Reproduction before fixing

Per the review-first rule, the new assertions were added to the workflow test
suite before touching the workflow, and the suite was run against the
unmodified code:

  • the new /resolve gate test failed (the comment-driven exclusion did not
    exist) — reproducing R2-1;
  • the gate test failed on the missing delay-automatic-review /
    precheck-pr enumeration and the unpinned workflow_dispatch disjunction —
    reproducing R2-2/R2-7;
  • the new distinct-run scenario failed: with run id 12345, an existing
    fallback comment linking actions/runs/123450 suppressed this run's comment
    through the unanchored substring match — reproducing R2-8's probe on the
    unmodified code.

After the workflow fixes, all 135 tests pass. Two surviving-mutant claims were
additionally spot-checked by temporarily mutating the fixed code and restoring
it: dropping [ -n "$run_head" ] && from the head guard fails exactly the new
run-head partial-failure test, and widening the dedup pattern to
*actions/runs/* fails exactly the new distinct-run test.

Changes

.github/workflows/qwen-code-pr-review.yml:

  • R2-1: added
    !(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@qwen-code /resolve'))
    to the fallback-comment gate, mirroring authorize's comment-driven
    /resolve branch; on issue_comment events github.event.inputs is empty,
    so the dispatch-only exclusion never fired there.
  • R2-2: added precheck-pr and delay-automatic-review to needs: and
    needs.<job>.result == 'failure' for each to the gate disjunction; both are
    skipped where they do not apply, so the gate stays correctly closed on
    those paths. The job comment now states the enumeration explicitly
    (self-hosted: authorize / review-config; hosted: precheck-pr /
    delay-automatic-review).
  • R2-8: anchored the dedup case pattern to
    *"actions/runs/${GITHUB_RUN_ID})"* — every marker body renders the URL as
    [workflow logs](${RUN_URL}), so the id is always immediately followed by
    ); a superstring run id can no longer suppress this run's comment.

scripts/tests/qwen-pr-review-workflow.test.js:

  • R2-1/R2-2/R2-7/R2-10: rewrote the gate test to pin the five-job needs
    enumeration, all five failure disjuncts, the
    (github.event_name != 'workflow_dispatch' || disjunct, and
    runs-on: 'ubuntu-latest'; split resolve exclusion into a dedicated test
    covering both the dispatch and the comment-driven path.
  • R2-4: pinned the guard text if [ -d "$RUNNER_ROOT/_diag" ]; then.
  • R2-5: added executed scenarios for each partial head-lookup failure on
    pull_request_target (runview_fail with a live PR head, and the
    previously dead prview_fail scenario with a live run head), both expecting
    a post.
  • R2-6: added a scenario where only a distinct run's fallback exists —
    using the superstring id 123450 against run 12345, which also pins the
    R2-8 anchor — expecting a post.
  • R2-8: the same-run dedup fixture now mirrors a prior fallback comment's
    rendered shape (See [workflow logs](...actions/runs/12345).), and the
    fallback job's body link shape the anchor depends on is pinned alongside the
    in-job copies.

Declined: R2-3 (route the dedup through upsert-bot-comment.sh)

Declined for this PR, not on the merits of consolidation itself:

  • The fix requires extending .github/scripts/upsert-bot-comment.sh with a
    post-if-absent mode plus a caller-supplied body predicate, and adding a
    base-ref checkout to this job. That script is shared CI machinery used by
    the docs-only relay and the stale-badge supersede step; changing its
    protocol risks regressing both, and .github/scripts/ is outside this PR's
    footprint.
  • The ~10% divergence is deliberate: this lookup dedups on marker plus this
    run's URL and skips instead of PATCHing. The inline copy is pinned
    textually and executed by the suite, so it cannot silently rot within this
    PR's lifetime.
  • Recommended follow-up: a dedicated PR consolidating all three comment
    protocol sites (upsert script, this dedup, and the R2-9 in-job lookup below)
    behind one script mode.

Deferred to next round

  • R2-9: real duplication path (fallback comment posted after a mid-review
    runner death, then a re-run of the same run failing normally mints a second
    marker-headed comment). Deferred by this round's batch bound; the planned
    fix is a fail-open, author-scoped marker + run-URL lookup in the in-job
    step, evaluated together with R2-3 — adding a third inline copy of the
    lookup protocol would deepen exactly the drift R2-3 flags.
  • R2-11: acknowledged; the one-line ordering assertion ships next round.

Conflict notes

--conflict false; no merge performed.

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-pr-review-workflow — 135 passed (135), was 131 before this round; pre-fix run showed the 3 expected failures reproducing R2-1/R2-2/R2-7/R2-8
  • mutation spot-checks (temporary, reverted): drop [ -n "$run_head" ] && → 1 failed (the new run-head test); widen dedup to *actions/runs/* → 1 failed (the new distinct-run test)
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, no ESLint findings)
  • npx prettier --check on both changed files — clean
  • YAML parse check of the workflow: fallback-comment needs/if/runs-on parse as intended
  • not run: integration tests (behavior exercised by the workflow test harness, not the bundled CLI); npm run generate:settings-schema (no settings source touched)
中文说明

PR #9255 —— round-2 评审反馈处理

发现与决定

发现 严重级别 决定
R2-1 —— /resolve 排除遗漏了评论驱动的 resolve 运行 Critical 已修复
R2-2 —— gate 遗漏了 delay-automatic-reviewprecheck-pr 的失败 Suggestion 已修复
R2-3 —— 去重查找重新内联了 upsert-bot-comment.sh 协议 Suggestion 拒绝(理由见下)
R2-4 —— _diag 健康探测守卫极性未被钉住 Suggestion 已修复
R2-5 —— head 已移动守卫的部分失败边界未被测试(prview_fail stub 为死代码) Suggestion 已修复
R2-6 —— 去重测试只演练了相同 run id Suggestion 已修复
R2-7 —— gate 测试未钉住 workflow_dispatch 析取式 Suggestion 已修复
R2-8 —— 去重匹配是未加界定的子串测试 Suggestion 已修复
R2-9 —— 跨 job 去重是单向的(job 内步骤没有去重) Suggestion 推迟到下一轮(说明见下)
R2-10 —— 临时托管 runner 部署不变量未被钉住 Suggestion 已修复
R2-11 —— marker 前置先于发布的顺序未被钉住 Suggestion 推迟到下一轮(说明见下)

本轮实现了 8 个发现(Critical 加 7 个 Suggestion);R2-9 与 R2-11 按每轮批次上限推迟,并通过评论回复跟踪;R2-3 附理由拒绝。

修复前先复现

按"先复现"原则,新的断言先加入 workflow 测试套件,然后在未修改的代码上运行:

  • 新的 /resolve gate 测试失败(评论驱动的排除项不存在)——复现 R2-1;
  • gate 测试在缺失的 delay-automatic-review / precheck-pr 枚举与未钉住的 workflow_dispatch 析取式上失败——复现 R2-2/R2-7;
  • 新的不同 run 场景失败:run id 为 12345 时,一条已存在的、链接 actions/runs/123450 的兜底评论通过未加界定的子串匹配抑制了本 run 的评论——在未修改代码上复现了 R2-8 的探针。

workflow 修复后,全部 135 个测试通过。另外对两个"存活变异体"的说法做了临时变异并还原的抽查:从 head 守卫中去掉 [ -n "$run_head" ] && 恰好使新的 run-head 部分失败测试失败;把去重模式放宽为 *actions/runs/* 恰好使新的不同 run 测试失败。

变更内容

.github/workflows/qwen-code-pr-review.yml

  • R2-1:在 fallback-comment gate 中新增 !(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '@qwen-code /resolve')),镜像 authorize 的评论驱动 /resolve 分支;issue_comment 事件上 github.event.inputs 为空,仅靠 dispatch 排除项在那里永不生效。
  • R2-2:在 needs: 中加入 precheck-prdelay-automatic-review,并在 gate 析取式中加入两者的 needs.<job>.result == 'failure';两者在不适用时均为 skipped,gate 在这些路径上仍正确保持关闭。job 注释现在明确写出枚举(自托管:authorize / review-config;托管:precheck-pr / delay-automatic-review)。
  • R2-8:把去重 case 模式加界为 *"actions/runs/${GITHUB_RUN_ID})"*——所有 marker 正文都把 URL 渲染为 [workflow logs](${RUN_URL}),id 后总紧跟 );超串 run id 不再能抑制本 run 的评论。

scripts/tests/qwen-pr-review-workflow.test.js

  • R2-1/R2-2/R2-7/R2-10:重写 gate 测试,钉住五个 job 的 needs 枚举、全部五个失败析取项、(github.event_name != 'workflow_dispatch' || 析取项以及 runs-on: 'ubuntu-latest';把 resolve 排除拆成专门测试,同时覆盖 dispatch 与评论驱动两条路径。
  • R2-4:钉住守卫文本 if [ -d "$RUNNER_ROOT/_diag" ]; then
  • R2-5:为 pull_request_target 上每种 head 查询部分失败新增执行形态场景(runview_fail 且 PR head 存活、以及此前是死代码的 prview_fail 场景且 run head 存活),均断言会发布。
  • R2-6:新增"只存在不同 run 的兜底评论"场景——用超串 id 123450 对 run 12345,同时也钉住了 R2-8 的界——断言会发布。
  • R2-8:同 run 去重用例现在镜像先前兜底评论的渲染形态(See [workflow logs](...actions/runs/12345).),锚点所依赖的兜底 job 正文链接形态也与 job 内各份一并钉住。

拒绝:R2-3(把去重改走 upsert-bot-comment.sh

就合并本身而言并非不认可,但对本 PR 予以拒绝:

  • 该修复需要给 .github/scripts/upsert-bot-comment.sh 扩展"不存在才发布"模式并支持调用方正文谓词,还要给该 job 增加 base-ref checkout。该脚本是 docs-only relay 与 stale-badge supersede 步骤共用的 CI 机制,改动其协议有让两者回归的风险,且 .github/scripts/ 不在本 PR 的足迹内。
  • 约 10% 的差异是有意为之:此查找以 marker 加本 run URL 去重、跳过而非 PATCH。内联拷贝被套件以文本钉住并实际执行,在本 PR 生命周期内不会悄悄腐化。
  • 建议后续:用一个专门的 PR 把三处评论协议站点(upsert 脚本、此去重、以及下面 R2-9 的 job 内查找)统一到同一脚本模式之下。

推迟到下一轮

  • R2-9:真实的重复路径(review 中途 runner 死亡后兜底评论已发布,随后同一 run 的重跑因普通原因失败又产生第二条带 marker 的评论)。受本轮批次上限推迟;计划中的修复是在 job 内步骤加 fail-open、按作者过滤的 marker+run-URL 查找,并与 R2-3 一并评估——再加第三份内联查找拷贝会加深 R2-3 所指出的漂移。
  • R2-11:已确认;一行顺序断言随下一轮提交。

冲突说明

--conflict false;未执行任何合并。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-pr-review-workflow —— 135 通过(135),本轮前为 131;修复前运行出现复现 R2-1/R2-2/R2-7/R2-8 的 3 个预期失败
  • 变异抽查(临时、已还原):去掉 [ -n "$run_head" ] && → 1 失败(新的 run-head 测试);放宽去重为 *actions/runs/* → 1 失败(新的不同 run 测试)
  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0,无 ESLint 发现)
  • 对两个变更文件运行 npx prettier --check —— 干净
  • workflow 的 YAML 解析检查:fallback-comment 的 needs/if/runs-on 按预期解析
  • 未运行:集成测试(该行为由 workflow 测试夹具演练,不经捆绑 CLI);npm run generate:settings-schema(未触碰 settings 源)

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

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


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5 (round 5 reported a finding).

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:reverse audit — did not converge within the reverse-audit round cap of 5 (round 5 reported a finding)。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.12)

echo "Skipping fallback comment: PR #${PR_NUMBER} is ${pr_state}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
body="**Qwen Code review did not complete successfully.** The review job failed before it could post details. A transient error is retried automatically; if you are seeing this, retry with \`@qwen-code /review\`. See [workflow logs](${RUN_URL})."

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.

[Suggestion] R3-1: The body asserts "The review job failed before it could post details", but this job's gate also opens on failures of precheck-pr, review-config, authorize and delay-automatic-review — on those paths review-pr was skipped, never ran, and the comment misnames the failing component. — Failure scenario: a transient API failure kills precheck-pr → the chain is skipped → the gate opens via needs.precheck-pr.result == 'failure' → the maintainer is told the review job failed and starts triage at review-pr's empty logs instead of the actually-failed upstream job — misdirection in the incident moment this job exists for. Witness: traced against the gate if: (lines 1779-1784), review-pr.needs (line 365) and GitHub's skipped-on-needs-failure semantics; all four upstream-failure paths reach this same static body.

Suggested change
body="**Qwen Code review did not complete successfully.** The review job failed before it could post details. A transient error is retried automatically; if you are seeing this, retry with \`@qwen-code /review\`. See [workflow logs](${RUN_URL})."
body="**Qwen Code review did not complete successfully.** The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with \`@qwen-code /review\`. See [workflow logs](${RUN_URL})."
中文说明

正文断言 "The review job failed before it could post details",但该 job 的 gate 同样会在 precheck-prreview-configauthorizedelay-automatic-review 失败时打开——在这些路径上 review-pr 是被跳过、从未运行的,评论会把出错的组件指错。故障场景:一次瞬时 API 失败杀死 precheck-pr → 整链被跳过 → gate 经 needs.precheck-pr.result == 'failure' 打开 → 维护者被告知 review job 失败,从而在 review-pr 的空日志上开始排查,而不是去看真正失败的上游 job——恰恰在这个 job 存在的事故时刻造成误导。证据:已对照 gate if:(1779-1784 行)、review-pr.needs(365 行)及 GitHub 的 needs 失败即跳过的语义核实,四条上游失败路径都会落到这段静态正文。建议改为中性表述(见 suggestion 块)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +456 to +458
sudo -n chown "$RUNNER_UID:$RUNNER_GID" "$dir" 2>/dev/null || true
sudo -n chmod u+rwx "$dir" 2>/dev/null || true
if touch "$probe" 2>/dev/null; then

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.

[Suggestion] R3-3: The health probe's repair-vs-fail-fast decision (probe → repair → re-probe → status) is pinned only by static toContain/toMatch assertions; no test executes this step's real bash through the repair branches. — Failure scenario: named surviving mutants — moving status=1 to right after the first failed touch ships a false fail-fast (a repairable directory aborts review-pr on every run until manual runner repair); deleting the second touch re-check ships a missed fail-fast (an unusable directory is reported "repaired" and the job dies in FinalizeJob — the PR #8894 incident this PR exists to prevent). Witness: probe ran the step's real bash with a stub sudo against a chmod-555 directory — mutant (a): REPAIRABLE → status:1 "failing fast" (original: status:0 "repaired"); mutant (b): UNREPAIRABLE → status:0 "repaired" (original: status:1); the suite stays 135/135 green under both mutants.

Suggested fix: add an executed-shape test mirroring runFallbackStep's stub harness — stub sudo, a read-only tmp dir standing in for $HOME, a fake GITHUB_WORKSPACE nested three deep — covering healthy (exit 0) / repairable (exit 0 + "repaired") / unrepairable (exit 1 + ::error::) / _diag-absent.

中文说明

健康探测的"修复还是快速失败"决策(探测 → 修复 → 复探 → status)目前只被静态 toContain/toMatch 断言钉住,没有任何测试真正执行这段 bash 的修复分支。故障场景:已点名的可存活变异——把 status=1 挪到首次 touch 失败后会带来误报式快速失败(可修复的目录导致 review-pr 每次运行都中止,直到人工修 runner);删掉第二次 touch 复探则会漏掉快速失败(不可用目录被报告为"已修复",job 最终死在 FinalizeJob——正是本 PR 要防的 PR #8894 事故)。证据:探针用 stub sudo 对 chmod-555 目录真实执行了该步 bash——变异 (a):可修复 → status:1(原版 status:0);变异 (b):不可修复 → status:0(原版 status:1);两种变异下测试套件均为 135/135 全绿。建议修复:仿照 runFallbackStep 的 stub 框架补一个执行型测试,覆盖健康/可修复/不可修复/无 _diag 四种路径。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1839 to +1841
bot_login=""
fallback_bodies=""
sleep 10

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.

[Suggestion] R3-4: The retry loop's post-failure reset lines are unobservable in every test scenario — the only lookup-failure scenario (lookup_fail) fails BOTH gh api user and the comments listing, so bot_login ends up empty via the failed command substitution itself and these resets never matter. A mutant deleting them survives. — Failure scenario: gh api user succeeds while the comments listing returns 5xx on all three attempts → in the mutant, bot_login stays non-empty with empty fallback_bodies, the post-loop [ -z "$bot_login" ] guard passes, the dedup case matches nothing, and the job posts on a failed listing — the exact "transient 5xx mints a permanent duplicate" outcome the step's own comment forbids. Witness: probe ran the step's real bash under partial failure — original exits 1 with ::error:: and posts nothing; mutant (resets deleted) exits 0 and POSTED a comment; suite 135/135 green under the mutant.

Suggested fix: add a comments_lookup_fail scenario (user lookup succeeds, *comments* exits 1) asserting status === 1 and posted === ''.

中文说明

重试循环在失败后的两行重置(bot_login="" / fallback_bodies="")在所有测试场景中都不可观测——唯一的查询失败场景 lookup_fail 同时令 gh api user 与评论列表失败,bot_login 本来就因命令替换失败而为空,重置行从未起作用,删掉它们的变异可以存活。故障场景:gh api user 成功而评论列表三次尝试全部 5xx → 变异体中 bot_login 非空而 fallback_bodies 为空,循环后的 [ -z "$bot_login" ] 守卫通过、dedup case 匹配不到任何内容,job 在一次失败的列表之上发布评论——正是该步注释明令禁止的"瞬时 5xx 铸成永久重复评论"。证据:探针在部分失败下真实执行该步 bash——原版 exit 1 且未发布;变异体 exit 0 且发布了评论;变异下套件 135/135 全绿。建议修复:新增 comments_lookup_fail 场景(user 查询成功、评论列表失败),断言 status === 1posted === ''

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1800 to +1801
PR_NUMBER: '${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'

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.

[Suggestion] R3-5: This RUN_URL env expression is read by no test — the executed tests inject a literal RUN_URL — so the invariant the dedup depends on (the rendered link ends exactly at the run id, matching *"actions/runs/${GITHUB_RUN_ID})"*) is verified only for the injected literal, not for this YAML. The marker got byte-identical pinning tests for exactly this drift class; the URL shape did not. — Failure scenario: appending any suffix here (e.g. an /attempts/1 deep-link, plausibly added when someone improves the log link) makes the posted body contain runs/12345/attempts/1) → the dedup pattern no longer matches on re-run → every re-run of a dead review silently posts a duplicate fallback comment, with no error signal anywhere. Witness: probe (step's real bash, stub gh, GITHUB_RUN_ID=12345): base shape → NO POST (dedup matches); /attempts/1 mutant → POSTED (duplicate); mutated YAML suite run 135/135 green.

Suggested fix: assert this step env RUN_URL matches /\/actions\/runs\/\$\{\{ github\.run_id \}\}'$/ (and ideally equals the in-job step's RUN_URL expression).

中文说明

这个 RUN_URL env 表达式没有任何测试读取——执行型测试注入的是字面量 RUN_URL——因此 dedup 所依赖的不变量(渲染出的链接恰好在 run id 处结束,才能匹配 *"actions/runs/${GITHUB_RUN_ID})"*)只对被注入的字面量成立,对这段真实 YAML 并无验证。隐藏标记针对同类漂移已有逐字节钉住的测试,URL 形状却没有。故障场景:在此行追加任何后缀(例如有人改进日志链接时加上 /attempts/1 深链),正文将包含 runs/12345/attempts/1) → dedup 模式在重跑时不再匹配 → 死 review 的每次重跑都会静默多发一条兜底评论,且没有任何报错信号。证据:探针(真实 bash + stub gh,GITHUB_RUN_ID=12345):原始形状 → 不发布(dedup 命中);/attempts/1 变异 → 发布(重复);变异下套件 135/135 全绿。建议修复:断言该 env 以 run id 结尾,并与 job 内步骤的 RUN_URL 表达式一致。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1699 to +1700
# dedupes on this marker plus this run's URL.
body="$(printf '%s\n\n%s' "$FALLBACK_MARKER" "$body")"

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.

[Suggestion] R2-9 (carried from round 2, ruled still standing at this commit): the cross-job dedup is one-directional — the new fallback-comment job lists existing marker comments before posting, but this in-job step posts with no marker/run-URL lookup (its body contains exactly one gh read, --json state,headRefOid, before gh pr comment). The fallback job's header claims "the same check dedupes re-runs", but re-runs re-fire THIS step (if: failure()), which has no check. — Failure scenario: run N dies mid-review (runner death; this step never runs) → fallback-comment posts comment #1 (marker + run N URL) → "Re-run failed jobs" (same run id) → review-pr fails again at step level → this step posts comment #2 with the same run URL → two near-identical comments for one run. Witness: not run — GitHub Actions re-run orchestration is the platform's; verdict rests on a full read of this step's body at HEAD. Acknowledged and deferred by the author automation in round 2; re-reporting because the mechanism still stands.

Suggested fix: give this step the sibling's guard before gh pr comment — the same bot-login-scoped, marker + actions/runs/${GITHUB_RUN_ID}) lookup (bounded retry optional here; on a failed lookup it can skip and let fallback-comment handle it).

中文说明

R2-9(第 2 轮携带,本轮代码核实仍然成立):跨 job 去重是单向的——新的 fallback-comment job 在发布前会列出已有标记评论,但 job 内的这一步发布前没有任何标记/run URL 查询(正文在 gh pr comment 之前只有一处 --json state,headRefOid 读取)。兜底 job 的头注释声称"同一检查为重跑去重",但重跑重新触发的是这一步(if: failure()),而它没有检查。故障场景:run N 在 review 中途死亡(runner 崩溃,此步未运行)→ fallback-comment 发出评论 #1(标记 + run N URL)→ 点击"Re-run failed jobs"(run id 不变)→ review-pr 再次在步骤级失败 → 此步带着同一 run URL 发出评论 #2 → 同一 run 出现两条几乎相同的评论。证据:未运行——重跑编排属于 GitHub Actions 平台;结论基于对 HEAD 处该步正文的完整阅读。第 2 轮作者自动化已确认并延期处理,因机制仍在,本轮继续报告。建议修复:在 gh pr comment 前给此步加上同款守卫(机器人登录作用域的标记 + run URL 查询;查询失败时可跳过、交给 fallback-comment 兜底)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1781 to +1782
(needs.review-pr.result == 'failure' ||
needs.authorize.result == 'failure' ||

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.

[Suggestion] R3-6: The gate disjunction admits only result == 'failure'. A review-pr that exceeds its job-level timeout-minutes (line 415, default 360) is auto-cancelled by GitHub with result 'cancelled' — which opens neither this gate nor the in-job step (failure() is false for a cancelled job). Both defenses miss on the timeout path. The workflow proves the authors hold this model elsewhere: Clean review worktrees uses if: 'always()' precisely because "a cancelled or timed-out review may not reach the CLI's process cleanup" (line 1705). — Failure scenario: a hang outside the CLI's own timeout enforcement (eight steps precede Run review and only Install capture tools has its own timeout) runs into the 360-min cap → review-pr result 'cancelled' → no in-job comment, gate closed → the PR is left with no review and no explanation — the same hole as the PR #8894 incident. Frequency is low (the cap sits far above the review budget by design), but the mechanism is deterministic once triggered. Witness: not run — GitHub Actions job-timeout semantics (conclusion cancelled, failure() false) only execute on GitHub's runtime; the quoted lines 415, 1653-1657, 1779-1784 and 1705-1711 carry the trace.

Suggested fix: add needs.review-pr.result == 'cancelled' to the disjunction — a run-level concurrency cancel cancels the whole run including the queued fallback job, so a fallback job that lives to evaluate the gate and sees 'cancelled' is overwhelmingly the job-timeout case (residual edge: manual single-job cancel, whose outcome is a benign retry-guidance comment) — and pin the chosen semantics in the gate test; or document in the gate comment that cancelled/timed-out runs are intentionally left silent.

中文说明

gate 的析取只接受 result == 'failure'review-pr 超过 job 级 timeout-minutes(415 行,默认 360)时会被 GitHub 自动取消、结果为 'cancelled'——既打不开这个 gate,也触发不了 job 内步骤(被取消的 job 上 failure() 为假)。超时路径上两层防御全部落空。workflow 在别处证明作者清楚这一模型:Clean review worktreesif: 'always()',理由正是"被取消/超时的 review 可能走不到 CLI 的进程清理"(1705 行)。故障场景:CLI 自身超时管辖之外的挂起(Run review 之前有八个步骤,只有 Install capture tools 有自己的超时)撞上 360 分钟上限 → review-pr 结果 'cancelled' → 无 job 内评论、gate 关闭 → PR 既无 review 也无解释——与 PR #8894 事故相同的空洞。频率低(上限按设计远高于 review 预算),但一旦触发机制是确定的。证据:未运行——job 超时语义只在 GitHub 运行时生效;引用 415、1653-1657、1779-1784、1705-1711 行为证。建议修复:在析取中加入 needs.review-pr.result == 'cancelled'(run 级并发取消会连同排队中的兜底 job 一起取消,因此活着评估 gate 并看到 'cancelled' 的兜底 job 几乎必然是 job 超时;残余边界是手动取消单个 job,其后果只是一条良性的重试指引评论),并在 gate 测试中钉住所选语义;或在 gate 注释中声明有意对被取消/超时的运行保持沉默。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1833 to +1834
if bot_login="$(gh api user --jq '.login')" \
&& [ -n "$bot_login" ] \

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.

[Suggestion] R3-7: The cross-job dedup's load-bearing identity agreement — the in-job comment must be posted by the same account this lookup resolves via gh api user — is pinned by no test. It works today because the in-job step (line 1660) happens to use the same CI_BOT_PAT; nothing asserts it. — Failure scenario: mutate line 1660 to github.token (a plausible edit — the adjacent ack step posts with GITHUB_TOKEN) → the in-job comment is authored by github-actions[bot], invisible to this author-scoped filter → on every ordinary failure (in-job step posts, job result stays failure, gate opens) this job posts a second "did not complete" comment for the same run. Witness: probe applied that mutant — all four workflow-parsing suites stayed green (164 + 178 tests); adding one assertion pinning inJobStep.env.GH_TOKEN to CI_BOT_PAT made the same mutant fail (expected '${{ secrets.GITHUB_TOKEN }}' to be '${{ secrets.CI_BOT_PAT }}'). Tree restored, baseline 135/135.

Suggested fix: in the scopes the dedup to the authenticated bot login test, add:

expect(inJobStep.env.GH_TOKEN).toBe("'${{ secrets.CI_BOT_PAT }}'");

with a comment that the author-scoped dedup only sees comments posted by the same account.

中文说明

跨 job 去重的承重不变量——job 内评论必须由本查询经 gh api user 解析出的同一账户发布——没有任何测试钉住。它今天成立只是因为 job 内步骤(1660 行)恰好也用 CI_BOT_PAT,并无任何断言保证。故障场景:把 1660 行改成 github.token(完全可能的编辑——相邻的 ack 步骤就用 GITHUB_TOKEN)→ job 内评论的作者变成 github-actions[bot],对这个按作者过滤的查询不可见 → 每次普通失败(job 内步骤已发布、job 结果仍为 failure、gate 打开)本 job 都会为同一 run 再发一条 "did not complete" 评论。证据:探针应用该变异——四个解析此 workflow 的测试套件全部保持绿色(164 + 178);加上一行把 inJobStep.env.GH_TOKEN 钉到 CI_BOT_PAT 的断言后,同一变异即失败。树已还原,基线 135/135。建议修复:在 scopes the dedup... 测试中补上该断言,并注明作者作用域去重只能看到同一账户发布的评论。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +2771 to +2773
' *comments*)',
' [ "${SCENARIO:-}" = "lookup_fail" ] && exit 1',
' cat "$COMMENTS_FILE"; exit 0 ;;',

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.

[Suggestion] R3-8: The stub gh routes on a substring and cats a plain-text fixture regardless of --json/--jq arguments, so the fallback job's dedup jq filter and head-guard queries are never executed anywhere in this suite — only three substrings of the filter are pinned statically. The block's own comment advertises "reverting any hunk must fail here", but query-shape mutants pass. (The live queries are currently correct — verified against real gh — so this is a test-fidelity gap, not a live bug.) — Failure scenario: two demonstrated surviving mutants: (a) .comments[].comments[0] — dedup inspects only the PR's FIRST comment; a prior marker comment is essentially never first, so cross-job and re-run dedup silently miss it and double-post; (b) --jq '.headSha'--jq '.conclusion'run_head becomes null, never equals the PR head SHA, and the stale-head guard skips EVERY pull_request_target fallback — suppressing the comment on exactly the path the job exists for. Witness: probe applied both mutants to the YAML one at a time — BASE 135/135, MUTANT(a) 135/135, MUTANT(b) 135/135; grep confirms no positive assertion references .comments[], --json comments, --json headSha or --jq '.headSha'.

Suggested fix: route the stub's comments listing through real JSON and the caller's --jq argument (e.g. jq -n --rawfile b "$COMMENTS_FILE" '{comments:[{author:{login:"qwen-code-ci-bot"},body:$b}]}' then apply the passed filter with jq), or add an executed test that runs the extracted --jq program with real jq against a gh-shaped fixture — with the matching comment SECOND, to pin .comments[] iteration.

中文说明

stub gh 按子串路由、并不顾 --json/--jq 参数直接 cat 纯文本 fixture,因此兜底 job 的 dedup jq 过滤器与 head 守卫查询在整个套件中从未被执行——只有过滤器的三个子串被静态钉住。该测试块自己的注释宣称"还原任何 hunk 都必须在这里失败",但查询形状的变异可以通过。注意:线上查询目前是正确(已对真实 gh 验证)的,所以这是测试保真度缺口,而非线上 bug。故障场景:两个已演示的可存活变异:(a) .comments[].comments[0]——dedup 只看 PR 的第一条评论,而标记评论几乎从不排在第一,跨 job 与重跑去重因此静默失效、导致重复发布;(b) --jq '.headSha'--jq '.conclusion'——run_head 变为 null,永远不等于 PR head SHA,陈旧 head 守卫会跳过所有 pull_request_target 兜底——恰恰在该 job 存在的路径上把评论吞掉。证据:探针将两个变异分别应用到 YAML——BASE 135/135、变异(a) 135/135、变异(b) 135/135;grep 确认没有任何正向断言引用 .comments[]--json comments--json headSha--jq '.headSha'。建议修复:让 stub 的评论列表走真实 JSON 并应用调用方的 --jq 参数,或补一个执行型测试,用真实 jq 对 gh 形状的 fixture 运行提取出的 --jq 程序——把匹配评论放在第二条,以钉住 .comments[] 的迭代。

— qwen3.8-max via Qwen Code /review (v0.21.12)

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.

Deferred to the next round, not dropped. This round implements eight findings — two of them source changes (the in-job dedup guard and the cancelled gate) plus six test additions — and the test-line budget for this window is already committed to those pins; the stub-gh rework (or an executed-jq test with the matching comment second) is the largest remaining harness change and deserves its own round. Two notes shape the follow-up: the live queries are verified correct (as this finding itself states — a test-fidelity gap, not a live bug), so nothing ships unprotected in the meantime; and the preferred shape is the finding's second option, an executed test that runs the extracted --jq program with real jq against a gh-shaped fixture, since rewiring the shared stub's routing touches all twelve existing scenarios.

中文说明

延期到下一轮,不是丢弃。本轮实现了 8 条发现——其中两条是源码变更(job 内去重守卫与 cancelled gate),六条是测试新增——本窗口的测试行预算已全部投入这些钉住;stub gh 的重构(或"把匹配评论放在第二条、用真实 jq 执行提取出的 --jq 程序"的执行型测试)是剩余最大的框架改动,值得单独一轮处理。两点说明决定跟进方式:线上查询已验证正确(该发现本身也指出这是测试保真度缺口、非线上 bug),因此在此期间没有任何行为处于无保护状态;首选形态是该发现的第二个选项——执行型测试,因为它不需要改动共享 stub 的路由(那会触及全部 12 个既有场景)。

Comment on lines +1699 to 1701
# dedupes on this marker plus this run's URL.
body="$(printf '%s\n\n%s' "$FALLBACK_MARKER" "$body")"
gh pr comment "$PR_NUMBER" \

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.

[Suggestion] R2-11 (carried from round 2, ruled still standing at this commit): the ordering invariant that this step prepends $FALLBACK_MARKER to the body BEFORE gh pr comment is pinned only by existence assertions (toContain of the printf line) — grep across every test file at HEAD finds no ordering assertion for this step, despite the round-2 reply stating the one-line ordering assertion would ship. The fallback job's side IS pinned (the executed test asserts posted.startsWith(marker)), but this in-job step is not executed by the harness. — Failure scenario: a regression moving the printf below gh pr comment keeps every test green (toContain still matches) and ships fallback comments without the marker on the in-job path — breaking the cross-job and re-run dedup that keys on it. Witness: grep at HEAD — FALLBACK_MARKER appears only in existence assertions and env injection; no indexOf/toBeLessThan ordering assertion exists anywhere.

Suggested fix: add the one-line ordering assertion the round-2 thread already proposed — an established pattern in this suite:

expect(
  inJobStep.run.indexOf(`body="$(printf '%s\\n\\n%s' "$FALLBACK_MARKER" "$body")"`),
).toBeLessThan(inJobStep.run.indexOf('gh pr comment'));
中文说明

R2-11(第 2 轮携带,本轮代码核实仍然成立):这一步在 gh pr comment 之前把 $FALLBACK_MARKER 前置到正文的次序不变量,目前只被存在性断言(对 printf 行的 toContain)钉住——在 HEAD 处 grep 全部测试文件都找不到针对该步的次序断言,尽管第 2 轮回复声称这条一行次序断言会随本轮推送。兜底 job 一侧已有钉住(执行型测试断言 posted.startsWith(marker)),但 job 内这一步并未被执行型框架覆盖。故障场景:把 printf 挪到 gh pr comment 之后的回归能让所有测试保持绿色(toContain 依然匹配),并在 job 内路径发出没有标记的兜底评论——破坏以标记为键的跨 job 与重跑去重。证据:HEAD 处 grep——FALLBACK_MARKER 只出现在存在性断言与 env 注入中,任何地方都没有 indexOf/toBeLessThan 次序断言。建议修复:补上第 2 轮线程已提出的一行次序断言(本套件的既有模式,见代码块)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

@wenshao

wenshao commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 236 passed · 0 failed · 236 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:236 通过 · 0 失败 · 236 总计

Verification report

PR #9255 Deep Verification — fix(ci): keep a fallback comment when the PR review runner dies

Verdict: findings — all 236/236 scripted assertions passed (0 fail); the central claim is proven load-bearing by execution. Two concrete, measured findings for reviewer attention (F1 cross-workflow census gap, F2 roll-out transition duplicate); neither is a regression in the changed code.

Verified head: 95ada4830bd50cf6bc1fb4a7cba363beb0d4b2b5 (merge-ref checkout, base tip 337da2143c).

中文摘要
  • 结论:findings(236/236 断言全部通过,无失败;中心结论经执行级 A/B 证实有效)。
  • A/B 结论:base 侧不存在 fallback-comment job,也没有任何 marker(结构证明 + 6 项断言);head 侧 in-job 评论体与 base 逐字节只差 marker 前缀;事故场景(review job 死亡、无人发评论)在 head 侧由新 job 实发一条带 marker 的兜底评论(16 场景 × 50 断言全绿)。
  • Findings
    • F1(中低)qwen-autofix.yml 的两处"可操作反馈"统计(BOT_COMMENT_FILTER 与 NEWEST 水位线计算)的排除列表都没有新 marker qwen-review-fallback。实测:兜底评论会被计为可操作反馈(计数 1→2)并推进水位线(04:00→06:00),可能为死掉的 review 触发一整轮 autofix agent。修复是在两个交替式中各加一个词(不同文件,建议跟进 PR),修复后的过滤行为已实测验证。
    • F2(低/一次性):marker 去重对合并前旧代码发出的评论不可见——同一 run 的"旧代码已发评论 → 新代码重跑"会再发一条重复评论(场景 16 实测)。仅影响合并过渡窗口内的重跑。
  • 未覆盖:yamllint(容器无 pip,环境性缺失,有证据);逐 commit 验证(shallow depth 2,3 个 commit 仅末个可达,验证的是聚合 diff);真实 GitHub 发帖的端到端(无 token;新 job 尚无生产产物可供校准);gh pr view --json comments 超一页分页;真实 self-hosted runner 上 root 属主目录的 chown 修复(本容器以 chmod 路径做了效果级验证);运行中途的权限损坏(PR 自身已声明超出范围)。

Scope selection

Central claim: when the review job dies or fails without posting its in-job fallback comment, the new fallback-comment job posts exactly one explanatory comment — deduped against an already-posted comment for the same run, never suppressed by other comments (ack, other runs, participants), fail-closed on lookup failures.

Secondary claims: (1) the preflight health probe detects unwritable runner directories, repairs when possible, and fails fast with a clear diagnostic otherwise; (2) the gate opens on any upstream failure that marks review-pr skipped, and never on skips or /resolve runs.

Budget went to: an executed A/B against the base build, two mock-free harnesses executing the real extracted bash (a semantics-faithful gh stub that runs the caller's actual --jq programs, and real directory trees for the probe), the targeted suite gate, an 8-mutant vacuity matrix, and the census measurement behind F1.

Central claim — A/B table

cell environment observable oracle base (337da2143c) head (95ada483)
Incident: review job dies before any comment workflow structure fallback-comment job exists absent (job list: precheck-pr, ack-review-request, review-config, delay-automatic-review, authorize, review-pr, resolve-pr; no marker anywhere) present, marker env defined once
In-job fallback body real extracted step bash vs stub gh posted body bytes posts guidance, no marker posts marker + \n\n + byte-identical base body (03-base-ab-injob.png)
Dead-run PR gets an explanation executed Post fallback comment step gh pr comment count + body 0 (nothing exists to post) 1, marker-headed, run-linked (01-… scenario 01)
Roll-out transition: base-shape comment exists head dedup vs base-shape fixture post/suppress n/a posts a duplicate (measured, see F2)

Structural arm: 6/6 assertions (base-structure.sh); body-diff arm: 6/6 (base-injob.mjs). Witness: 03-base-ab-injob.png.

Executed scenario matrix (head, 01-fallback-scenario-matrix.png)

The affected suite at head — 135/135 tests green in 4.13 s — is witnessed by 00-suite-gate-head.png.

16 scenarios, 50 scripted assertions, all pass — including the ones the PR's own harness cannot reach: my stub gh applies the caller's real --jq filter to full payloads, so the author-scope + marker filter execute, whereas the PR's stub pre-applies the filter semantics and never runs it.

# scenario expected observed
01 incident: ack + human + planted-marker + other-run comments, no same-run fallback post once, marker-headed, run-linked
02 same-run bot fallback exists suppress, exit 0
03 only run 123450's fallback exists (anchoring: id superstring of 12345) post
04 only the ack comment (links run URL, no marker) post
05 participant-planted marker + same-run URL post (author scope defeats planting)
06 dedup lookup fails persistently exit 1, no post, exactly 3 attempts
07 comments listing fails persistently exit 1, no post, 3 full re-resolutions
08 lookup fails twice, succeeds on 3rd recover, post once
09 PR state lookup fails exit 1, no post
10 PR merged meanwhile exit 0, no post
11 pull_request_target, head moved skip before any comment lookup
12 issue_comment run, heads differ guard not applied, post
13/14 pull_request_target, run-head / pr-head lookup fails degrade to POSTING ✅ ✅
15 PR number undeterminable exit 0, zero gh calls
16 base-shape (marker-less) same-run comment not deduped → duplicate (F2) ✅ (as designed today)

Health probe (head, 02-health-probe-matrix.png)

7 scenarios on real directory trees laid out as <root>/_work/<owner>/<repo>, 19 assertions, all pass: healthy layout silent + exit 0; broken $HOME without sudo → warning + ::error:: + exit 1; broken + repairable → "repaired", exit 0; _diag present-and-broken is probed and named in the error (proving three-level resolution); _diag absent is skipped; unset RUNNER_TEMP aborts loudly via ${RUNNER_TEMP:?}; broken runner root names the root itself, not _work. Residual: ownership-corruption repair (chown on a root-owned dir) was validated at effect level via the chmod path only — real passwordless-sudo escalation is not available in this container.

Gate enumeration (static, complete)

review-pr needs {review-config, delay-automatic-review, authorize}; authorize needs {precheck-pr}; delay-automatic-review needs {authorize}. The transitive failure closure is exactly the four jobs the gate enumerates plus review-pr itself — complete. authorize.if only admits PR-scoped events (github.event.issue.pull_request for comments), so PR_NUMBER can never resolve to a plain issue through any gate-opening path; cancelled runs yield 'cancelled' results, which the failure-keyed gate correctly ignores.

Vacuity / mutation matrix (04-mutation-matrix.png)

8 single-hunk mutants of the workflow, each applied → suite run → restored clean (git-asserted):

mutant change PR suite classification
M1 anchor drop ) from the dedup run-URL match KILLED ('another run's fallback' executed test) pinned
M2 diag polarity if [ -d _diag ]if [ ! -d _diag ] KILLED (structural polarity test) pinned
M3 gate keying == 'failure'!= 'success' KILLED pinned
M4 author scope drop select(.author.login == "$bot_login") KILLED pinned
M5 resolve exclusion drop the issue_comment /resolve guard KILLED (1 failed | 134 passed) pinned
M6 two levels ../../..../.. KILLED pinned
M7 backoff sleep 10sleep 1 SURVIVED coverage gap — the bounded retry count is pinned (3 attempts asserted); the delay value is not. Not dead code: the sleep executes in production.
M8 job timeout timeout-minutes: 54 SURVIVED coverage gap — job-level cap not pinned. Not dead code: Actions enforces it.

Positive controls: the suite's 6 kills above, and my independent harness run against M1 — which turns scenario 03 red (and cascades scenario 01's post assertions) — quoted beside the survivors.

Corrections

None — no inaccurate claims from earlier rounds needed correcting. (One correction of my own harness is disclosed under Methodology.)

Findings

F1 — qwen-autofix censuses count the new fallback comment as actionable feedback (medium-low)

The PR adds a new bot comment type but qwen-autofix.yml's two comment censuses do not know about it:

  1. BOT_COMMENT_FILTER (line 3581) excludes known non-actionable bot markers from N_ISSUE_COMMENTS; qwen-review-fallback is absent.
  2. The NEWEST-watermark computation (line ~4573) carries its own inline marker alternation; also absent.

Measured by executing the censuses' exact jq programs (sliced verbatim from qwen-autofix.yml) with the workflow's own variable values (05-autofix-census-finding.png, harness/autofix-census.mjs):

probe current filter without fallback comment patched filter (+marker)
actionable-feedback count 2 (fallback counted) 1 1
NEWEST watermark 2026-08-16T06:00 (advanced by fallback) 04:00 04:00

Consequence: on a PR inside the autofix fleet whose review dies, the fallback comment alone selects the PR for a review-address round — a full agent cycle spent "addressing" a failure notification. The PR description's risk section says "other workflows on the same runner pool are untouched" — true for the pool, but the comment stream is shared state, and this cost is one the description did not name.

Reproduce: node tmp/pr9255-verify-20260816-080753/harness/autofix-census.mjs

Suggested fix (measured, different file — follow-up PR or amend)

Add qwen-review-fallback| to both alternations in qwen-autofix.yml (the BOT_COMMENT_FILTER assignment and the inline test("&lt;!\-\- (...) ") in the NEWEST computation). The patched-filter cells above are the measurement: with the token added, the fallback comment is excluded from both the count and the watermark while every other fixture behaves identically (positive control in the same harness: the qwen-triage-marked comment is excluded by the current filter, the human MEMBER comment is counted). No test currently pins the full list — scripts/tests/qwen-autofix-workflow.test.js spot-checks it (toContain('qwen-triage'), toContain('qwen-review-suggestion-summary') near line 7897), so the fix should land with a matching toContain('qwen-review-fallback') there; without that fixture the axis stays unpinned.

F2 — roll-out transition mints one duplicate on same-run re-runs (low, one-time window)

Dedup matches marker + run URL, but base-version in-job comments carry no marker. Measured (scenario 16 + base-injob.mjs): a run that posted its in-job comment under base code, then fails again and re-runs under head code (same run_id), posts a second fallback comment. Self-limiting: it only affects re-runs of runs that started before merge, and the Risk & Scope section already accepts "one extra fallback-style comment" for crash → re-run sequences — but that acceptance was argued from the re-run posting behavior, not from this marker-blindness, so naming it explicitly.

Reproduce: node tmp/pr9255-verify-20260816-080753/harness/fallback-matrix.mjs (scenario 16).

Not covered

  • yamllint — environmental: this container ships Python 3.11 with no pip module, no ensurepip, and pip3 install --user (the repo installer's path) fails with permission denied; the pinned yamllint 1.35.1 could not be installed. actionlint 1.7.12 (the repo's pinned version, proven live against a planted violation) parses the whole workflow and passes; YAML parse is additionally proven by every harness's extraction.
  • Per-commit verification — the checkout is depth 2; of the PR's 3 commits (metadata snapshot) only the head 95ada483 is locally reachable, so per-commit attribution was out of reach. The aggregate HEAD^1..HEAD diff is what every cell above verifies.
  • Live end-to-end posting — no GitHub token exists in this sandbox and the fallback job has never run in production, so the replay of the new step is uncalibrated against a real emitted artifact (there is none yet); calibration here is the structural A/B plus the byte-diff of the in-job body. A stub gh encodes the documented API shapes (comments[].author.login, --jq application); real-API pagination of gh pr view --json comments beyond one page is likewise unverified — on very long-lived PRs an old marker comment could sit outside the fetched window (re-runs dedup on recent comments, so practical risk is low).
  • chown repair of root-owned directories on a real runner — this container has no sudo binary and we run uid 1000; repair was proven via the chmod path with an effect-faithful shim, and the probe's sudo -n chown invocation was observed, but actual passwordless-sudo escalation is a runner-side property.
  • Hosted-fallback geometry — on ubuntu-latest fallback runners three levels above the workspace is the runner home; reasoned from layout, not measured (the probe is harmless there: fresh VM, everything writable).
  • Mid-run corruption — scoped out by the PR itself; the probe says so in its own comment.
  • Repo-wide test gate — only the affected suite (scripts/tests/qwen-pr-review-workflow.test.js, 135 tests) ran, per targeted-gate policy.

Methodology

Environment: the CI verify container itself (node:22-bookworm lane runtime), uid 1000, node v22.23.2, bash 5.2.15, gh 2.97.0 (unauthenticated), jq 1.6; npm ci + npm run build pre-done at the merge ref. Runtime facts measured in-place ($RUNNER_TEMP = /__w/_temp here, no sudo/zstd/shellcheck binaries shipped) rather than read from YAML.

Each run: block under test was extracted verbatim through a YAML parser (harness/extract.mjs) into harness/*.sh, checked with bash -n (proven live), the repo-pinned shellcheck 0.11.0 (zero findings above note: severity; 84 style notes of the same classes the repo's own scripts carry), and repo-pinned actionlint 1.7.12 on the full workflow (clean; proven live with a planted expression violation). The repo's shellcheck gate only scans standalone .sh files — workflow-embedded bash is never shellchecked by it, and actionlint's shellcheck integration is explicitly disabled in scripts/lint.js — a pre-existing coverage observation, not a PR defect.

The fallback-step harness (harness/fallback-matrix.mjs) executes the step's real bash via bash --noprofile --norc against a stub gh whose fidelity choice is the point: it runs the caller's own --jq program with real jq over realistic fixtures (mixed authors, ack comment, planted marker), so the dedup filter executes rather than being assumed — the PR's own executed tests stub this away. The health harness (harness/health-matrix.mjs) breaks real directories via mode bits and observes the extracted probe. The vacuity round mutated the workflow in place, ran the suite, and restored with git checkout after each mutant (clean-tree asserted every time); M1 additionally ran through my harness as positive control. The full round as printed — killed-by test names, survivors, and the positive control — is 04-mutation-matrix.png. One incident of my own: the first positive-control pass ran green because the harness executed a stale pre-extracted copy of the step instead of the live workflow — the exact "scenario never reached the code under test" failure; fixed to extract per run, after which M1 correctly turned scenario 03 red and the restored head returned to 50/50.

Raw logs: logs/ (per-mutant vitest logs, shellcheck output, census log, harness-on-M1); harnesses and counts in harness/ and counts/; base worktree removed after the A/B cells were captured.

Evidence images

00-suite-gate-head

01-fallback-scenario-matrix

02-health-probe-matrix

03-base-ab-injob

04-mutation-matrix

05-autofix-census-finding

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot
qwen-code-ci-bot dismissed their stale review August 16, 2026 08:03

Dismissed by triage re-run: this round-2 /review was submitted on an earlier head (0ab52be). Its one Critical (R2-1: /resolve exclusion only covered workflow_dispatch) has since been fixed and test-pinned at 95ada48, and round 3 re-audit left only suggestions. See the stage=3 triage comment.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round 3 — PR #9255

Commit: 3cad099256fix(ci): close round-3 review gaps in the fallback-comment defenses (#9255)

Eight of the nine round-3 findings are resolved in code; one (R3-8, the stub-gh jq-fidelity rework) is deferred to the next round with a reply on its thread. Every new or changed test was mutation-checked: each named mutant was applied to the workflow and confirmed to fail the suite (8/8 caught), and the tree was restored to green afterwards. No base-conflict work (--conflict false, no merge performed).

Feedback dispositions

Finding Disposition Change
R3-1 (fallback body misnames the failing component) Resolved The fallback job's body no longer claims the review job failed — on the four upstream-failure paths review-pr was skipped, never ran. The body now reads "The review pipeline failed before a review could be posted." No test pinned the old wording (verified by grep).
R3-3 (health probe pinned only statically) Resolved Added an executed harness runHealthProbe that runs the step's REAL bash against a fake runner tree with a stub sudo, covering all four requested paths: healthy (exit 0, no warning/repair noise), repairable (exit 0 + "repaired write access"), unrepairable (exit 1 + both ::error:: lines), and _diag-absent (exit 0 — an inverted [ -d ] guard would fail fast on a healthy runner). Mutant controls: status=1 moved to the first failed touch fails the repairable test; dropping the post-repair re-probe fails the unrepairable test.
R3-4 (retry-loop reset lines unobservable) Resolved Added the comments_lookup_fail scenario exactly as suggested: gh api user succeeds, the comments listing exits 1 on all attempts. Asserts status === 1, posted === '', ::error:: emitted. Mutant control: deleting the two post-failure resets posts on the failed listing and fails the test.
R3-5 (RUN_URL env read by no test) Resolved New test pins the fallback step's RUN_URL env to end exactly at the run id (/\/actions\/runs\/\$\{\{ github\.run_id \}\}$/) and asserts it is byte-identical to the in-job step's RUN_URL. Mutant control: an /attempts/1 suffix fails the test.
R2-9 (cross-job dedup one-directional, carried from round 2) Resolved The in-job step now runs the sibling's guard before building/posting its body: bot-login-scoped lookup of marker + actions/runs/<run id>) comments; on a match it skips ("already exists"), and a FAILED lookup defers to the fallback-comment job (exit 0) instead of posting — the job result stays failure, so the fallback job still runs and re-checks with retries, per the finding's suggested semantics. Executed tests cover all three branches via the harness's new useInJobStep mode (the stub gained a *state,headRefOid* route answering TSV, placed before the *headRefOid* route). Mutant control: deleting the guard double-posts and fails the dedup test.
R3-6 (gate admits only 'failure', not job-timeout 'cancelled') Resolved Added needs.review-pr.result == 'cancelled' to the gate disjunction and documented the chosen semantics in the job header comment (a run-level cancel cancels this queued job too, so a live evaluation seeing 'cancelled' is overwhelmingly the job-timeout case; the residual manual single-job cancel gets benign retry guidance). The gate test pins the new line. Only review-pr's cancellation opens the gate — a manually cancelled upstream job stays silent by design.
R3-7 (identity agreement unpinned) Resolved The scopes the dedup… test now asserts both steps' GH_TOKEN env is ${{ secrets.CI_BOT_PAT }} (parsed env values carry no YAML quotes, so the suggested assertion was adjusted to the parsed shape). Mutant control: swapping the in-job step to GITHUB_TOKEN fails the test.
R3-8 (stub gh never executes the jq filter) Deferred to next round See the reply on its thread: the live queries are verified correct (the finding itself says this is a test-fidelity gap, not a live bug), and this round's test budget is already committed to the eight higher-value pins above. The finding's suggested executed-jq test remains the intended shape for the follow-up.
R2-11 (in-job marker-prepend ordering unpinned, carried from round 2) Resolved Added the ordering assertion promised in round 2 — indexOf(printf line) < indexOf('gh pr comment') on the in-job step, the suite's established pattern. Mutant control: moving the printf below gh pr comment fails it (and the new executed in-job post test).

Also disclosed, unchanged: the reverse audit did not converge within its round cap, and actionlint's embedded-shell source mapping for this workflow remains unsupported (tool limitation, carried from the review's own disclosure).

Verification

Commands actually run this round (all from the PR checkout at commit 3cad099256 unless noted):

  • npm run build — passed
  • npm run typecheck — passed (tsc --noEmit across workspaces)
  • npm run lint — passed (exit 0, no errors/warnings)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js — 144 passed (144); baseline before this round was 135 passed
  • npx prettier --check on both changed files — passed
  • bash -n on the extracted Post fallback comment on failure, Post fallback comment, and Verify runner directory health run blocks — passed
  • Mutation battery (8 mutants, one at a time, suite re-run per mutant, tree restored): M1 resets-deleted, M2 false-fail-fast, M3 missed-fail-fast, M4 injob-guard-deleted, M5 run-url-suffix, M6 injob-token-swap, M7 printf-moved-below-post, M8 cancelled-dropped — ALL CAUGHT
  • Integration tests / npm run bundle — not applicable: the touched behavior lives entirely in the workflow YAML and its scripts-test suite, nothing is exercised through the bundled CLI
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

Autofix 第 3 轮 — PR #9255

提交:3cad099256fix(ci): close round-3 review gaps in the fallback-comment defenses (#9255)

第 3 轮共 9 条发现,其中 8 条已在代码中解决;1 条(R3-8,stub gh 的 jq 保真度重构)延期到下一轮,并已在其线程中回复说明理由。每一条新增或修改的测试都做了变异验证:将每个点名的变异逐一应用到 workflow 上并确认套件失败(8/8 全部被捕获),随后恢复测试树至全绿。无 base 冲突处理(--conflict false,未执行 merge)。

反馈处置

发现 处置 变更
R3-1(兜底正文把出错组件指错) 已解决 兜底 job 的正文不再声称 review job 失败——在四条上游失败路径上 review-pr 是被跳过、从未运行的。正文改为 "The review pipeline failed before a review could be posted."。已用 grep 确认没有测试钉住旧措辞。
R3-3(健康探测只有静态钉住) 已解决 新增执行型框架 runHealthProbe:用 stub sudo 在伪造的 runner 目录树上真实执行该步的 bash,覆盖要求的全部四条路径——健康(exit 0、无 warning/修复噪音)、可修复(exit 0 + "repaired write access")、不可修复(exit 1 + 两条 ::error::)、无 _diag(exit 0——若 [ -d ] 守卫方向写反,健康 runner 也会误快速失败)。变异对照:把 status=1 挪到首次 touch 失败后会使"可修复"测试失败;删掉修复后的复探会使"不可修复"测试失败。
R3-4(重试循环的重置行不可观测) 已解决 按建议新增 comments_lookup_fail 场景:gh api user 成功、评论列表三次尝试全部失败。断言 status === 1posted === ''、输出含 ::error::。变异对照:删除两行失败后重置会在失败的列表之上发布评论,该测试失败。
R3-5(RUN_URL env 无测试读取) 已解决 新增测试把兜底步骤的 RUN_URL env 钉住为恰好在 run id 处结尾(/\/actions\/runs\/\$\{\{ github\.run_id \}\}$/),并断言它与 job 内步骤的 RUN_URL 逐字节一致。变异对照:追加 /attempts/1 后缀会使测试失败。
R2-9(跨 job 去重单向,第 2 轮携带) 已解决 job 内步骤现在在构造/发布正文前执行与兜底 job 同款的守卫:按机器人登录作用域查询"标记 + actions/runs/<run id>)"的评论;命中则跳过("already exists"),查询失败则让位给 fallback-comment job(exit 0)而不是发布——job 结果仍为 failure,兜底 job 仍会运行并带重试地复查,正是该发现建议的语义。执行型测试经框架新增的 useInJobStep 模式覆盖全部三个分支(stub 新增 *state,headRefOid* 路由、以 TSV 应答,并置于 *headRefOid* 路由之前)。变异对照:删除该守卫会重复发布并使去重测试失败。
R3-6(gate 只接受 'failure',不含 job 超时的 'cancelled' 已解决 gate 析取中加入 needs.review-pr.result == 'cancelled',并在 job 头注释中记录所选语义(run 级取消会连同这个排队中的 job 一起取消,因此活着评估 gate 并看到 'cancelled' 几乎必然是 job 超时;残余的手动单 job 取消只会得到一条良性的重试指引)。gate 测试钉住新增行。仅 review-pr 的取消会打开 gate——手动取消上游 job 仍保持静默(设计如此)。
R3-7(身份一致性无钉住) 已解决 scopes the dedup… 测试现在断言两个步骤的 GH_TOKEN env 均为 ${{ secrets.CI_BOT_PAT }}(解析后的 env 值不含 YAML 引号,故对建议中的断言按解析形状做了调整)。变异对照:把 job 内步骤换成 GITHUB_TOKEN 会使测试失败。
R3-8(stub gh 从不执行 jq 过滤器) 延期到下一轮 见该线程的回复:线上查询已验证正确(该发现本身也说明这是测试保真度缺口、非线上 bug),本轮的测试预算已全部投入上面 8 条更高价值的钉住。该发现建议的执行型 jq 测试仍是后续跟进的目标形态。
R2-11(job 内标记前置次序无钉住,第 2 轮携带) 已解决 补上第 2 轮承诺的次序断言——对 job 内步骤断言 indexOf(printf 行) < indexOf('gh pr comment'),即本套件既有模式。变异对照:把 printf 挪到 gh pr comment 之后会使其失败(新增的 job 内执行型发布测试同样失败)。

同时如实披露(未变化):reverse audit 未能在其轮数上限内收敛;actionlint 对该 workflow 的 embedded-shell source mapping 仍不支持(工具限制,沿用审查自身的披露)。

验证

本轮实际执行的命令(除特别说明外均在 PR checkout 的 3cad099256 提交上运行):

  • npm run build — 通过
  • npm run typecheck — 通过(全 workspace 的 tsc --noEmit)
  • npm run lint — 通过(exit 0,无 error/warning)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js — 144 通过(144);本轮前基线为 135 通过
  • 对两个改动文件执行 npx prettier --check — 通过
  • 对提取出的 Post fallback comment on failurePost fallback commentVerify runner directory health 三个 run 块执行 bash -n — 通过
  • 变异批次(8 个变异,逐个应用、逐个重跑套件、随后恢复测试树):M1 删除重置行、M2 误报快速失败、M3 漏报快速失败、M4 删除 job 内守卫、M5 run-URL 后缀、M6 job 内 token 替换、M7 printf 挪到发布之后、M8 删除 cancelled — 全部被捕获
  • 集成测试 / npm run bundle — 不适用:本次改动的行为完全位于 workflow YAML 及其 scripts 测试套件中,没有任何内容经捆绑后的 CLI 验证
  • npm run generate:settings-schema — 不适用:未改动任何 settings 源

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

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


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 309 passed · 0 failed · 309 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:309 通过 · 0 失败 · 309 总计

Verification report

PR #9255 Deep Verification (round 4) — fix(ci): keep a fallback comment when the PR review runner dies

Verdict: findings — all 309/309 scripted assertions passed (0 fail); the central claim is re-proven load-bearing by execution at the new head. The two carried-over findings both stand, re-measured from scratch (F1 now spans six census sites, not two); no new blocking finding emerged from the round-3 delta.

Verified head: 3cad099256a97036ee3c894f3e09b1582f451dbc (merge-ref checkout; base tip 3186d4ea67).

中文摘要
  • 结论:findings(309/309 脚本断言全部通过;中心结论在新 head 上再次经执行级验证证实)。本轮为跟进轮:上一轮(head 95ada483)的两项发现均仍然存在,已在新 head 上全部重新实测(未沿用旧数据)。
  • A/B 结论:base 侧无 fallback-comment job、无 marker、无健康探测(结构证明 15/15);head 侧 in-job 评论体与 base 逐字节只差 marker 前缀;事故场景在 head 侧实发一条带 marker 的兜底评论(71 场景断言全绿,含 in-job 交叉去重与 defer 语义)。
  • Findings
    • F1(中低,维持)qwen-autofix.yml六处评论过滤(BOT_COMMENT_FILTER 变量 + 五处内联交替式)都缺少新 marker qwen-review-fallback。实测兜底评论被计为可操作反馈(1→2)、推进水位线(04:30→06:00)、进入 live 复查计数(1→2);打补丁后的过滤行为已实测。修复需在另一文件改六处,建议跟进 PR。
    • F2(低/一次性,维持):marker 去重对合并前旧代码发出的无 marker 评论不可见——同一 run 的"旧代码已发 → 新代码重跑"会再发一条重复(job 侧与 in-job 侧两条路径均已实测)。
  • 突变矩阵:共 16 次单 hunk 突变体应用(13 个基础突变体 + 2 个精化再突变 + 1 次独立 harness 交叉验证),其中 13 次被套件或独立 harness 以预期断言杀死(含锚点、极性、gate 键控、作者作用域、resolve 排除、三层路径、in-job 去重块、defer→fail-open、RUN_URL 形状、marker 值);2 个存活(重试退避值、job 5 分钟上限)均为覆盖缺口而非死代码,且都是上一轮已报告的存活项的再测量;1 次作废(突变误中他 job 的同名行,由 M9-fixed 取代)。
  • 未覆盖:yamllint(容器无 pip3,环境性,有证据);逐 commit 验证(depth 2,仅聚合 diff 可验证,第 4 个 commit 的增量无法用 git 分离);真实 GitHub 发帖端到端(无 token;新 job 无生产产物可校准);gh 评论分页超一页;真实 sudo/chown 提权(以 chmod 效果级 + sudo 缺失路径验证)。

Previous-round finding status (follow-up round)

The previous round verified head 95ada483 (commit 3 of 4). Since then commit 4 (3cad0992, "close round-3 review gaps") landed and the base moved (337da2143c3186d4ea67). 95ada483 is not locally reachable (depth-2 checkout), so every measurement below was rebuilt and re-executed at the new head from scratch — nothing was carried forward by diffing the old report. The suite grew 135 → 144 tests between rounds, corroborating that the round-3 commit touched the test surface; the aggregate HEAD^1..HEAD diff is what every cell verifies.

# finding (round 3) severity status at head 3cad0992
F1 qwen-autofix censuses count the fallback comment as actionable feedback medium-low stands — re-measured, surface widened: six filter sites lack the marker (the round-3 report measured two; a full sweep of the file at the current base finds five inline alternations, not one). Repro: node tmp/pr9255-verify-20260816-094340/harness/autofix-census.mjs (22/22).
F2 roll-out transition mints one duplicate on same-run re-runs (base-shape comments carry no marker) low stands — re-measured on both dedup paths: fallback job (matrix scenario 17) and in-job step (scenario 27) both post a duplicate over a marker-less same-run comment.

No declined/deferred rows existed in the previous round.

Scope selection

Central claim: when the review job dies or fails without posting its in-job fallback comment, the new fallback-comment job posts exactly one explanatory comment — deduped against an already-posted comment for the same run, never suppressed by other comments (ack, other runs, participant-planted markers), fail-closed on lookup failures.

Secondary claims: (1) the preflight health probe detects unwritable runner directories, repairs when possible, fails fast otherwise; (2) the gate opens on any upstream failure that marks review-pr failed/skipped, and never on skips or /resolve runs.

Round-3-delta probes (new this round): the in-job cross-job dedup block — same-run suppression, defer-on-failure semantics (exit 0, never fail-open), the anchor on the in-job side — plus mutants of both dedup sites and of the defer branch, which the previous round had not individually exercised.

Budget went to: the executed scenario matrix (71 assertions), structural + byte-diff A/B, health matrix, gate-completeness analysis, the autofix census re-measurement, the 144-test suite gate, a 13-mutant matrix plus two refined re-mutants, and the lint gates.

Central claim — A/B table

cell environment observable oracle base (3186d4ea) head (3cad0992)
Incident: review job dies before any comment workflow structure fallback-comment job / marker / health step exist all absent; qwen-review-fallback literal nowhere in the file present; job-list delta exactly +fallback-comment (02-health-gate-base-ab.png, base-ab 15/15)
In-job fallback body real extracted step bash vs stub gh posted body bytes posts guidance, no marker posts marker + \n\n + byte-identical base body (01-fallback-scenario-matrix.png, cells 29/30)
Dead-run PR gets an explanation executed Post fallback comment step gh pr comment count + body 0 (nothing exists to post) 1, marker-headed, run-linked, retry guidance (scenario 01)
Roll-out transition: base-shape comment exists head dedup vs marker-less fixture post/suppress n/a posts a duplicate (F2, scenarios 17 + 27)

Witnesses: 02-health-gate-base-ab.png (structure), 01-fallback-scenario-matrix.png (behavior, incl. the byte-diff cells).

Executed scenario matrix (head, 01-fallback-scenario-matrix.png)

71 scripted assertions, all pass. The stub gh applies the caller's real --jq program with real jq over full comment payloads (mixed authors, ack comment, participant-planted marker), so the dedup filter's author scope and marker test execute rather than being assumed.

# scenario expected observed
01 incident: human + ack + participant-planted marker + other-run fallback present, no same-run fallback post once, marker-headed, run-linked
02 same-run bot fallback exists suppress, exit 0
03 only run 123450's fallback exists (id is a superstring of 12345) post (anchor holds)
04 only the ack comment (links the run URL, no marker) post
05 participant-planted marker + same-run URL post (author scope defeats planting)
06 user lookup fails persistently — with a same-run comment present exit 1, no post, exactly 3 attempts, ::error::
07 comments listing fails persistently exit 1, no post, 3 full re-resolutions
08 comments listing fails twice, succeeds on 3rd recover, post once, exactly 3 attempts
09 user lookup fails twice, succeeds on 3rd recover, post
10 PR state lookup fails exit 1, no post
11 PR merged meanwhile exit 0, no post, state named
12 pull_request_target, head moved skip before any comment lookup
13 issue_comment run, heads differ guard not applied, posts; zero run-view calls
14/15 pull_request_target, run-head / pr-head lookup fails degrade to POSTING ✅ ✅
15b pull_request_target, heads equal posts
16 PR number undeterminable exit 0, zero gh calls
17 base-shape (marker-less) same-run comment not deduped → duplicate (F2) ✅ (as designed today)
18 in-job default failure body byte-pinned: marker + exact guidance
19 in-job: same-run fallback (cross-job dedup) suppress
20/21 in-job: user/comments lookup fails defer exit 0, no post, summary names the fallback job ✅ ✅
22 in-job: only another run's fallback post (in-job anchor holds)
23 in-job: state+head lookup fails fail-open skip (fallback job is the net)
24/25 in-job: PR closed / head moved past EXPECTED_HEAD_SHA skip clean ✅ ✅
26 in-job: quota failure kind quota-aware body, marker-prepended
27 in-job: marker-less same-run comment duplicate (F2 via in-job path)
28 in-job: participant-planted marker author scope holds

Health probe (head, 02-health-gate-base-ab.png)

9 scenarios on real directory trees laid out as <root>/_work/<owner>/<repo>, 21 assertions, all pass: healthy layout silent + exit 0; broken $HOME/RUNNER_TEMP/runner-root with failing sudo → ::error:: + fail fast; repairable → "repaired" + exit 0; _diag present-and-broken is probed and named in the error (three-level resolution); _diag absent is skipped; unset RUNNER_TEMP aborts loudly via ${RUNNER_TEMP:?}; sudo binary absent entirely still fails fast and clean (H9 — the || true absorbs command-not-found, diagnosis intact).

Gate enumeration (programmatic, complete — 02-health-gate-base-ab.png)

Computed the transitive needs closure of review-pr from the parsed YAML: exactly {review-config, delay-automatic-review, authorize, precheck-pr}; asserted the gate enumerates precisely that closure plus review-pr (failure for all upstreams, failure+cancelled for review-pr, never != 'success'), runs on ubuntu-latest with a 5-minute cap and pull-requests: write only, posts as CI_BOT_PAT — the same identity as the in-job step, which is what makes the author-scoped dedup see the in-job comment. 36/36 assertions.

Corrections

One correction of the previous verification round's own coverage claim (not of any review comment or PR text): the round-3 report said F1 concerned two census sites (BOT_COMMENT_FILTER and the NEWEST watermark). A full sweep of qwen-autofix.yml at the current base finds six sites carrying marker alternations — the BOT_COMMENT_FILTER variable plus five byte-identical inline alternations (lines 4573, 4659, 5013, 5112, 5212). The finding's substance is unchanged; its surface is wider, and the suggested fix must edit all six.

Findings

F1 — qwen-autofix censuses count the new fallback comment as actionable feedback (medium-low, STANDS)

The PR adds a new bot comment type (posted as qwen-code-ci-bot, which is REVIEW_BOT in qwen-autofix.yml and therefore passes every author gate); qwen-autofix.yml's six comment filters do not know its marker:

  1. BOT_COMMENT_FILTER (line 3581) — feeds N_ISSUE_COMMENTS;
    2–6. five inline test("&lt;!\-\- (…) ") alternations (lines 4573, 4659, 5013, 5112, 5212) — NEWEST watermark, live re-check census, eval feedback list, over-budget summary, and the issue-level comment context handed to the agent.

Measured by executing the censuses' jq programs sliced verbatim from the file with the workflow's own variable values (04-autofix-census-f1.png, harness/autofix-census.mjs, 22/22):

probe current filter without fallback comment patched filters (+marker, all sites)
actionable-feedback count (N_ISSUE_COMMENTS) 2 (fallback counted) 1 1
NEWEST watermark 2026-08-16T06:00 (advanced by fallback) 04:30 04:30
live re-check census (LIVE_NEW) 2 1 1

Positive controls in the same run: a qwen-triage-marked comment and the ack are excluded by the current filters; the human MEMBER comment is counted. Regex-execution across all six sites confirms the fallback body matches none of the current alternations and would match each once qwen-review-fallback| is added.

Consequence (unchanged from round 3): on a PR in the autofix fleet whose review dies, the fallback comment alone can select the PR for a review-address round — a full agent cycle spent "addressing" a failure notification — and the comment's body enters the context the agent is told to address. The PR's Risk & Scope says "other workflows on the same runner pool are untouched" — true for the pool; the comment stream is shared state, and this cost remains unnamed in the description.

Reproduce: node tmp/pr9255-verify-20260816-094340/harness/autofix-census.mjs

Suggested fix (measured; different file — follow-up PR)

Add qwen-review-fallback| to all six alternations in qwen-autofix.yml (the BOT_COMMENT_FILTER assignment at line 3581 and the five inline test("&lt;!\-\- (…) ") sites). The patched-filter cells above are the measurement: with the token added at every site, the fallback comment is excluded from count, watermark, and live census while every other fixture behaves identically. The five inline alternations are byte-identical today (asserted), so one sed-shaped edit applies to all of them — but the count is six sites, and no test currently pins the full list: scripts/tests/qwen-autofix-workflow.test.js spot-checks it (toContain('qwen-triage') etc.), so the fix should land with a matching toContain('qwen-review-fallback') there; without that fixture the axis stays unpinned.

F2 — roll-out transition mints one duplicate on same-run re-runs (low, one-time window, STANDS)

Dedup matches marker + run URL, but base-version in-job comments carry no marker. Re-measured at the new head on both dedup paths (01-fallback-scenario-matrix.png, scenarios 17 and 27): a run that posted its in-job comment under base code, then fails again and re-runs under head code (same run_id), posts a second fallback comment — whether the re-run's comment comes from the fallback job or from the in-job step. Self-limiting: only re-runs of runs that started before merge. The Risk & Scope section accepts "one extra fallback-style comment" for crash → re-run sequences, but argued it from re-run posting behavior, not from this marker-blindness — naming the mechanism explicitly, as in round 3.

Reproduce: node tmp/pr9255-verify-20260816-094340/harness/fallback-matrix.mjs (scenarios 17, 27).

Vacuity / mutation matrix (03-mutation-matrix.png)

Single-hunk mutants of the workflow at head; each applied → full suite run → restored (git status --porcelain clean after every mutant). The suite at head is 144/144 green unmutated (00-suite-gate-head.png, 5.7 s).

mutant change suite result killing test (intended assertion) classification
M1a in-job dedup pattern disabled KILLED 'in-job step dedupes on a fallback comment this run already has' pinned
M1a-clean in-job pattern → suffix match KILLED same test pinned
M1b fallback-job dedup pattern disabled KILLED 'dedupes on the marker plus this run URL' pinned
M1b-pure fallback-job ) anchor dropped, same-run kept KILLED 'still posts when only another run's fallback exists' — expected '' not to be '' pinned
M2 _diag guard polarity inverted KILLED (2 tests) 'probes the runner root three levels up…' pinned
M3 gate == 'failure'!= 'success' KILLED 'opens the gate on any upstream failure…' pinned
M4 author scope dropped from both dedups KILLED 'scopes the dedup to the authenticated bot login…' pinned
M5 issue_comment /resolve exclusion dropped KILLED 'never opens the gate on a /resolve run…' pinned
M6 runner root two levels up KILLED 'probes the runner root three levels up…' pinned
M7 in-job cross-job dedup block removed KILLED (2 tests) 'in-job step dedupes…' pinned
M8 in-job lookup failure: defer → fail-open post KILLED 'in-job step defers to the fallback job when its dedup lookup fails' pinned
M9-fixed fallback-job RUN_URL gains /attempts suffix KILLED 'pins the run-URL shape the dedup anchors on' pinned
M10 marker env value changed KILLED 'defines the fallback marker once…' pinned
M11 backoff sleep 10sleep 1 SURVIVED coverage gap — the bounded retry count is pinned (3 attempts asserted); the delay value is not. Not dead code: the sleep executes in production. (Pre-existing survivor, re-measured.)
M12 job timeout-minutes: 54 SURVIVED coverage gap — job-level cap not pinned. Not dead code: Actions enforces it. (Pre-existing survivor, re-measured.)

Positive controls: the suite's own 11 kills above; and my independent harness run against M1b-pure — scenario 03 (superstring run) goes red while scenario 02 (same-run suppression) stays green, isolating exactly the anchor's job. Two harness incidents, both adjudicated: the first M1a/M1b attempts produced quote-structure side effects in the case pattern (the ) anchor cannot be removed without moving a quote boundary in bash), so refined mutants (M1a-clean, M1b-pure) were run and killed cleanly; the first M9 attempt mutated resolve-pr's identical RUN_URL line instead of the fallback job's (survived for that reason — discarded, replaced by M9-fixed). Completeness note: the suite has no in-job other-run fixture — the in-job side of the superstring exclusion is proven by my harness scenario 22 and by the byte-identity of the two patterns, not by a suite fixture.

Bounded residuals (not findings)

  • Cross-job dedup race: if a failed run's fallback job is still in flight while a manual single-job re-run's in-job step also posts, one duplicate is theoretically possible in the seconds-wide window between the fallback job's dedup read and its post. Inherent to comment-based cross-job dedup (no atomic upsert across jobs); at most one extra comment, never a missing one.
  • Body fidelity on re-runs: when the fallback job already posted a generic comment for a run, a later in-job step on the re-run suppresses its quota/timeout-specific body in favor of no duplicate. Dedup-over-fidelity is the designed tradeoff; the run link in the standing comment still reaches the logs.
  • Manually cancelled upstream jobs (single-job cancel of authorize etc.) yield a skipped review-pr that the failure-keyed gate does not admit — silent by design of the failure keying (a run-level cancel takes the fallback job down with it; the PR's own comment documents the tradeoff).

Not covered

  • yamllint — environmental: no pip3 in this container (node scripts/lint.js --setup reports pip3: Permission denied; command -v pip3 empty), so the pinned yamllint 1.35.1 could not be installed. actionlint 1.7.12 (repo-pinned, proven live against a planted expression violation) parses the whole workflow clean; YAML parse is additionally proven by every harness's extraction.
  • Per-commit verification / round-3 delta isolation — the checkout is depth 2; of the PR's 4 commits (metadata snapshot) only head 3cad0992 is locally reachable, and 95ada483 (the previously verified head) is not, so commit 4's delta cannot be git-separated from the aggregate. Every claim above verifies the aggregate HEAD^1..HEAD diff; delta scoping used the previous report's descriptions plus the observed suite growth (135 → 144 tests).
  • Live end-to-end posting — no GitHub token exists in this sandbox and the fallback job has never run in production, so the replay of the new step is uncalibrated against a real emitted artifact (there is none yet); calibration here is the structural A/B plus the byte-diff of the in-job body. Real-API pagination of gh pr view --json comments beyond one page is likewise unverified (on very long-lived PRs an old marker comment could sit outside the fetched window; re-runs dedup on recent comments, so practical risk is low).
  • chown repair of root-owned directories on a real runner — no sudo binary in this container; repair was proven via the chmod effect path, and H9 proves the probe degrades cleanly when sudo is absent entirely. Actual passwordless-sudo escalation is a runner-side property.
  • Hosted-fallback geometry — on ubuntu-latest fallback runners three levels above the workspace is the runner home; reasoned from layout, not measured (the probe is harmless there: fresh VM, everything writable).
  • Mid-run corruption — scoped out by the PR itself.
  • Merge freshness — verified against the merge ref as fetched (base tip 3186d4ea); whether main moved after the fetch is unobservable in-sandbox.
  • Repo-wide test gate — only the affected suite (scripts/tests/qwen-pr-review-workflow.test.js, 144 tests) ran, per targeted-gate policy.

Methodology

Environment: the CI verify container itself (node:22-bookworm lane runtime), uid 1000, node v22.23.2, bash 5.2.15, jq 1.6; npm ci + npm run build pre-done at the merge ref. Runtime facts measured in-place ($RUNNER_TEMP = /__w/_temp, no sudo/pip3/zstd binaries shipped) rather than read from YAML.

Every run: block under test was extracted verbatim through a YAML parser (harness/extract.mjs) into harness/*-step.sh, checked with bash -n, and executed via bash --noprofile --norc -eo pipefail (the runner's documented default shell) with one documented pre-substitution (${{ vars.QWEN_REVIEW_MAX_TIMEOUT_MINUTES }}300 for the in-job block's sole expression context). The scenario harness (harness/fallback-matrix.mjs + harness/gh-stub.sh) drives the real bash against a stub gh whose fidelity choice is the point: it runs the caller's own --jq program with real jq over realistic fixtures, so the dedup filter executes rather than being assumed; per-kind attempt counters in the stub pin the retry counts. The health harness (harness/health-matrix.mjs) breaks real directory trees via mode bits. The census harness (harness/autofix-census.mjs) slices the jq programs line-anchored from qwen-autofix.yml and executes them under the workflow's own variable values. The mutation round mutated the workflow in place, ran the full suite per mutant, and restored with a byte-for-byte rewrite of the original content, git status --porcelain-asserted clean each time; mutants M1a/M1b/M9 required one refinement round each after cause analysis (documented above the matrix). Lint gates (05-lint-gates.png): repo-pinned actionlint 1.7.12 and shellcheck 0.11.0 installed via node scripts/lint.js --setup, each proven live against a planted violation before use; shellcheck ran with the repo's flag set over the extracted blocks. The first two harness bugs of this round (a counter snapshot read after cleanup; scenario names not matching the stub's injection keys) were found by the harnesses' own red lines, root-caused, fixed, and re-run — the green totals above are the re-runs.

Raw logs: logs/ (per-harness and per-mutant logs); harnesses in harness/; evidence captures in evidence/ produced with scripts/verify-capture.mjs.

Evidence images

00-suite-gate-head

01-fallback-scenario-matrix

02-health-gate-base-ab

03-mutation-matrix

04-autofix-census-f1

05-lint-gates

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

— qwen3.8-max via Qwen Code /review (v0.21.12)

try {
stdout = execFileSync(
'bash',
['-c', useInJobStep ? inJobStep.run : step.run],

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.

[Suggestion] The executed in-job harness runs the in-job step's run block verbatim, but that block still contains the raw GitHub Actions expression MAX_TIMEOUT_MINUTES="${{ vars.QWEN_REVIEW_MAX_TIMEOUT_MINUTES }}" — bash raises bad substitution, leaves the variable unset, and continues. — Failure scenario: a maintainer extending the harness with an in-job FAILURE_KIND: 'timeout' scenario gets silently wrong behaviour — [ "$TIMEOUT_MINUTES" -lt "$MAX_TIMEOUT_MINUTES" ] with the latter empty always takes the else branch, so the small-timeout body variant is unreachable by executed tests (verified by probe: the raw block posts the max-timeout body for both 60 and 360; substituting '300' first restores both arms). The file's own convention routes such values through env: ('Run review' does, and the expression-length suite pins it ${{ }}-free).

Suggested fix: substitute before executing, e.g. (useInJobStep ? inJobStep.run : step.run).replaceAll('${{ vars.QWEN_REVIEW_MAX_TIMEOUT_MINUTES }}', '300'), or move the value into the in-job step's env: block mirroring MAX_TIMEOUT_MINUTES_VAR in 'Run review'.

中文说明

已执行的 in-job 测试框架原样运行 in-job 步骤的 run 块,但该块仍包含原生 GitHub Actions 表达式 MAX_TIMEOUT_MINUTES="${{ vars.QWEN_REVIEW_MAX_TIMEOUT_MINUTES }}"——bash 报 bad substitution、变量保持未设并继续执行。— 故障场景:维护者若为 in-job 增加 FAILURE_KIND: 'timeout' 场景,会得到静默错误的行为——后者为空时 [ "$TIMEOUT_MINUTES" -lt "$MAX_TIMEOUT_MINUTES" ] 恒走 else 分支,小超时 body 变体无法被执行测试覆盖(探针已验证:原始块对 60 与 360 都发最大超时 body;先替换为 '300' 可恢复两个分支)。本文件既有约定是经由 env: 传值('Run review' 即如此,且表达式长度套件钉住其不含 ${{ }})。

建议修复:执行前先替换,如 (useInJobStep ? inJobStep.run : step.run).replaceAll('${{ vars.QWEN_REVIEW_MAX_TIMEOUT_MINUTES }}', '300');或将该值移入 in-job 步骤的 env: 块,仿照 'Run review' 的 MAX_TIMEOUT_MINUTES_VAR

— qwen3.8-max via Qwen Code /review (v0.21.12)

# than burning hours of review budget to die at finalize. Only catches
# corruption already present at job start; mid-run corruption is covered
# by the fallback-comment job instead.
- name: 'Verify runner directory health'

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.

[Suggestion] The only self-healing repair for runner-pool corruption lives in review-pr, the last job of the chain — downstream of the self-hosted jobs (authorize / review-config) that the same corruption kills first, so the repair is usually unreachable while the failure persists. — Failure scenario: an ecs-qwen runner with a root-owned $HOME (the exact PR #8894 trigger) kills authorize/review-config (verified: authorize runs self-hosted for same-repo heads; review-config has no needs and schedules first); review-pr is skipped without starting, so this repair never runs and the corrupted runner stays in the pool to kill the retry. The diff's own rationale concedes the trigger "can kill the chain's earlier self-hosted jobs first" — it covers that with a comment, not with repair.

Suggested fix: run the same probe (at minimum probe+repair) in the other self-hosted jobs of this workflow (authorize, review-config), or extract it as a shared preflight for every ecs-qwen job in this file.

中文说明

针对 runner 池损坏的唯一自愈修复位于链条最末的 review-pr——处在同样会被该损坏杀死的自托管 job(authorize/review-config)下游,因此故障持续期间修复通常不可达。— 故障场景:$HOME 属主为 root 的 ecs-qwen runner(正是 PR #8894 的触发条件)杀死 authorize/review-config(已核实:同仓库 head 时 authorize 跑在自托管上;review-configneeds、最先被调度);review-pr 未启动即被跳过,本修复从不执行,损坏的 runner 留在池中继续杀死重试。diff 自身的理由也承认触发条件"可能先杀死链条上更早的自托管 job"——但只以注释覆盖,未以修复覆盖。

建议修复:在本 workflow 其他自托管 job(authorizereview-config)中运行同样的探测(至少探测+修复),或抽取为本文件所有 ecs-qwen job 共享的前置检查。

— qwen3.8-max via Qwen Code /review (v0.21.12)

// incident's trigger can kill the chain's self-hosted jobs first
// (authorize / review-config), and a transient API failure can kill the
// hosted ones (precheck-pr / delay-automatic-review).
expect(job.needs).toEqual([

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.

[Suggestion] The fallback gate's upstream coverage is pinned as a literal five-job list, never derived from review-pr's transitive needs closure — today's agreement is real but coincidental (verified set-equal; job.needs appears exactly once in the file). — Failure scenario: a future change adds a new required job upstream of review-pr without touching fallback-comment; that job fails transiently, review-pr is marked skipped, the gate checks only its five literal jobs — none failed — and no fallback comment posts, silently reintroducing the exact unexplained-skip this PR exists to eliminate with every test still green.

Suggested fix: compute the transitive needs closure of doc.jobs['review-pr'] and assert job.needs is set-equal to closure ∪ {'review-pr'} and that job.if contains a needs.<id>.result == 'failure' clause for each — the derived-check style this file already uses.

中文说明

兜底门禁对上游的覆盖被钉成一个字面量五-job 列表,从未由 review-pr 的传递 needs 闭包推导——当前的一致是真实但巧合的(已验证集合相等;job.needs 在文件中仅出现一次)。— 故障场景:未来在 review-pr 上游新增一个必需 job 却未改动 fallback-comment;该 job 瞬时失败,review-pr 被标记 skipped,门禁只检查那五个字面量 job——无一失败——于是不发兜底评论,在所有测试仍为绿色的情况下,悄悄重新引入本 PR 旨在消除的"无解释跳过"。

建议修复:计算 doc.jobs['review-pr'] 的传递 needs 闭包,断言 job.needs 与闭包 ∪ {'review-pr'} 集合相等,且 job.if 对其中每个 job 都含 needs.<id>.result == 'failure' 子句——即本文件已采用的推导式检查风格。

— qwen3.8-max via Qwen Code /review (v0.21.12)

# posting on a failed listing is how a transient 5xx mints one.
bot_login="$(gh api user --jq '.login' 2>/dev/null)" || bot_login=""
fallback_bodies=""
if [ -n "$bot_login" ] \

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.

[Suggestion] The in-job dedup's error path "comments listing fails while gh api user succeeds" is never executed — the only in-job scenarios run are already, default, and lookup_fail; the fallback-job twin (comments_lookup_fail) IS tested. — Failure scenario: verified surviving mutant — decoupling the failed lookup from the condition (hoisted assignment + || true) leaves the suite green; the condition then succeeds with an empty body on a transient 5xx and the step posts without dedup, minting a permanent duplicate on the exact re-run case this block was written for (attempt 1 died, the fallback job already posted for this run id, attempt 2 hits a transient listing failure).

Suggested fix: add one scenario — runFallbackStep('comments_lookup_fail', { useInJobStep: true }) expecting status 0, posted '', and summary containing deferring to the fallback-comment job.

中文说明

in-job 去重的错误路径"gh api user 成功但评论列表失败"从未被执行——in-job 只跑 alreadydefaultlookup_fail 三个场景;其 fallback-job 孪生路径(comments_lookup_fail)却有测试。— 故障场景:已验证存活的变异体——把失败的查询与条件解耦(提升赋值 + || true)后套件仍全绿;瞬时 5xx 时条件以空 body 成功,步骤未经去重就发布,在正是本块为之编写的重跑场景上铸成永久重复(尝试 1 死亡、fallback job 已就该 run id 发布、尝试 2 遇到瞬时列表失败)。

建议修复:新增一个场景——runFallbackStep('comments_lookup_fail', { useInJobStep: true }),期望 status 0、posted 为 ''、summary 含 deferring to the fallback-comment job

— qwen3.8-max via Qwen Code /review (v0.21.12)

continue
fi
echo "::warning::no write access to $dir; attempting single-directory repair"
sudo -n chown "$RUNNER_UID:$RUNNER_GID" "$dir" 2>/dev/null || true

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.

[Suggestion] The repair branch's chown is unverified by any test: the stub sudo no-ops chown (it only honours chmod) and no textual pin mentions it, so deleting the chown line leaves the whole suite green (verified by probe). — Failure scenario: on the step's own incident trigger (root-owned directories), chmod u+rwx without chown grants write to root, not the runner user, so repair can never succeed in exactly the case the step was written for — every incident degrades to fail-fast requiring manual runner repair instead of self-healing, while CI stays green. The harness cannot create root-owned fixtures without root, but the stub could still observe the invocation.

Suggested fix: have the stub sudo append "$@" to a log file (like the gh stub's $CALLS) and assert in the repair test that a chown <uid>:<gid> <broken-dir> invocation occurred.

中文说明

修复分支的 chown 未被任何测试验证:stub sudochown 不做任何事(只执行 chmod),也没有文本钉住它,因此删除 chown 行后整个套件仍为绿色(探针已验证)。— 故障场景:在该步骤自身针对的事故触发条件(root 属主目录)下,缺少 chownchmod u+rwx 把写权限给了 root 而非 runner 用户,修复在该步骤正是为之编写的场景中永远无法成功——每起事故都退化为需人工修复 runner 的快速失败而非自愈,CI 却保持绿色。框架在无 root 时无法构造 root 属主夹具,但 stub 仍可记录该调用。

建议修复:让 stub sudo"$@" 追加到日志文件(如 gh stub 的 $CALLS),并在修复测试中断言出现过 chown <uid>:<gid> <broken-dir> 调用。

— qwen3.8-max via Qwen Code /review (v0.21.12)

echo "Skipping fallback comment: PR #${PR_NUMBER} is ${pr_state}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
body="**Qwen Code review did not complete successfully.** The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with \`@qwen-code /review\`. See [workflow logs](${RUN_URL})."

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.

[Suggestion] Fallback comments are fire-and-forget: nothing updates, retracts, or consolidates a marker comment after posting (repo-wide, the marker's only consumers are the two dedup filters). — Failure scenario: a run dies, the fallback posts "retry with @qwen-code /review", the retry succeeds and the real review posts — a stale, still-actionable failure claim persists beside the completed review, and a contributor following its advice mints another full review run; on a persistently unhealthy pool each instructed retry and each synchronize run posts one more near-identical comment. This file already upserts the ack and the docs-only badge (upsert-bot-comment.sh --update-only); the fallback marker is the only singleton bot comment with no such path.

Suggested fix: route the post through upsert-bot-comment.sh (one marker comment per PR, updated with the latest dead run's URL), and/or supersede a stale fallback comment on the success path — at minimum, decide and document that staleness/accumulation is accepted.

中文说明

兜底评论是"发后即忘":发布后没有任何机制更新、撤回或合并标记评论(全仓库范围内,该标记的唯一消费者是两处去重过滤器)。— 故障场景:某 run 死亡,兜底发布"请用 @qwen-code /review 重试",重试成功、真正的 review 发出——一条仍然可操作的陈旧失败声明与已完成的 review 并存,贡献者照做又会触发一次完整 review;在持续不健康的 runner 池上,每次按其指引的重试、每次 synchronize 运行都会再发一条几乎相同的评论。本文件对确认评论与仅文档徽章都已采用 upsert(upsert-bot-comment.sh --update-only);兜底标记是唯一没有此路径的单例 bot 评论。

建议修复:经由 upsert-bot-comment.sh 发布(每 PR 一条标记评论,更新为最新死亡 run 的 URL),和/或在成功路径上取代陈旧的兜底评论——至少明确并记录接受陈旧/累积。

— qwen3.8-max via Qwen Code /review (v0.21.12)

' case "$*" in',
' *comments*)',
' case "${SCENARIO:-}" in lookup_fail | comments_lookup_fail) exit 1 ;; esac',
' cat "$COMMENTS_FILE"; exit 0 ;;',

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.

[Suggestion] R3-8 (carried from round 3, ruled still standing at this commit): the executed harness's stub gh cats the fixture verbatim and never applies the --jq dedup filter, so the filter's semantics are unexecuted for BOTH steps — the comment claiming the stub "pre-applies the dedup filter's semantics" overstates the coverage. — Failure scenario: verified surviving mutant — changing the filter's projection | .body to | .id (in either step's run block) leaves the suite green, because the stub ignores --jq; at runtime the filter then returns comment node IDs, the case "$fallback_bodies" in *"actions/runs/${GITHUB_RUN_ID})"*) anchor never matches an ID, and the cross-job/re-run dedup silently stops matching — every re-run of a dead review double-posts, the exact regression this PR exists to prevent.

Suggested fix: have the stub apply the real filter — store the fixture as JSON, extract the --jq argument, and pipe through jq so filter mutants change the stub's output; or add one executed assertion that runs each step's literal --jq string through jq against a fixture.

中文说明

R3-8(承接自第 3 轮,在本 commit 判定为仍然存在):已执行框架的 stub gh 原样输出夹具、从不应用 --jq 去重过滤器,因此两个步骤的过滤器语义都未被执行——声称 stub"预先应用去重过滤器语义"的注释夸大了覆盖。— 故障场景:已验证存活的变异体——把过滤器投影 | .body 改为 | .id(任一步骤的 run 块)后套件仍绿,因为 stub 忽略 --jq;运行时过滤器将返回评论节点 ID,case "$fallback_bodies" in *"actions/runs/${GITHUB_RUN_ID})"*) 锚点永远匹配不到 ID,跨 job/重跑去重悄悄失效——死掉 review 的每次重跑都会重复发布,正是本 PR 旨在防止的回归。

建议修复:让 stub 应用真实过滤器——以 JSON 存储夹具、提取 --jq 参数并交给 jq 处理,使过滤器变异能改变 stub 输出;或新增一个执行式断言,把每个步骤的字面量 --jq 字符串对夹具用 jq 运行一遍。

— qwen3.8-max via Qwen Code /review (v0.21.12)

Comment on lines +1821 to +1822
runs-on: 'ubuntu-latest'
timeout-minutes: 5

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.

[Suggestion] The fallback job's timeout-minutes: 5 is pinned by no assertion, unlike the identical invariant this file already enforces for the capture-tools step (which pins both the value and the worst-case retry budget arithmetically). — Failure scenario: verified surviving mutant — shrinking it below the loop's wall-clock floor (3 × sleep 10 plus up to six gh calls with their own backoff) makes GitHub cancel the job mid-loop on exactly the degraded-API conditions the loop exists to survive — no comment, no fail-closed summary — while the suite stays green (probe: timeout-minutes: 0.4 left it 144/144; adding the pin flipped it red).

Suggested fix: add expect(job['timeout-minutes']).toBe(5); beside the existing runs-on pin (the timeout pin alone is the verifiable guard — the gh calls have no script-level timeout, so a budget formula has no finite inputs to compute from).

中文说明

兜底 job 的 timeout-minutes: 5 没有任何断言钉住,而本文件对 capture-tools 步骤已强制同样的不变量(既钉数值、又以算术钉最坏重试预算)。— 故障场景:已验证存活的变异体——把它缩小到循环墙钟下限(3 × sleep 10 加最多六次自带退避的 gh 调用)以下,GitHub 会在恰是该循环为之存在的 API 降级条件下于循环中途取消 job——无评论、无失败关闭摘要——而套件仍保持绿色(探针:timeout-minutes: 0.4 时仍 144/144;加上该钉住后转红)。

建议修复:在现有 runs-on 钉住旁添加 expect(job['timeout-minutes']).toBe(5);(仅超时钉住即为可验证的防线——gh 调用无脚本级超时,预算公式没有可计算的有限输入)。

— qwen3.8-max via Qwen Code /review (v0.21.12)

@yiliang114 yiliang114 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.

LGTM — CI green at 3cad099, approving per maintainer request.

@wenshao
wenshao added this pull request to the merge queue Aug 16, 2026
Merged via the queue into QwenLM:main with commit b744248 Aug 16, 2026
174 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.13.

doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Aug 17, 2026
…LM#9255)

* fix(ci): keep a fallback comment when the PR review runner dies

A review job that dies abnormally never reaches its in-job fallback
comment step: the runner worker crash in FinalizeJob on the PR QwenLM#8894
run (EACCES creating under the runner home directory) left the PR with
no review and no explanation.

- Probe write access to $HOME, $RUNNER_TEMP and the runner root at job
  start, repair single-directory ownership with the existing sudo
  pattern, and fail fast with a clear message when repair is impossible
  instead of burning the review budget to die at finalize.
- Add a fallback-comment job on an ephemeral hosted runner that posts
  the retry guidance whenever review-pr fails. It derives the PR number
  from the event payload (dead job outputs do not survive a crash) and
  dedupes on a qwen-review-fallback comment marker plus this run's URL,
  so the in-job step, the ack comment, and re-runs never double-post.

* fix(ci): harden the PR review fallback comment (QwenLM#9255)

Review-round fixes for the fallback-comment defenses:

- Probe the actual runner root (three levels above the workspace, not
  two) and the _diag subdirectory FinalizeJob writes in; a writable
  parent does not prove an existing subdirectory writable.
- Open the fallback gate on authorize/review-config failures too — the
  incident's trigger can kill those earlier self-hosted jobs first, and
  a failed dependency marks review-pr 'skipped', which the old gate
  never matched. Guarded against resolve dispatch runs, which skip
  review-pr by design.
- Author-scope the dedup lookup (resolved dynamically like
  upsert-bot-comment.sh) so a planted marker cannot suppress the
  fallback, and fail closed with bounded retry when the lookup or the
  state check fails instead of fail-open toward duplicates or a green
  job that never posted.
- Skip the stale fallback when the PR head moved, but only on
  pull_request_target events where the run head is comparable —
  comment/review runs report main's tip, and posting wins over silence
  when the comparison is unavailable.
- Define the marker once in a workflow-level env and pin all of the
  above in the workflow test suite, executing the fallback step's real
  bash against a stubbed gh.

* fix(ci): close the fallback-comment gate gaps from round-2 review (QwenLM#9255)

- Exclude comment-driven /resolve runs from the fallback gate:
  authorize runs on `@qwen-code /resolve` issue comments where
  github.event.inputs is empty, so the dispatch-only exclusion never
  fired there and a failed resolve run was misdiagnosed as a dead
  review recommending the wrong command.
- Enumerate precheck-pr and delay-automatic-review failures in the
  gate: either failure marks review-pr 'skipped' (a transient API 5xx
  in delay's re-check step, or the fork-PR chain root dying before it
  posts anything), which the old gate never matched — silence,
  against its own "a skipped review is as unexplained as a dead one"
  norm. Both are 'skipped' where they do not apply, so the gate stays
  closed there.
- Anchor the cross-job dedup on the run URL's closing paren: run ids
  grow digits over time, so the unanchored substring let a later
  run's fallback comment (id 123450) suppress an earlier run's (id
  12345) re-run comment; every marker body renders the URL as
  [workflow logs](...runs/<id>), so the id is always followed by ')'.
- Pin the previously surviving mutants in the workflow suite: the
  _diag probe guard polarity, the workflow_dispatch disjunction, the
  ephemeral-hosted-runner placement, and the fallback body's
  marker-link shape the anchor relies on; add executed coverage for
  each head-lookup partial failure and for a distinct run's fallback
  not suppressing this run's comment.

* fix(ci): close round-3 review gaps in the fallback-comment defenses (QwenLM#9255)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
qqqys pushed a commit to qqqys/qwen-code that referenced this pull request Aug 26, 2026
… the failure comment (QwenLM#10114)

The fallback-comment gate admits a cancelled review-pr on purpose: a
job-level timeout is auto-CANCELLED by GitHub (failure() false), which
opens neither the failure-only gate nor the in-job step, so silence
there would leave a timed-out review unexplained (QwenLM#9255). But the same
'cancelled' result also arrives when a run or job is cancelled
mid-review after the upstream chain finished, and that flavor got the
full failure body — "The review pipeline failed before a review could
be posted. A transient error is retried automatically…" — none of
which is true for a cancellation. On PR QwenLM#9729, run 32875478404 was
run-cancelled two minutes into the review with no successor run, so
the QwenLM#9716 supersede guard correctly did not match, and the comment
read as a pipeline outage to the PR author.

The two flavors are not separable in needs — both reach the gate as
review-pr 'cancelled' with upstream green — so the fix branches inside
the step on the wired-in needs result: a cancelled review-pr now posts
one body accurate for both flavors (no failure/auto-retry claims,
retry instruction kept for the timeout flavor, run-URL markdown link
kept for the cross-job dedup), and everything else keeps the failure
body. The step runs under set -u, so a dropped env wiring fails the
step loudly instead of silently reverting cancelled runs to the false
body.

Mutation-verified: neutralizing the cancelled branch fails both new
tests; the restored workflow passes the suite at base parity.

Fixes QwenLM#10109
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants