Skip to content

fix(ci): refuse a partial jobs page list before reporting - #15174

Merged
wenshao merged 8 commits into
mainfrom
fix/15138-partial-jobs-pages
Oct 23, 2026
Merged

wenshao merged 8 commits into
mainfrom
fix/15138-partial-jobs-pages

Conversation

@wenshao

@wenshao wenshao commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

The main-branch failure reporter only publishes a job list it can prove it finished reading, and it tells its readers when it could not. The paginated fetch is merged into a staging file and promoted only when the jobs it holds account for the total the response itself declares; a response carrying no total cannot be certified either, so it takes the same fallback as a failed page: the staging file is removed, an empty list is published, and a warning is emitted. The summary step then adds one sentence when the list was short. Because the issue writer now publishes that same summary body, one flag covers both surfaces — the PR comment and the issue record — and since each occurrence posts a fresh body, the sentence is always about the run it sits in and can never go stale.

Why it's needed

When a run has more than one page of jobs and a later page fails, the merge keeps whichever pages arrived. The reporter then holds a list that looks complete: the failing-job count is plausible, the summary names real jobs, and nothing downstream can tell it apart from a full read. The infrastructure classifier consumes that same list and counts only the jobs present, so a genuine regression sitting on a missing page could be classified as a runner problem — the run is rerun, the issue is suppressed, and the rerun's own green result means nothing re-enters the reporter to file it. Two follow-on defects surfaced during review: the first version of the receipt treated a missing total as nothing to check, which published a populated page as the whole run, and the degradation note was append-only into a shared issue body. Upstream has since replaced that issue planner with a workflow-identity aggregate that posts a fresh comment per run, so the append-only mechanism is gone with it; this branch carries only the receipt and the per-run sentence.

Reviewer Test Plan

How to verify

Run the failure-reporter meta-test; it executes the step bodies under a fake gh that emulates paginated responses. Four pagination cases matter: a later page fails with a declared total, a later page fails with no declared total, gh exits 0 while carrying fewer jobs than it declares, and both pages arrive intact. The first three must publish {"jobs":[]}, leave no staging file, emit the warning, and report jobs_complete=false; the last must publish both jobs, report jobs_complete=true, and stay silent. The summary step is then spawned twice on the same fixtures: with jobs_complete=false its body must carry the "could not be read in full" sentence, with true it must not. A pin keeps the producer's jobs_complete output and the consumer's JOBS_COMPLETE binding from drifting apart.

To see each guard rather than take it on trust, apply the mutation named in the evidence table and confirm the suite turns red on that mutation alone.

Evidence (Before & After)

Behaviour rows were measured on the step-level harness against the base for the before column; every mutation, lint and size row was measured at the commit named in its row.

Check Mutation or command Observed Measured at
Partial second page, declared total fake gh: page 1 of 150 jobs, page 2 answers 403 before: 100 of 150 jobs published as the whole run, infra_only=true; after: empty list, warning, infra_only=false, issue filed base 461be87 / ef371fd
Response with no total_count fake gh: populated page, then an API error body rejected, {"jobs":[]}, staging file removed, warning emitted ef371fd
Both pages intact fake gh: two pages, 150 jobs all 150 jobs and both failures preserved, no warning ef371fd
gh exits 0 but short fake gh: one page declaring total_count: 2, carrying one job, exit 0 rejected — the status cannot see it, the receipt is the only guard 2e77947c03
Meta-test npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/main-ci-failure-issue-workflow.test.js 76 passed (76) 2e77947c03
Pin covers the fetch drop --paginate from the workflow AssertionError: expected 'log_dir="${RUNNER_TEMP}/failed-logs" …' to contain 'gh api "repos/${REPO}/actions/runs/${…' 2e77947c03
Degradation flag set jobs_complete=true on the fallback path AssertionError: expected 'true' to be 'false' 2e77947c03
Summary states the degradation delete the note from the summary step AssertionError: expected 'body<<QWEN_CI_COMMENT_…' to contain 'The job list could not be read in full' 2e77947c03
Wiring is pinned rename id: 'download' to id: 'fetch_logs' AssertionError: expected undefined to be truthy 2e77947c03
Format and lint prettier --check, eslint on the changed files clean 2e77947c03
Workflow shell lint actionlint -shellcheck= -ignore 'SC2016:' … (the flags CI passes) exit 0, no findings 2e77947c03
Workflow size ratchet .github/scripts/check-workflow-size.sh 27541 bytes, limit 27613 2e77947c03

Tested on

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

macOS ran the suites locally with bash 5 on PATH, because the step body uses a builtin the system bash 3.2 lacks; Linux is where CI runs them and was not executed locally.

Environment (optional)

Local unit tests only; no sandbox, no CLI session. The harness spawns the workflow's step bodies verbatim with a fake gh.

Risk & Scope

  • Main risk or tradeoff: a fetch whose pages disagree with the declared total is treated as unreadable, so the report names no job. That is the existing fallback for a failed log — precision lost, issue still filed — and both bodies now say so. The workflow sits at 27541 of 27613 bytes on the size ratchet, so further logic needs prose moved out first.
  • Not validated / out of scope: the reviewer's suggestion of a bounded retry on the fetch. An immediate retry repeats the same walk inside the same rate-limit window, and the neighbouring reads in this step deliberately warn and continue instead of retrying; the degradation is now visible, which was the silent part. The separate silent-drop path when the failure destination lookup itself fails also stays untouched — an existing test pins that tradeoff deliberately, and the linked issue keeps it open for a maintainer decision.
  • Breaking changes / migration notes: none. This head is a merge of main; the upstream restructure of the issue writer means the earlier plan-step note logic is no longer present, and the summary body it replaces is published per run. CI note: web-shell E2E Smoke has been red across recent heads with failures in specs this PR does not touch (at 9f18c4661b: 343 passed, 10 flaky, 3 failed in managed-context-compression, web-shell.browser-notifications and web-shell.excel-preview), while Test, Lint & Static, Integration Tests and Managed Agent gate passed on the same head. Each push re-runs the suite.

Linked Issues

Refs #15138 — this addresses the partial-pagination finding only. The destination-lookup finding in the same issue is untouched and remains open for discussion.

中文说明

本 PR 做了什么

main 分支的失败上报器只发布它能证明自己读完的 job 列表,并在读不全时明确告诉读者。分页抓取结果先合并进暂存文件,只有当其中 job 数与响应自身声明的总数相符时才替换正式文件;响应若根本没有声明总数,同样不能被认定为完整,因此走与「某一页失败」相同的兜底:删除暂存文件、发布空列表、发出警告。列表偏短时,summary 步骤追加一句说明。由于 issue 写入现在直接发布这份 summary 正文,一个标记就同时覆盖两个出口 —— PR 评论与 issue 记录 —— 且每次发生都以新正文发布,这句话永远描述它所在的那次运行,不会过期。

为什么需要

当一次运行有多页 job 而后面某一页失败时,合并步骤会保留已经到达的那几页。上报器于是持有一份看起来完整的列表:失败 job 数量看似合理、摘要里点名了真实 job,下游没有任何东西能把它与完整读取区分开。基础设施分类器读的是同一份列表,只统计其中存在的 job,因此落在缺失页上的真实回归可能被判成 runner 问题 —— 运行被重跑、issue 被抑制,而重跑自身变绿意味着再没有任何东西会重新进入上报器去建这个 issue。评审过程中还暴露出两个次生缺陷:凭据的第一版把「缺少总数」当成「无需校验」,会把一页数据发布成整次运行;降级说明则是「只增不删」地写进共享的 issue 正文。上游随后把该 issue 规划器换成了按 workflow 身份聚合、每次运行发布一条新评论的写入器,因此「只增不删」的机制随之一并消失;本分支只保留凭据与「按次」的说明句。

评审测试计划

如何验证

运行失败上报器的元测试;它用假 gh 模拟分页响应并逐字执行步骤脚本。四个分页用例最关键:后续页失败且声明了总数、后续页失败但无总数、gh 退出 0 但携带的 job 少于声明、以及两页完整。前三种必须发布 {"jobs":[]}、不留下暂存文件、发出警告并报告 jobs_complete=false;最后一种必须发布两个 job、报告 jobs_complete=true 且保持沉默。随后在同一批夹具上把 summary 步骤跑两次:jobs_complete=false 时正文必须带「读不全」的说明,true 时不得带。另有断言把生产者的 jobs_complete 输出与消费者的 JOBS_COMPLETE 绑定钉在一起,防止两者漂移。

要亲自验证每条守卫而不是采信结论,按证据表里写明的变异改一处,确认只有对应的断言变红。

证据(前后对比)

行为类各行在基线版本的步骤级 harness 上测得;变异、lint 与体积各行均在表格标注的 commit 上测得。

检查 变异或命令 观测结果 测量所在 commit
后续页失败、声明总数 假 gh:第 1 页 150 个 job,第 2 页返回 403 修复前:100/150 被当作整次运行发布,infra_only=true;修复后:空列表、警告、infra_only=false,仍建 issue 基线 461be87 / ef371fd
响应没有 total_count 假 gh:一页有数据,随后 API 错误体 被拒绝,{"jobs":[]},暂存文件已删,发出警告 ef371fd
两页完整 假 gh:两页共 150 个 job 150 个 job 与两个失败全部保留,无警告 ef371fd
gh 退出 0 但偏短 假 gh:单页声明 total_count: 2、只带 1 个 job、退出 0 被拒绝 —— 退出码看不到,只有凭据能守 2e77947c03
元测试 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/main-ci-failure-issue-workflow.test.js 76 个全部通过 2e77947c03
断言覆盖抓取命令 从 workflow 删掉 --paginate AssertionError: expected 'log_dir="${RUNNER_TEMP}/failed-logs" …' to contain 'gh api "repos/${REPO}/actions/runs/${…' 2e77947c03
降级标记 在兜底分支把 jobs_complete 设为 true AssertionError: expected 'true' to be 'false' 2e77947c03
正文说明降级 删掉 summary 步骤里的说明行 AssertionError: expected 'body<<QWEN_CI_COMMENT_…' to contain 'The job list could not be read in full' 2e77947c03
接线被固定 把 id: 'download' 改名为 id: 'fetch_logs' AssertionError: expected undefined to be truthy 2e77947c03
格式与 lint 对改动文件跑 prettier --check、eslint 干净 2e77947c03
workflow shell lint actionlint -shellcheck= -ignore 'SC2016:' …(与 CI 相同参数) exit 0,无发现 2e77947c03
workflow 体积棘轮 .github/scripts/check-workflow-size.sh 27541 字节,上限 27613 2e77947c03

测试平台

macOS 本机用 PATH 上的 bash 5 运行测试,因为步骤脚本使用的某个内建在系统 bash 3.2 中不存在;Linux 是 CI 运行环境,未在本机执行。Windows 不适用。

环境(可选)

仅本地单元测试;未使用沙箱,未启动 CLI 会话。harness 用假 gh 逐字执行 workflow 的步骤脚本。

风险与范围

  • 主要风险或取舍:如果各页结果与声明的总数不一致,这次抓取会被当作不可读,报告就不点名任何 job。这正是日志下载失败时已有的兜底 —— 损失精度,issue 仍然创建 —— 而且现在两份正文都会说明。workflow 体积为 27541/27613 字节,再加逻辑需先把说明文字搬出。
  • 未验证/超出范围:评审建议的「有界重试」。立即重试会在同一个限流窗口内重复同一次抓取,而本步骤相邻的读取本就是告警后继续、不重试;现在降级已可见,这才是原先「静默」的部分。另外,失败目标发现自身失败时的静默丢弃路径也未改动 —— 已有测试刻意固定该取舍,链接的 issue 将其保持开放等待维护者决策。
  • 破坏性变更/迁移说明:无。本 head 是一次 main 合并;上游重写了 issue 写入器,因此早先 plan 步骤里的说明逻辑不再存在,取而代之的 summary 正文按次发布。CI 说明:web-shell E2E Smoke 在最近几个 head 上一直红,失败在本 PR 未触及的 spec 里(9f18c4661b:343 通过、10 个 flaky、3 个失败,分别在 managed-context-compression、web-shell.browser-notifications、web-shell.excel-preview),而同一 head 上 Test、Lint & Static、Integration Tests、Managed Agent gate 均通过。每次推送都会重跑该套件。

关联 issue

Refs #15138 —— 本 PR 只处理分页不完整这一项。同一 issue 中的目标发现项未改动,仍待讨论。

`--paginate` prints one document per page and the step merges them with
`jq -s`, so a page 2 failure leaves a list holding only page 1. The exit
status cannot tell that apart from a complete read, and the partial file
reads exactly like a whole run to the failed-job projections and to the
infra classifier — which counts only the jobs present, turning a regression
parked on the missing page into a rerun instead of an issue.

Stage the merge in a temp file and promote it only when the merged job
count matches the payload's own total_count; otherwise report without the
job list. The receipt keeps the guard independent of the shell's pipefail
setting, which the meta-test now exercises by spawning the step without it.
@wenshao

wenshao commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verification report

Scope: the partial-pagination finding only. The destination-lookup finding in #15138 is untouched.

Method: step-level harness. The workflow's run: bodies were executed verbatim with a fake gh that emulates --paginate (one JSON document per page, non-zero exit when a later page fails), plus the repository meta-test. A CLI session cannot reach this code path, so no headless or interactive run applies.

Before the fix

  • Fixture: total_count: 150; page 1 carries 100 jobs (one failing), page 2 carries 50 (one failing) and responds 403.
  • The reporter consumed 100 of 150 jobs as the complete set: failed-jobs.tsv named only the page-1 job, only that job's log was downloaded, and the failure summary rendered a comment naming one of the two real failures with no sign that anything was missing.
  • Worst case, confirmed by composing the downstream steps on the same payload: when the page-1 failures all carry infrastructure annotations, the infrastructure classifier returns infra_only=true, which reruns the workflow and suppresses the per-commit issue — a real regression on the missing page is never filed, and the rerun's own green completion means nothing re-enters the reporter.
  • Control: with both pages present the reporter published all 150 jobs and both failures.

After the fix

  • Same fixture: the merge is rejected (listed 100 of 150 jobs), the staged file is removed, the published list is empty, a warning annotation is emitted, and the per-commit issue is still filed. failed-jobs.tsv is empty and no log download is attempted.
  • Infrastructure classification on the same payload now returns infra_only=false, so the run files an issue instead of rerunning.
  • Page 1 failing outright still degrades the same way: empty list, warning, exit 0.
  • Control unchanged: both pages present yields all 150 jobs, both failures, both logs, and a comment naming both jobs.

Repository tests

  • Failure-reporter meta-test: 61/61, including the new case. That case is spawned without pipefail on purpose — the completeness receipt, not the pipeline status, is what must reject the truncated list.
  • Platform-lane suite that parses the same workflow: 38/38.

Notes

  • Production runs these steps with pipefail enabled, so a failed page does raise an exit status today. That was never the whole gap: the merge had already written the pages it received, and the exit status only decides whether a warning prints. The receipt is what stops that file being consumed, and it holds with or without pipefail.
  • Environment: local runs used bash 5 on PATH; the system bash 3.2 lacks a builtin the step body uses. CI runs Linux.

@qwen-code-review-bot

qwen-code-review-bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval withheld — 1 PR CI workflow run(s) on 9b9bd5e did not finish green; see the updated table in the Stage 2 comment. Re-run @qwen-code /triage after fixes. finalize run

⚠️ 延迟审批已搁置 —— 9b9bd5e 有 1 个 PR CI workflow 未以绿色完成,详见 Stage 2 评论中已更新的表格。修复后可重新运行 @qwen-code /triage。查看 finalize 运行

Copy link
Copy Markdown

I tested the exact new jq -s expression from this PR with jq 1.7. There is one fail-open inverse in the no-receipt case, contrary to the PR's stated fallback contract.

The new condition is:

([.[].total_count] | max) as $total
| if $total == null or ($doc.jobs | length) == $total then $doc
  else error("listed ...") end

When none of the emitted page objects carries total_count, $total is null and the or arm accepts the nonempty, unverified $doc. This is precisely the input for which the explanatory comment says the reporter should fall back to an empty list. Minimal local negative control, using the same expression:

printf '%s\n' \
  '{"jobs":[{"id":9001,"conclusion":"failure"}]}' \
  '{"message":"API rate limit exceeded"}' |
  jq -sc '{jobs: [ .[].jobs[]? ]} as $doc | ([.[].total_count] | max) as $total | if $total == null or ($doc.jobs | length) == $total then $doc else error("incomplete") end'
# ACTUAL: {"jobs":[{"id":9001,"conclusion":"failure"}]}
# Expected under the stated receipt contract: error -> delete stage, publish {"jobs":[]}, emit warning

This is an isolated jq fixture, not a claim that the real GitHub jobs endpoint normally omits total_count or that this occurred in production. The existing 2-page failure test has a non-null total_count, so it cannot catch this branch.

Bounded fix: require a finite/integer nonnegative $total receipt and exact equality, rather than accepting $total == null; add a no-receipt negative fixture and retain the existing complete two-page positive case. Separately, if metadata from multiple returned pages disagrees, consider rejecting that mismatch rather than using max alone as a certificate. No change requested for finding 1 or for write-token boundaries.

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — every required heading is there, the Tested-on table is filled in honestly (macOS ✅ / Windows N/A / Linux ⚠️) with the reason for each, and the Chinese section is a full parallel translation rather than a summary.

Problem: observed and source-verified, with one honest caveat. #15138 is open and records this as finding #2 out of @doudouOUC's post-merge review of #14522, acceptance test included. Reading the base code, the defect is actually slightly worse than the issue describes: defaults.run.shell: 'bash' means the step runs as bash -e {0} with no pipefail, so when page 2 fails the pipeline's status is jq's (0), the if ! branch never fires, and the partial list is promoted with no warning at all — not merely "consumed as complete". The caveat: the trigger needs a run with more than 100 jobs, and that does not happen today. Measured peak total_count across the watched workflows — SDK Java 13, Desktop Checks 10, E2E Tests 9, Qwen Code CI 8, over 20 runs each — is 13. So this fixes a real but currently dormant path. That is a question about priority rather than a defect in the change, and I raise it as a question only.

Direction: aligned. This is the repo's own failure-reporting infrastructure, and the scoping is disciplined — it takes one of the issue's two findings and explicitly leaves the other (the destination-lookup budget walk) open for a maintainer decision instead of bundling both. CHANGELOG: no direct reference, which is expected for a workflow-only change.

Size: core paths are not touched. Production lines 28 (.github/workflows/main-ci-failure-issue.yml, 21+/7−), test lines 107 (scripts/tests/main-ci-failure-issue-workflow.test.js, 98+/9−), no generated or schema files. Under both thresholds, so no size escalation.

Approach: the scope feels right and I would not cut anything. The remedy is the one the issue prescribed — atomic staging plus a total_count receipt. Choosing the receipt over set -o pipefail is the correct call and worth saying out loud: pipefail would depend on a shell option this workflow does not set, whereas total_count travels in the response regardless. Nothing unrelated is riding along in the diff.

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

One note on timing, in the interest of transparency: the head moved while this review was running. I had started against cdd0a9a, at which point the branch was 1 ahead / 29 behind and Lint & Static was red on the lint-gate freshness check for that reason. The pre-post head check caught the move and I refused to post a review attesting to code I had not re-read, then restarted against 9b9bd5e — a merge of main into the branch, which resolves the staleness. The merge touches none of the workflow, the size baseline, the sibling meta-test or the PR template, and the resulting workflow file is byte-identical to the one I reviewed, so every finding below still applies to the current head.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 所有必需小节齐全,测试平台表格如实填写(macOS ✅ / Windows N/A / Linux ⚠️)并说明了原因,中文部分是逐段对应的完整翻译而非摘要。

问题:已观测且经源码验证,但有一点需要如实说明。#15138 处于 open 状态,将本项记录为 @doudouOUC 对 #14522 合并后评审中的第 2 项发现,并附了验收测试。阅读基线代码后可以确认,缺陷实际比 issue 描述的更严重一点:defaults.run.shell: 'bash' 意味着该步骤以 bash -e {0} 运行且未开启 pipefail,因此第 2 页失败时整个管道的退出码取自 jq(0),if ! 分支根本不会触发,不完整的列表会被直接采用,连一条警告都没有 —— 不只是"被当作完整结果消费"。需要说明的是:触发前提是一次运行的 job 数超过 100,而这目前不会发生。实测各被监听 workflow 的 total_count 峰值(每个取 20 次运行)—— SDK Java 13、Desktop Checks 10、E2E Tests 9、Qwen Code CI 8 —— 最高为 13。所以本 PR 修复的是一条真实但当前处于休眠状态的路径。这只是关于优先级的疑问,而非改动本身的缺陷,我也仅以提问方式提出。

方向:对齐。这是仓库自身的失败上报基础设施,范围划分克制 —— 只处理该 issue 两项发现中的一项,并明确把另一项(目标发现的预算遍历)留给维护者决策,没有打包在一起。CHANGELOG:无直接关联,对纯 workflow 改动而言属于预期。

规模:未触及核心路径。生产代码 28 行(.github/workflows/main-ci-failure-issue.yml,21+/7−),测试 107 行(scripts/tests/main-ci-failure-issue-workflow.test.js,98+/9−),无生成/schema 文件。低于两个阈值,无需按规模上报。

方案:范围合理,我不会砍任何部分。采用的正是 issue 所指定的做法 —— 原子暂存加 total_count 校验。选择校验而不是 set -o pipefail 是正确的,值得点明:pipefail 依赖本 workflow 并未设置的 shell 选项,而 total_count 无论如何都在响应里。diff 中没有夹带无关改动。

风险:无升级风险信号 —— 两个文件都不匹配与回滚相关的路径。

关于时间点,为透明起见说明一下:审查进行期间 head 发生了变化。我最初是针对 cdd0a9a 开始的,当时分支领先 1 个、落后 29 个提交,Lint & Static 正因这个原因在 lint gate 新鲜度检查上失败。发布前的 head 校验发现了这一变化,我拒绝发布一份为未重新阅读的代码背书的审查,随后改为针对 9b9bd5e 重新开始 —— 那是把 main 合并进分支,staleness 问题因此解决。这次合并没有触及 workflow、体积基线、同级元测试或 PR 模板,合并后的 workflow 文件与我审查过的版本逐字节相同,所以下文所有结论对当前 head 依然适用。

进入代码审查 🔍

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 9b9bd5e857e496cbd60e06090ca399fd1fe47b82 · re-run with @qwen-code /triage

@qwen-code-review-bot

qwen-code-review-bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Code review

The fix is correct. I checked the load-bearing claims against the source and the live API rather than taking them from the description.

Verified

  • total_count really is the receipt it is claimed to be. On live runs it equals the merged job length (e.g. run 39733887999 → total_count=4, merged 4, one page), and max tolerates the error body gh api prints before exiting non-zero — [2,null] | max → 2 — so a parked rate-limit page cannot poison the comparison.
  • The degraded path costs precision but never the issue, as claimed. With {"jobs":[]} the infra classifier's total -gt 0 guard keeps infra_only=false, the analyzer identifies no tests, and the plan falls back to the per-commit marker — so the issue is still filed.
  • Atomicity holds. jobs_tmp is ${jobs_json}.tmp in the same RUNNER_TEMP, so mv is a same-filesystem rename, and nothing globs it into scope: the comment helper receives --jobs-json explicitly and the log glob is confined to failed-logs/. Both branches clean the staging file up, and the new test asserts that.
  • The new test is genuinely load-bearing, not decorative. Remove only the completeness guard and it goes red — without pipefail jq exits 0, the partial list is promoted, and truncated.jobs is [9001] instead of []. The positive control also covers the failure that only page 2 carries.
  • The AGENTS.md meta-test rule is satisfied. Two test files parse this workflow; ci-platform-lanes.test.js reads it structurally (job if conditions) and pins nothing in the changed step, so no stale pin is left behind. The old warning string was pinned only in the file this PR rewrites.
  • The workflow-size ratchet passes. The merged workflow file is 26556 bytes against a 23517 baseline with a 4096 allowance (ceiling 27613), so .size-baseline needs no bump. Worth stating because CI had not reached that check when I looked — see below.

One suggestion, non-blocking. The new comment says a run that yields no receipt "falls back to the empty list the per-commit body already tolerates, and says so" — but the $total == null case does not say so. It takes the then branch: [] | max and [null] | max are both null, so jq emits {"jobs":[]} and exits 0, and the step mvs that into place with no warning. The else branch that emits the warning is reached only on a receipt mismatch or unparseable output. The behaviour is unchanged from before this PR and still fails safe, so this is purely about the prose — but in a workflow where the rationale comments carry the design intent and the meta-test pins them, a comment promising a warning the code does not emit will mislead whoever reads it next. Either drop "and says so", or warn in the null branch too.

Not verified: whether total_count on the /attempts/{n}/jobs endpoint is attempt-scoped or run-scoped. I could not check it empirically — no run in the last 500 has run_attempt > 1, and no watched run exceeds 13 jobs. If it were run-scoped, every attempt-2 report would falsely reject and degrade to an empty job list. That direction fails safe (the issue is still filed; only the job names are lost), so it is not a blocker, but it is the one assumption in the guard I could not put evidence behind.

CI test evidence

Quoted from the current head's own check-runs via the API. No PR code was built or executed.

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

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Managed Agent gate ✅ success
Select the Managed Agent tier ✅ success

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

Nothing is red on this head. That is a change from when I started: on the pre-merge head cdd0a9a, Lint & Static had concluded failure, and it is worth recording why, because it explains the gap in the evidence above.

That failure was not this PR's code. The lane died in check-lint-gate-freshness.mjs, before any lint or test step ran:

The lint gate changed on 'main' after this branch last incorporated it:

  - eslint.legacy-core-barrel-imports.mjs: 0ac1fcca9179 feat(cli): align OpenTUI
    input, editing and status lines with Ink (#14964) (2026-10-22)

This lane checks out the branch head alone, so its green proves the branch
passes the gate AS THE BRANCH DEFINES IT — with the files above, that gate
is stale. Merge or rebase 'main' into this branch and push to
re-validate under the current gate.
##[error]Process completed with exit code 1.

I classified that as staleness from the diff and the commit graph, not from anything the log asserts: the branch head was 1 ahead of and 29 behind 0ac1fcca9179, and that commit is itself an ancestor of main (main was 12 ahead / 0 behind it). The PR touches neither eslint.legacy-core-barrel-imports.mjs nor any lint config. The author has since merged main into the branch, and the lane is re-running.

The consequence is the part that matters for reading this review. check-workflow-size.sh sits in that same job, after the freshness gate, so on the pre-merge head it never ran — I computed the ratchet by hand instead (26556 against a 27613 ceiling, passes; the merge left both the file and the baseline untouched, so that arithmetic still holds). And the lane that executes the new meta-test is Test (ubuntu-latest, Node 22.x), via npm run test:scripts, which runs only after test:ci:workspaces passes and is still in progress. So at this moment there is no CI result for the new test yet — the table above will be rewritten in place once CI settles.

Sandboxed verification would settle the remaining gap: @qwen-code /verify — the guard's behaviour is asserted only by the new meta-test, which no CI lane has executed yet, and by the author's own account the suites were run on macOS with a newer bash on PATH because the step body uses mapfile, which system bash 3.2 lacks, while Linux (where CI runs them) was not executed locally. An A/B run would show the truncated-page case failing closed and the complete case promoting both jobs on the platform CI actually uses.

中文说明

代码审查

修复是正确的。关键结论我是对照源码和线上 API 验证的,没有直接采信 PR 描述。

已验证:

  • total_count 确实可以充当所说的"凭据"。真实运行中它等于合并后的 job 数(例如 run 39733887999:total_count=4,合并 4 个,单页);而 max 能容忍 gh api 在退出非零前打印的错误响应体 —— [2,null] | max → 2 —— 所以被搁置的限流页不会污染比较结果。
  • 降级路径如声明的那样只损失精度、不丢 issue。当内容为 {"jobs":[]} 时,基础设施分类器的 total -gt 0 判断使 infra_only=false,分析器识别不出测试,计划回退到 per-commit 标记 —— issue 依然会被创建。
  • 原子性成立。jobs_tmp 是同一 RUNNER_TEMP 下的 ${jobs_json}.tmp,因此 mv 是同文件系统内的重命名;也没有任何 glob 会把它纳入消费范围:comment helper 显式接收 --jobs-json,日志 glob 限定在 failed-logs/。两个分支都会清理暂存文件,新测试也断言了这一点。
  • 新测试是真正起作用的,不是摆设。只移除完整性校验它就会变红 —— 没有 pipefail 时 jq 退出码为 0,不完整列表被采用,truncated.jobs 会是 [9001] 而不是 []。正向对照也覆盖了只存在于第 2 页的那个失败。
  • 满足 AGENTS.md 的元测试规则。有两个测试文件解析这个 workflow;ci-platform-lanes.test.js 只做结构化读取(job 的 if 条件),并未固定被改动步骤中的任何内容,因此没有留下失效的断言。旧的警告文案只在本 PR 重写的文件中被固定。
  • workflow 体积棘轮通过。合并后的 workflow 文件为 26556 字节,基线 23517、容差 4096(上限 27613),因此无需更新 .size-baseline。之所以要专门说明,是因为我查看时 CI 还没跑到这项检查 —— 见下文。

一条非阻塞建议:新注释说没有凭据时"回退到 per-commit 正文已能容忍的空列表,并作出说明",但 $total == null 这一分支并不会作出说明。它走的是 then 分支:[] | max 和 [null] | max 都是 null,于是 jq 输出 {"jobs":[]} 并以 0 退出,步骤把它 mv 到正式位置且不产生任何警告。真正发出警告的 else 分支只在凭据不匹配或输出无法解析时才会到达。这一行为与本 PR 之前一致,且仍是安全方向,所以这纯粹是文字问题 —— 但在这个由注释承载设计意图、且元测试会固定注释的 workflow 里,一条承诺了代码并不会发出的警告的注释,会误导下一个阅读它的人。要么去掉"并作出说明",要么在 null 分支也发出警告。

未验证:/attempts/{n}/jobs 上的 total_count 是按 attempt 计还是按整个 run 计。我无法实测 —— 最近 500 次运行中没有 run_attempt > 1 的,被监听的运行也没有超过 13 个 job 的。如果它是按 run 计的,那么每一次 attempt 2 的上报都会被误判为不完整并降级为空 job 列表。这个方向仍是安全的(issue 照旧创建,只是丢失 job 名称),所以不构成阻塞,但这是该守卫中唯一我无法拿出证据的假设。

CI 测试证据

以上表格取自当前 head 自身的 check-runs(经 API 获取),未构建或执行任何 PR 代码。表格由标记区域包裹,CI 结束后可原地更新。

这个 head 上没有红色项。与我开始审查时相比这是一个变化:在合并前的 head cdd0a9a 上,Lint & Static 曾判定为 failure,记录一下原因很有必要,因为它解释了上面证据中的缺口。

那次失败不是本 PR 代码导致的。该泳道在 check-lint-gate-freshness.mjs 就中止了,任何 lint 或测试步骤都还没跑。我依据 diff 与提交图(而非日志中的任何自述)判定这是新鲜度问题:当时分支 head 相对 0ac1fcca9179 领先 1 个、落后 29 个提交,而该提交本身就是 main 的祖先(main 相对它领先 12、落后 0)。本 PR 既没有改 eslint.legacy-core-barrel-imports.mjs,也没有改任何 lint 配置。作者此后已把 main 合并进分支,该泳道正在重新运行。

对阅读本审查真正重要的是其后果:check-workflow-size.sh 位于同一个 job、在新鲜度检查之后,所以在合并前的 head 上它从未执行 —— 体积棘轮是我手工算出来的(26556 对 27613 上限,通过;这次合并没有改动该文件与基线,因此这个计算依然成立)。而真正执行新元测试的泳道是 Test (ubuntu-latest, Node 22.x),通过 npm run test:scripts,且只在 test:ci:workspaces 通过后才运行,目前仍在进行中。也就是说此刻新测试还没有任何 CI 结果 —— CI 结束后上表会被原地重写。

沙箱验证可以补上剩余缺口:@qwen-code /verify —— 守卫的行为目前只由新元测试断言,而尚无任何 CI 泳道执行过它;按作者自己的说明,测试是在 macOS 上用 PATH 上较新的 bash 跑的,因为步骤脚本用到了系统 bash 3.2 没有的 mapfile,而 Linux(CI 实际运行环境)未在本地执行过。一次 A/B 运行可以在 CI 真正使用的平台上展示:截断页场景安全失败、完整场景把两个 job 都采用。

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 9b9bd5e857e496cbd60e06090ca399fd1fe47b82 · re-run with @qwen-code /triage

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the fix is correct, well-scoped and fails safe; the two reservations are non-blocking, and the only thing outstanding is CI that has not finished running.

My independent read before looking at the diff was: stage the merged pages somewhere the consumers cannot see, and promote only against a completeness receipt taken from the response itself, because this workflow runs bash -e {0} and never sets pipefail. That is exactly what the PR does, and for the right reason — the author's note that the receipt holds "even where the shell runs without pipefail" is the crux of the change and it is correct. I did not find a simpler path I would prefer, and I would not cut any of the scope.

What I most like is the discipline around it. #15138 carries two findings; this PR takes one, says so plainly, and leaves the other open for a maintainer decision rather than bundling a larger change. The test has a positive control alongside the negative case, and I confirmed it is load-bearing — strip the guard and it goes red. The failure direction is safe everywhere in this change: the worst case is a report that names no job, never a report that does not happen.

My reservations, neither blocking. The comment about the no-receipt case overstates what the code does — it says the reporter "says so", and on the $total == null path it does not. Small, but this workflow's comments are where the design intent lives. And I could not verify that total_count is attempt-scoped rather than run-scoped, because the repo has no recent rerun to measure against; if it were run-scoped, attempt-2 reports would silently lose their job names.

The honest caveat on necessity: the bug cannot fire today. No watched workflow has run above 13 jobs, so nothing paginates and the partial-page path is unreachable. This is a correct fix for a dormant defect. I would still take it — 28 production lines, fail-safe, and it closes out a documented finding from a post-merge review — and the day a lane grows past 100 jobs is precisely the day you do not want to discover the reporter had been silently truncating. But if the bar is "fix what is currently firing", parking it is a defensible call, and that is a maintainer's judgement rather than mine.

Not approving in this run, for a mechanical reason only. Three lanes are still in progress on this head — including Test (ubuntu-latest, Node 22.x), the one that actually executes the new meta-test via npm run test:scripts. Nothing is red. Approving now would attest to a test result that does not exist yet, so approval is deferred until CI lands green on 9b9bd5e857e496cbd60e06090ca399fd1fe47b82; the deferred approval fires only on that exact commit and is withheld if anything lands red or the head moves again.

Worth recording for whoever reads the thread later: the head moved mid-review. I had started against cdd0a9a, where the branch was 29 commits behind and Lint & Static was red on the lint-gate freshness check — a staleness verdict, not a defect in this change. The pre-post head check caught the move and stopped me posting a review pinned to a commit that was no longer current. The author's merge of main resolves that gate, and because the merge touches neither the workflow, the size baseline, the sibling meta-test nor the template — the resulting workflow file is byte-identical to the one I reviewed — the review above was carried over rather than repeated. If the head moves again, this pass no longer applies and a fresh one is needed.

@wenshao no action needed from you to unblock CI — the merge already cleared the freshness gate. The one thing I would consider changing is the "and says so" clause in the new comment, since the $total == null path promotes the empty list silently; either drop the clause or warn there too. Everything I could check statically came out clean.

中文说明

置信度:4/5 —— 修复正确、范围克制、失败方向安全;两条保留意见均不阻塞,唯一未完成的是尚未跑完的 CI。

在看 diff 之前我自己的判断是:把合并后的分页结果暂存到消费方看不到的位置,只有对照响应自身携带的完整性凭据校验通过才采用,因为这个 workflow 以 bash -e {0} 运行且从未设置 pipefail。这正是本 PR 的做法,而且理由是对的 —— 作者指出凭据"即使在 shell 未开启 pipefail 时也成立",这是整个改动的关键,且判断正确。我没有找到更简单、更愿意采用的路径,也不会削减任何范围。

最欣赏的是它的克制。#15138 包含两项发现;本 PR 只取其一,明确说明,并把另一项留给维护者决策,没有打包成更大的改动。测试在负向用例外还配了正向对照,而且我确认它是真正起作用的 —— 去掉守卫就会变红。整个改动的失败方向都是安全的:最坏情况是上报中不点名任何 job,绝不会是上报没有发生。

我的保留意见,两条都不阻塞。关于"无凭据"分支的注释夸大了代码的实际行为 —— 它说上报器"会作出说明",而 $total == null 这条路径并不会。问题不大,但这个 workflow 的设计意图正是写在注释里的。另外我无法验证 total_count 是按 attempt 计还是按整个 run 计,因为仓库近期没有可供测量的重跑;如果它按 run 计,attempt 2 的上报会静默丢失 job 名称。

关于必要性,需要如实说明:这个 bug 今天无法触发。被监听的 workflow 没有一次超过 13 个 job,因此不会分页,部分页路径不可达。这是对一处休眠缺陷的正确修复。我仍然倾向于接受 —— 28 行生产代码、失败方向安全,并且落实了合并后评审中一项有记录的发现 —— 而且某个泳道增长到超过 100 个 job 的那一天,恰恰是最不希望发现上报器一直在静默截断的时候。但如果标准是"只修当前正在发生的问题",把它搁置也是站得住脚的,那是维护者的判断,不是我的。

本次运行不予批准,原因纯属机制层面。这个 head 上有三个泳道仍在进行中 —— 包括 Test (ubuntu-latest, Node 22.x),也就是通过 npm run test:scripts 真正执行新元测试的那个。没有红色项。此时批准等于为一个尚不存在的测试结果背书,因此批准推迟到 CI 在 9b9bd5e857e496cbd60e06090ca399fd1fe47b82 上全绿之后;该推迟批准只对这个确切的提交生效,若有任何一项变红或 head 再次变化则不会执行。

有一点值得记录,方便后来阅读该 thread 的人:审查期间 head 发生了变化。我最初针对 cdd0a9a 开始,当时分支落后 29 个提交,Lint & Static 在 lint gate 新鲜度检查上失败 —— 那是新鲜度判定,不是本改动的缺陷。发布前的 head 校验发现了这一变化,阻止我发布一份钉在已不再当前的提交上的审查。作者合并 main 已解决该门禁;并且由于这次合并没有触及 workflow、体积基线、同级元测试或模板 —— 合并后的 workflow 文件与我审查过的版本逐字节相同 —— 上述审查是沿用而非重做。如果 head 再次变化,本次审查即不再适用,需要重新进行。

@wenshao 你不需要为解除 CI 阻塞做任何事 —— 那次合并已经清除了新鲜度门禁。我唯一会考虑修改的是新注释里"并作出说明"那半句,因为 $total == null 这条路径是静默采用空列表的;要么删掉这半句,要么在该分支也发出警告。所有我能静态检查的项都是干净的。

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 9b9bd5e857e496cbd60e06090ca399fd1fe47b82 · re-run with @qwen-code /triage

@Nakagawa-master Nakagawa-master left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Current 9b9bd5e blocking inverse to the fail-closed contract, independently reproduced with jq 1.7 (not a hypothetical shell-status issue).

The production predicate in .github/workflows/main-ci-failure-issue.yml currently reads:

if $total == null or ($doc.jobs | length) == $total then $doc
else error("listed ...") end

But null is lack of a completeness receipt, and the or arm promotes the partial, unverifiable page body as a complete job list. Example exact reproducer:

printf '%s\n' \
  '{"jobs":[{"id":9001,"conclusion":"failure"}]}' \
  '{"message":"API rate limit exceeded"}' |
jq -sc '{jobs: [ .[].jobs[]? ]} as $doc | ([.[].total_count] | max) as $total | if $total == null or ($doc.jobs | length) == $total then $doc else error("listed \($doc.jobs | length) of \($total) jobs") end'
# exit 0: {"jobs":[{"id":9001,"conclusion":"failure"}]}

Confirmed in a real local jq-1.7 run. This contradicts the PR's own explanatory contract, "a run that yields no receipt at all falls back to the empty list," and its new test only covers a non-null total_count:2 truncation. It is a materially distinct negative test input.

Smallest conservative source fix:

if $total != null and ($doc.jobs | length) == $total
then $doc
else error("missing or incomplete total_count receipt")
end

Please add a real spawnSync negative case where the gh stub emits a populated jobs response without total_count followed by an API error body, asserting run-jobs.json is {"jobs":[]}, the staging file is absent, and the workflow warning is emitted. Then a complete real-paged response remains the positive control.

I saw the automated Stage 2 comment call this fix fail-safe; the above executable inverse means that judgment is too broad at this head, regardless of other passing CI. This review addresses only this exact missing-receipt branch; it does not allege a production incident or claim ownership of the receiving author's implementation.

`$total == null or …` treated a response that carries no `total_count` as
complete, so a populated first page followed by an API error published its
partial list — the inverse of the contract the step's own comment states,
reproduced with jq: the predicate exits 0 and emits page 1 alone.

Make the receipt part of the pass condition: a page set is promoted only
when it declares a total and the merged jobs match it. No total means no
proof of completeness, so it takes the same fallback as a failed fetch —
empty list, staging file removed, warning. The new negative case feeds a
populated page without `total_count` and asserts that fallback.
@wenshao

wenshao commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — the reproducer holds, and it is fixed in ef371fd.

You are right that this was an inverse of the contract rather than a hypothetical: total_count is the only completeness receipt this step has, so its absence must mean "not certified", never "nothing to check". The predicate now reads:

if $total != null and ($doc.jobs | length) == $total then $doc
else error("incomplete job list: merged \($doc.jobs | length) of \($total)") end

A response with no receipt therefore takes the same fallback as a failed fetch: staging file removed, {"jobs":[]} published, warning emitted. The step comment that claimed that fallback now says what the code actually does.

Your requested negative case is in the suite: the step is spawned under bash -e (no pipefail) against a gh stub that emits a populated page without total_count followed by an API error body, asserting run-jobs.json is {"jobs":[]}, no staging file remains, and the warning is emitted. The complete-pages control still promotes both jobs, including the failure only page 2 carries. The meta-test is 61/61 locally on the rebased head.

One correction for the record, because it changes how the original gap reads: defaults.run.shell: 'bash' runs these steps as bash --noprofile --norc -eo pipefail. Explicit bash is the value that adds pipefail; the Linux default bash -e {0} is the one without it (GitHub's workflow-syntax shell table). So a later-page failure did fire the warning — the gap was that jq had already written the merged file, and every consumer read it regardless. That is exactly why the receipt has to be required rather than merely checked when present.

On the two red checks on the previous head: Test failed in test:standalone:runtimes on a @oven/bun-linux-x64 download assertion, and web-shell E2E Smoke failed eight specs on connect ECONNREFUSED 127.0.0.1:4170. Neither touches the files this PR changes; this push starts a fresh run for both, and I will follow them.

@Nakagawa-master Nakagawa-master left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Follow-up to my earlier CHANGES_REQUESTED review on 9b9bd5e. I reviewed the actual new head ef371fd06f34672adcacce42e3ca2032c8d96790 and the two changed files, together with @wenshao's explicit reproducer acknowledgment.

The blocking no-total_count inverse has been corrected. The workflow now requires $total != null and ($doc.jobs | length) == $total before promoting the staged JSON. The new noReceipt negative fixture asserts empty fallback, warning, and staging cleanup; the incomplete second-page negative and full two-page positive are also present. That addresses the specific source-level condition on which my prior request for changes was based. The author owns the correction and tests; my earlier review supplied the counterexample, not the code.

Review scope / CI distinction: On this head, Ubuntu Node Test, Lint & Static and no-AK Integration checks have passed. web-shell E2E Smoke is currently FAILED (the native job log repeatedly shows ECONNREFUSED 127.0.0.1:4170 and multiple browser visibility timeouts); a separate review-pr check remains in progress. This narrow workflow/test approval is not a claim that the PR has all checks green, that the smoke failure is known to be unrelated, or that the implementation has been merged or exercised in production. Maintainers should make the independent CI and merge decisions from exact-head data. The attempt-scoped semantics of total_count on retried runs remain an honest nonblocking unknown, not a reason to reinstate the proven inverse.

I approve the current fix for the specific issue I blocked. Thanks for turning a concrete negative case into receiver-owned tested code.

@wenshao

wenshao commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

评审提交:ef371fd06f34672adcacce42e3ca2032c8d96790(相对 base 034e35056b59ce57203485cb8d168c3b1013290f)。完整检查了两个变更文件和 job 列表的下游消费路径,未发现本次改动引入的可操作缺陷(C=0)。

  • 此前阻塞项已修复。 当前条件要求 $total != null 且合并数量完全匹配;缺失 total_count 的非空响应不会再被发布。失败分支删除暂存文件、写入空列表并发出 warning;新增负向用例覆盖了此前的反例。
  • 独立验证了完整链路。 在精确 head 的脚本上,以 150 个 job 的两页夹具分别验证后续页失败、缺失 receipt、首页失败、完整分页和合法空列表,并在开启/关闭 pipefail 的两种模式下与 base 对照。base 的截断场景会发布 100 个 job,并把只有第一页基础设施失败的列表判为 infra_only=true;head 发布空列表、无日志下载、infra_only=false,后续 summary/plan 正常生成按提交追踪的报告。完整分页仍保留全部 150 个 job 和两页上的失败项,且不发出 warning。此验证使用本地 gh 替身,没有实际创建 issue 或触发重跑。
  • 补充真实 attempt 计数证据。 对 SDK Java run 39628985638 的只读 API 实测:attempts/1/jobs 和 attempts/2/jobs 分别返回 total_count=13、实际 13 个 job,且各自的 run_attempt 均匹配;同一 run 的 jobs?filter=all 返回 26 个 job、total_count=26。这个样本支持 attempt endpoint 的计数按 attempt 隔离,补上了之前讨论中未验证的假设。
  • 相关元测试 99/99 通过: failure-reporter 61/61,platform-lanes 38/38。运行环境为 macOS、Node 22.22.2、Bash 5.3.15、jq 1.7.1;未在本地运行完整构建或全部测试。

CI 状态单独说明:该 head 的 Ubuntu Test、Lint & Static、no-AK Integration 已通过;Web Shell E2E Smoke 仍失败,日志包含反复的 ECONNREFUSED 127.0.0.1:4170。本次代码评审结论不代表 CI 全绿,也未确定该失败的根因。#15138 的 destination lookup 问题仍在本 PR 范围之外。

@qwen-code-review-bot qwen-code-review-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 explored to full depth (tool budget reached): "agent reverse-audit (round 1)": whether the test_macos leg's npm run test:ci step (ci.yml:1705, 1792) is fatal or warning-downgraded the way test_windows is (ci.yml:1974-1985 captures S…; "agent 1a": none — I stopped with budget remaining, not at the ceiling..

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/main-ci-failure-issue.yml — actionlint embedded-shell source mapping is not yet supported.

中文说明

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":whether the test_macos leg's npm run test:ci step (ci.yml:1705, 1792) is fatal or warning-downgraded the way test_windows is (ci.yml:1974-1985 captures S…;"agent 1a":none — I stopped with budget remaining, not at the ceiling.。

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

未检查(工具限制,非阻断):.github/workflows/main-ci-failure-issue.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

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

Comment thread .github/workflows/main-ci-failure-issue.yml
Comment thread .github/workflows/main-ci-failure-issue.yml
Comment thread scripts/tests/main-ci-failure-issue-workflow.test.js Outdated
The reporter degraded to an empty list with only an annotation on its own
job, so the PR comment and the issue looked exactly like a run with no
identifiable failure — the readers who need the signal never saw it. Carry
a jobs_complete output onto both bodies: when it is false they say the
report may name fewer failing jobs than the run had, and the issue body
guards the sentence so a recurrence does not stack a second copy.

Also widen the meta-test pin back over the fetch's producer, so --paginate
and per_page=100 stay pinned: the earlier rewrite started the span at the
merge and left both unpinned while the comment still claimed they rode it.
The case now rejects both a truncated page set and a response carrying no
total_count, so a name that mentions only "stops short of total_count"
understates it. The flag's comment said "both reports" where the two
readers are the PR comment and the issue body.

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

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-2 no bounded retry on the all-or-nothing fetch — still stands, author declined with a documented rationale (PR description, Risk & Scope); already reported (comment 4348575273)

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": could not execute the vitest suite — the review worktree has no node_modules ( ls node_modules/.bin/vitest → not found), so the new test's own greenness is u…; "agent reverse-audit (round 1)": did not independently re-verify the helper's plan --existing merge ( splitOccurrenceBlock tail preservation) that the re-plan fixture at test:1096 depends on…; "agent reverse-audit (round 2)": could not execute scripts/tests/main-ci-failure-issue-workflow.test.js — the review worktree has no installed node_modules ( npx vitest fails with Cannot ….

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/main-ci-failure-issue.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • scripts/tests/main-ci-failure-issue-workflow.test.js:624 — [probe] the receipt harness spawns without production's -o pipefail and no fixture covers gh-exits-0-short, so the receipt's only witness is the harness's own shell
中文说明

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

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)":could not execute the vitest suite — the review worktree has no node_modules ( ls node_modules/.bin/vitest → not found), so the new test's own greenness is u…;"agent reverse-audit (round 1)":did not independently re-verify the helper's plan --existing merge ( splitOccurrenceBlock tail preservation) that the re-plan fixture at test:1096 depends on…;"agent reverse-audit (round 2)":could not execute scripts/tests/main-ci-failure-issue-workflow.test.js — the review worktree has no installed node_modules ( npx vitest fails with Cannot …。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

未检查(工具限制,非阻断):.github/workflows/main-ci-failure-issue.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

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

Comment thread .github/workflows/main-ci-failure-issue.yml Outdated
Comment thread .github/workflows/main-ci-failure-issue.yml
Comment thread scripts/tests/main-ci-failure-issue-workflow.test.js Outdated

Copy link
Copy Markdown

Exact-head CI readback for maintainer review (2026-10-23 JST; no new code request). I rechecked OPEN #15174 at 9f18c4661b97da6cf5b4ccbac182ad76e327ef55, including the receiving-hosted web-shell E2E job 120015756968.

The final Playwright summary matters more than the first-attempt failure list: 343 passed, 10 flaky (passed on retry), 3 failed. The final failures were managed-context-compression.spec.ts (refresh at 1280px), web-shell.browser-notifications.spec.ts (manual notification grant across reload) and web-shell.excel-preview.spec.ts (unsent Blob / invalid workbook). The job's Vite proxy also logged ECONNREFUSED 127.0.0.1:4170 for /workspace/models late in the run; this alone does not establish the failure root. The separate head-matched Ubuntu Node Test, no-AK Integration, Lint & Static and Managed Agent gate succeeded.

Compare caf8ed5…9f18c46: the latest commit changes only a workflow comment and a test-case title; this PR's own changed files are the failure-reporting workflow plus its JS regression test, not Web Shell code. That is a strong scope distinction, not proof the E2E failures are baseline-only or that current CI is green. Useful receiver-owned decision: compare these three failed scenarios and the proxy log with a contemporaneous clean main/other branch on the same runner and inspect artifacts before deciding whether a browser defect is attributable to this patch. The review/merge decision stays with maintainers; I did not rerun Actions or claim a complete end-user deployment.

This is a new exact-HEAD browser/CI classification only; my earlier missing-total_count source review is already addressed by the independent author wenshao, and I am not reopening or re-crediting that fixed condition.

The note added by the previous commit was append-only: once one degraded
run wrote it into a tracking issue, every later replan preserved it --
including runs whose job list was read in full and receipt-certified -- so
a rebuilt failure block sat under a permanent claim that the list might be
short, and the substring guard then refused a later degraded run a fresh
note of its own.

Strip the exact sentence before deciding whether to re-add it, so a
certified run clears a stale copy and a degraded run speaks for itself.

Pin the producer-to-consumer wiring (step id and both env mirrors), add the
plan path's negative control, and cover gh exiting 0 with a short list.
main's #15256 replaced the signature-based issue planner with a
workflow-identity aggregate that comments the failure summary onto one
issue, so this resolution carries the branch's receipt and flag into the
new shape:

- the download step keeps the total_count receipt, the atomic promotion and
  the jobs_complete output;
- the summary step keeps the JOBS_COMPLETE note, which now reaches both the
  PR comment and the issue record, because the issue job publishes
  comment_body;
- the plan-step note and its strip-then-append guard go away with the step
  itself: the aggregate posts a fresh comment per run, so a stale copy
  cannot accumulate.
@wenshao

wenshao commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Maintainer local verification — real GitHub API + fault injection

Verified at head 2e77947c0380caf4aaa544ef4e6a7256b17c4f97 (base 69df02bb9a609d32941932c6dc8b65094056d6fd). Verdict: merge-ready. No blocking findings. I re-ran the two claims the PR rests on — that gh api --paginate can leave a plausible partial list, and that total_count is a usable receipt on real wire data — and then exercised the verbatim step bodies end to end.

What was run

  • Both arms, fresh worktrees: head 2e77947c03, base 69df02bb9a.
  • Real wire, no stubs: gh 2.103.0 against api.github.com, run 39908521630 (SDK Java, 13 jobs). --paginate with per_page=1 produced 13 separate JSON documents, each carrying total_count: 13; merged 13/13. The verbatim fetch step promoted 13 jobs with jobs_complete=true on both arms (base has no such output). A nonexistent attempt returned the real {"message":"Not Found",...} body on stdout with exit 1, and head fell back to {"jobs":[]} + warning + jobs_complete=false.
  • Mid-walk 403: real gh driving a local HTTPS GitHub-API emulator (trusted via SSL_CERT_FILE), so --paginate followed a real Link: rel="next" and hit the failure itself. No hand-written gh stub in the fetch path.
  • Whole analyze chain, not isolated steps: verbatim Download failed job logs → Detect infrastructure-only failures → Build the PR failure summary, with a real failed job log downloaded from main. The table below is the resulting published body and infra_only, i.e. what the rerun classifier and the readers actually see.

Before / after (page 1 declares 2 jobs; page 2 is the regression)

Input base head
page 2 → 403 1/2 jobs published, infra_only=true, no note → rerun, no issue 0/2, jobs_complete=false, infra_only=false, note present → issue filed
page 1 has no total_count, page 2 → 403 1/2 published, infra_only=true, no note 0/2, false, note present
both pages intact 2/2, infra_only=false, no note 2/2, jobs_complete=true, no note
gh exits 0 with 1 of declared 2 jobs 1/2 published, no warning at all, infra_only=true, no note 0/2, warning, false, note present
single page, no total_count, exit 0 1/1 published as whole run 0/2, false, note present
total_count: 0 0/0 0/0, jobs_complete=true, no false degradation
page 1 itself 403 0/0 + warning 0/0 + warning + false + note

The no-receipt row is the one the first revision missed; the exit-0-short row is the one the exit status cannot see. Both are rejected by the receipt alone.

Scope note on the receipt vs. pipefail

Under the Actions shell (bash -eo pipefail) a page-2 403 already routes the pipeline to the else branch through gh's exit status. The receipt's independent value is therefore in the exit-0-but-short and missing-total cases, plus the no-pipefail spawn the meta-test uses. I re-ran the page-2 cell without pipefail: base still published 1/2, infra_only=true, no note; head published 0/2, false, note present. So the guard, not the shell option, is what rejects.

Gates and mutations

Check Result
meta-test head 76/76, base 75/75
actionlint (CI invocation) head exit 0, base exit 0
workflow size ratchet head pass, base pass
prettier / eslint changed files clean / 0 errors
M1 $total == null or … suite 2 failed; cell publishes 1 job and jobs_complete=true
M2 drop --paginate suite 1 failed; cell 0 jobs / false
M3 jobs_complete=true on fallback suite 2 failed; flag true, note absent
M4 delete the summary sentence suite 1 failed; note absent
M5 delete the jobs_complete output suite 1 failed; flag null, note appears on a complete list

Every mutation was applied to the head worktree, measured, and restored with git checkout HEAD --; the tree was clean and at 2e77947c03 after each. The five mutations are all pinned by the PR's own suite, and four of them are also visible at the published-body level.

Evidence

real GitHub pagination and receipt

page 2 fails, before and after

gh exits 0 with a short list

gates and mutation matrix

Artifacts: tmp/pr15174-verify-20261023-145810/ (harness, per-cell JSON, real-run logs, mutation diffs).

中文摘要

维护者本地真实环境验证

head 2e77947c03,base 69df02bb9a。结论:可以合并,无阻塞问题。

  • 真实视野:gh 2.103.0 直接打 api.github.com,run 39908521630(13 个 job)。用 per_page=1 让真实分页产生 13 个 JSON 文档,每页都带 total_count: 13,合并 13/13;两臂的抓取步骤都保留了全部 13 个 job,head 报 jobs_complete=true。不存在的 attempt 返回真实的 404 错误体(stdout + exit 1),head 落回 {"jobs":[]} + 警告 + false。
  • 中途 403:让真实 gh 通过本地 HTTPS 仿 GitHub 走真实 Link 分页并在第 2 页收到 403,不是手写 gh 桩。
  • 整条 analyze 链:逐字执行 Download failed job logs → Detect infrastructure-only failures → Build the PR failure summary,日志用 main 上真实失败 job 的日志,所以表里的 infra_only 和正文就是下游实际看到的东西。
  • 前后对比:第 2 页 403 时,base 发布 1/2 个 job、infra_only=true、无说明 → 只会重跑、不建 issue;head 发布 0/2、false、带说明 → 建 issue。gh 退出 0 但列表偏短时,base 连警告都没有;head 用 total_count 凭据拒绝。完整两页、零 job、首页 403 均符合预期。
  • no-pipefail 对照:base 仍是 1/2 + true + 无说明;head 0/2 + false + 说明——拒绝来自凭据,不是 shell 选项。
  • 门禁:head 元测试 76/76,base 75/75;actionlint 两臂 0;体积棘轮两臂通过;prettier/eslint 干净。
  • 变异:M1 去掉「必须声明总数」、M2 删 --paginate、M3 兜底置 true、M4 删说明句、M5 删 jobs_complete 输出——套件分别红 2/1/2/1/1 条,对应 cell 也红;每次都用 git checkout HEAD -- 还原并确认树干净。
  • 范围说明:Actions 的 bash -eo pipefail 下,page-2 403 本来就会因 gh 非零退出走兜底,所以凭据真正独立承重的是「退出 0 但偏短」「缺 total_count」以及元测试的 no-pipefail 生成方式。这不是缺陷,是说明。

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed head 2e77947c0380caf4aaa544ef4e6a7256b17c4f97. Two files: .github/workflows/main-ci-failure-issue.yml (+27/-3) and scripts/tests/main-ci-failure-issue-workflow.test.js (+154/-9). I read the whole production hunk and the workflow's downstream consumers at head.

No Critical findings. Both blocking items from earlier rounds are resolved at this head, and I verified each in the current file rather than from the disposition replies.

The fail-open receipt inverse is fixed, and pinned

The blocking review reproduced that if $total == null or ($doc.jobs | length) == $total then $doc promotes an uncertifiable page body: null is the absence of a completeness receipt, and the or arm treated it as a licence. At head the predicate is the conservative form that review asked for:

if $total != null and ($doc.jobs | length) == $total then $doc
else error("incomplete job list: merged \($doc.jobs | length) of \($total)") end

All three shapes now fail closed. A short merge (total_count: 2, one page's jobs) errors; an all-pages-failed response carries no total_count, so max is null and the != null conjunct errors; only a fully accounted list is promoted. The promotion is staged — jq writes ${jobs_json}.tmp, and mv replaces run-jobs.json only on success, while the failure branch does rm -f "${jobs_tmp}" and writes an explicit {"jobs":[]}. That matters on a reused self-hosted runner, where a previous attempt's run-jobs.json would otherwise survive into this one; overwriting it with a certified-empty list is what keeps the infra classifier from reading stale jobs.

The tests pin the producer and the predicate together, not just the predicate: the assertion span starts at gh api "repos/${REPO}/actions/runs/${WORKFLOW_RUN_ID}/attempts/${RUN_ATTEMPT}/jobs?per_page=100" --paginate and runs through > "${jobs_tmp}"; then mv "${jobs_tmp}" "${jobs_json}", so dropping --paginate or per_page=100 reddens the suite. refuses a job list it cannot certify complete spawns the real receipt expression against an executable gh stub on PATH and asserts the resulting run-jobs.json, that the staging file is absent, and the warning text; the no-total_count case is there as its own input, and a complete run asserts stdout carries no warning as the positive control. That is the negative case the blocking review specified.

The stated reason for the change is the part worth recording: a partial list is indistinguishable from a complete one to every consumer, including the infra classifier, which would rerun a regression parked on the missing page instead of filing it. jobs_complete is exported as a step output and both human-facing bodies read it, so a short report says so.

The append-only degradation note is gone with the mechanism that made it append-only

The fix-induced Critical was that the degradation sentence, once written into a tracking issue's human-owned tail, was re-emitted by every --existing merge and could never be retracted by a later clean run. That mechanism no longer exists at this head:

  • The sentence is generated per run inside the analyze job from JOBS_COMPLETE: '${{ steps.download.outputs.jobs_complete }}' (:287, :304-305) and lands in steps.summary.outputs.body, published as comment_body (:63).
  • The PR path (:479-500) writes that body to a fresh file and upserts a comment keyed by a per-run marker, <!-- qwen-managed-agent-ci-failure:${WORKFLOW_RUN_ID} -->, so a run updates its own comment and never appends to another run's text.
  • The issue path (:364-377) is "Create or comment on the workflow issue" against the aggregate marker <!-- main-ci-failure-aggregate:${WORKFLOW_ID} -->. There is no --existing and no planner step anywhere in the head file — the strip-then-append merge the finding was about left with the upstream aggregate change.

So a later run that reads the full job list simply omits the sentence; nothing persists it forward. The retraction property the finding demanded now comes from the publication model rather than from a guard.

Non-blocking items, recorded not reopened

R1-2 (no bounded retry on the all-or-nothing fetch) stands as declined with a written rationale: the failures this branch sees are 403/429/5xx, where an immediate retry repeats the same walk inside the same rate-limit window. R1-1, R1-7-annotation-only, R2-1 (producer→consumer wiring pin) and R2-2 (the != 'true' negative control) are all implemented at head. The deferred [probe] about the receipt harness spawning without -o pipefail does not weaken the receipt: the pipeline's exit status comes from jq, the last element, and jq is what errors on an uncertified merge — so pipefail is not what makes the gate fail closed. Worth noting for the record that reviewDecision still reads CHANGES_REQUESTED because the automated review at 9f18c4661b was never dismissed, even though the Critical backing it is resolved here.

CI

gh pr checks at this head shows no failures and nothing pending among the lanes that run: Test (ubuntu-latest, Node 22.x) (48m30s, the lane that executes the changed test file), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, Managed Agent gate, Select the Managed Agent tier, Classify PR, route, label, assign and Remind on force-push all pass. The Java, native, distribution and parity lanes are skipped for this diff, which touches only a workflow file and a script test. An earlier "deferred approval withheld" note belongs to the superseded head 9b9bd5e.

@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. The receipt gate is fail-closed, the four pagination scenarios are covered with distinct fixtures (truncated page, no total_count, gh-exits-0-short, and the positive control), and the summary degradation test covers both sides of the flag. The wiring pin (step id + JOBS_COMPLETE binding) protects against drift.

Previously raised concerns on earlier heads are addressed: the no-total_count inverse was corrected (Nakagawa-master confirmed on ef371fd), the append-only concern is moot after upstream replaced the issue writer with per-run comments, and R2-1/R2-2 from the bot's review on 9f18c46 now have matching assertions and negative controls on this head.

中文说明

凭据门为 fail-closed,四种分页场景各有独立夹具(后续页失败、无 total_count、gh 退出 0 但偏短、正向对照),summary 降级测试覆盖了 flag 的两侧。接线钉(step id + JOBS_COMPLETE 绑定)防漂移。

旧 head 上已提出的顾虑均已处理:no-total_count 逆条件已被校正(Nakagawa-master 在 ef371fd 确认),append-only 因上游把 issue 写入器替换为按次评论而不再成立,bot 在 9f18c46 的 R2-1/R2-2 在当前 head 已有对应的断言与负向对照。

@wenshao
wenshao merged commit 6a50ae8 into main Oct 23, 2026
125 of 126 checks passed
@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Released in v0.26.1.

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.

5 participants