Skip to content

feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes#7693

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:triage-finalize-deferred-approval
Jul 25, 2026
Merged

feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes#7693
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:triage-finalize-deferred-approval

Conversation

@wenshao

@wenshao wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Removes the triage agent's in-agent CI polling and moves the "wait for CI" half of the review to a new deterministic finalize workflow. Stage 2b now fetches check-runs exactly once, reports pending checks honestly, and wraps the CI table in qwen-triage-ci region markers keyed to the reviewed SHA. When Stage 3 reaches a clean approve verdict while checks are still running, it no longer approves immediately — the comment carries an approve-on-green marker instead. A new qwen-triage-finalize.yml workflow fires on workflow_run completion of Qwen Code CI / E2E Tests and, in plain bash over the API (no model, no checkout), rewrites the marked table region with the settled results and posts the commit-pinned approval only when every check on that SHA landed green — failing closed on red checks, a moved head, or a closed/draft PR, and flipping the triage status comment to say which way it resolved.

Why it's needed

The Stage 2b poll loop (sleep 60, up to ~10 minutes) can never catch this repo's unit suite, which takes ~30 minutes: the agent starts polling around minute 9 of a 32-minute check, waits its full budget, and always gives up with "CI still running at review time". Concretely on #7680: PR opened 14:42, triage burned 14:51→15:01 polling, Stage 3 approved at 15:02:10, and Test (ubuntu-latest) finished at 15:14:17 — the approval attested to a result that did not exist yet, and 10 minutes of ECS runner time plus per-poll model turns were spent to obtain nothing. With this change the stage comments land ~10 minutes sooner (roughly halving the triage job), and the deferred approval lands at CI completion — the theoretical minimum for a review that includes the unit-test signal — with full evidence instead of before it.

Reviewer Test Plan

How to verify

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-triage-finalize-workflow qwen-triage-workflow — 26 tests pass (13 new). The new suite asserts the security-relevant structure (no checkout, bot-author filter before honoring any marker, SHA-pinned approval, closed green set, self-check exclusion, per-SHA serialized concurrency) and executes the extracted html_escape / replace_region bash helpers against crafted bodies (region replaced in place, prose preserved, missing end marker → fail closed, escape order & first).
  • Read .qwen/skills/triage/references/pr-workflow.md Stage 2b/Stage 3: polling is now forbidden, the table template carries the region markers, and the approve path branches on the PENDING count computed from the existing single fetch (staleness in that direction can only defer, never approve early).
  • Marker forgery: the finalize job looks up markers only in comments whose author is the bot identity behind the PAT (gh api user), so a third party pasting approve-on-green into a PR comment is ignored.
  • Head movement: approval is pinned via commit_id and additionally re-checks head.sha/state/draft at the last moment; any mismatch posts a "stale" status note instead of approving.

Evidence (Before & After)

Before (#7680, run 30102148473): triage job 14:43→15:02 (~19.7 min, of which ~10 min was the poll loop), Stage 2 comment says "still running after ~10 minutes of polling", approval posted 15:02:10, unit suite finished 15:14:17.

After: the agent posts Stage 2/3 with the once-fetched table (pending rows marked) and no sleep loops; finalize-triage-ci updates the table and resolves the deferred approval when the last CI workflow completes. New-workflow behavior is covered by the structural/functional test suite above; it activates on merge (workflow_run listens on the default branch).

Tested on

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

Environment (optional)

vitest via scripts/tests/vitest.config.ts; YAML validated with the repo's yaml package and the embedded script with bash -n.

Risk & Scope

  • Main risk or tradeoff: the deferred approval depends on a workflow_run firing after the marker is posted. The long pole (Qwen Code CI, ~30 min) always completes well after Stage 3 (~10 min), so the ordering holds in practice; if every listed workflow somehow finished before the marker landed, the approval simply stays deferred (visible in the status comment) until the next /triage re-run — fail-closed, never fail-open.
  • Not validated / out of scope: the live workflow_run path can only be exercised after merge (GitHub runs the default-branch version); the CI table region renders identically to the current hand-written table but row texts become mechanical (✅ success etc.) after the first finalize pass. Shortening the 30-minute unit suite itself (the real lower bound) is out of scope.
  • Breaking changes / migration notes: none. Old-format Stage 2 comments without region markers are left untouched by the finalize job; re-runs regenerate comments in the new format.

Linked Issues

None — follows up on the triage-latency discussion around #7646 / #7648 (static unattended review) using #7680 as the measured example.

中文说明

本 PR 做了什么

移除 triage agent 的 CI 轮询,把"等待 CI"这一半职责移交给新的确定性 finalize workflow。Stage 2b 现在只拉取一次 check-runs,如实报告 pending 状态,并用绑定被审 SHA 的 qwen-triage-ci 定界符包裹 CI 表格。Stage 3 得出干净的 approve 结论但检查仍在运行时,不再立即审批——评论中改为携带 approve-on-green 标记。新增的 qwen-triage-finalize.ymlQwen Code CI / E2E Tests 完成时经 workflow_run 触发,以纯 bash 调 API(无模型、无 checkout)原地重写定界区内的表格,并且只有当该 SHA 上所有检查全绿时才提交 pin 到该 commit 的审批——红色检查、head 变更、PR 关闭/草稿一律 fail-closed,并同步更新 triage status 评论说明结果。

为什么需要

Stage 2b 的轮询循环(sleep 60,上限约 10 分钟)永远等不到本仓库约 30 分钟的单测套件:agent 在一个 32 分钟检查的第 9 分钟左右开始轮询,耗尽全部预算后必然以"CI still running at review time"放弃。以 #7680 为例:PR 14:42 打开,triage 在 14:51→15:01 空转轮询,Stage 3 于 15:02:10 审批,而 Test (ubuntu-latest) 15:14:17 才跑完——审批为一个尚不存在的结果背了书,10 分钟 ECS runner 时间和每轮轮询的模型调用一无所获。本改动使阶段评论提前约 10 分钟落地(triage job 时长约减半),延迟审批则在 CI 完成时(这是包含单测信号的审查的理论下界)带着完整证据落地。

审查者验证方案

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-triage-finalize-workflow qwen-triage-workflow —— 26 个测试通过(新增 13 个)。新测试断言安全关键结构(无 checkout、先验证评论作者是 bot 才认标记、审批 pin SHA、绿色集合封闭、排除自身 check、按 SHA 串行并发),并实际执行抽取出的 html_escape / replace_region bash 函数(区域原地替换、正文保留、缺失结束标记则 fail-closed、& 最先转义)。
  • 阅读 .qwen/skills/triage/references/pr-workflow.md Stage 2b/Stage 3:禁止轮询,表格模板带定界符,approve 路径按既有单次拉取算出的 PENDING 分支(该方向的数据陈旧只会推迟审批,绝不会提前审批)。
  • 标记伪造:finalize 只在作者为 bot 本人(gh api user)的评论中查找标记,第三方粘贴 approve-on-green 无效。
  • Head 变更:审批经 commit_id pin 定,且在最后时刻复核 head.sha/状态/草稿;任何不一致都改为发布 "stale" 状态说明而非审批。

证据(Before & After)

Before(#7680,run 30102148473):triage job 14:43→15:02(约 19.7 分钟,其中约 10 分钟为轮询),Stage 2 评论写着 "still running after ~10 minutes of polling",15:02:10 审批,单测 15:14:17 完成。

After:agent 以单次拉取的表格(pending 行如实标注)发布 Stage 2/3,无任何 sleep;finalize-triage-ci 在最后一个 CI workflow 完成时更新表格并落定延迟审批。新 workflow 行为由上述结构/功能测试覆盖;合并后生效(workflow_run 监听默认分支版本)。

风险与范围

  • 主要风险/权衡:延迟审批依赖标记发布后仍有 workflow_run 触发。长尾检查(约 30 分钟的 Qwen Code CI)总是远晚于 Stage 3(约 10 分钟)完成,顺序在实践中成立;若所列 workflow 都在标记落地前完成,审批只会保持延迟(status 评论可见),等下次 /triage 重跑——只会 fail-closed,不会 fail-open。
  • 未验证/范围外:workflow_run 实链路只能在合并后验证(GitHub 运行默认分支版本);首次 finalize 后表格行文变为机械格式(✅ success 等)。缩短 30 分钟单测本身(真正的下界)不在本 PR 范围。
  • 破坏性变更/迁移说明:无。旧格式(无定界符)的 Stage 2 评论 finalize 不会触碰;重跑会以新格式重写评论。

关联 Issue

无——是 #7646 / #7648(无人值守静态审查)之后关于 triage 延迟的后续,以 #7680 为实测样本。

…l after CI completes

The triage agent's Stage 2b polled pending checks for up to 10 minutes, but
this repo's unit suite runs ~30 minutes, so the poll always burned its full
budget, gave up with 'CI still running', and Stage 3 could then approve before
the suite finished (observed on a PR approved 12 minutes before its Test job
completed).

Split the wait out of the agent entirely:

- pr-workflow.md Stage 2b now forbids polling: fetch check-runs once, report
  pending checks honestly, and wrap the CI table in qwen-triage-ci region
  markers keyed to the reviewed SHA.
- Stage 3 defers a clean-verdict approval when checks are still pending: the
  comment carries an approve-on-green marker instead of an immediate APPROVE.
- New qwen-triage-finalize.yml fires on workflow_run completion of 'Qwen Code
  CI' / 'E2E Tests' and, with plain bash over the API (no model, no checkout),
  rewrites the marked table region with the settled results and posts the
  commit-pinned approval only when every check landed green — failing closed
  on red checks, a moved head, or a closed/draft PR, and flipping the triage
  status comment to say which way it resolved.

Markers are honored only in comments authored by the bot identity itself, and
check names (attacker-influenced on fork PRs) go through the same HTML-escape
chain the skill mandates for file paths.

Stage comments now land ~10 minutes sooner and the approval, when deferred,
lands at CI completion with full evidence instead of before it.
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments above for the result.

Qwen Triage 已完成 —— 查看运行。结果见上方各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all sections present with substantive content, including the bilingual translation.

Problem: observed and measured, not theoretical. The #7680 timeline shows the poll loop burning 14:51→15:01 (10 minutes) and still giving up, with Stage 3 approving at 15:02:10 while Test (ubuntu-latest) didn't finish until 15:14:17 — the approval attested to a result that didn't exist yet. The ~30-minute unit suite structurally outlives any in-agent poll budget.

Direction: aligned. Moving the "wait for CI" half out of the time-bounded agent run into a deterministic workflow_run job is the right architectural split — the agent does the review (which needs a model), the finalize job does the mechanical "did CI land green?" check (which doesn't). The triage job roughly halves in wall time, and the deferred approval lands with full evidence at the theoretical minimum (CI completion).

Size: not applicable — no core module paths touched. The 3 changed files are .github/workflows/, .qwen/skills/triage/references/, and scripts/tests/.

Approach: the scope feels right. Each of the three files is necessary: the new workflow (the core deliverable), the pr-workflow.md update (agent instructions to emit region markers and the approve-on-green marker instead of polling), and the test file (validates the security-relevant structure and executes the extracted bash helpers). The latest commit (75ee714) closes a real guardrail bypass — the marker was previously emitted before the fork-refactor check ran — and aligns the finalize table with the gate's data source. I don't see a materially simpler path. The diff is focused with no unrelated changes.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 所有章节齐全,包含中文翻译。

问题:已观测、已量化,非理论性问题。#7680 的时间线显示轮询循环在 14:51→15:01 空转 10 分钟后仍然放弃,Stage 3 于 15:02:10 审批,而 Test (ubuntu-latest) 直到 15:14:17 才完成——审批为一个尚不存在的结果背了书。约 30 分钟的单测套件在结构上超出了任何 agent 内轮询预算。

方向:对齐。将"等待 CI"这一半从有时间限制的 agent 运行中移出,交给确定性的 workflow_run 任务,是正确的架构拆分——agent 做审查(需要模型),finalize 任务做机械性的"CI 是否全绿"检查(不需要模型)。triage 任务的墙钟时间约减半,延迟审批在理论下界(CI 完成时)带着完整证据落地。

规模:不适用——未触及核心模块路径。3 个变更文件分别在 .github/workflows/.qwen/skills/triage/references/scripts/tests/

方案:范围合理。三个文件各有必要:新 workflow(核心交付物)、pr-workflow.md 更新(agent 指令,发出区域标记和 approve-on-green 标记而非轮询)、测试文件(验证安全关键结构并实际执行抽取出的 bash 辅助函数)。最新提交(75ee714)修复了一个真实的护栏绕过——标记之前在 fork-refactor 检查运行之前就已发出——并将 finalize 表格与门控的数据源对齐。没有看到更简路径。diff 聚焦,无无关改动。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 75ee7140d41f2e774b4be4f6b349fa7bf45ff91e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given the problem (the agent's 10-minute poll budget structurally can't catch a ~30-minute unit suite), I would split the "wait for CI" responsibility out of the agent run entirely: fetch check-runs once, wrap the CI table in machine-readable markers, and let a deterministic workflow_run job — no model, no checkout — update the table and post the deferred approval when CI settles. The security model would need bot-author verification on markers (they're public text), SHA-pinned approval, head-movement detection, and a closed green set.

Comparison with the diff: the PR's approach matches this almost exactly. The implementation is clean, well-commented (the "why" comments earn their place), and the security model is sound throughout:

  • No checkout — the finalize job is a single bash step with no actions/checkout. PR code never sits next to the write PAT. The test asserts this structurally (expect(workflowText).not.toContain('uses:')).
  • Bot-author verification — every marker lookup filters select(.user.login == $bot) before select(.body | contains($m)). A third party pasting approve-on-green into a comment is ignored. The test asserts ≥3 author-filtered lookups.
  • SHA-pinned approvalcommit_id="$HEAD_SHA" on the reviews API, plus a last-moment re-check of head.sha, state, and draft. Mismatch → stale note, not approval. The stale check runs BEFORE the red/deferred verdicts, so a cancel-in-progress firing on a stale SHA can't stamp a red status over the new head's comment.
  • Closed green set["success","neutral","skipped"]; anything unknown is NOT green. Fail-closed on all uncertainty. The gate_counts jq filter is exercised by the test suite against a fixture covering every conclusion class plus non-PR events and a re-run dedup pair.
  • Guardrail re-assertion (75ee714) — the finalize job independently checks (.head.repo.full_name // "") != .base.repo.full_name and grep -qiE '^[[:space:]]*refactor' before approving. A deleted fork (null head.repo) is treated as a fork → blocked. This closes the bypass where the marker could have been emitted before the guardrail ran.
  • Table-gate alignment (75ee714) — table_rows now restricts check-runs to suites of the same deduped event == "pull_request" workflow runs the gate trusts, so the rendered table and the approval gate agree on what "CI" means. Bot orchestration plumbing no longer appears as CI evidence.
  • Idempotent re-runs — checks for an existing approval at the same SHA before posting. The already-approved branch also repairs the status comment if a later PENDING>0 firing flipped it back to "deferred". Per-SHA concurrency group with cancel-in-progress: false so queued runs re-read fresh state.
  • HTML escaping — check names (fork-controlled on pull_request triggers) pass through the same html_escape chain as the triage skill's sanitize_path, with tr -d '\r\n\000' and cut -c1-120 bounding.
  • replace_region — fixed-string matching via grep -qF and awk index(), fail-closed on missing end marker (returns 1, no output file written). The END { if (inr) exit 1 } guard catches the case where the end marker only appears before the begin marker. Empty region files are refused up front.
  • Stale-fire protection — when the head moved and a newer re-review already owns the status comment (bot sha= markers exist for the current head), the stale finalize stays silent instead of clobbering the newer review's status.

The set -uo pipefail without -e is intentional and correct — the script handles errors explicitly with if ! checks and || fallbacks; -e would cause premature exits on expected non-zero returns.

The pr-workflow.md changes integrate cleanly: the poll instruction is replaced with "never poll", the region marker template is added, the guardrail computation moves before the marker emission (closing the bypass), and the Stage 3 approval logic branches on the PENDING count computed from workflow runs filtered to event == "pull_request". The staleness direction is safe — a check that completed after the single fetch is merely treated as still pending, which defers; it can never approve early.

The tests cover the security-relevant structure (no checkout, bot-author filter, SHA-pinned approval, closed green set, self-check exclusion, per-SHA concurrency, guardrail re-assertion) and execute the extracted bash helpers against crafted inputs — html_escape (escape order & first), gate_counts (all conclusion classes, non-PR events, re-run dedup, exact counts), table_rows (suite-filtered, deduped, skipped-filtered, failure-first sort), and replace_region (region replaced in place with prose preserved, missing end marker → fail closed, reversed markers → fail closed).

No critical blockers or AGENTS.md violations found.

sequenceDiagram
    participant P1 as Triage Agent
    participant P2 as GitHub CI
    participant P3 as Finalize Workflow
    participant P4 as PR Comments and Reviews

    P1->>P4: Post Stage 2 with CI table in region markers
    P1->>P4: Post Stage 3 with approve-on-green marker (if CI pending)
    P2->>P3: workflow_run completed
    P3->>P3: Fetch check-runs and workflow runs for reviewed SHA
    P3->>P4: Rewrite CI table region in place
    alt All PR CI runs green and head unchanged
        P3->>P4: Post SHA-pinned approval
    else Red runs, head moved, or PR closed
        P3->>P4: Update status (withheld, stale, or deferred)
    end
Loading

Testing

CI status for 75ee714 (single fetch, no polling):

Final CI results for 75ee714 (all PR CI workflow runs completed green):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
label ✅ success
precheck-pr / precheck ✅ success
authorize ✅ success
remove-suspicious-attachments ✅ success
route ✅ success
review-pr ✅ success
delay-automatic-review ✅ success
Remind on force-push ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Approval gate (workflow runs with event == "pull_request"): total=1, pending=0, non-green=0. Qwen Code CI completed with success. No failures attributable to this PR.

This PR touches CI/triage infrastructure, not a TUI surface — no real-scenario testing needed. N/A.

中文说明

代码审查

独立方案: 鉴于问题(agent 的 10 分钟轮询预算在结构上无法等到约 30 分钟的单测套件),我会将"等待 CI"的职责完全移出 agent 运行:拉取一次 check-runs,用机器可读标记包裹 CI 表格,让一个确定性的 workflow_run 任务——无模型、无 checkout——在 CI 结束后更新表格并提交延迟审批。安全模型需要:对标记做 bot 作者验证(标记是公开文本)、SHA 固定审批、head 变更检测、以及封闭的绿色集合。

与 diff 的对比: PR 的方案几乎完全匹配。实现干净,注释到位,安全模型全程可靠:

  • 无 checkout —— finalize 任务只有一个 bash 步骤,没有 actions/checkout。PR 代码不会与写入 PAT 共存。
  • Bot 作者验证 —— 每个标记查找都先过滤 bot 作者再检查标记内容。第三方粘贴标记会被忽略。
  • SHA 固定审批 —— reviews API 上使用 commit_id,加上最后时刻对 head/状态/草稿的复核。不匹配则发 stale 说明,不审批。
  • 封闭绿色集合 —— success/neutral/skipped 为绿色,任何未知结论都不是绿色。所有不确定性 fail-closed。
  • 护栏重断言(75ee714)—— finalize 任务在审批前独立检查 fork-refactor 护栏。已删除的 fork(null head.repo)视为 fork → 拦截。修复了标记可能在护栏运行前就已发出的绕过。
  • 表格-门控对齐(75ee714)—— table_rows 现在将 check-runs 限制在门控信任的去重 event == "pull_request" workflow runs 的套件中,表格和审批门控对"CI"的定义一致。
  • 幂等重跑 —— 发布前检查同一 SHA 是否已有审批。已审批分支还会修复被后续 PENDING>0 触发翻回"deferred"的状态评论。
  • HTML 转义 —— 检查名经过与 triage skill 相同的转义链。
  • replace_region —— 固定字符串匹配,缺失结束标记时 fail-closed,拒绝空区域文件。
  • 过期触发保护 —— head 前移且新 review 已接管时保持沉默。

未发现关键阻塞项或 AGENTS.md 违规。

测试

75ee714 的 CI 状态(单次拉取,无轮询):所有 PR CI workflow 运行均以绿色完成。审批门控:total=1, pending=0, non-green=0。无归因于本 PR 的失败。

本 PR 触及 CI/triage 基础设施,非 TUI 界面——无需真实场景测试。N/A。

Qwen Code · qwen3.8-max-preview

Reviewed at 75ee7140d41f2e774b4be4f6b349fa7bf45ff91e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; the problem is measured, the architecture is right, the security model is thorough, and four rounds of self-review caught real bugs before they shipped.

Stepping back: this PR fixes a structural flaw in the triage process — the agent's poll budget can never catch the unit suite, so the "CI evidence" section was always a shrug, and the approval once landed 12 minutes before the suite finished (#7680). The fix is the right split: the agent does what needs a model (the review), and a deterministic bash job does what doesn't (did CI land green?). No model tokens, no checkout, no sleep loops.

What stands out across the four commits is the quality of the self-review. Each round caught a real issue: the original RED gate had a jq error that would have approved regardless of failures (d2cb023), the E2E Tests trigger was dead code (d2cb023), a stale finalize firing could clobber a newer review's status comment (cadf3f0), and the approve-on-green marker could bypass the fork-refactor guardrail because it was emitted before the check ran (75ee714). That last one is the most important — without it, a fork refactor PR could have been auto-approved on green CI despite the explicit policy against it. The fix moves the guardrail computation before the marker emission and re-asserts it structurally in the finalize job itself.

The 75ee714 commit also aligns the finalize table with the gate's data source — table_rows now restricts check-runs to suites of deduped event == "pull_request" workflow runs, so the rendered table and the approval gate agree on what "CI" means. Without this, bot orchestration plumbing (route, label, precheck) appeared as CI evidence and the table disagreed with the gate about what was pending.

Every change in the diff is necessary for the stated goal. No drive-by refactors, no scope creep. The pr-workflow.md changes integrate cleanly with the existing instructions, and the staleness direction is safe (a check that completed after the single fetch is merely treated as pending — defers, never approves early).

CI is all green on 75ee714Qwen Code CI completed with success, and the approval gate (workflow runs filtered to event == "pull_request") shows total=1, pending=0, non-green=0.

LGTM, looks ready to ship. ✅

中文说明

置信度:5/5 —— 每个阶段都干净;问题已量化,架构正确,安全模型周全,四轮自审在发布前捕获了真实 bug。

退一步看:这个 PR 修复了 triage 流程中的结构性缺陷——agent 的轮询预算永远等不到单测套件,审批曾经在套件完成前 12 分钟就落地(#7680)。修复方案是正确的拆分:agent 做需要模型的事(审查),确定性 bash 任务做不需要模型的事(CI 是否全绿)。

四个提交中最突出的是自审的质量。每一轮都捕获了真实问题:原始 RED 门控有 jq 错误(d2cb023),E2E Tests 触发器是死代码(d2cb023),过期触发可能覆盖新 review 的状态评论(cadf3f0),以及 approve-on-green 标记可能绕过 fork-refactor 护栏(75ee714)。最后一个最重要——没有它,fork refactor PR 可能在 CI 全绿时被自动审批,尽管有明确的政策禁止。修复将护栏计算移到标记发出之前,并在 finalize 任务中结构性地重断言。

75ee714 还将 finalize 表格与门控的数据源对齐——table_rows 现在将 check-runs 限制在去重的 event == "pull_request" workflow runs 的套件中,表格和审批门控对"CI"的定义一致。

diff 中的每个改动都是目标所必需的。没有顺手重构,没有范围蔓延。

75ee714 的 CI 全绿——Qwen Code CIsuccess 完成,审批门控显示 total=1, pending=0, non-green=0。

Qwen Code · qwen3.8-max-preview

Reviewed at 75ee7140d41f2e774b4be4f6b349fa7bf45ff91e · 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.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Review — feat(triage): stop in-agent CI polling, finalize evidence and approval after CI completes

The diagnosis is right and the architecture is the right shape: a deterministic workflow_run job with no model, no checkout, contents: read, PAT-only writes, fixed-string marker matching, and bot-author verification via gh api user is a strictly better way to attest CI than an agent sleep 60 loop that structurally cannot outlast a 30-minute suite. The token (QWEN_CODE_BOT_TOKEN || CI_BOT_PAT) matches what qwen-triage.yml posts comments with, so the author filter resolves to the same identity — I verified that.

But I ran the workflow's own code against real data and found two blockers that invert the PR's central safety claims. Not mergeable as-is.


🔴 Blocker 1 — the red-check gate never fires; a failed CI auto-approves

.github/workflows/qwen-triage-finalize.yml (the RED= computation):

RED=$(jq '[.[] | select(.status == "completed")
  | select(["success","neutral","skipped"] | index(.conclusion // "") | not)] | length' /tmp/checks.json)

In jq, | rebinds .. Inside index(...) the input is the array ["success","neutral","skipped"], not the check object, so .conclusion is evaluated against an array:

$ jq '[.[] | select(.status=="completed")
  | select(["success","neutral","skipped"] | index(.conclusion // "") | not)] | length' checks.json
jq: error (at checks.json:11042): Cannot index array with string "conclusion"
exit=5  RED=[]

There is no set -e, so RED is the empty string. Then if [ "$RED" -gt 0 ] raises [: : integer expression expected, returns non-zero, and control falls through to the approve path. End-to-end replay of the gate block verbatim, input = one skipped + one failure on Test (ubuntu-latest, Node 22.x):

Checks for abc1234: total=2 pending=0 non-green=
gate.sh: line 13: [: : integer expression expected
>>> REACHED THE APPROVE PATH (gh api ... -f event=APPROVE) <<<

A/B control — same harness, only the jq replaced with a correct form:

Checks for abc1234: total=2 pending=0 non-green=1
PR #X: 1 non-green check(s); approval withheld.

So the deferred path posts a commit-pinned APPROVE on a PR whose unit suite went red. That is worse than the status quo it replaces (today the agent reads conclusions itself and withholds), and it is the exact opposite of "fail-closed on red checks".

Fix — bind before the membership test:

RED=$(jq '[.[] | select(.status == "completed")
  | select((.conclusion // "") | IN("success","neutral","skipped") | not)] | length' /tmp/checks.json)

and validate the three counters are numeric before branching (case "$RED" in ''|*[!0-9]*) echo "::warning::…"; exit 0 ;; esac), so a future jq failure can never mean "approve".

Note the test expect(script).toContain('["success","neutral","skipped"]') passes on this broken code — see the test-coverage section.


🔴 Blocker 2 — the finalize pass deletes the CI evidence it exists to preserve

I replayed the region-rendering block verbatim against the real check-runs of #7680, the PR this change is motivated by (f35fc8dd, 96 check-runs):

| <code>ack-review-request</code> | ⏭️ skipped |     ×6
| <code>authorize</code>          | ⏭️ skipped |     ×5 (+1 success)
| <code>review-config</code>      | ⏭️ skipped |     ×6
…
| …and 36 more checks | see the Checks tab |

60 rows rendered: 45 skipped, 15 success, and not one meaningful CI check:

check in the rendered table?
Test (ubuntu-latest, Node 22.x)
Test (macos-latest / windows-latest)
web-shell E2E Smoke
Serve A/B

Three compounding causes:

  1. No dedup by name. filter=latest dedups per check suite, not per name. Every pull_request_target / issue_comment bot run mints a new suite with the same job names — on perf(web-shell): paint the composer git chip before git status completes #7680 that is 96 check-runs across only 35 distinct names (authorize ×7, review-pr ×6, resolve-pr ×6, …).
  2. No skip filter. 68 of 96 rows are skipped conditional bot jobs.
  3. Alphabetical sort -f + head -n 60. The cut lands mid-review-config, so everything from S/T/u/w onward — i.e. every real test job — is truncated away.

So on a typical PR, finalize-triage-ci overwrites the agent's curated, human-meaningful table with 60 rows of duplicated skipped bot jobs and silently drops the unit-test result. Worse, a red check that sorts late is dropped too, leaving a table that reads as clean.

Fix (verified against the same data — 96 rows → 16, and it contains exactly the checks that matter, Test (ubuntu-latest, Node 22.x) included):

jq -r 'group_by(.name) | map(max_by(.id))
  | map(select(.conclusion != "skipped"))
  | sort_by([ (.status == "completed"), ((.conclusion // "") | IN("success","neutral")), (.name | ascii_downcase) ])
  | .[] | [.name, .status, (.conclusion // "")] | @tsv' /tmp/checks.json

Sorting non-green first is the important half: it guarantees truncation can never hide a failure. Keep skipped in the approval gate (it is correctly green there) — just out of the table.


🟠 Medium

3. E2E Tests is a dead trigger. e2e.yml has no pull_request trigger — only push / schedule / workflow_dispatch. So if: github.event.workflow_run.event == 'pull_request' drops 100% of its firings. The claimed second convergence path ("so a late E2E finish also converges the table") cannot happen; Qwen Code CI is the only workflow that can ever fire this job. The new test asserts the two-entry list, codifying it.

4. The deferred approval can be silently dropped forever. PENDING counts every check-run on the SHA, and this repo sprays them onto the PR head from pull_request_target / issue_comment workflows — on #7680 the head SHA carries triage, review-pr, route, remove-suspicious-attachments, and a whole batch created at 15:39, long after CI finished. If any of those is in flight when finalize reads check-runs, PENDING > 0continue — with no status-comment update — and, since only Qwen Code CI can re-trigger the job (finding 3), no further firing ever arrives. Suggested: list the other pull_request workflows (SDK Python, Serve A/B, Web-shell Visuals, Qwen Autofix) in workflows:, and have the still-deferred branch update the status comment so the outcome is never invisible.

5. The red gate runs before the head/state re-check. ci.yml sets cancel-in-progress for PR refs, so a force-push cancels the in-flight run → workflow_run fires on the now-stale SHA → cancelled is non-green → update_status "$PR" red posts "CI finished with N non-green check(s) on <stale sha>". Because STATUS_MARKER is not SHA-scoped, that clobbers the status comment belonging to the new head. Move the CURRENT_HEAD != HEAD_SHA / state / draft re-check above the RED branch (or additionally gate on github.event.workflow_run.conclusion != 'cancelled'). Currently masked by blocker 1; it goes live the moment that is fixed.

6. replace_region truncates the comment when the end marker precedes the begin marker. grep -qF only proves both strings exist, not their order; awk then opens the region and eats to EOF. Reproduced:

input : "quoted example: <!-- /qwen-triage-ci -->" / prose / <begin> / | old | / FOOTER reviewed commit
output: quoted example… / prose / <begin> / | new | / <end>      ← exit=0, FOOTER gone

exit 0, so the caller PATCHes the truncated body — losing the signature and the reviewed-commit footer, which is the attestation the whole triage flow rests on. The skill doc's "do not quote the marker text elsewhere" is a soft guard on LLM-authored prose; make it structural: END { if (inr) exit 1 } in the awk and check its status. The existing test covers only "end marker entirely absent", which grep already catches.


🟡 Low / nits

  • Empty check-runs response ⇒ approve. TOTAL=0 gives PENDING=0, RED=0 → approve path. Add [ "$TOTAL" -gt 0 ] || { echo "::warning::no check-runs; skipping"; exit 0; }.
  • The self-check exclusion is a no-op, and the comment states it as fact. workflow_run runs are attributed to the default branch, so this job's check-run never lands on the PR head SHA. Evidence: Serve A/B Publish and Web-shell Visuals Publish (both workflow_run) appear nowhere in perf(web-shell): paint the composer git chip before git status completes #7680's 96 head-SHA check-runs, while their pull_request counterparts do. Harmless as defense-in-depth, but the workflow comment ("attaches to the SAME head SHA") and the skill's added select(.name != "finalize-triage-ci") clause should say "belt-and-braces" rather than assert it.
  • The cmp -s "already current" branch is unreachable. The region embeds RUN_URL, which carries github.run_id and differs on every firing, so the bodies never compare equal and every trigger PATCHes. Either drop the run link from inside the region or diff with it masked.
  • "(fork?)" in the missing-token message is misleadingworkflow_run always runs in the base-repo context with secrets available.
  • MAX_ROWS accounting: TOTAL is counted before truncation but after self-exclusion, so "…and N more" is consistent — worth keeping in mind if the dedup fix lands (recount after dedup).

✅ Verified good

  • 26/26 tests pass locally (13 existing + 13 new), bash -n clean on the extracted script, and yamllint-clean — the double-quoted if: is fine because .yamllint sets allow-quoted-quotes: true.
  • scripts/tests/ is wired into CI via test:scriptstest:ci, so the new suite really runs.
  • Fixed-string matching everywhere (grep -F, index(), contains) — no regex over untrusted text.
  • Escape chain matches the skill's sanitize_path (& first, control chars stripped, length-bounded, <code>-wrapped), and the helper test proves the ordering.
  • Per-SHA concurrency with cancel-in-progress: false; approval idempotence via the state == "APPROVED" and .commit_id == $sha pre-check; commit_id pinning.

Test coverage

Executing the extracted bash helpers is the right instinct — please extend it to the decision logic, which is where both blockers live. expect(script).toContain('["success","neutral","skipped"]') passes on code that raises a jq error every single run: a string assertion that reads as a safety guarantee while asserting nothing about behavior. Concretely, feed a fixture array covering success / neutral / skipped / failure / cancelled / timed_out / action_required / null / queued through the actual TOTAL / PENDING / RED expressions and assert the counts — that single test would have caught blocker 1. A rendering test over a fixture with duplicate names, skipped rows, and >60 entries would have caught blocker 2. Also, expect(workflowText).not.toContain('npm ') is brittle — it breaks if a comment ever mentions npm.

Repro

gh api "repos/QwenLM/qwen-code/commits/f35fc8dd10e7290696a2941a40549faf2bf92b6d/check-runs?per_page=100" \
  --paginate --jq '.check_runs' | jq -s 'add // []' > checks.json
# blocker 1
jq '[.[] | select(.status=="completed")
  | select(["success","neutral","skipped"] | index(.conclusion // "") | not)] | length' checks.json
# blocker 2
jq -r '.[] | [.name,.status,(.conclusion // "")] | @tsv' checks.json | sort -f | head -60 | grep -c 'Test (ubuntu'

中文说明

总体评价

问题诊断准确,架构方向正确:确定性的 workflow_run 任务、无模型、无 checkout、contents: read、仅用 PAT 写入、全程定长字符串匹配、通过 gh api user 校验评论作者——这确实比一个结构上不可能等到 30 分钟测试套件的 sleep 60 轮询更好。令牌(QWEN_CODE_BOT_TOKEN || CI_BOT_PAT)与 qwen-triage.yml 发评论所用的一致,作者过滤能对上,这一点我已验证。

但我拿真实数据实跑了该 workflow 自身的代码,发现两个阻塞问题,且都恰好推翻了本 PR 的核心安全声明。当前不可合并。

🔴 阻塞 1 —— 红灯判定永不生效,CI 失败反而会自动审批

jq 中 | 会重绑 .,因此 index(...) 内部的 .conclusion 是对数组 ["success","neutral","skipped"] 求值,直接报错 Cannot index array with string "conclusion"(exit=5)。脚本没有 set -e,于是 RED 为空串;[ "" -gt 0 ]integer expression expected 并返回非零,直接落入审批分支

端到端复现(输入:1 个 skipped + Test (ubuntu-latest, Node 22.x)failure):

total=2 pending=0 non-green=
[: : integer expression expected
>>> 到达 APPROVE 分支 <<<

A/B 对照(仅把 jq 换成正确写法):non-green=1approval withheld.

即:单测已经红了,延迟审批仍会提交一个 pin 到该 commit 的 APPROVE。这比它所替代的现状更差。修复:select((.conclusion // "") | IN("success","neutral","skipped") | not),并在分支前校验三个计数均为数字。

🔴 阻塞 2 —— finalize 会把它本该保全的 CI 证据删掉

#7680(本 PR 的动机样本,f35fc8dd,96 条 check-run)实跑渲染逻辑:渲染出的 60 行里 45 行是 skipped、15 行 success,而 Test (ubuntu-latest / macos / windows)web-shell E2E SmokeServe A/B 一条都没有

三个叠加原因:① 未按名字去重(filter=latest 只在 check suite 内去重,96 条仅 35 个不同名字,authorize 出现 7 次);② 未过滤 skipped(96 条中 68 条);③ sort -f 字母序 + head -n 60,截断点正好落在 review-configS/T/u/w 开头的真实测试任务全被切掉。红灯若排序靠后同样会被切掉,留下一张"看着很干净"的表。

修复(同一数据验证:96 → 16 行,且包含 Test (ubuntu-latest, Node 22.x)):按 .name 去重取最新、剔除 skipped非绿优先排序(保证截断永远切不掉失败项)。skipped 在审批门禁里仍按绿计算,只是不进表格。

🟠 中等

  1. E2E Tests 触发器是死的e2e.yml 没有 pull_request 触发器,if 条件会 100% 过滤掉它的所有触发;实际只有 Qwen Code CI 能触发本任务,"E2E 晚到也能收敛表格"不成立,且新测试把这个两元素列表固化了下来。
  2. 延迟审批可能被永久静默丢弃PENDING 统计该 SHA 上的所有 check-run,而本仓库的 pull_request_target / issue_comment bot workflow 会不断往 head SHA 上追加 check(perf(web-shell): paint the composer git chip before git status completes #7680 上有 triagereview-prrouteremove-suspicious-attachments,甚至 15:39 还新增了一批)。只要读取时有任一在跑,就 PENDING > 0continue且不更新 status 评论,而后续再无触发。建议把其余 pull_request workflow 加入 workflows: 列表,并在"仍延迟"分支更新 status 评论。
  3. 红灯判定排在 head/状态复核之前ci.yml 对 PR ref 开了 cancel-in-progress,force-push 会取消旧 run → 在过期 SHA 上触发 → cancelled 记为非绿 → 发出"CI 有 N 项未通过(过期 SHA)",而 STATUS_MARKER 不带 SHA,会覆盖新 head 的 status 评论。应把 CURRENT_HEAD != HEAD_SHA 复核前移。
  4. replace_region 在结束标记先于开始标记出现时会截断评论grep -qF 只证明两个字符串存在、不保证顺序,awk 随后一路吃到 EOF,且返回 0,调用方会 PATCH 掉被截断的正文——签名与 reviewed-commit footer(整套 triage 的背书凭据)随之丢失。已复现。建议 awk 加 END { if (inr) exit 1 } 并检查其退出码;现有测试只覆盖"完全没有结束标记",那种情况 grep 已经拦住了。

🟡 低

  • check-runs 返回空数组会导致审批TOTAL=0PENDING=0RED=0 → 走审批分支。建议加 TOTAL -gt 0 兜底。
  • 自身 check 排除是空操作,且注释把它当作事实陈述workflow_run 运行归属默认分支,本任务的 check-run 不会落到 PR head SHA 上(证据:同为 workflow_runServe A/B PublishWeb-shell Visuals Publishperf(web-shell): paint the composer git chip before git status completes #7680 的 96 条 head-SHA check 中一条都没有)。作为冗余防御无害,但注释与 skill 文档中的 select(.name != "finalize-triage-ci") 应改成"保险起见"的措辞。
  • cmp -s 的"已是最新"分支不可达:区域内嵌了含 github.run_idRUN_URL,每次触发都不同,因此每次都会 PATCH。
  • "(fork?)" 提示有误导:workflow_run 始终在 base 仓库上下文运行,secrets 可用。

✅ 已验证的优点

26/26 测试本地通过(13 旧 + 13 新);bash -n 通过;yamllint 通过(.yamllint 设了 allow-quoted-quotes: true,双引号的 if: 合规);scripts/tests/test:scriptstest:ci 确实在 CI 中运行;全程定长字符串匹配;转义链与 skill 的 sanitize_path 一致(& 最先、剔除控制字符、限长、<code> 包裹);按 SHA 串行且不取消;审批幂等(state == "APPROVED" and .commit_id == $sha)与 commit_id pin。

测试覆盖

把 bash 辅助函数抽出来实跑是对的方向,建议把同样的做法延伸到判定逻辑——两个阻塞问题都在那里。expect(script).toContain('["success","neutral","skipped"]') 在一段每次运行都报 jq 错误的代码上依然通过:它读起来像一条安全保证,实际上没有断言任何行为。建议用覆盖 success / neutral / skipped / failure / cancelled / timed_out / action_required / null / queued 的 fixture 跑真实的 TOTAL / PENDING / RED 表达式并断言计数——仅这一条测试就能拦住阻塞 1;再加一条含重名、skipped、>60 条目的渲染测试即可拦住阻塞 2。另外 expect(workflowText).not.toContain('npm ') 过于脆弱。

…ion, dead trigger

Review findings on the finalize workflow, all reproduced before fixing:

- Blocker 1: the RED jq used the array-first membership form, where | rebinds
  . and .conclusion indexes an array — jq exits 5 every run, RED comes back
  empty, [ "" -gt 0 ] errors, and control falls through to the approve path:
  a red CI auto-approved. The gate now binds the conclusion before the
  membership test (IN(...)), and the counters are numeric-validated so any
  future jq failure reads as 'cannot attest', never 'approve'.
- Blocker 2: the table rendered raw check-runs — on a real PR (96 runs, 35
  names, 68 skipped) alphabetical sort + head -60 truncated away every actual
  test job. table_rows now dedups per name (latest run), drops skipped rows,
  and sorts running/non-green first so the cap can only cut green rows.
  Replayed against the same PR: 96 rows -> 16, unit suite present.
- The approval gate now reads workflow runs filtered to event=pull_request
  (deduped per workflow) instead of head-SHA check-runs, which also carry
  long-running bot orchestration jobs that would wedge PENDING above zero at
  the exact moment the last CI workflow fires — silently dropping the
  deferred approval forever. The skill's Stage 3 PENDING count matches.
- E2E Tests had no pull_request trigger (dead entry); the workflows list is
  now exactly the six pull_request-triggered workflows, so the last finisher
  always re-fires the job.
- Head/state re-check moved before the red/deferred verdicts so a
  cancel-in-progress firing on a stale SHA cannot stamp a red status over
  the new head's comment; the still-deferred branch now updates the status
  comment instead of staying invisible.
- replace_region fails closed when the end-marker text only precedes the
  begin marker (awk END guard) — previously that shape truncated the comment
  body, eating the signature and reviewed-commit footer.
- Region content is deterministic (no run URL) so the no-op cmp works;
  empty run list or unavailable gate skips approval; comment wording fixed
  (workflow_run jobs are attributed to the default branch, so the self-check
  exclusion is belt-and-braces, not load-bearing).

Tests now execute the decision logic, not just grep for it: gate_counts and
table_rows run against fixtures covering every conclusion class, non-PR
events, re-run dedup, skipped filtering, ordering, and both marker-order
failure shapes. 30/30 passing.
@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — both blockers reproduced exactly as described before I touched anything, and they're humbling ones: the jq form at the heart of the "fail-closed" claim errored on every single run. All findings addressed in d2cb023.

Blocker 1 (red gate never fires → red CI auto-approves) — reproduced (jq: error … Cannot index array with string "conclusion", exit 5). The gate now binds the conclusion before the membership test — select((.conclusion // "") | IN("success","neutral","skipped") | not) — and all three counters go through numeric validation (case … '' | *[!0-9]*) that flips GATE_OK=false and skips approval, so any future jq failure reads as "cannot attest", never "approve". A fixture test now runs the real gate_counts through every conclusion class (success/neutral/skipped/failure/cancelled/timed_out/action_required/null+queued) and asserts the exact counts — it fails on the old code.

Blocker 2 (table truncates away the evidence) — reproduced on #7680's data: 96 check-runs, 35 names, 68 skipped, Test (ubuntu-latest) absent from the first 60 alphabetical rows. Adopted your fix as table_rows(): dedup per name (latest id), drop skipped from the table (still green for the gate), sort running/non-green first so the row cap can only ever cut green rows. Replayed against the same data: 96 → 16 rows, unit suite present, 0 skipped rows.

Gate data source (also closes finding 4) — the approval gate no longer reads head-SHA check-runs at all; it reads actions/runs?head_sha= filtered to event == "pull_request", deduped per workflow_id (latest run wins, mirroring branch-protection semantics). On #7680's SHA that filter drops 16 of 21 runs (pull_request_target ×3, pull_request_review ×9, pull_request_review_comment ×4) and leaves 5/0 pending/0 red — so bot orchestration runs can no longer wedge PENDING above zero at the moment the last CI workflow fires. The skill's Stage 3 PENDING count uses the same filter, so the agent and the finalize job can't disagree about what "pending" means. The still-deferred branch now also updates the status comment, so a deferred approval is never invisible.

Finding 3 (dead E2E trigger) — confirmed (e2e.yml: push/schedule/dispatch only). The list is now exactly the six pull_request-triggered workflows (Qwen Code CI, Qwen Autofix, SDK Java, SDK Python, Serve A/B, Web-shell Visuals), so whichever finishes last re-fires the job; the test pins the list and its rationale.

Finding 5 (stale red clobbers the new head's status) — the head/state/draft re-check now runs before any red/deferred verdict; a cancel-in-progress firing on a stale SHA takes the stale path (which is also the accurate message, since triage doesn't auto-rerun on synchronize).

Finding 6 (end-before-begin truncates the comment) — reproduced, fixed with the END { if (inr) exit 1 } guard; a new test covers exactly that shape (quoted end marker before the region → exit 1, comment untouched).

Lows — all taken: empty run list / unreadable gate → skip approval; self-exclusion comments reworded to belt-and-braces (your default-branch-attribution evidence is right); region content is now deterministic (no RUN_URL) so the cmp no-op branch actually works; "(fork?)" dropped; the truncation counter counts post-dedup rows. not.toContain('npm ') replaced with not.toContain('uses:') — stronger and not brittle.

30/30 tests passing (17 in the finalize suite, including the fixture-driven decision-logic tests you asked for — both would have caught the blockers).

中文说明

感谢审查——两个阻塞问题在动手前均按描述完整复现,且都很扎心:"fail-closed" 声明的核心 jq 每次运行都在报错。全部发现已在 d2cb023 处理。

阻塞 1(红灯判定失效 → 红 CI 自动审批):已复现(jq exit 5)。判定改为先绑定 conclusion 再做成员测试(IN(...)),三个计数器增加数字校验,任何 jq 失败一律"无法背书"、绝不落入审批分支。新增 fixture 测试实跑 gate_counts,覆盖全部 conclusion 类别并断言精确计数——旧代码跑不过这条测试。

阻塞 2(表格截断证据):在 #7680 数据上复现(96 条 check、35 个名字、68 条 skipped,字母序前 60 行没有 Test (ubuntu-latest))。采纳你的修复:按名字去重(取最新 id)、表格剔除 skipped(门禁中仍算绿)、运行中/非绿排最前,行数上限只可能切掉绿色行。同数据复验:96 → 16 行,单测在列。

门禁数据源(同时解决发现 4):审批门禁不再读 head SHA 的 check-runs,改读 actions/runs?head_sha= 且仅保留 event == "pull_request",按 workflow 去重取最新。在 #7680 的 SHA 上该过滤剔除 21 个 run 中的 16 个(pull_request_target ×3、pull_request_review ×9、pull_request_review_comment ×4),剩 5 个 / 0 pending / 0 red——bot 编排任务再也不会在最后一个 CI workflow 完成的瞬间把 PENDING 卡在非零。skill 的 Stage 3 PENDING 采用同一过滤,agent 与 finalize 对"pending"的定义不会分叉。"仍在延迟"分支现在也会更新 status 评论,延迟审批不再不可见。

发现 3(E2E 死触发器):确认无 pull_request 触发。列表改为恰好六个 pull_request 触发的 workflow,最后完成者必然重新触发本任务;测试固化了列表与理由。

发现 5(过期红灯覆盖新 head 的状态):head/状态/草稿复核前移至任何红/延迟判定之前;cancel-in-progress 在过期 SHA 上的触发走 stale 路径(这也是更准确的提示,因为 triage 不会在 synchronize 时自动重跑)。

发现 6(end 先于 begin 时截断评论):已复现,awk 加 END { if (inr) exit 1 } 修复;新增测试恰好覆盖该形态(引用的结束标记在区域之前 → exit 1、评论不动)。

低优先级:全部采纳——空 run 列表/门禁不可读则跳过审批;自身 check 排除的注释改为"保险起见"(你的默认分支归属证据是对的);区域内容改为确定性(去掉 RUN_URL),cmp 幂等分支真正可达;删除 "(fork?)";截断计数改为去重后行数;not.toContain('npm ') 换成更强且不脆的 not.toContain('uses:')

30/30 测试通过(finalize 套件 17 条,含你点名要的判定逻辑 fixture 测试——两条均可拦住原阻塞问题)。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

中文说明

已审查。 建议见行内评论。

— qwen3.8-max-preview via Qwen Code /review

Comment thread scripts/tests/qwen-triage-finalize-workflow.test.js Outdated
Comment thread .github/workflows/qwen-triage-finalize.yml
Comment thread .github/workflows/qwen-triage-finalize.yml Outdated
…eview's status comment

The status comment is deliberately not SHA-scoped (the triage workflow
creates it unscoped; scoping only the finalize side would orphan the
pairing), so a finalize firing for an old SHA that loses the race against a
newer head's green approval would overwrite the ✅ status with a stale
warning. Guard the stale path: when the current head already carries bot
sha= markers (a re-review owns the status comment), stay silent; when the
head moved with no re-review yet — triage does not auto-rerun on
synchronize — the stale note is accurate and still posts. Closed/draft PRs
now just log instead of flipping the status.

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

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

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

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Review

Strong direction, and the security posture is the best part: no uses:, no checkout, every input via env: (no ${{ }} inside run:), markers honored only in bot-authored comments, approval pinned to commit_id. I extracted the step script and ran it against a stubbed API to confirm the behaviour rather than read it — forged markers in a third-party comment produce zero writes, a moved head posts the stale note instead of approving, a red gate withholds, and a re-fire on unchanged state does no PATCH and no second approval. html_escape neutralises < > | @ [ ] ( ) * \`` and double-escapes a pre-supplied & correctly (&` first), and the 60-row cap only ever truncates green rows. 30 tests pass, prettier and eslint are clean.

Four things I'd fix before merge.

1. The fork-refactor approval guardrail now runs after the marker is posted

In pr-workflow.md, the approve-on-green marker is emitted in Step 1 (post the reflection comment), but GUARD — the deterministic isCrossRepository && title ~ ^refactor check — is computed in Step 2, ~20 lines later. The marker's emission condition is only "verdict is approve and PENDING > 0"; it never mentions the guardrail.

That marker is now an approval with a CI precondition attached. So for a fork refactor PR the sequence is: agent decides "approve" → posts a comment carrying approval authority → then evaluates the guardrail that says "do not approve no matter how clean every stage looked". Withdrawing it requires the agent to notice and edit the comment it just posted. The 3/5 confidence cap at line 535 is the only thing standing in the way, and that's exactly the eyeball judgement the guardrail says not to rely on ("Decide it deterministically — do not eyeball it"; the rule exists because such a PR was once wrongly auto-approved and merged).

Cheapest robust fix is to re-assert the guardrail in the finalize job, where it costs zero extra API callsPR_STATE_JSON already carries everything:

IS_FORK="$(jq -r '(.head.repo.full_name // "") != .base.repo.full_name' <<<"$PR_STATE_JSON")"
PR_TITLE="$(jq -r '.title' <<<"$PR_STATE_JSON")"
if [ "$IS_FORK" != false ] && printf '%s' "$PR_TITLE" | grep -qiE '^[[:space:]]*refactor'; then
  echo "PR #$PR: fork refactor — approval guardrail blocks the deferred approval."
  update_status "$PR" guarded
  continue
fi

(A deleted fork makes .head.repo null → != base → treated as fork → blocked. Fail-closed.) That turns the guardrail into a structural property of the approving code path instead of a prompt-ordering property — which is the same argument this PR makes for treating markers as forgeable. Worth also moving the GUARD computation into Step 1 so the doc reads in the order it executes.

2. The finalized table renders bot-orchestration check-runs as CI evidence

table_rows reads all check-runs on the head SHA and excludes only $SELF_CHECK_NAME. The gate deliberately reads a different universe (event == "pull_request" workflow runs) — but the table doesn't, so the two disagree on what "CI" means.

On this PR's own head (cadf3f0), here is what the finalize job would render after dropping skipped rows:

Check in the PR's CI suite?
Classify PR
Test (ubuntu-latest, Node 22.x)
web-shell E2E Smoke (ubuntu-latest, Node 22.x)
Remind on force-push
label
precheck-pr / precheck
remove-suspicious-attachments
route

Five of eight rows are bot plumbing. The agent's hand-written table quoted the checks that matter; the first finalize pass replaces it with one where Remind on force-push ✅ is presented as test evidence. It also makes the caption lie: PENDING comes from workflow runs, so a still-running orchestration job renders ⏳ running underneath "Final CI results … after CI completed".

Both are fixed by filtering the table to the suites the gate already trusts — /tmp/runs.json is on disk and carries check_suite_id:

table_rows() { # $1 checks-file  $2 runs-file
  jq -r --arg self "$SELF_CHECK_NAME" --slurpfile runs "$2" '
    ([$runs[0][] | select(.event == "pull_request") | .check_suite_id]) as $ok
      | map(select(.name != $self))
      | map(select(.check_suite.id as $s | $ok | index($s)))
      | …' "$1"
}

On cadf3f0 that leaves exactly the three CI rows.

3. The status comment can get stuck at "⏳ Approval still deferred" after the approval has landed

PENDING > 0 calls update_status "$PR" deferred unconditionally — including after the deferred approval already went out. When things settle green, the ALREADY = true branch continues before update_status … approved, so nothing ever puts it back. Reproduced end-to-end against the stub:

>>> approval already on the PR; one listed workflow re-running
PR #1: 1 PR CI workflow run(s) still in progress; approval stays deferred.
STATUS -> ⏳ Approval still deferred — 1 PR CI workflow run(s) still in progress for `aaaaaaa`
>>> re-run finishes green, finalize fires again
PR #1: already approved at aaaaaaa.
STATUS -> ⏳ Approval still deferred — 1 PR CI workflow run(s) still in progress for `aaaaaaa`   ← permanent

Reachable whenever any firing observes PENDING > 0 after the approval — a manual job re-run, Qwen CI Failure Patrol, or a label event starting Qwen Autofix (its pull_request: [labeled, unlabeled] trigger is in the gate's universe). Cosmetic, but the status comment is precisely the surface this PR adds so the outcome is visible without opening Stage 2. One line: call update_status "$PR" approved in the ALREADY branch, or skip the deferred update when an approval for this SHA already exists.

4. An empty check-runs response blanks the table instead of leaving it alone

The fetch failure is guarded; an empty result is not. jq -s 'add // []' yielding [] — or every check on the SHA being skipped — produces a header-only table that overwrites the agent's evidence:

Final CI results for `aaaaaaa` (auto-updated by the triage finalize job after CI completed):

| Check | Result |
| ----- | ------ |

Everywhere else this workflow reads "cannot read" as "do nothing" (missing end marker → leave untouched; unparseable counters → skip approval). Same discipline here: [ "$ROWS" -gt 0 ] || skip the table rewrite. Related, replace_region's getline line < rfile return value is unchecked — an unreadable or empty /tmp/region.md silently deletes the region and its markers, so no later run can repair it. [ -s /tmp/region.md ] before the loop closes both.

Nits

  • No github.repository == 'QwenLM/qwen-code' guard. 25 workflows in .github/workflows/ have one, including every bot workflow. Without it a fork with CI_BOT_PAT configured runs the full finalize against the fork. Harmless today, but it's the house convention.
  • Table header drifts. The skill template says | Check | Conclusion |; the finalize job writes | Check | Result |, so the header silently changes on the first pass. Pick one.
  • PR description is stale relative to d2cb023/cadf3f0: it says the workflow fires on Qwen Code CI / E2E Tests, but E2E Tests is deliberately excluded (correctly — no pull_request trigger) and a test asserts its absence; and the test-plan counts read "26 tests pass (13 new)" where the suites now run 30 (17 new).

The workflow list itself checks out — Qwen Code CI, Qwen Autofix, SDK Java, SDK Python, Serve A/B, Web-shell Visuals are exactly the six workflows in the repo with a pull_request trigger, and Qwen Code CI has no path filter, so the long pole always exists. Concurrency is genuinely convergent: GitHub drops the pending run in a group, never the running one, so the last firing always re-reads fresh state.

中文说明

评审

方向很好,安全姿态是最亮眼的部分:没有 uses:、不 checkout、所有输入经 env: 传入(run: 内没有任何 ${{ }})、标记仅在 bot 自己发的评论中生效、审批 pin 到 commit_id。我把 step 脚本抽出来,用打桩的 API 实际跑了一遍而不是只读代码——第三方评论中伪造的标记产生 次写入;head 变更时发布 stale 说明而不审批;红色检查时搁置;状态不变时重复触发不会 PATCH、也不会重复审批。html_escape 正确中和了 < > | @ [ ] ( ) * \``,并且对已存在的 & 做了正确的二次转义(&` 最先);60 行上限只会截断绿色行。30 个测试通过,prettier 与 eslint 干净。

有四点建议在合并前处理。

1. fork-refactor 审批护栏现在跑在标记发布之后

pr-workflow.md 中,approve-on-green 标记在 Step 1(发布反思评论)产生,而 GUARD——那个确定性的 isCrossRepository && title ~ ^refactor 检查——却在约 20 行之后的 Step 2 才计算。标记的产生条件只写了"结论为 approve 且 PENDING > 0",完全没有提到护栏。

这个标记如今就是一份"附带 CI 前置条件的审批"。于是对 fork refactor PR 的时序变成:agent 判定 approve → 发布一条带有审批权力的评论 → 然后才去评估那个写着"无论各阶段多干净都不要审批"的护栏。要撤回就得靠 agent 自己发现并编辑刚发出的评论。唯一的阻拦是第 535 行的 3/5 置信度上限,而那恰恰是护栏明令不要依赖的目测判断("Decide it deterministically — do not eyeball it";这条规则的存在正是因为曾有这样一个 PR 被错误自动审批并合并)。

最省事又稳妥的修法是在 finalize job 里重新断言这条护栏,零额外 API 调用——PR_STATE_JSON 里已经全有了:

IS_FORK="$(jq -r '(.head.repo.full_name // "") != .base.repo.full_name' <<<"$PR_STATE_JSON")"
PR_TITLE="$(jq -r '.title' <<<"$PR_STATE_JSON")"
if [ "$IS_FORK" != false ] && printf '%s' "$PR_TITLE" | grep -qiE '^[[:space:]]*refactor'; then
  echo "PR #$PR: fork refactor — approval guardrail blocks the deferred approval."
  update_status "$PR" guarded
  continue
fi

(fork 被删除时 .head.repo 为 null → != base → 视为 fork → 拦截,fail-closed。)这样护栏就成为审批代码路径的结构性属性,而不是提示词顺序的属性——这正是本 PR 用来论证"标记可被伪造"的同一套逻辑。同时建议把 GUARD 的计算前移到 Step 1,让文档的阅读顺序与执行顺序一致。

2. finalize 后的表格把 bot 编排类 check-run 当成了 CI 证据

table_rows 读取 head SHA 上的全部 check-runs,只排除了 $SELF_CHECK_NAME。而审批门禁刻意读的是另一个集合(event == "pull_request" 的 workflow run)——表格却没有,于是两边对"什么算 CI"的定义不一致。

在本 PR 自己的 head(cadf3f0)上,丢弃 skipped 之后 finalize 会渲染出:

检查 属于该 PR 的 CI suite?
Classify PR
Test (ubuntu-latest, Node 22.x)
web-shell E2E Smoke (ubuntu-latest, Node 22.x)
Remind on force-push
label
precheck-pr / precheck
remove-suspicious-attachments
route

八行里五行是 bot 管道。Agent 手写的表格引用的是真正要紧的检查;第一次 finalize 会把它替换成一张把 Remind on force-push ✅ 当作测试证据呈现的表。这也让标题文案自相矛盾:PENDING 来自 workflow run,因此一个仍在运行的编排 job 会在"Final CI results … after CI completed"下面渲染成 ⏳ running

两个问题都可以通过把表格过滤到门禁已经信任的 suite 来解决——/tmp/runs.json 就在磁盘上,且带有 check_suite_id

table_rows() { # $1 checks-file  $2 runs-file
  jq -r --arg self "$SELF_CHECK_NAME" --slurpfile runs "$2" '
    ([$runs[0][] | select(.event == "pull_request") | .check_suite_id]) as $ok
      | map(select(.name != $self))
      | map(select(.check_suite.id as $s | $ok | index($s)))
      | …' "$1"
}

cadf3f0 上,这样恰好只剩那三行 CI。

3. 审批落地之后,状态评论可能永久卡在"⏳ 审批仍在延迟中"

PENDING > 0 会无条件调用 update_status "$PR" deferred——包括延迟审批已经发出之后。等一切转绿时,ALREADY = true 分支在 update_status … approved 之前continue 了,于是再也没有东西把它改回来。用打桩端到端复现:

>>> PR 上已有审批;某个受监听 workflow 正在重跑
PR #1: 1 PR CI workflow run(s) still in progress; approval stays deferred.
STATUS -> ⏳ Approval still deferred — 1 PR CI workflow run(s) still in progress for `aaaaaaa`
>>> 重跑绿色完成,finalize 再次触发
PR #1: already approved at aaaaaaa.
STATUS -> ⏳ Approval still deferred — 1 PR CI workflow run(s) still in progress for `aaaaaaa`   ← 永久停留

只要审批之后任何一次触发观察到 PENDING > 0 就会命中——手动重跑某个 job、Qwen CI Failure Patrol、或者一次打标签事件启动 Qwen Autofix(它的 pull_request: [labeled, unlabeled] 触发器正好落在门禁集合里)。这是外观问题,但状态评论恰恰是本 PR 为了"不打开 Stage 2 也能看到结果"而新增的界面。一行即可:在 ALREADY 分支里调用 update_status "$PR" approved,或者当该 SHA 已有审批时跳过 deferred 更新。

4. check-runs 返回空数组时会清空表格,而不是保持原样

拉取失败有保护,返回空结果没有。jq -s 'add // []' 得到 []——或者该 SHA 上所有检查都是 skipped——会产生一张只有表头的表并覆盖掉 agent 的证据:

Final CI results for `aaaaaaa` (auto-updated by the triage finalize job after CI completed):

| Check | Result |
| ----- | ------ |

这个 workflow 在其他地方一律把"读不到"当作"什么都不做"(缺结束标记 → 不改动;计数不可解析 → 跳过审批)。这里也应同样处理:[ "$ROWS" -gt 0 ] || 跳过表格重写。相关地,replace_regiongetline line < rfile 的返回值没有检查——/tmp/region.md 不可读或为空时会静默删除整个区域连同定界符,之后任何一次运行都无法修复。在循环前加 [ -s /tmp/region.md ] 可同时堵住这两条。

小问题

  • 缺少 github.repository == 'QwenLM/qwen-code' 守卫。 .github/workflows/ 下有 25 个工作流带这个守卫,所有 bot 工作流都带。没有它的话,一个配置了 CI_BOT_PAT 的 fork 会对着 fork 自己跑完整的 finalize。今天无害,但这是仓库惯例。
  • 表头漂移。 skill 模板写的是 | Check | Conclusion |,finalize 写的是 | Check | Result |,第一次 finalize 后表头会悄悄变化。二者统一即可。
  • PR 描述相对 d2cb023/cadf3f0 已过期:描述说工作流在 Qwen Code CI / E2E Tests 上触发,但 E2E Tests 是被刻意排除的(这是对的——它没有 pull_request 触发器),而且有测试断言它不在列表里;测试方案里的数字写着"26 个测试通过(新增 13 个)",实际现在是 30 个(新增 17 个)。

工作流清单本身没问题——Qwen Code CIQwen AutofixSDK JavaSDK PythonServe A/BWeb-shell Visuals 恰好就是仓库里全部六个带 pull_request 触发器的工作流,且 Qwen Code CI 没有路径过滤,因此长尾检查始终存在。并发控制确实是收敛的:GitHub 丢弃的是同组里 pending 的那个运行,从不丢弃正在运行的,所以最后一次触发总能读到最新状态。


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

…with the gate

Second review round, all four findings reproduced or confirmed before fixing:

- The approve-on-green marker was emitted in Step 1 while the fork-refactor
  GUARD only ran in Step 2 — a marker that slipped out on a fork refactor
  would have been honored by the finalize job on green CI, bypassing the
  guardrail entirely. GUARD now computes in Step 1 and gates the marker's
  emission, and the finalize job re-asserts it structurally from the PR
  state it already fetched (null head.repo = deleted fork = blocked), with
  a 'guarded' status message instead of an approval.
- table_rows now restricts check-runs to the suites of the same deduped
  event=pull_request workflow runs the gate trusts. Without it, 5 of 8
  rendered rows on this PR's own head were bot plumbing presented as CI
  evidence; with it, 115 raw check-runs reduce to exactly the 3 CI rows.
- A firing that saw PENDING>0 after the approval landed flipped the status
  comment back to 'deferred' with nothing to ever right it; the
  already-approved branch now repairs the status.
- Zero surviving table rows (failed runs fetch, missing suite ids) skips
  the region rewrite instead of blanking the agent's table, and
  replace_region refuses an empty region file (an unchecked getline would
  have deleted the region and its markers unrecoverably).

Nits: the house github.repository guard on the job, the table header
matches the skill template, and the run-URL stays out of the region so the
no-op cmp keeps working.
@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

All four fixed in 75ee714, each verified the same way you found them.

1. Guardrail bypass — the worst of the round, and correctly diagnosed: the marker was an approval with a CI precondition, minted before the check that says "never approve this class of PR". Two-sided fix: GUARD now computes in Step 1 and gates the marker's emission (approve verdict + PENDING > 0 + GUARD ok + no Stage-0 escalation), and the finalize job re-asserts it structurally from PR_STATE_JSON exactly as you sketched — null head.repo counts as fork, grep -qiE '^[[:space:]]*refactor', and a new 🛡️ guarded status message instead of an approval. The prompt-ordering property became a code-path property, which is indeed the same argument this PR makes about markers.

2. Table/gate universe mismatch — adopted your check_suite_id filter, with the suite set taken from the deduped event == "pull_request" runs (superseded runs' suites drop out too). Replayed on this PR's own head: 115 raw check-runs → exactly the 3 CI rows you predicted (Classify PR, Test (ubuntu-latest), web-shell E2E Smoke); on #7680's data: 96 → 12, all from the PR's own suites. This also fixes the caption contradiction, since a still-running orchestration job can no longer render under "Final CI results".

3. Stuck "deferred" status — reproduced logic confirmed; the ALREADY branch now calls update_status approved, so the next settle-green firing repairs whatever a PENDING > 0 firing wrote after the approval landed.

4. Blank-table hazard — zero surviving rows now skips the region rewrite entirely (warning logged, agent's table left standing), and replace_region refuses an empty/unreadable region file up front ([ -s ]), closing the unchecked-getline marker-deletion path.

Nits: repository guard added to the job if (house convention), header unified to | Check | Conclusion | per the skill template, and the PR description refreshed (trigger list, gate description, 31/18 test counts). The behavioral suite now covers the suite filter (a superseded run's suite and a bot suite in the fixture must not render) alongside the gate fixture.

中文

四项均已在 75ee714 修复,并按你的方法逐一复验。

1. 护栏绕过——本轮最严重且诊断准确:标记本质是"附带 CI 前置条件的审批",却在"这类 PR 永不审批"的检查之前发出。双侧修复:GUARD 前移至 Step 1 并作为标记发出的门槛(approve 结论 + PENDING > 0 + GUARD ok + 无 Stage 0 上报),finalize 侧按你的草案从 PR_STATE_JSON 结构性重断言——head.repo 为 null 视为 fork、grep -qiE '^[[:space:]]*refactor'、新增 🛡️ guarded 状态文案而非审批。提示词顺序属性变成了代码路径属性——正是本 PR 论证"标记可伪造"的同一逻辑。

2. 表格与门禁集合不一致——采纳 check_suite_id 过滤,suite 集合取自去重后event == "pull_request" run(被取代 run 的 suite 一并剔除)。在本 PR 自身 head 上复验:115 条原始 check-run → 恰好你预测的 3 行 CI;在 #7680 数据上:96 → 12,全部来自 PR 自身 suite。这同时修复了标题自相矛盾的问题。

3. 状态卡在"延迟中"——逻辑复现确认;ALREADY 分支现在调用 update_status approved,下一次全绿触发即可修复审批落地后被 PENDING > 0 触发覆盖的状态。

4. 空表覆盖风险——存活行数为零时整体跳过区域重写(记录 warning、保留 agent 手写表格),replace_region 开头即拒绝空/不可读的区域文件([ -s ]),堵住未检查 getline 导致标记被删的路径。

Nit:job if 加上仓库守卫(仓库惯例)、表头统一为 | Check | Conclusion |、PR 描述已刷新(触发列表、门禁描述、31/18 测试计数)。行为测试套件新增 suite 过滤覆盖(fixture 中被取代 run 的 suite 与 bot suite 必须不渲染)。

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

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

中文说明

⚠️ 已从批准降级为评论:CI still running。 已审查。

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

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

LGTM, looks ready to ship. ✅

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

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 25, 2026
Merged via the queue into QwenLM:main with commit 1f9318f Jul 25, 2026
62 checks passed
pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Jul 25, 2026
…n model injection (QwenLM#7703)

* fix(triage): resolve stage comment ids by marker at patch time, harden model injection

Two hardenings from shepherding QwenLM#7693, plus a wording fix:

- Re-run comment updates now resolve the target comment id by its stage
  marker (bot-author-filtered, startswith match) immediately before each
  PATCH, instead of trusting remembered ids or list positions. Observed on
  a real re-run: the agent PATCHed the stage=3 comment with stage=1 content
  mid-run before self-correcting — with four bot comments in the thread,
  remembered-id bookkeeping is fragile.
- The model-name injection step previously no-opped silently if the
  'qwen3.7-max' literal ever left the skill (shipping the wrong signature
  in every comment), and corrupted the skill text on model names carrying
  sed metacharacters (/ & \). It now fails the job loudly when the target
  literal is missing and escapes the replacement. Covered by a behavioral
  test that runs the extracted step script against fixture files.
- The finalize status text said 'stage comments above', but the status
  comment is created first, so the stage comments are below it — now
  'in this thread'.

* chore(triage): drop unrelated formatting churn from qwen-triage.yml

The previous commit let prettier rewrite untouched lines (runs-on quoting,
comment spacing) while formatting the edited step. Restore those lines to
main's form; the diff now carries only the injection hardening and the
status wording fix.

* test(triage): pin the stage_comment_id recipe's load-bearing constraints

Guards the startswith match and the bot-author filter in the skill's re-run
comment-id recipe against silent regression — a contains match or a dropped
author filter re-introduces the wrong-comment-overwrite bug this PR fixes.

* test(triage): shim BSD sed only on darwin in the injection test

The extracted step script uses GNU 'sed -i' (the step only runs on ubuntu
runners), but this suite also runs in the macOS merge-queue job where BSD
sed needs an extension argument after -i. Rewrite to sed -i '' on darwin
only — on GNU sed a separated '' parses as the sed script, so the
unconditional rewrite would break the Linux runs that mirror production.
pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Jul 25, 2026
…it association (QwenLM#7706)

Live verification of the finalize loop (QwenLM#7693) on its first real fork PR
caught the deferred approval being silently dropped: CI landed green, the
approve-on-green marker was in place, but commits/:sha/pulls returned an
empty list for the PR's current head — the association endpoint is not
reliable for fork-branch commits (workflow_run.pull_requests is likewise
empty for forks). The run logged 'No open PR; nothing to finalize' and
exited, so the approval never posted.

Resolve PRs by filtering the open-PR list on head.sha as the primary
source — it cannot miss the PR a current-head firing belongs to — and keep
the association endpoint as the second source (when it works it also
surfaces PRs whose head moved past the SHA, which powers the stale note).
Union both, deduped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants