fix(ci): retry sandbox image builds and file an issue when a release build fails - #9916
Conversation
…build fails The v0.22.0 tag build died on a transient ETXTBSY during `npm ci` and was never retried, so ghcr.io/qwenlm/qwen-code:0.22.0 was never published while npm already served 0.22.0. Every sandbox-based CI lane (/resolve, sandboxed review, autofix) then crashes with "manifest unknown" until the image exists. Add one bounded retry to the buildx step: the first attempt carries continue-on-error so a successful retry turns the job green, and the retry gates on the first attempt's outcome alone (a failure() gate would read false once continue-on-error absorbs the first attempt). The publish condition is shared through one job-level env so the two build steps cannot drift. Add a follow-up job that files or updates one issue per version when both attempts fail — for tag pushes and for publishing dispatches alike, since the issue body itself recommends that dispatch as the recovery path. Dedup uses an exact body marker matched client-side, because GitHub search tokenizes the colon out of the marker and never finds these issues. Extend the existing workflow gate test to pin the retry contract and the issue-job gate. Fixes #9898
|
Re-run at the author's request, on the updated head — two commits since the last pass: the round-4 hardening of the failure reporter, and a wording fix on the documented dedup gap. Template ✓ — all sections present, bilingual summary included. Problem: observed, well-evidenced. The Direction: aligned. Release-pipeline reliability for a failure mode that demonstrably broke sandboxed CI lanes. Success-path behavior is unchanged; the only new writes are a retried build step and, on double failure, one issue filed with the job-scoped Size: not a core-module change — 262 production lines (workflow 94, new Approach: scope still feels right. The issue-filing logic lives in Risk: no high-risk-path matches from the revert-history signal. The Moving on to code review. 🔍 中文说明应作者请求重跑,针对更新后的 head —— 距上一轮新增两个 commit:按 review 第四轮加固失败上报脚本,以及修正已记录去重缺口的措辞。 模板 ✓ —— 各部分齐全,含中文总结。 问题:已观测、证据充分。 方向:对齐。 发布流水线的可靠性修复,针对的是实际打断过沙箱 CI lane 的失败模式。构建成功时行为不变;新增写入只有一次重试的构建步骤,以及双重失败时一个用 job 级 规模: 非核心模块改动 —— 262 行生产代码(工作流 94、新增 方案:范围依然合理。 建 issue 的逻辑在 风险: 回滚历史信号无高风险路径命中。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewRe-read the full diff at the current head. The design still matches my independent proposal for this incident — one bounded retry, one deduplicated issue per version, publish decision defined once and exported — and the two commits since the last pass are pure round-4 review responses, all of which I verified in the diff:
The contracts verified in earlier rounds still hold at this head and remain pinned with mutation-killing assertions: One non-blocking note carried over from earlier rounds: the retry step duplicates the first build step's Test evidence — the PR's own CI (unattended run; no local execution)Both The suite grew from 20 to 26 cases with the round-4 hardening and executes green; the size gate passes with the new baseline (I verified the recorded 9256 matches the workflow's actual byte size at this head). actionlint and yamllint ran clean. shellcheck reports non-fatal SC2154/SC2312 warnings on the new script — the same pattern the existing
One row per check name (latest run) at the reviewed commit; failures sort first. / 每个检查名一行(取最新一次运行),失败项排在最前。 What no pre-merge lane can settle, stated plainly: the retry and issue-filing paths only execute on tag push or publishing dispatch — the workflow's Real-scenario testing: N/A — pure CI workflow change, nothing user-visible (unattended run; no tmux attempted). 中文说明代码审查:在当前 head 重读完整 diff。方案仍与独立评估一致——一次有界重试、每版本一个去重 issue、发布条件单点定义并导出。上一轮之后的两个 commit 都是 review 第四轮的回应,逐条在 diff 中核实:运行记录匹配收紧为完整 测试证据:无人为运行,全部来自该 PR 自身 CI(API + job 日志)。两条 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean re-review at the updated head: the round-4 findings are verifiably addressed in the current diff, the follow-up Stepping back: this is still the right response to a real incident, now thoroughly hardened. The two commits since the last pass did exactly what the gate wants from review rounds — the run-bullet matcher got strict, the strip got portable, the fallback got re-checked after the strip, and the gap that can't be fixed in code (a human removing the dedup marker) got documented precisely instead of papered over. The approach matches my independent proposal; I didn't find a simpler path it missed, and every change in the diff serves the stated goal. The standing The only thing no pre-merge lane can prove is the tag-push runtime behavior itself; that's inherent to release-pipeline changes. The first real execution is the next release, and the pinned contracts plus the script's behavioral replay are as much pre-merge signal as this kind of change can carry. If I had to maintain this in six months, the comments in that workflow — the Approving, pinned to the reviewed commit. 中文说明置信度:4/5 —— 更新后 head 的重审干净利落:第四轮提出的各项发现已在当前 diff 中可核实地修复,针对本 head 的后续 整体看:这仍然是对真实事故的正确回应,且已充分加固。上一轮之后的两个 commit 做的正是门槛希望 review 轮次做的事——运行记录匹配收紧、标题剥离可移植、兜底在剥离后复查、代码修不了的缺口(人为删除去重 marker)被精确记录而非粉饰。方案与我的独立评估一致,没有找到更简的路径,diff 中每一处改动都服务于既定目标。现存的 唯一任何合入前 lane 都无法证明的是 tag push 时的运行时行为本身;这是发布流水线改动的固有属性。首次真实执行是下一次发布,钉住的契约测试加脚本行为回放已经是这类改动在合入前能承载的全部信号。如果六个月后由我来维护,工作流里的那些注释—— 批准,锚定在已审查的 commit 上。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Requesting changes on one blocking item — see the staged comments above for the full review.
The workflow-size ratchet fails on this PR's own growth: .github/workflows/build-and-publish-image.yml is 9843 bytes against a 4638-byte baseline + 4096-byte allowance, and the Test (ubuntu-latest, Node 22.x) job died at that gate before vitest ran — so the new workflow-gate tests have not actually executed in CI yet. The gate's error names both fixes: move the issue-filing script into .github/scripts/ and trim prose, or update .github/workflows/.size-baseline in this PR with the justification (the vitest mirror reads the same baseline file).
The design and implementation themselves review clean — this is the only blocker.
The workflow-size ratchet rejects growth past the recorded baseline +4096 bytes; the inline issue-filing step grew build-and-publish-image.yml by ~5.2 KB. Move the step body to .github/scripts/image-build-failure-issue.sh (the gate's own recommended remedy), leaving the job as a thin env + script call. No behavior change; the gate test now pins the script call and reads the dedup contract from the script.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
Closeout — round 1 (cap-4, 2 Criticals + 2 Suggestions handled):
scripts/tests/build-and-publish-image-workflow.test.js 10/10 (7 baseline + 3 new), |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
.github/workflows/build-and-publish-image.yml:134 — [review] retry step duplicates the build definition without a parity pin; a one-sided edit keeps the gate tests green (deferred — code unchanged since round 1).github/scripts/image-build-failure-issue.sh:27 — [review] unvalidated dispatch version can poison or bypass the dedup marker — trailing space files permanent duplicates, crafted value hijacks another version's issue (deferred — code unchan…
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — the merge_group-only Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) lanes were skipped in CI and run the changed workflow-gate suite; it ran only on the Linux lane and locally (17/17 green), and its Windows-lane behavior is unverified.
Not reviewed: verification and reverse audit — both prompts were built, but no agent was launched with either — the posted findings cannot be counted as verified, and the pass that hunts what the rest of the review missed cannot be certified.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
scripts/tests/build-and-publish-image-workflow.test.js:31 — [probe] hand-rolled regex extractions reimplement workflow-helpers getWorkflowJob/getWorkflowStep; lookaheads terminate on comment text, so rewording a comment empties them (deferr….github/scripts/image-build-failure-issue.sh:22 — [review] version normalization restated in the script and the build job's Process version step with nothing cross-pinning them; a future tag-derivation change desyncs marker from published t….github/workflows/build-and-publish-image.yml:147 — [probe] retry step with: block parity unpinned; a one-sided future edit publishes from stale configuration on the exact transient-failure path this PR exists for (deferred — code unchanged….github/scripts/image-build-failure-issue.sh:41 — [probe] dedup check-then-use race: concurrent same-version runs double-file a permanent duplicate; no concurrency: key on the workflow (deferred — code unchanged since round 2)
Convergence: round 3 posted 6 inline comment(s), 3 of them reported for the first time; the previous round posted 8 (0 new). Findings keep coming back to the same files: scripts/tests/build-and-publish-image-workflow.test.js (findings in round 1; 2 more now); .github/workflows/build-and-publish-image.yml (findings in round 1; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)
[Critical] .github/scripts/image-build-failure-issue.sh:52 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
[Critical] .github/scripts/image-build-failure-issue.sh:54 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
[Critical] .github/scripts/image-build-failure-issue.sh:58 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — the merge_group-only Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) lanes were skipped in CI and run the changed workflow-gate suite; it ran only on the Linux lane and locally (17/17 green), and its Windows-lane behavior is unverified。
未审查:验证与反向审计——两份 prompt 都已构建,但都没有 agent 用它们启动——发布的发现不能算作已验证,搜寻评审遗漏问题的工序也无法作证。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 6 条行内评论,其中 3 条是首次提出;上一轮发布了 8 条(其中 0 条首次提出)。发现反复回到同一批文件:scripts/tests/build-and-publish-image-workflow.test.js(第 1 轮已出过发现,本轮又有 2 条);.github/workflows/build-and-publish-image.yml(第 1 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
[Critical] .github/scripts/image-build-failure-issue.sh:52 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
[Critical] .github/scripts/image-build-failure-issue.sh:54 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
[Critical] .github/scripts/image-build-failure-issue.sh:58 SC2016 — Expressions don't expand in single quotes, use double quotes for that. [lint]
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
Closeout pass on the 10 unresolved bot suggestions — 9 handled, 1 deferred. Pushed Handled
Deferred (next round)
Verification: |
A build job that fails before its first step runs (runner provisioning failure) never executes publish-decision, so push_image stays empty and file-failure-issue is skipped despite failure() being true. Closing the gap structurally would restate the publish predicate and re-introduce the drift this PR removes, so document it on the job comment instead: future "failed publish, no issue filed" investigations start here, and a scheduled npm-vs-GHCR reconciliation remains the backstop.
…kflow size baseline The retry logic and failure-issue filing steps added by this PR grew the workflow from 4638 to 8887 bytes, past the 4096-byte allowance. Record the new size so the size gate passes.
|
CI failed at the "Check workflow file size" gate: The growth is from this PR's own retry logic and failure-issue filing steps, so I recorded the new size in Local run of |
# Conflicts: # .github/workflows/.size-baseline
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 276 passed · 0 failed · 276 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:276 通过 · 0 失败 · 276 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9916 —
|
| arm | change vs HEAD | S1 first fails, retry succeeds | S2 both fail | result |
|---|---|---|---|---|
| HEAD | — | job green, retry ran, no issue (correct: image published) | job red, retry ran, issue filed | 44/44, 01-semantics-ab-head-10-scenarios.png |
| C1 | retry gate failure() |
job green, retry skipped, no issue — silent non-publish, the #9898 shape | job green, no issue — double failure invisible | broken as predicted, 16/16 |
| C2 | no continue-on-error |
job red and issue filed although the retry published the image | same as HEAD | broken as predicted, 16/16 |
| C3 | issue gate restates startsWith(ref,'refs/tags/v') |
— | S4 dispatch publish failure: no issue filed (drift loss); S2 tag push still files | broken as predicted, 16/16 |
(02-controls-reproduce-the-broken-behavior.png.) All ten HEAD scenarios passed their intent, including: versionless publishing dispatch skipped by design (S5), non-publishing dispatch skipped (S6), checkout/login failure on a publishing run still files (S7/S10), fork guarded (S9), and the documented pre-first-step gap behaving exactly as the workflow comment says — push_image stays empty and the gate skips despite failure() (S8). The four PUSH_IMAGE expansion checks agree with the login gate in every event shape.
Script replay (mock-free, real bash + real jq)
script-replay.mjs runs the shipped script under bash --noprofile --norc with the workflow step's exact env contract; only gh is stubbed, and the stub encodes real gh semantics (open-by-default state filter, --label filter, --limit, per-issue body store, argv recording) — so --state open and the create-time label are behaviorally observable, not text-pinned. 77/77 (03-script-replay-real-bash-real-jq.png): create with both labels and exact title/marker/body; dedup on open+labeled marker issues; recurrence newest-first with previous runs and head/tail annotations preserved; pre-block bodies upgraded; moved-marker bodies still dedup; same-run repeat not duplicated; 10-run cap drops the three oldest; v-prefix tag push and typed-v dispatch normalize to the same marker; no version → ::error:: exit 1 with zero writes; closed or unlabeled marker issue → fresh create; null body → loud jq failure, no writes; hostile dispatch input ($(…), backticks) interpolated literally with no command execution.
Mutation matrix on the PR's own suite
16 single-point mutants, each run against scripts/tests/build-and-publish-image-workflow.test.js in a scratch dir; unmutated control 20/20 green first. 15/16 killed (04-mutation-matrix-15-of-16-killed.png). Positive controls: M12 (.body→.title, the escape the PR comment cites) killed by 5 red tests; M13 (marker loses :${version}) by 6.
| mutant | verdict | mutant | verdict |
|---|---|---|---|
M1 retry gate → failure() |
killed | M11 --state open→all |
killed |
M2 drop continue-on-error |
killed | M12 .body→.title |
killed (5) |
| M3 rename build step id | killed | M13 marker w/o version | killed (6) |
M4 continue-on-error on retry |
killed | M14 create w/o dedup label | killed |
M5 PUSH_IMAGE → ${{ false }} |
killed | M15 oldest-first order | killed |
| M6 gate restates predicate | killed | M16 max_runs=10→3 |
survived |
| M7 drop versionless-skip | killed | M17 drop occurrences marker | killed |
| M8 drop repo guard | killed | ||
M9 drop needs: |
killed |
M16 classification: coverage gap (behavior is correct; nothing in the PR's suite asserts the cap; my replay R10 pins it against the real script). Completeness reporting, not a merge condition.
Size gate and static gates
- Recorded baseline
8887== shippedwc -c8887(the "generator" re-run). Gate at HEAD withWORKFLOW_SIZE_BASE_SHA=HEAD^1: green, exit 0. Planted+5000 Bgrowth →::error:: … grew to 13887 … (allowance 4096), exit 1; new workflow without an entry →::error:: … has no entry, exit 1 (05-size-gate-live-proof.png). Official vitest mirror:workflow-size.test.js195/195. - actionlint (repo flags, pinned 1.7.12): clean at HEAD; planted
steps.does-not-exist.outcomecaught, exit 1 — and its error text independently enumerates the valid step object, corroborating the retry gate'ssteps.build-and-push.outcomereference. shellcheck (pinned 0.11.0, repo flags): 14 advisory findings on the new script (7× SC2154 env refs, 3× SC2016 printf-format, 4× SC2312) — same convention as siblingupsert-deferred-issue.sh(9 SC2154), and the gate is advisory anyway (see Findings).bash -nclean; planted syntax errors exit 2 (06-static-gates-live.png). Official in-tree run of both gate suites: 215/215.
Findings
- Low — stranded-heading duplication on human-mutilated bodies. If a human deletes the
<!\-\- image-build-failure-occurrences -->line but leaves the old run bullets, the rebuilt body carries two## Failed runsheadings (the strip only fires when the heading is the last head line). Repro: replay R7 fixture; data and dedup are preserved. Measured fix inmutants/fix-r7/(replace the tail-check withprintf '%s\n' "$(grep -vxF -- "${runs_heading}" "${head_file}")" > "${head_file}"): R7 heading 2→1, all 77 replay assertions pass, normal-recurrence body byte-identical (zero collateral), PR suite 20/20 green on both sides — i.e. the suite pins nothing along this axis; the R7 fixture is the pin a fix should ship with. Optional polish; not blocking. - Completeness —
max_runs=10unpinned by the PR's suite (M16 survivor). My replay pins it; suggest the fixture if the author wants it in-tree. - Nit — 14 new advisory shellcheck findings on the new script under the repo's gate command. Matches sibling convention; no CI impact.
- Pre-existing (not this PR) — the shellcheck gate can never fail:
scripts/lint.js --shellcheckpipes through a trailingsed, whose exit 0 masks shellcheck's. Observed at HEAD with thousands of pre-existing warnings. The PR's additions ride on this advisory gate. - Pre-existing (not this PR) — expression-injection surface in the untouched
Process versionstep (INPUT_VERSION="${{ github.event.inputs.version }}"inlined into a run block). Requires write access to dispatch; unchanged by this PR; the PR's own env-var plumbing for the same input is injection-safe.
Not covered
- Docker build/push and live issue filing end-to-end (need a tag push or publishing dispatch; no token in this sandbox). The replay reproduces the script's mechanism and wire shapes, not the live
ghAPI; the semantics A/B models documented Actions behavior, not live Actions. - Per-commit attribution: 13 of the PR's 14 commits are unreachable at the depth-2 checkout; the aggregate
HEAD^1..HEADdiff was verified instead. - yamllint could not be installed (no root, no pip); YAML well-formedness is proven by the
yamlparse the simulator requires plus actionlint's parse. - Label existence (
scope/ci-cd,type/bug) cannot be checked from inside the sandbox; local evidence: the repo's release-notes tooling uses thescope/*taxonomy and the author's stated live probe (28 open labeled issues). If a label were missing,gh issue create --labelfails loud — the job goes red on a run whose build already failed, so the failure mode is visible. - The pre-first-step gap (runner provisioning failure files no issue) is verified to behave exactly as the workflow comment documents; closing it structurally would restate the predicate the PR consolidates — the author's documented tradeoff, with the scheduled reconciliation backstop named.
Methodology
Environment: the CI verify container (node 22, bookworm), tree at refs/pull/9916/merge, built at HEAD before the round. semantics-simulator.mjs drives the parsed YAML's expressions through a scenario matrix (head + three one-change controls); script-replay.mjs spawns the shipped script under the step's env contract with a semantics-encoding gh stub and real jq 1.6; mutation-matrix.mjs runs the PR's vitest suite per single-point mutant in scratch dirs; size/static gates run the repo's own scripts and pinned linters with planted-violation live proofs. Raw logs in logs/, harnesses and mutants in the artifact dir, captures in evidence/.
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/build-and-publish-image-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/build-and-publish-image-workflow.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/build-and-publish-image-workflow.test.js: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 2 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 3 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 4 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 5 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
scripts/tests/build-and-publish-image-workflow.test.js:40 — [probe] hand-rolled extraction regexes re-implement workflow-helpers getWorkflowJob/getWorkflowStep; rewording a terminator comment empties the extraction.github/workflows/build-and-publish-image.yml:194 — [probe] workflow→script env wiring (EVENT_NAME/TAG_NAME/INPUT_VERSION/RUN_URL) pinned by no test; mutants stay green.github/workflows/build-and-publish-image.yml:184 — [probe] file-failure-issue permissions block pinned by no test; deleting issues:'write' leaves the full suite green and the alert dead on first use
Convergence: round 4 posted 13 inline comment(s), 12 of them reported for the first time; the previous round posted 6 (3 new). Findings keep coming back to the same files: .github/scripts/image-build-failure-issue.sh (findings in round 1; 10 more now); scripts/tests/build-and-publish-image-workflow.test.js (findings in round 3; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:the executable-script lint — .github/scripts/image-build-failure-issue.sh: shellcheck is not installed。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 13 条行内评论,其中 12 条是首次提出;上一轮发布了 6 条(其中 3 条首次提出)。发现反复回到同一批文件:.github/scripts/image-build-failure-issue.sh(第 1 轮已出过发现,本轮又有 10 条);scripts/tests/build-and-publish-image-workflow.test.js(第 3 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
…9916) - Replace GNU-only `head -n -1` with POSIX `sed '$d'` so the stranded-heading strip no longer corrupts the body on BSD userland (R4-1). - Skip the bash replay suite on win32, where backslash RUNNER_TEMP and the ';'-separated PATH cannot express it; the YAML pins still run there (R4-2). - Re-check head readability AFTER the normalization strip, which can itself empty the head and used to drop the narrative permanently (R4-8). - Admit only recorded-run shapes into the machine block so a bullet-shaped human annotation is no longer reordered into it or clipped by the cap (R4-13). - Remove the marker-restore branch: with the run shape pinned, every body that matched the dedup carries its marker in head+tail, so it was unreachable (R4-9). - Cross-reference the sibling split/merge contract in both implementations (R4-5), disable SC2016 with rationale on the literal-backtick formats (R4-6), and document the label-removal residual gap on the job (R4-11). - Behavioral witnesses: run-cap, stranded-heading, marker-survival, empty-head and empty-after-strip prose fallbacks, and the annotation shape; each guard mutation-probed red. Pin the dedup label on the list call too (R4-10).
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round 5 closeout — all 13 actionable round-4 findings handled in
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI (merge_group-only lanes) and the changed workflow-gate suite ran only on the Linux lane and locally; Windows/macOS lane behavior is unverified.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
.github/workflows/build-and-publish-image.yml:147 — [probe] retry step with: parameters (context/platforms/tags/labels/build-args) pinned by no test; a one-sided edit publishes a silently different image on the transient-failure path (defer…
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI (merge_group-only lanes) and the changed workflow-gate suite ran only on the Linux lane and locally; Windows/macOS lane behavior is unverified。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round 5 — no action requiredThis round's feedback contains no actionable items:
No code changes were made this round. The branch remains at 中文说明Autofix 第 5 轮 —— 无需处理本轮反馈中没有任何可处理的事项:
本轮未做任何代码改动。分支保持在 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
Closeout — the four round-4 findings (R4-1/R4-2 Critical, R4-3/R4-4 Suggestions) were verified already fixed at head
Targeted suite: 26/26 pass at head ( |
…ob (#9916) Round 4 removed the unreachable marker-restore branch (R4-9) but left its residual gap undocumented: the dedup lookup only finds the tracked issue while the version marker survives in the body, so a human edit deleting the marker orphans the issue and the next failure files a duplicate. Fold the marker into the job's existing known-gap note alongside its sibling, the scope/ci-cd label (R4-11), and record the workflow's new size.
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9916 (address-review)This round processes the closeout instruction from the maintainer ( Feedback triage
DispositionsResolved in code at
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-only lane) and the changed workflow-gate suite did not run on that platform locally.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only lane) and the changed workflow-gate suite did not run on that platform locally.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-only lane) and the changed workflow-gate suite did not run on that platform locally。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only lane) and the changed workflow-gate suite did not run on that platform locally。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/build-and-publish-image.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — PR #9916: no action requiredFeedback triageEverything newer than the last evaluation (2026-08-25T10:18:20Z) is confirmation, not new work. No findings, change requests, failed checks, or still-red checks were reported this round.
Diff growth this window: source 7 / test 215 net lines against 400/400 budgets — well within budget, no growth audit triggered. Verification performed (read-only round — no code changes, no commit)
DispositionNo code change and no commit this round. 中文说明Autofix 审查轮次 — PR #9916:无需任何操作反馈分类上次评估(2026-08-25T10:18:20Z)之后的所有内容都是确认性质的,不含新工作。本轮没有报告任何新发现、修改请求、失败检查或持续失败的检查。
本窗口 diff 增长:源码净 7 行 / 测试净 215 行,预算为 400/400 —— 远在预算之内,未触发增长审计。 已执行的验证(只读轮次 —— 无代码改动、无提交)
处置本轮不做任何代码改动,也不提交。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 280 passed · 0 failed · 280 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:280 通过 · 0 失败 · 280 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9916 —
|
| # | previous finding | severity | status at new head |
|---|---|---|---|
| F1 | stranded-heading duplication when a human edits the body | low | partially fixed. Shape A (marker and bullets removed → stranded heading is the last head line): fixed and pinned in-tree (emits the run-block heading exactly once after a stranded one); replay R7 measures 1 heading; strip-guard mutant M20 killed by 2 tests. Shape B (marker removed, bullets kept): stands, unchanged — replay R8 measures 2 headings, data and dedup preserved (captured body in logs/f1-shape-b-rebuilt-body.md). New bounding evidence: the mjs sibling uses the identical tail-anchored strip (head.replace(/\n*##\s+Recurrences\s*$/, '')), so shape B is the sibling-contract-consistent behavior; a one-sided fix would diverge the pair the R4-5 comment ties together. Measured fix available (Finding 1). |
| F2 | max_runs=10 unpinned by the suite |
completeness | fixed. Mutant M16 (max_runs=3) now killed by the new caps the recorded runs at the newest ten; my replay R4 independently pins the cap against the real script (13 prior runs + new → exactly 10, four oldest dropped, newest first). |
| F3 | 14 new advisory shellcheck findings | nit | improved: 14 → 11. The 3× SC2016 are disabled inline with rationale (R4-6, on the literal-backtick formats); remaining 7× SC2154 (workflow-provided env refs) + 4× SC2312, same convention as sibling scripts (gate is advisory-only, see Finding 3). |
| F4 | pre-existing: scripts/lint.js --shellcheck can never fail (trailing sed masks shellcheck's exit) |
pre-existing | stands. scripts/lint.js is untouched by this PR (0 diff lines); masking re-demonstrated with the repo's exact pipe shape: a file shellcheck errors on exits 0 through the pipe, 1 unpiped (06-static-gates-live.png). |
| F5 | pre-existing: expression-injection surface in the untouched Process version step |
pre-existing | stands, unchanged. 0 PR-diff lines touch the step; INPUT_VERSION="${{ github.event.inputs.version }}" still inlined at workflow line 56. The PR's own env-var plumbing for the same input (line 202) remains the injection-safe path the script consumes. |
Central claim and A/B proof
Central claim: a failed release image build retries once, and a still-failed publishing run files/updates exactly one issue per version — deduplicated client-side by an exact body marker, preserving human annotations, without false positives on successful or non-publishing runs.
The A/B is a semantics A/B (semantics-simulator.mjs): the real workflow YAML is parsed with the repo's yaml package, every gate expression is evaluated verbatim by a recursive-descent Actions-expression evaluator encoding the documented semantics (explicit if: replaces the default success(); continue-on-error flips conclusion not outcome; job conclusion from step conclusions; failure() is conclusion-based at both scopes; job outputs reach dependent jobs after a producing-step ran; missing inputs compare as ''). Because every arm runs through the same model, a model error would have to hit arms asymmetrically to flip a verdict. HEAD: 38/38 across 10 scenarios (01-semantics-ab-head-10-scenarios.png), including: retry success turns the job green with no issue (S1), double failure files (S2), versionless publishing dispatch skipped by design (S5), non-publishing dispatch keeps push=false and skips (S6), checkout/login failures on publishing runs still file (S7/S10), fork guarded (S9), and the documented pre-first-step gap behaving exactly as the workflow comment says (S8: push_image stays empty, gate skips). The 4 PUSH_IMAGE expansion checks agree with the login gate on every event shape.
| arm | one-point change vs HEAD | reproduced hazard | result |
|---|---|---|---|
| C1 | retry gate outcome == 'failure' → failure() |
S1: retry skipped, job green — silent non-publish (the #9898 shape); S2: double failure invisible (job green, no issue) | 3/3 predicted breakages, 02-controls-reproduce-broken-behavior.png |
| C2 | drop continue-on-error from the first build step |
S1: retry published the image, yet job red and issue filed for an image that exists | 3/3 |
| C3 | issue gate restates startsWith(github.ref, 'refs/tags/v') |
S4 publishing-dispatch failure: no issue filed (drift loss); S2 tag push still files | 2/2 |
Script replay (mock-free, real bash + real jq)
script-replay.mjs runs the shipped script under the workflow step's exact env contract (bash .github/scripts/image-build-failure-issue.sh, cwd = repo root); only gh is stubbed, and the stub encodes real gh semantics so the script's flags are behaviorally observable: --state (default open), --label (all must match), newest-first ordering, --limit truncation, per-issue body store. 96/96 (03-script-replay-96-assertions.png): create with exact title and both labels; open-only + label-filtered dedup (a closed or unlabeled marker issue is invisible → fresh create); --limit 200 window observable (the marker issue, oldest of 205 open labeled issues, falls beyond the newest-200 window → create); v-prefix normalization both directions; same-run repeat not duplicated; cap 10 drops the four oldest of 14, newest first; marker moved onto a run line below the block survives rebuild through tail re-emission (the removed marker-restore branch stays unnecessary); empty head and emptied-after-strip heads fall back to generated prose; bullet-shaped annotation kept out of the machine block; hostile dispatch input ($(touch …), backticks) stays literal with zero execution; versionless run hard-fails with ::error:: and zero writes; two matching issues edit the newest.
Mutation matrix on the PR's own suite
22 single-point mutants against scripts/tests/build-and-publish-image-workflow.test.js in scratch dirs; unmutated control 26/26 green first; positive control M12 (.body→.title, the escape the PR comment cites) killed by 11 red tests in the same collected file. 21/22 killed (04-mutation-matrix-21-of-22-killed.png). All four round-4 guards killed and attributed to the exact witness tests the commit message names: R4-8 post-strip re-check → restores the narrative when the body starts at the occurrences marker + …when the head normalizes down to nothing (2 red); R4-10 list-call label → records the recurrence… (1 red); R4-13 run-shape filter → keeps a bullet-shaped annotation out of the recorded runs (1 red); stranded-heading strip → 2 red. Carry-over guards M1–M15, M17 all killed; M16 (max_runs=10→3) killed this round by the new cap test (previous round's survivor, now pinned).
Sole survivor M23 (sed '$d' → head -n -1) — classified environment-bound, not a coverage gap: GNU head accepts negative counts and the two are byte-identical on multi-line, single-line, and empty inputs (measured), so no test in this container can observe the mutation; the BSD regression it guards is only observable on BSD userland, which this lane cannot run.
Size gate and static gates
- Recorded baseline
9256== shippedwc -c9256(generator re-run); gate at HEAD withWORKFLOW_SIZE_BASE_SHA=HEAD^1green; planted +5000 B →::error:: … grew to 14275 … (allowance 4096)exit 1; planted missing entry →::error:: … has no entryexit 1 (05-size-gate-live-proof.png). Official vitest mirrorworkflow-size.test.js: 186/186. - actionlint (repo flags, pinned 1.7.12): clean at HEAD; planted
steps.does-not-exist.outcomecaught, exit 1 — its error text independently enumerates the valid step object includingbuild-and-pushwithoutcome/conclusion, corroborating the retry gate's reference. shellcheck (pinned 0.11.0, repo flags) on the new script: 11 advisories (7× SC2154, 4× SC2312).bash -nclean; planted syntax error exits 2 (06-static-gates-live.png). Official in-tree run of the PR suite at HEAD: 26/26.
Findings
- Low (carried over, stands) — shape-B stranded-heading duplication. When a human deletes the
<!\-\- image-build-failure-occurrences -->line but keeps the old run bullets, the rebuilt body carries two## Failed runsheadings — the strip only fires when the heading is the last head line. Repro:script-replay.mjsscenario R8 (count 2) orlogs/f1-shape-b-rebuilt-body.mdfor the exact rebuilt body. No data loss, dedup unaffected (the version marker lives in the head); human-mutilation-only trigger. Bounding new in this round: the mjs sibling (splitOccurrenceBlock/renderIssueBody) implements the same tail-anchored strip, so this script faithfully mirrors the sibling contract the R4-5 cross-reference names — fixing only this script would diverge the pair. Measured fix (scratchmutants/fix-r7/, replace the tail check withprintf '%s\n' "$(grep -vxF -- "${runs_heading}" "${head_file}")"— any heading line above the marker is stranded by definition sincewrite_bodyre-emits the real one): replay 96/96 on the fixed variant with shape-B count 2 → 1, normal-recurrence R1 body byte-identical (zero collateral), PR suite 26/26 green on both sides — i.e. the suite pins nothing along this axis; the R8 fixture is the pin a fix should ship with, applied to both implementations (07-fix-variant-replay.png). Optional polish; not blocking. - Nit — a recorded run written after a human annotation is demoted to prose. Under the R4-13 shape filter, once any non-run line appears below the occurrences marker, everything after it lands in the tail — including genuine run-URL bullets. A demoted run URL is preserved (as prose, above the rebuilt block) but is no longer deduped against the machine block; if that exact run fails again, its URL appears twice in the body (once as prose, once in the block). Requires a human to interleave a bullet annotation between two recorded runs and the older run to fail again; cosmetic. Design-consistent with the stated R4-13 intent (only recorded-run shapes enter the machine block).
- Nit — 11 advisory shellcheck findings remain on the new script under the repo's gate flags (improved from 14; see status table). Matches sibling convention; the gate is advisory-only (Finding/previous F4 explains why it cannot fail regardless).
- Pre-existing (not this PR) — F4 shellcheck-gate exit masking, re-demonstrated at this head.
- Pre-existing (not this PR) — F5 expression-injection surface in the untouched
Process versionstep, re-confirmed untouched.
Not covered
- Docker build/push and live issue filing end-to-end (need a tag push or publishing dispatch; no token in this sandbox). The replay reproduces the script's mechanism and wire shapes against a semantics-encoding stub, not the live
ghAPI — it is uncalibrated against live behavior: no real emitted artifact (a filed issue's actual body/argv) is retrievable in this token-free sandbox. What would calibrate it: a capturedgh issue list/view/create/editexchange from one real filing run (for example the manual recovery the PR description cites). The semantics A/B models documented Actions behavior (assumptions E1–E8 listed in the harness header), not live Actions. - Per-commit attribution: 1 of the PR's 16 commits is reachable at the depth-2 shallow checkout (
git rev-list --count HEAD^1..HEAD^2= 1 vs 16 in the snapshot); the aggregateHEAD^1..HEADdiff was verified. The two delta commits' claims were verified behaviorally (every round-4 guard mutation-probed, tests pin the contracts) rather than by per-commit diff review. - yamllint could not be installed (
pip3: Permission deniedin-container); YAML well-formedness is proven by theyamlparse the simulator requires plus actionlint's parse. - BSD behavior of
sed '$d'vshead -n -1(M23): GNU equivalence proven byte-identical on three shapes; BSD userland unreachable in this container. The win32 replay-skip path (R4-2) is likewise unreachable on this Linux lane; the YAML pins demonstrably still run here (26/26). - Label existence (
scope/ci-cd,type/bug) cannot be checked without a token; a missing label fails loud atgh issue create --labelon a run whose build already failed. - The flakiness gate (5 identical rounds) is run by the workflow itself, outside this round.
Methodology
Environment: the CI verify container (node 22.23.2, bookworm, bash 5.2.15, jq 1.6), tree at refs/pull/9916/merge, built at HEAD before the round. semantics-simulator.mjs drives the parsed YAML's expressions through a 10-scenario matrix (head + three one-change controls); script-replay.mjs spawns the shipped script under the step's env contract with a semantics-encoding gh stub and real jq (one harness bug found and fixed before scoring: jq 1.6 reserves $label, which silently emptied the stub's list output — every dedup scenario was re-run after the fix); mutation-matrix.mjs runs the PR's vitest suite per single-point mutant in scratch dirs symlinked to the repo's node_modules; size/static gates run the repo's own scripts and pinned linters (installed via scripts/lint.js --setup) with planted-violation live proofs in scratch worktrees. Raw logs in logs/, harnesses and mutants in the artifact dir, captures in evidence/.
Flakiness gate log
rounds=5 files=1 skipped=0
file scripts/tests/build-and-publish-image-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/build-and-publish-image-workflow.test.js
per-file results (P=pass F=fail I=infra-exit, one letter per run):
scripts/tests/build-and-publish-image-workflow.test.js: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 2 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 3 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 4 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
round 5 · scripts/tests/build-and-publish-image-workflow.test.js: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no code changesThis round triaged four inline items, four review submissions, and the sandboxed-verification report. Nothing requires a code change: the automated reviewer has approved ( Inline findings — re-verified resolved (rc:3852990060, rc:3852990936, rc:3852991742, rc:3852992718)These four items reached this round as replies confirming that the round-4 findings were addressed in
The focused suites were re-run at this head: 26/26 ( Verification-report finding 1 — shape-B stranded heading (ic:5410657430): deferred to the follow-up queueVerified real with a direct probe against the shipped script: when a human deletes the occurrences-marker line but keeps the run bullets, the rebuilt body carries two
Verification-report finding 2 — run demoted to prose: declinedDesign-consistent under the R4-13 run-shape filter: the machine block ends at the first non-run-shaped line, so a recorded run written below a human annotation survives as prose. That trade-off was explicitly accepted in its thread; re-opening it would churn the pinned shape filter without a defect to fix. Verification-report finding 3 — 11 advisory shellcheck findings: declinedThe shellcheck gate is advisory-only (finding 4 below explains why it cannot fail regardless). The remaining SC2154 items reference workflow-provided env vars and the SC2312 items follow the established sibling-script convention; the three SC2016 items were already disabled inline with rationale in round 4. Verification-report findings 4–5 — pre-existing, not this PR: deferred to the follow-up queueBoth re-measured real at this head but outside this PR's purpose and untouched by it (0 diff lines each): the shellcheck exit-code masking in Verification
No code changed this round, so no build/typecheck/lint run applies. 中文说明Autofix 评审轮 —— 无代码变更本轮分诊了 4 条行内条目、4 个评审提交以及沙箱验证报告。无需任何代码变更:自动评审器已 批准( 行内发现 —— 复核确认已解决(rc:3852990060、rc:3852990936、rc:3852991742、rc:3852992718)这四条本轮以"回复"形式出现,确认 round-4 的发现已在
聚焦测试套件已在该 head 上重跑:26/26( 验证报告发现 1 —— shape-B 孤立标题(ic:5410657430):推迟到后续队列通过直接对交付脚本做探针验证为真实存在:当人工删除出现次数标记行但保留运行项目符号时,重建后的正文会出现两个
验证报告发现 2 —— 运行被降级为散文:拒绝在 R4-13 运行形状过滤下属于设计内行为:机器块在第一个非运行形状行处结束,因此写在人工注释下方的记录运行会作为散文保留。该取舍已在其线程中被明确接受;重新打开它只会搅动已钉住的形状过滤,而无缺陷可修。 验证报告发现 3 —— 11 条 shellcheck 建议:拒绝shellcheck 门禁仅为建议性质(下方发现 4 解释了它无论如何都无法失败)。剩余的 SC2154 条目引用的是工作流提供的环境变量,SC2312 条目遵循既有的兄弟脚本惯例;三条 SC2016 已在 round 4 中以行内理由禁用。 验证报告发现 4–5 —— 既有问题、不属于本 PR:推迟到后续队列两者在当前 head 上重新度量确认存在,但超出本 PR 目的且本 PR 未触碰(各 0 行 diff): 验证
本轮未改动代码,因此不适用 build/typecheck/lint 运行。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings. Approved.
Checked:
- Retry contract:
continue-on-error: trueon the first build step; retry gated onsteps.build-and-push.outcome == 'failure'(notfailure(), which would be false oncecontinue-on-errorabsorbs the first attempt — silently skipping the retry). Job conclusion is correct in all three cases (success, retry-heals, retry-fails). PUSH_IMAGEconsolidation: the env var is evaluated once at job level and referenced consistently in the login gate (env.PUSH_IMAGE == 'true') and both build steps (push: '${{ env.PUSH_IMAGE }}'). The publish-decision step exports it before any step that can fail, so thefile-failure-issuegate sees the value even on checkout/login failures.file-failure-issuegate:failure() && github.repository == 'QwenLM/qwen-code' && needs.build-and-push-to-ghcr.outputs.push_image == 'true' && (github.event_name == 'push' || github.event.inputs.version != ''). Each clause is load-bearing and covered by a test that asserts deletion breaks the suite.- Bash script merge contract: writer (
write_prose) and reader (jq contains()) use the same marker string. The awk state machine (head/runs/tail) correctly classifies lines; bullet-shaped human annotations that don't match the run-URL regex land intail, notruns. Theprintf/sedpattern for in-place normalization is bash-safe (word expansion precedes redirect setup). - Dedup lookup:
gh issue list --state open --label scope/ci-cd --limit 200 --json number,body+ client-sidejq contains(). The--state openand--labelfilter are both tested (deletion breaks the suite). The--searchpath is deliberately avoided because GitHub tokenizes the colon out of the marker. - Cross-file:
main-failure-signature.mjschange is four comment lines only, no logic touched. - Tests: YAML-assertion suite (pins retry contract, PUSH_IMAGE expression, login gate, failure-issue gate) + shell-replay integration suite (12 scenarios;
replayablegates onprocess.platform !== 'win32' && jq --versionexit 0).
CI: Test (ubuntu-latest, Node 22.x) ✓ · Desktop Shell (ubuntu-22.04, windows-2022) ✓ · web-shell E2E Smoke ✓ · Secret scan ✓ · Dependency CVE audit ✓. All non-skipped checks green.
Unreviewed dimensions:
- macOS and Windows workflow-test lanes: merge_group-only, not exercised on this PR.
- actionlint: no CI job runs it on this PR; author reports local clean run.
- Docker build and issue-filing paths: require tag push or publishing dispatch; not exercisable from a PR by design (acknowledged in the PR description).
Cross-check against prior reviews: All critical and blocking findings from rounds 1–4 (qwen-code-ci-bot) are addressed at this head: BSD head -n -1 replaced with sed '$d' (R4-1), Windows bash-test guard added via process.platform !== 'win32' (R4-2), cap/stranded-heading/empty-head/bullet-annotation fixtures added (R4-3/4/7/13). Remaining open items (R4-5 duplicate merge contract, R4-9 marker-removal gap, R4-11 label-removal gap, R1-9 no closure on recovery) are documented design limitations, tracked in follow-up issue #9960 where applicable, and not blockers.
Reviewed with AI assistance.
|
Released in v0.22.2. |













What this PR does
This PR makes the sandbox image publish step of the release pipeline self-healing and observable. The image build gets one bounded retry that survives transient buildx failures, and when a release build still fails, a follow-up job files (or updates) one GitHub issue per version so the missing image is visible and actionable instead of silently breaking downstream automation. The publish condition is consolidated into a single job-level expression so the login gate and both build steps cannot drift apart, and the existing workflow gate test is extended to pin the new contracts.
Why it's needed
The
v0.22.0tag build failed on a transientETXTBSYduringnpm ciinside the docker build and was never retried, soghcr.io/qwenlm/qwen-code:0.22.0was never published while npm already served0.22.0aslatest. Every sandbox-based CI lane (/resolve, sandboxed review, autofix) installs@latest, tries to enter the matching:0.22.0sandbox image, and crashes withmanifest unknown— for example the/resolverun on PR #9641 died before doing any work. The failure was silent for two days because nothing reports a released npm version that has no matching sandbox image. Details and run links are in #9898.Reviewer Test Plan
How to verify
continue-on-error: trueand the retry gates onsteps.build-and-push.outcome == 'failure'alone, so a successful retry turns the job green (afailure()gate would read false oncecontinue-on-errorabsorbs the first attempt, silently skipping the retry).node scripts/lint.js --actionlint(oractionlintdirectly) reports nothing for the changed workflow.vitest run scripts/tests/build-and-publish-image-workflow.test.jspasses; the new cases pin the retry contract, the sharedPUSH_IMAGEexpression, the issue-job gate, and the client-side dedup.scope/ci-cdissues fetched, exact-marker match returns empty as expected). Note the lookup deliberately avoidsgh issue list --search(GitHub tokenizes the colon out of the marker) and--arg(not supported bygh issue list); it exports the issue list to JSON and matches withjqclient-side.Evidence (Before & After)
N/A — non-user-visible CI change. Before: failed run 32580293377 left
0.22.0unpublished (recovered manually by rerunning the failed jobs; the tag exists on GHCR now). After: the same failure shape retries once, and if it still fails, one issue per version documents it.Tested on
Environment (optional)
GitHub Actions
ubuntu-latestsemantics validated statically: actionlint clean, workflow gate test assertions verified with a standalone mirror (the focused vitest run needs the full dependency tree, which a fresh worktree does not have — CI runs the real vitest), and the dedup query validated against the livegh/jqshape. The docker build path itself is only exercised on tag push / publishing dispatch; this PR does not trigger either.Risk & Scope
continue-on-error: trueon the first build step annotates the run as having a failed step even when the retry succeeds — cosmetic noise in exchange for a correct job conclusion. The issue job posts with the workflow'sGITHUB_TOKEN(issues: write), no bot PAT.Linked Issues
Fixes #9898
中文说明
这个 PR 做了什么
让发布流水线里的沙箱镜像发布环节自愈、可观测:镜像构建增加一次有界重试,可以扛过 buildx 的瞬时失败;如果发布构建仍然失败,新增的 job 会为每个版本创建(或更新)一个 GitHub issue,让"镜像缺失"这件事可见、可处理,而不是静默打断下游自动化。发布条件收敛为一个 job 级共享表达式,登录门槛和两个构建步骤不会再各自漂移;同时扩展了已有的工作流门槛测试,把新契约钉住。
为什么需要
v0.22.0tag 的镜像构建在 docker build 内部的npm ci阶段撞上瞬时ETXTBSY失败,之后没有任何重试,导致ghcr.io/qwenlm/qwen-code:0.22.0一直不存在,而 npm 上latest已经是0.22.0。所有走沙箱的 CI lane(/resolve、沙箱 review、autofix)都会装@latest、尝试进入对应的:0.22.0沙箱镜像,然后以manifest unknown崩溃——例如 PR #9641 上的/resolve就是在做任何事之前直接挂掉。这个失败静默了两天,因为没有任何机制上报"发布了 npm 版本但没有对应沙箱镜像"这种状态。详情和 run 链接见 #9898。评审测试计划
如何验证
continue-on-error: true,重试步骤只以steps.build-and-push.outcome == 'failure'为门槛,这样重试成功后 job 能翻绿(如果用failure()做门槛,continue-on-error吸收掉第一次失败后该条件恒为假,重试会被静默跳过)。node scripts/lint.js --actionlint(或直接actionlint)对该工作流无告警。vitest run scripts/tests/build-and-publish-image-workflow.test.js通过;新增用例钉住了重试契约、共享的PUSH_IMAGE表达式、issue job 门槛和客户端去重。scope/ci-cdopen issue,精确 marker 匹配按预期返回空)。注意该查找刻意避开了gh issue list --search(GitHub 搜索会把冒号从 marker 里切掉)和--arg(gh issue list不支持);它把 issue 列表导出为 JSON,用jq在客户端精确匹配。证据(前后对比)
N/A —— 非用户可见的 CI 改动。改动前:失败 run 32580293377 导致
0.22.0未发布(后来通过重跑失败 jobs 手动恢复,GHCR 上现在已有该 tag)。改动后:同样的失败形态会先重试一次,如果仍失败,则每个版本建一个 issue 记录。测试环境
见上方表格:仅 Linux 静态验证(actionlint + 测试断言 + 真实 API 探针)。
环境说明
按 GitHub Actions
ubuntu-latest语义做静态验证:actionlint 干净;工作流门槛测试的断言用独立镜像脚本验证过(聚焦的 vitest 运行需要完整依赖树,新 worktree 里没有——CI 会跑真正的 vitest);去重查询按真实的gh/jq形态验证过。docker 构建路径本身只会在 tag push / 发布 dispatch 时触发,本 PR 不会触发这两种事件。风险与范围
continue-on-error: true会让 run 里留下一个"步骤失败"的注解,即使重试成功也是如此——用表面噪音换正确的 job 结论。issue job 用工作流自带的GITHUB_TOKEN(issues: write)发布,不引入机器人 PAT。关联 Issue
Fixes #9898