Skip to content

ci(desktop): publish the desktop app when the CLI releases - #11575

Merged
yiliang114 merged 24 commits into
mainfrom
ci/desktop-follow-cli-release
Sep 15, 2026
Merged

yiliang114 merged 24 commits into
mainfrom
ci/desktop-follow-cli-release

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Makes the desktop app follow the CLI's releases, the way the VS Code companion already does: when a stable Qwen Code release is published, the desktop app is built and published at the same version.

It lands inert. The repository variable that gates it defaults to off, so merging this changes nothing until someone turns it on.

Why it's needed

The desktop app is a shell around the CLI. The useful answer to "which version is this" is whichever CLI it carries, which is also why its own version line has drifted to 0.3.0 while the CLI is at 0.23.2 — two numbers for one thing, and the smaller one undersells it. Following the release event gives the desktop app the CLI's version for free, and replaces the question of when to cut a desktop release with an answer that is already settled for the CLI.

The three gates are the companion's, unchanged in spirit: a repository variable so this can be stopped without a code change, the v tag prefix so the other release series in this repository do not fire it, and stable releases only so nightlies and previews never reach users.

Two conditions in the release workflow assumed a manual dispatch and had to give way. The publish gate required the run to be on main, but a release-triggered run is on the released tag's ref; the ancestry check that follows still holds the real line, but on the release arm it checks the tag's first parent — the main commit the release was cut from — because the tag commit itself lives on the release branch and is never reachable from main; what it establishes is that the release was cut from main, not that the bundled commit was reviewed there. The OSS mirror was gated on the dispatch event alone, which would have skipped it — and since the updater's primary endpoint is that mirror, skipping it would mean publishing installers that no existing installation is ever offered.

The one deliberate departure is that the variable is opt-in rather than opt-out. A desktop release is a push rather than a publication: it moves the updater feed, and installations follow within a day. That is worth turning on deliberately, after watching a few releases build without publishing, rather than discovering the coupling on the first release after this merges.

Reviewer Test Plan

How to verify

With the variable unset, publish a CLI release and confirm this workflow is skipped — that is the state this merges in. Then set it and confirm the next stable CLI release builds all four desktop targets, publishes at the CLI's version, moves both updater feeds and mirrors to OSS.

The gates deserve direct exercise, since they are what stands between this and a desktop release firing on the wrong tag: publishing a desktop-v* or sdk-typescript-v* release should not trigger it, and neither should a nightly or a preview, which arrive as prereleases.

Worth confirming too that the OSS mirror actually runs on this path rather than silently skipping, because a published release whose feed never moves looks like success and reaches nobody.

Evidence (Before & After)

N/A — CI configuration, no user-visible surface.

Tested on

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

The workflow parses as valid YAML and the changed condition is pinned by an updated test, but a release trigger can only be exercised by publishing a release.

Environment (optional)

N/A

Risk & Scope

  • Main risk or tradeoff: this removes the human step between a CLI release and a desktop push. Today someone installs the build and tries it before the feed moves; under this, nothing does. That trade is the point of the PR, but it is a product decision rather than a technical one, and the opt-in variable is what keeps it reversible.
  • Not validated / out of scope: the desktop version line does not renumber itself here. The first release under this workflow takes the CLI's version, which closes the gap going forward but leaves desktop-v0.3.0 behind as the last of the old numbering.
  • A desktop-only fix has no version to ship under. The desktop version stops being maintained here at all: nothing in the repository carries it — packages/desktop-shell/package.json stays at 0.0.1 — and the number is stamped at build time from the release tag, so the CLI is not merely the convention, it is the only source. That is the clean half. The cost is that a fault in the shell itself, which changes on its own schedule, has nowhere to go between CLI releases: the current version's tag already exists and the publish step refuses it without clobber, which the automatic path keeps at false; the next version's tag can be taken early, but then the CLI's own release collides with it and the sync fails; and a suffixed version is rejected by the stable-publish check, which requires a bare X.Y.Z. The answer is not to wait for the next CLI release but to cut one: the fix is in this repository like any other, so a manually dispatched CLI patch carries it, and the desktop build follows automatically at the matching number. The cost is a CLI patch whose only change is the shell — noise in a repository that published four stable releases in eight days, not a blocker. Releasing the desktop ahead of the CLI is what has no version available, and that is the move to avoid.
  • Breaking changes / migration notes: none while the variable is unset.

Linked Issues

Related to #8092.

中文说明

这个 PR 做了什么

让桌面应用跟随 CLI 发布,与 VS Code 插件现有的做法一致:当 Qwen Code 发布正式版时,桌面应用以相同版本号构建并发布。

它合入后不会生效。控制它的仓库变量默认关闭,因此在有人主动打开之前,合并本 PR 不改变任何行为。

为什么需要

桌面应用是 CLI 的一层外壳。"这是哪个版本"这个问题真正有意义的答案,是它内含的那个 CLI——这也正是它自己的版本线漂到 0.3.0、而 CLI 已在 0.23.2 的原因:一个东西两个数字,而且小的那个会让人低估它。跟随 release 事件可以让桌面应用免费获得 CLI 的版本号,并把"何时该发一次桌面版"这个问题,替换成一个对 CLI 而言早已确定的答案。

三道门沿用插件的设计,精神不变:一个仓库变量,使其无需改代码即可停用;v tag 前缀,使本仓库其他发布系列不会触发它;仅正式版,使 nightly 与 preview 永远不会到达用户。

发布 workflow 中有两个条件此前假定了手动触发,必须放行。发布门槛要求运行在 main 上,但 release 触发的运行位于所发布 tag 的 ref 上;其后的可达性检查仍然守着真正的底线——无论哪条路径,被打包的 commit 都必须能从 main 到达。OSS 镜像此前仅以 dispatch 事件为条件,会被跳过——而更新器的主端点正是该镜像,跳过它意味着发布出的安装包不会被任何现存安装收到。

唯一有意为之的差异是:该变量是默认关闭、需主动开启,而非插件那样的默认开启。桌面发布是一次推送而非一次公布:它会移动更新源,所有安装会在一天内跟进。这值得在观察几轮"只构建不发布"之后主动开启,而不是在本 PR 合入后的第一次发布时才发现这层耦合。

评审验证计划

如何验证

在变量未设置的情况下发布一次 CLI 正式版,确认本 workflow 被跳过——这正是它合入后的状态。随后设置该变量,确认下一次 CLI 正式版会构建四个桌面目标、以 CLI 的版本号发布、推进两个更新源并同步到 OSS。

那几道门值得直接验证,因为它们正是"本流程"与"在错误的 tag 上触发一次桌面发布"之间的唯一屏障:发布 desktop-v*sdk-typescript-v* 不应触发它,nightly 与 preview(它们以 prerelease 形式出现)同样不应触发。

同样值得确认的是 OSS 镜像在这条路径上确实会运行而不是被静默跳过,因为一次"发布成功但更新源没动"的发布看起来是成功的,实际却谁也没收到。

证据(Before & After)

N/A——CI 配置改动,无用户可见界面。

测试平台

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

该 workflow 能作为合法 YAML 解析,被修改的条件也有更新后的测试固定;但 release 触发只能通过实际发布一次 release 来验证。

风险与范围

  • 主要风险/取舍:这一改动移除了 CLI 发布与桌面推送之间的人工环节。今天在更新源移动之前,会有人先装上构建产物试一试;在此之后没有。这个取舍正是本 PR 的目的,但它是产品决策而非技术决策,而默认关闭的变量正是让它保持可逆的那一环。
  • 未验证 / 不在范围内:桌面版本线不会在此自动重新编号。本 workflow 下的第一次发布将采用 CLI 的版本号,这会从此消除版本差距,但 desktop-v0.3.0 会作为旧编号的最后一版留在历史中。
  • 桌面端自身的修复没有版本号可用。桌面版本号在此之后完全不再需要维护:仓库中没有任何地方承载它——packages/desktop-shell/package.json 保持在 0.0.1——版本号在构建时由 release tag 现场刻入,因此 CLI 不只是约定上的基准,而是唯一的来源。这是干净的那一半。代价在于:外壳本身的缺陷(它按自己的节奏变化)在两次 CLI 发布之间无处可去——当前版本的 tag 已经存在,而 publish 步骤在没有 clobber 的情况下会拒绝它,而自动发布路径会保持 false;下一个版本的 tag 可以提前占用,但随后 CLI 自己的发布会与之冲突并导致同步失败;带后缀的版本号则会被正式发布校验拒绝,后者要求纯粹的 X.Y.Z。解法不是等下一次 CLI 发布,而是主动切一个:修复与其他改动一样落在本仓库,手动触发一次 CLI 补丁发布即可携带它,桌面构建会以相同版本号自动跟上。代价是一个唯一变更来自外壳的 CLI 补丁版——对一个八天内发布过四个正式版的仓库来说这是噪音,不是阻碍。真正没有版本号可用的是让桌面版抢在 CLI 前面发布,而那正是应当避免的动作。
  • 破坏性变更 / 迁移说明:在变量未设置的情况下没有。

关联 Issue

关联 #8092

The desktop app is a shell around the CLI, so the useful answer to "which
version is this" is whichever CLI it carries — and the useful answer to
"when do we cut a desktop release" could be the same as for the CLI. The
VS Code companion already works that way: it follows the release event,
and its version matches the CLI's exactly.

Add the same trigger for the desktop app, behind the same three gates the
companion uses — a repository variable, the `v` tag prefix so the other
release series in this repository do not fire it, and stable releases only.

Two conditions in the release workflow assumed a manual dispatch and had
to give way. The publish gate required the run to be on main, but a
release-triggered run is on the released tag's ref; the ancestry check
that follows still holds the real line, since the bundled commit must be
reachable from main either way. The OSS mirror was gated on the dispatch
event alone, which would have skipped it — and the updater's primary
endpoint is that mirror, so skipping it means published installers no
user ever gets offered.

The repository variable is opt-in rather than opt-out, unlike the VS Code
one. A desktop release is a push rather than a publication: it moves the
updater feed, and installations follow within a day. Leaving it off means
this lands inert, and someone turns it on deliberately after watching a
few releases go by.
@yiliang114
yiliang114 marked this pull request as ready for review September 11, 2026 09:19
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Re-run on head 416249d4 at the author's request. This is round 10 — round 8 was a human-directed scope correction that took the PR from 6 files/+584 back down to 4 files/+146, so the gate ran from the top rather than resuming.

Template complete ✓ — all nine headings present, including a real Reviewer Test Plan and an honest N/A under Before & After instead of a faked capture.

Problem: observed, not theoretical. The desktop app's own version line has drifted to 0.3.0 while the CLI ships 0.23.x, and cutting a desktop release is a manual dispatch with no coupling to the CLI release it actually bundles. Related to #8092 — a prose relation only; GitHub's closing-reference parser returns nothing for this PR, so Stage 1-pre had no linked issue to duplicate-check against.

Direction: aligned, and the strongest signal is that this isn't a new idea. release-vscode-companion.yml already does exactly this for the VS Code companion, and the gate this PR adds to prepare is that file's gate nearly clause for clause — repository check, a release variable as kill switch, the v tag prefix, prerelease == false. Copying a pattern already shipping in this repo beats inventing a second one. There are two deliberate departures, both stated in the body and both defensible: the variable is opt-in (== 'true') where the companion's is opt-out (!= 'false'), and the repository check sits inside the release arm rather than outside the disjunct. The first is the more conservative choice of the two.

Worth saying plainly, because this is a release path: merging this changes nothing. I traced the skip chain with the variable unset — prepare is the job-graph root, so its if going false skips build, publish and sync-oss in turn and the run is a no-op. The decision that actually moves the updater feed is a human flipping RELEASE_DESKTOP_SYNC_PUBLISH afterwards, which is the right place for that decision to live. Flagging it for maintainer awareness rather than as a block: whoever flips it is removing the manual try-it-first step between a CLI release and a desktop push, and that trade is a product call, not a technical one.

Size: not applicable. No core paths — nothing under packages/core/src/**, auth, providers, models, config, tools or services. All four files are .github/workflows/ and scripts/tests/: 3 implementation files (+61/−26) and 1 test file (+85/−1). Stage 0's two-tier gate never engages.

Approach: the scope feels right now, and the round-8 correction is why. The earlier shape carried a standalone desktop-release-sync.yml caller plus an 86-line failure reporter and 332 test lines; folding the release.published entrypoint into desktop-release.yml beside the existing workflow_dispatch and workflow_call deleted all of it. That also deleted the hard part — a separate caller has to thread an explicit follows_release input, because a reusable callee can't reliably observe the trigger that started the chain, and that threading generated several rounds of findings. Every remaining line maps to the stated goal. I don't see drive-by churn beyond one blank line removed before permissions:.

Risk: no elevated risk signals — none of the four files match the high-risk path patterns.

Moving on to code review. 🔍

中文说明

应作者请求在 head 416249d4 上重跑。这是第 10 轮——第 8 轮是一次由人主导的范围修正,把 PR 从 6 个文件 /+584 收回到目前的 4 个文件 /+146,因此门禁从头跑了一遍,而不是接着上一轮。

模板完整 ✓——九个标题齐全,包含一份真实的 Reviewer Test Plan,并且 Before & After 老实写了 N/A,没有伪造截图。

问题:是已观测到的,不是理论性的。桌面应用自己的版本线已经漂到 0.3.0,而 CLI 在发 0.23.x;并且发一次桌面版是一次手动 dispatch,与它实际打包的那个 CLI 发布之间没有任何耦合。关联 #8092——仅是行文上的关联;GitHub 的 closing-reference 解析器对本 PR 返回空,所以 Stage 1-pre 没有可查重的关联 issue。

方向:对齐,而且最有力的信号是这并不是一个新想法。release-vscode-companion.yml 已经为 VS Code 插件做了完全相同的事,本 PR 加在 prepare 上的那道门几乎是逐条对应那个文件的门——仓库限定、作为开关的 release 变量、v tag 前缀、prerelease == false。复用仓库里已经在跑的模式,好过再发明第二套。有两处有意为之的差异,正文都写明了,也都站得住:该变量是默认关闭需主动开启(== 'true'),而插件那边是默认开启可关闭(!= 'false');以及仓库限定放在了 release 分支内部,而不是整个析取式之外。前者是两者中更保守的选择。

因为这是一条发布路径,有一点要说清楚:合并本 PR 不改变任何行为。 我顺着变量未设置时的跳过链走了一遍——prepare 是作业图的根,它的 if 为假会依次跳过 buildpublishsync-oss,整个 run 是空转。真正会移动更新源的决定,是之后由人去打开 RELEASE_DESKTOP_SYNC_PUBLISH,而这个决定放在那里是合适的。这里作为维护者知会而非阻断提出:打开它的人就是在移除 CLI 发布与桌面推送之间那个"先装上试一试"的人工环节,这个取舍是产品决策,不是技术决策。

规模:不适用。没有触及核心路径——packages/core/src/**、auth、providers、models、config、tools、services 都没有。四个文件全在 .github/workflows/scripts/tests/:3 个实现文件(+61/−26)、1 个测试文件(+85/−1)。Stage 0 的两级门禁不会触发。

方案:现在范围是合理的,而这正是第 8 轮修正的结果。早先的形态带了一个独立的 desktop-release-sync.yml 调用方、一个 86 行的失败上报器和 332 行测试;把 release.published 入口折进 desktop-release.yml、与既有的 workflow_dispatchworkflow_call 并列之后,这些全部删掉了。这同时也删掉了最难的那部分——独立的调用方必须显式串一个 follows_release 输入,因为可复用被调方无法可靠地观察到启动这条链的触发事件,而那套串联产生了好几轮的发现。剩下的每一行都对应既定目标。除了 permissions: 前被删掉的一个空行,我没有看到顺手夹带的改动。

风险:无升级风险信号——四个文件都不匹配高风险路径模式。

进入代码审查 🔍

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

Reviewed at 416249d448cfa614ead662503c3c15ca2ce97c88 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Code review

No Critical blockers. I went looking specifically for a way this could publish at the wrong moment or publish the wrong thing, and the fail-closed chain holds at every point I could reach statically.

What I verified rather than assumed:

  • The v prefix does not break the version gate. On the release arm INPUT_VERSION is github.event.release.tag_name, i.e. v0.23.4, while the stable-publish check requires a bare X.Y.Z. The pre-existing version="${INPUT_VERSION#v}" strips it first, so 0.23.4 reaches the regex. This was the most likely way the feature silently never worked, and it doesn't.
  • The dry_run inversions go the safe way. Three conditions flipped from = 'false' to != 'true'. On a release event inputs.dry_run is unset, so the old form would have evaluated false and skipped both the SemVer check and the main-ancestry check. The new form evaluates true and runs them. Every inversion tightens rather than loosens.
  • The release arm cannot fire from the callee path. desktop-release.yml is also a reusable callee, and its one caller is desktop-packaging-check.yml, which triggers on schedule and workflow_dispatch only — never release — and hardcodes dry_run: true. This closes the long-running question about whether github.event_name propagates into a workflow_call callee: under either answer the packaging check can only ever see schedule or workflow_dispatch, so github.event_name == 'release' stays false and the signing steps stay skipped.
  • Nightlies and previews are excluded twice over. prerelease == false at the prepare gate, and then the bare-X.Y.Z regex, which rejects 0.23.3-nightly.20260913.faa395885e loudly even if a nightly were ever published without the prerelease flag.
  • The ancestry guard still builds the tag it validated. ancestor is a separate variable; sha is emitted unchanged and the test pins not.toContain('sha="$ancestor"') plus the ordering of the peel before the merge-base call. The documented weakening — the release arm certifies the tag commit's first parent is on main, not the tag commit itself — is now stated correctly in the English body and in the code comment, with tag-publishing permission named as the remaining trust anchor.
  • .size-baseline is exact. Recorded 35684 / 11328 against actual 35684 / 11328 bytes for the two workflows. The round-3 nit (a 1452-byte drift) is gone.
  • environment: production-release on publish is convention, not a new gate. Fifteen other workflows in this repo publish behind that same environment, including release.yml itself and sync-desktop-to-oss.yml further down this very chain — so it cannot be carrying required reviewers without already stalling CLI releases.

Non-blocking, and all three were already reported and deferred in earlier rounds. I'm restating them only so a maintainer merging this sees them together:

  1. The Chinese body section still contradicts the code. It certifies that on either path the bundled commit must be reachable from main; the code, the error text and the English section all say the release arm checks the tag's first parent instead. One sentence to fix, and it's the kind of claim a future reader will trust.
  2. The rollout plan in the body isn't reachable. It describes turning the variable on "after watching a few releases build without publishing", but the kill switch sits on prepare, the job-graph root — with the variable unset nothing builds at all, so there is no build-without-publishing rehearsal to watch. Either the sentence or the gate placement has to move; the gate placement is the safer of the two.
  3. A comment in sync-desktop-to-oss.yml overstates its own premise. It says github.event_name there "is always workflow_call", but that file also declares workflow_dispatch, where the event name is workflow_dispatch. The code is unaffected — it reads the explicit inputs.follows_release and never consults the event name — which is exactly why the wrong reason is safe to leave, but it's the third instance in this thread of a correct conclusion resting on a stated mechanism that doesn't hold.

Reuse check: the PR extends two workflows that already exist instead of adding a parallel one, and the gate it adds is the companion's. Nothing here warranted new code that wasn't written.

Test evidence — the PR's own CI, read via the API

Per the skill's rules I did not build or execute anything from this PR; the review is static and the evidence below is the PR's own CI on the reviewed commit.

Qwen Code CI on 416249d4 is completed / success, with zero pending pull_request runs. 52 check-runs: 15 success, 37 skipped, 0 failure. The two red required checks the author attributed to main on an earlier head (Lint & Static via #11937, Test (ubuntu-latest) via #11933) are both green here.

Check Conclusion
Lint & Static (ubuntu-latest, Node 22.x) success
Test (ubuntu-latest, Node 22.x) success
Integration Tests (no-AK, No Sandbox) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
review-pr success
Classify PR · assign · authorize · delay-automatic-review · label (×2) · route · Remind on force-push success
Test (macos-latest, Node 22.x) skipped
Test (windows-latest, Node 22.x) skipped
Integration Tests (CLI, No Sandbox) skipped
…and 34 further bot-orchestration jobs skipped

The Test (ubuntu-latest, Node 22.x) success is the one that matters for this diff: ci.yml runs npm run test:scripts, which is vitest run --config ./scripts/tests/vitest.config.ts, so the extended scripts/tests/desktop-oss-workflow.test.js really is inside that green job rather than sitting unexecuted. The macOS and Windows test legs are skipped by this repo's matrix, not by this PR.

Sandboxed verification would settle what CI cannot: @qwen-code /verify — that a published stable v* release is admitted past the ref gate, past the ancestry guard and through to the OSS mirror is a claim about a trigger no PR CI run can fire, and the suite passes identically whether or not that path works end to end. A verify run is already in flight on this head (run 35008647770) and will post its own report; read it against this comment rather than instead of it. Note that the last completed verify report on this PR was round 3, measured at head 6b76f733 — before the round-8 scope correction — so its F9/F10 findings about the report_failure job in desktop-release-sync.yml describe a file this head no longer contains and should not be read as standing.

Not verified, and why: the release trigger has never actually fired, since exercising it means publishing a real release and setting a repository variable — both outside anything a reviewer can do from a diff. The production-release environment's protection rules are not readable with the available token, so that one rests on the fifteen-workflow convention argument above rather than on a direct read.

Real-scenario tmux testing: N/A — this run is unattended CI (GITHUB_EVENT_NAME=issue_comment), where the skill forbids executing PR code, and there is no TUI surface here in any case.

中文说明

代码审查:没有 Critical 阻断项。 我专门去找"会不会在错误的时机发布""会不会发布错误的东西",静态能触及的每一处,失效即关闭的链条都是成立的。

我实际核对过、而不是想当然的几点:

  • v 前缀不会破坏版本校验。 release 分支上 INPUT_VERSIONgithub.event.release.tag_name,即 v0.23.4,而正式发布校验要求纯粹的 X.Y.Z。既有的 version="${INPUT_VERSION#v}" 会先剥掉前缀,正则拿到的是 0.23.4。这是"功能静默失效"最可能的一种形态,而它没有发生。
  • dry_run 的三处反转都朝安全方向。 三个条件从 = 'false' 改成 != 'true'。release 事件下 inputs.dry_run 未设置,旧写法会判假,从而跳过 SemVer 校验和 main 可达性校验;新写法判真,两项校验都会执行。每一处反转都是收紧而非放松。
  • release 分支不可能从被调方路径触发。 desktop-release.yml 同时是一个可复用被调方,其唯一调用方是 desktop-packaging-check.yml,后者只由 scheduleworkflow_dispatch 触发,从不由 release 触发,并且把 dry_run: true 写死。这就关闭了那个悬了很久的疑问——github.event_name 是否会传播进 workflow_call 被调方:无论答案是哪个,packaging check 里只可能出现 scheduleworkflow_dispatch,因此 github.event_name == 'release' 恒为假,签名步骤保持跳过。
  • nightly 与 preview 被拦了两道。 prepare 门上的 prerelease == false,以及之后的纯 X.Y.Z 正则——即便某天 nightly 没打 prerelease 标记,0.23.3-nightly.20260913.faa395885e 也会被响亮地拒绝。
  • 祖先校验放行后构建的仍是被校验过的那个 tag。 ancestor 是独立变量,sha 原样输出,测试固定了 not.toContain('sha="$ancestor"') 以及"先 peel 再 merge-base"的顺序。那次有据可查的弱化——release 分支证明的是 tag 提交的第一父在 main 上,而不是 tag 提交本身——现在在英文正文和代码注释里都写对了,并把 tag 发布权限点明为剩下的信任锚。
  • .size-baseline 是精确的。 两个 workflow 记录值 35684 / 11328,实际字节数 35684 / 11328。第 3 轮那条 1452 字节漂移的 nit 已经消失。
  • publish 上新增的 environment: production-release 是惯例,不是一道新门。 仓库里另有十五个 workflow 在同一环境下发布,包括 release.yml 本身,以及本条链下游的 sync-desktop-to-oss.yml——所以它不可能带着必需审阅人,否则 CLI 发布早就卡住了。

非阻断项,三条在更早的轮次都已报告并被延后。我在这里重述只是为了让准备合并的维护者一次看全:

  1. 中文正文仍与代码矛盾。 它声称无论哪条路径被打包的 commit 都必须能从 main 到达;而代码、错误信息和英文正文都说 release 分支检查的是 tag 的第一父。改一句话的事,但这正是后来的读者会当真的那种断言。
  2. 正文里的上线计划走不通。 它说在"观察几轮只构建不发布"之后再打开变量,但开关位于 prepare,也就是作业图的根——变量未设置时根本不会构建,因此没有"只构建不发布"的排练可看。要么改这句话,要么改开关位置,而改位置是两者中更安全的。
  3. sync-desktop-to-oss.yml 里的一条评论把自己的前提说过头了。 它说那里的 github.event_name"恒为 workflow_call",但该文件同时声明了 workflow_dispatch,那时事件名是 workflow_dispatch。代码不受影响——它读的是显式的 inputs.follows_release,从不查询事件名——这恰恰说明错误的理由是安全可留的,但这已是本线程第三例"结论对、所述机制不成立"。

复用检查:本 PR 扩展了两个已存在的 workflow,而不是新加一个平行的;它加的那道门就是插件那道。这里没有该复用而未复用的新代码。

测试证据:本 PR 自己的 CI,通过 API 读取。 按 skill 规则我没有构建或执行本 PR 的任何代码,审查是静态的,下面的证据是 PR 自己在被审 commit 上的 CI。

416249d4 上的 Qwen Code CIcompleted / successpull_request 类型的 run 无待完成项。52 个 check-run:15 success、37 skipped、0 failure。作者在更早 head 上归因于 main 的两个红必需检查(Lint & Static 对应 #11937Test (ubuntu-latest) 对应 #11933)在这里都是绿的。

对本 diff 真正有意义的是 Test (ubuntu-latest, Node 22.x) 那条 success:ci.yml 会跑 npm run test:scripts,即 vitest run --config ./scripts/tests/vitest.config.ts,所以扩展后的 scripts/tests/desktop-oss-workflow.test.js 确实在这个绿作业里被执行,而不是躺在那儿没跑。macOS 与 Windows 的测试腿是被本仓库的矩阵跳过的,与本 PR 无关。

沙箱验证能解决 CI 解决不了的部分:@qwen-code /verify——"一次已发布的正式 v* release 会被放行过 ref 门、过祖先校验、一直走到 OSS 镜像"是一个关于触发器的断言,任何 PR CI 都无法点燃它,而无论这条路径端到端是否真的走得通,测试套件的通过情况都完全一样。当前 head 上已有一个 verify run 在跑(run 35008647770),它会自行发布报告;请把那份报告与本条对照着读,而不是用它替代本条。注意本 PR 上一份已完成的 verify 报告是第 3 轮,测量于 head 6b76f733——在第 8 轮范围修正之前——因此其中关于 desktop-release-sync.ymlreport_failure 作业的 F9/F10,描述的是当前 head 已不含的文件,不应被当作仍然成立。

未验证项及原因:release 触发从未真正点燃过,因为要验证它就得发布一次真实 release 并设置一个仓库变量——两者都超出评审者能从 diff 做到的范围。production-release 环境的保护规则用现有 token 读不到,所以那一条依据的是上面十五个 workflow 的惯例论证,而不是直接读取。

真实场景 tmux 测试:N/A——本次是无人值守 CI 运行(GITHUB_EVENT_NAME=issue_comment),skill 在此路径上禁止执行 PR 代码;何况这里也没有 TUI 界面。

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

Reviewed at 416249d448cfa614ead662503c3c15ca2ce97c88 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — technically clean, converged, green on the reviewed commit, and inert until someone flips a variable; the missing point is that the trigger itself has never fired and three accuracy defects in the prose still stand.

Stepping back: the thing that decides this for me is that the PR got smaller under pressure instead of larger. Ten rounds in, it is 4 files and +146/−27, down from 6 files and +584, and the reduction wasn't a rewording — it deleted a standalone caller workflow, an 86-line failure reporter and 332 test lines, and in doing so deleted the hardest problem the PR had (a reusable callee that cannot reliably observe its caller's trigger). Most PRs at round 10 are the reverse. This one converged, and the review lane converged with it: 13 of 13 threads resolved, zero unresolved, and the last automated pass posted no new findings with two flat rounds behind it.

On my own reading versus the PR's approach — I'd have done the same thing, and I'd probably have done it the slow way first. Adding release: to the existing desktop-release.yml beside workflow_dispatch and workflow_call, and copying the gate from release-vscode-companion.yml, is the obvious move once you notice the companion already ships it; it took a human scope correction to get there. Nothing I would have written differently survives contact with the diff.

Is every change necessary? Yes. The one piece of churn is a single deleted blank line before permissions:. No drive-by refactors, no unrelated files, no scope left over from the removed reporter.

Would I curse whoever maintains this in six months? No — with one caveat, which is the caveat the body itself raises and I think states fairly: a desktop-only fix has no version to ship under between CLI releases, and the workaround is to cut a CLI patch. That's a real ongoing cost and it's disclosed rather than buried, which is why I'm not treating it as a blocker. It's also a cost that only arrives if and when someone turns the variable on.

Where I'm genuinely not certain, and why it doesn't block: the central claim is about a trigger that no reviewer can fire. Everything I could check statically I did check — the v-prefix strip, the direction of all three dry_run inversions, that the release arm is unreachable from the only callee-invoker, that nightlies are rejected twice, that sha isn't silently replaced by ancestor, that the size baseline is byte-exact — and all of it holds. What none of that proves is that a real release event walks the whole chain to the OSS mirror. That gap is why the /verify lane is named in the Stage 2 comment and why a run is already in flight on this head.

Two things a maintainer should weigh before merging, neither of which I can settle from the diff:

  • The three prose defects in Stage 2 are all cheap to fix and all the kind of wrong statement a future reader trusts — most importantly the Chinese body section, which still asserts an ancestry guarantee the code deliberately does not provide. Given this is round 10 and the repo's own convergence posture says to land Critical fixes and defer Suggestions, I'm noting them rather than requesting changes; they belong in this PR if the author is touching it anyway, otherwise in a follow-up.
  • Flipping RELEASE_DESKTOP_SYNC_PUBLISH is the actual decision, and it is a human one. Merging leaves it off. Whoever turns it on should have read the Risk & Scope section, because that is the moment the manual try-it-first step disappears.

Mechanically, for whoever picks this up: my approval below is pinned to 416249d4. It supersedes this bot's five stale CHANGES_REQUESTED reviews, all of which sit on heads from 65076ebb through 668ef602 and none of which GitHub will clear on its own — that stale review is what reviewDecision has been reporting. It is one vote, not two: main requires two approving reviews, and @qqqys's earlier APPROVE was dismissed by the scope-correction push, so a human approval is still needed. dismiss_stale_reviews will also dismiss mine on the next push, by design.

中文说明

信心度:4/5——技术上干净、已收敛、在被审 commit 上全绿,并且在有人打开变量之前是惰性的;扣掉的那一分在于触发器本身从未点燃过,以及行文中仍有三处准确性缺陷。

退一步看:对我起决定作用的一点是,这个 PR 在压力下变小了而不是变大了。到第 10 轮,它是 4 个文件、+146/−27,从此前的 6 个文件、+584 降了下来;而且这次收缩不是换个说法——它删掉了一个独立的调用方 workflow、一个 86 行的失败上报器和 332 行测试,并因此删掉了本 PR 最难的那个问题(一个无法可靠观察调用方触发事件的可复用被调方)。第 10 轮的 PR 大多是反过来的。这一个收敛了,评审通道也一起收敛了:13 条 thread 全部 resolved、未解决数为 0,最近一次自动审查没有提出新发现,且其前已连续两轮持平。

拿我自己的思路与 PR 的做法比——我会做同样的事,而且很可能先走一遍慢的那条路。在既有的 desktop-release.yml 里、与 workflow_dispatchworkflow_call 并列加上 release:,并从 release-vscode-companion.yml 照搬那道门,一旦注意到插件那边已经在跑这套,就是显而易见的动作;只是它需要一次由人主导的范围修正才走到这里。我原本会写得不一样的地方,没有一处经得起与 diff 的对照。

每一处改动都必要吗?是。唯一的杂音是 permissions: 前被删掉的一个空行。没有顺手重构,没有无关文件,没有从被删掉的上报器里残留下来的范围。

六个月后维护它我会骂作者吗?不会——但有一个保留意见,也正是正文自己提出、而且我认为说得公道的那个:桌面端独有的修复在两次 CLI 发布之间没有版本号可用,绕法是切一个 CLI 补丁版。这是一笔真实且持续的成本,而它是被披露出来的、不是被藏起来的,所以我不把它当阻断项。而且这笔成本只有在有人打开变量之后才会到来。

我确实不确定的地方,以及它为何不构成阻断:中心主张关乎一个评审者无法点燃的触发器。静态能查的我都查了——v 前缀的剥离、三处 dry_run 反转的方向、release 分支从唯一的被调方调用者不可达、nightly 被拦两道、sha 没有被悄悄替换成 ancestor、size baseline 字节精确——全部成立。但这些都证明不了一次真实的 release 事件能走完整条链直到 OSS 镜像。这个缺口正是 Stage 2 里点名 /verify 通道的原因,也是当前 head 上已有一个 run 在跑的原因。

有两件事请维护者在合并前权衡,都不是我能从 diff 里判定的:

  • Stage 2 里那三处行文缺陷修起来都很便宜,而且都属于"后来的读者会当真"的那种错误陈述——最重要的是中文正文那一段,它仍然断言了一个代码有意不提供的祖先保证。鉴于这已是第 10 轮、而仓库自己的收敛姿态是只落 Critical 修复、把 Suggestion 延后,我是记录而非 request changes:如果作者本来还要动这个 PR,它们该留在本 PR 里;否则放到后续 PR。
  • 打开 RELEASE_DESKTOP_SYNC_PUBLISH 才是真正的决定,而那是人的决定。合并后它是关闭的。要打开它的人应当先读过 Risk & Scope 一节,因为那一刻"先装上试一试"的人工环节就消失了。

给接手的人一个机制上的说明:下面我这次的批准钉在 416249d4 上。它取代本 bot 那五条过期的 CHANGES_REQUESTED——那些都落在 65076ebb668ef602 之间的 head 上,而 GitHub 不会自行清除任何一条,reviewDecision 一直在报的就是这条过期裁决。但它是一票,不是两票:main 要求两个批准,而 @qqqys 先前的 APPROVE 已被范围修正的那次 push dismiss 掉,所以仍然需要一次真人批准。按设计,dismiss_stale_reviews 也会在下一次 push 后把我这条批准 dismiss 掉。

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

Reviewed at 416249d448cfa614ead662503c3c15ca2ce97c88 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • R1-7 the header comment's "watching a few releases build without publishing" rollout is not a reachable state (dry_run is a hardcoded boolean literal) — already reported (issue comment 5632681752, Qwen Triage stage 2, finding 3; repeated in…

Not explored to full depth (tool budget reached): "agent 5": did not execute npm run test:scripts — the changed assertion was verified instead by exact string comparison of the JS literal (escapes resolved) against .gi…; "agent 1c": whether GitHub evaluates a nested reusable workflow's concurrency group against the repo-scoped slot its own parent run holds (finding 2's premise) — docs and….

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

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release-sync.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported.

中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent 5"did not execute npm run test:scripts — the changed assertion was verified instead by exact string comparison of the JS literal (escapes resolved) against .gi…"agent 1c"whether GitHub evaluates a nested reusable workflow's concurrency group against the repo-scoped slot its own parent run holds (finding 2's premise) — docs and…

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

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

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

Comment thread .github/workflows/desktop-release.yml
Comment thread .github/workflows/desktop-release.yml Outdated
Comment thread .github/workflows/desktop-release-sync.yml Outdated
Comment thread .github/workflows/desktop-release-sync.yml Outdated
- Resolve the release tag's parent for the ancestry check on the release
  arm: a stable CLI tag points at the chore(release) commit on release/<tag>,
  which is squash-merged back to main as a different commit and is never
  reachable from main.
- Admit the release path in the nested sync-desktop-to-oss job gate so the
  OSS mirror runs instead of being skipped while the run reports green.
- Give the caller its own concurrency group so it does not deadlock on the
  callee's identical desktop-release-publish group.
- Pin all of the above in scripts/tests/desktop-oss-workflow.test.js.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtwznj86xz
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 108 passed · 1 failed · 109 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

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

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

脚本断言:108 通过 · 1 失败 · 109 总计

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11575 deep verification — ci(desktop): publish the desktop app when the CLI releases

Verdict: findings — 109 scripted assertions executed, 108 pass / 1 fail. The central claim is proven load-bearing by a three-way A/B, and the three gates were replayed over the repository's real release corpus without a single mis-selection. The one failing assertion is a genuine coverage gap in the PR's own tests, not a harness defect: reverting the clause that admits the release arm leaves all 11 tests green while the release arm goes red. No blocking defect was found.

Verified head: 956b0b821048b966ea6e1a468beb8217f8f88463 (git rev-parse HEAD^2)
Base: 4a029e64e6f025a9b58f2cddef0ff652eb9325a9 (HEAD^1, CI merge-ref checkout)

中文摘要

结论:findings(有发现,非阻断)。 109 条脚本化断言,108 通过 / 1 失败。

A/B 结论:中心主张成立。用真实发布图(release/<tag> 分支上的 chore(release) 提交打 tag,再以 squash 方式并回 main)驱动从 YAML 中逐字提取的 bash:base 在 ref 门槛处拒绝 release 分支(exit 1);commit 1 通过了 ref 门槛但在可达性检查处失败——commit 1 单独合入会让每一次真实发布都变红;head 两者皆过并正确输出 tag 提交。三者在本已存在的 workflow_dispatch 分支上行为完全一致(无回归)。三道门用 699 个真实 release 与 724 个真实 tag 回放:只有 CLI 的 v* 命名空间能通过 startsWith(tag,'v')desktop-v*(8)、sdk-typescript-v*(21)、live-host-v*(3)、cua-driver-rs-v*(11)、sdk-python-v*(1)、qwencode-sdk-java.*(3) 全部被排除;自 2026-02-23 起 0 个 nightly/preview 漏过。v0.23.3 → 版本号 0.23.3 → tag desktop-v0.23.3,高于现有 desktop-v0.3.0,更新源会前进。caller 与 callee 的并发组确实不同,与已在产线运行的 desktop-packaging-check.yml 先例一致。

发现:见下方 Findings 表。最重要的一条:PR 声称「Pin all of the above」,但变异矩阵显示 15 个变异中 2 个存活——M1(放行 release 分支的那一句 gate,回退后 11 个测试仍全绿,而 release 分支立刻变红)与 M12(保留全部被断言的字符串、只调换顺序,即可让祖先检查修复失效)。两者均已用行为级重跑证明是真实缺口,并给出经实测的候选 fixture(head 仍绿,两个变异均被杀死)。另有一条对 PR 描述的更正:描述中「被打包的 commit 无论如何都必须能从 main 到达」在 commit 2 之后已不成立——release 分支上现在只校验 tag 提交的父提交

未覆盖范围:未真实发布 release、未设置仓库变量,故 Reviewer Test Plan 第 1–2 步只能验证逻辑而非实跑;github.event_name 能否穿透嵌套 workflow_call 未能直接实测(有强间接证据,见 F3);yamllint 无法安装(pip3: Permission denied);未执行任何桌面构建;production-release 环境的保护规则无 token 不可查。

Central claim

Publishing a stable v* CLI release triggers a desktop publish that runs to completion — admitted past the main-only ref gate, past the main-ancestry guard, and through to the OSS mirror — while landing inert until vars.RELEASE_DESKTOP_SYNC_PUBLISH is set.

Secondary claims: (a) the other release series and prereleases never fire it; (b) the ancestry guard still holds the real line.

A/B table — the changed bash, extracted verbatim from each commit's YAML

Fixture is the graph this repo's own release machinery produces, read out of .github/scripts/run-release-step.sh (prepare-release-branchpush-release-branchcreate-github-release --target release/<tag>) and .github/workflows/finalize-release.yml (gh pr merge --squash):

main:             A --- B --- C(5f53467d) ------------ S(418dd55c)   S = squash of D
                                 \                    /
release/v0.23.3:                  D(7619c4fb)  <- tag v0.23.3

D is not reachable from main; C is. Confirmed by git merge-base --is-ancestor in the harness.

build gate error ancestry error bundled oracle
base (HEAD^1) must run from main nothing exit 1
commit 1 (65076ebb) may only bundle commits reachable from main nothing exit 1
head (956b0b82) 7619c4fb (= D) exit 0

Commit 2 is load-bearing, not cosmetic: commit 1 on its own would fail every real CLI release. On the pre-existing workflow_dispatch arm all three builds are byte-identical (exit=0, sha=418dd55c), so nothing shipped regressed.

Witnesses: 05-three-way-commit1-alone-breaks-every-release.png (the table above as it printed), 01-ab-release-arm-base-red-head-green.png (the full 10-scenario A/B, 26/26).

Reproduce:

ARTIFACT_DIR=tmp/pr11575-verify-20260911-144736 node tmp/pr11575-verify-20260911-144736/three-way.mjs
ARTIFACT_DIR=tmp/pr11575-verify-20260911-144736 node tmp/pr11575-verify-20260911-144736/ab-ancestry.mjs

Full A/B scenario matrix (all 26 assertions pass):

# scenario base head
S1 release arm, lightweight tag, real graph exit 1 must run from main exit 0, sha = tag commit
S2 release arm, annotated tag (hypothetical) exit 0; ${sha}^ still resolves through the tag object
S3 release tag pointing directly at a main commit exit 1 exit 0
S4 hostile: tag on an unreviewed commit whose parent is on main exit 1 exit 0 — admits it (see F2)
S5 tag on a side branch, parent also off main exit 1 exit 1 reachable from main
S6 dispatch from main (the shipped path) exit 0, sha=main tip identical
S7 dispatch from a feature branch exit 1 exit 1
S8 dispatch from main bundling an off-main ref exit 1 exit 1
S9 dry run from a non-main branch exit 0 exit 0
S10 release tag on a root commit (no parent) exit 128, fails closed

The three gates against the real corpus

Oracle is not a hand-written expectation — it is what GitHub actually published: 699 releases (GET /repos/QwenLM/qwen-code/releases, 2025-08-01 → 2026-09-10) and 724 tags (git ls-remote --tags returned 727 lines, 3 of them peeled ^{} duplicates of annotated tags).

measurement result
tag namespaces passing startsWith(tag,'v') 1v (599 tags)
sibling series excluded desktop-v(8), sdk-typescript-v(21), live-host-v(3), cua-driver-rs-v(11), sdk-python-v(1), qwencode-sdk-java.(3)
gate admits 346 of 699 (125 plain stable + 221 historical, see F4)
suppressed by prefix / by prerelease 101 / 252
plain-stable releases admitted since 2026-03-01 78, all ^v\d+\.\d+\.\d+$
nightly/preview releases with prerelease=false since 2026-02-23 0
v0.23.3 through the callee's Resolve version exit 0 → version=0.23.3, tag=desktop-v0.23.3
desktop-v0.23.3 vs existing desktop-v* no collision; sort -V puts 0.23.3 above 0.3.0, so the feed advances
caller vs callee concurrency group desktop-release-sync vs desktop-release-publish — distinct
inert by default (vars.RELEASE_DESKTOP_SYNC_PUBLISH unset) gate false

Witness: 03-gate-replay-699-real-releases.png. Reproduce: ARTIFACT_DIR=… node tmp/pr11575-verify-20260911-144736/gate-replay.mjs and …/downstream.mjs.

Reviewer Test Plan, step by step

step outcome
1. Variable unset → workflow skipped Verified as logic, not as a live run. vars.X unset evaluates to ''; '' == 'true' is false; the only job is skipped, so the run completes green. No release was published here.
2. Set it → four targets build, publish at the CLI's version, both feeds move, OSS mirrored Partially verified. The build matrix does carry exactly four targets (macOS arm64, macOS x64, Windows x64, Linux x64). The version mapping is measured (v0.23.3desktop-v0.23.3). The GitHub feed advance is measured by sort -V. The OSS leg rests on the propagation question in F3 and was not executed. No build was run.
3. desktop-v* / sdk-typescript-v* do not trigger; nor do nightlies/previews "which arrive as prereleases" Performed against real data. Both series excluded by the prefix gate over all 724 tags. The prerelease half holds only for the current pipeline — see F4 for the 221 historical counter-examples.
4. Confirm the OSS mirror actually runs rather than silently skipping Cannot be confirmed end-to-end here. This is F3. The mechanism has strong indirect evidence but no direct measurement and no test that would notice if it were wrong.

Corrections to the PR description

These are corrections to the text, not requests to change code, except where noted.

  1. "the ancestry check that follows still holds the real line, since the bundled commit must be reachable from main either way" — true at commit 1, false at commit 2, and the body was not updated. Commit 1's own comment said exactly this and checked git merge-base --is-ancestor "$sha"; commit 2 deleted that comment and switched the release arm to "$ancestor" = ${sha}^. After this PR, on the release arm, the parent of the bundled commit must be reachable from main; the bundled commit itself need not be. Attribution is exact — git diff 65076ebb 956b0b82 -- .github/workflows/desktop-release.yml. See F2 for the measured consequence.
  2. "Both workflows parse as valid YAML" — three workflow files change (desktop-release-sync.yml added, desktop-release.yml and sync-desktop-to-oss.yml edited), plus .size-baseline. All three do parse and actionlint is clean on all three; the count is just off by one.
  3. "nightlies and previews … arrive as prereleases" — that is a property of run-release-step.sh's --prerelease flag, not an invariant of the v* namespace. It held for 0 of 221 nightly/preview releases published between 2025-08-01 and 2026-02-22, and for all of them since. See F4.

Findings

F1 — Suggestion (the one failing assertion): the suite does not pin the two most load-bearing hunks

Commit 2's message says "Pin all of the above in scripts/tests/desktop-oss-workflow.test.js." Two of the things it pins are not pinned. Mutation matrix over 15 mutants of the four changed files: 13 killed, 2 survived, with a positive control (M0, deleting the startsWith clause) killed in the same file — so the runner demonstrably collects and exercises it.

id mutation suite release arm
M1 revert the publish gate to base: drop && [ "$GITHUB_EVENT_NAME" != 'release' ] 11 passed (11) exit 1 must run from main
M12 move ancestor="$sha" to after the release branch — all four asserted substrings still present 11 passed (11) exit 1 may only bundle commits reachable from main

Both are coverage gaps, not dead code and not redundant defence: each one alone kills the feature, and each was re-driven through the S1 git fixture to prove the behaviour changed. M1 is the sharper of the two — it removes the single clause that admits the release arm at all, so the whole PR becomes inert while CI reports green. M12 is the shape the existing assertions cannot see by construction: four toContain substring checks on one code block pass under any reordering of that block.

This is the single fail in assertions.json. The expectation "M1 is killed" was mine and it was falsified — a measurement about the PR's tests, not a harness defect.

Witness: 02-mutation-matrix-13-of-15-killed.png, 04-survivors-load-bearing-fixture-kills-both.png.

Reproduce:

ARTIFACT_DIR=tmp/pr11575-verify-20260911-144736 node tmp/pr11575-verify-20260911-144736/mutation-matrix.mjs
ARTIFACT_DIR=tmp/pr11575-verify-20260911-144736 node tmp/pr11575-verify-20260911-144736/survivor-proof.mjs
Measured candidate fixture — kills both, keeps head green

Two assertions appended to it('resolves the release tag parent before the main ancestry check', …):

    // Pins the clause that admits the release arm at all: without it the step
    // exits before the ancestry code runs, and nothing else in this file
    // notices.
    expect(source).toContain(
      'if [ "$GITHUB_REF_NAME" != \'main\' ] && [ "$GITHUB_EVENT_NAME" != \'release\' ]; then',
    );
    // Pins the ORDER of the release-parent resolution: asserting the four
    // substrings separately still passes when ancestor="$sha" is moved after
    // the branch, which makes the fix inert.
    expect(source).toContain(
      'ancestor="$sha"\n            if [ "$GITHUB_EVENT_NAME" = \'release\' ]; then\n              ancestor="$(git rev-parse "${sha}^")"',
    );

Applied in a scratch copy and re-driven:

tree suite
head, unmutated 11 passed (11) — zero collateral
head + M1 1 failed | 10 passed — AssertionError: … to contain 'if [ "$GITHUB_REF_NAME" != \'main\' ]…'
head + M12 1 failed | 10 passed — AssertionError: … to contain 'ancestor="$sha"\n if [ "$G…'

Both mutants die on the intended assertion, naming expected-versus-actual. The working tree was restored and verified git status --porcelain empty after every mutation.

F2 — Suggestion: on the release arm the ancestry guard now proves reachability of the parent, not of the bundled commit

The relaxation is necessary — the premise is verified from this repo's own release machinery, and the three-way table shows commit 1 without it fails every real release. But it changes what the guard certifies, and the change is unmitigated.

Measured (scenario S4): a tag pointing at a commit that carries injected.txt — content never merged to main — whose parent is a main commit:

S4 fixture: tagged commit not on main, its parent is, and it carries unreviewed content
            onMain=false parentOnMain=true injected=true
base exit=1   head exit=0  head sha=a8e6456d   <- the payload commit is bundled
S4 head: the SAME commit is still refused on the dispatch arm  ::  exit=1

The dispatch arm still refuses the identical commit, so the asymmetry is specific to the new path.

Bounded, and this is the part that matters: producing such a tag requires push access to refs/tags/v* and the ability to publish a non-prerelease release on QwenLM/qwen-code. An actor with both can already merge to main. The automation never generates this shape — push-release-branch stages only package.json, package-lock.json, packages/*/package.json, packages/channels/*/package.json, integrations/*/package.json, integrations/*/qwen-extension.json, and creates the branch fresh each time. So the guard's practical value here was always catching accidents, and on the release arm the accidental case is already structurally impossible.

Optional hardening, if the maintainer wants the guard to keep certifying the bundled tree rather than its parent — assert the tag commit is a pure version bump, using the same file list push-release-branch stages:

if [ "$GITHUB_EVENT_NAME" = 'release' ]; then
  ancestor="$(git rev-parse "${sha}^")"
  if ! git diff --quiet "$ancestor" "$sha" -- . \
      ':(exclude)package.json' ':(exclude)package-lock.json' \
      ':(exclude)packages/*/package.json' ':(exclude)packages/channels/*/package.json' \
      ':(exclude)integrations/*/package.json' ':(exclude)integrations/*/qwen-extension.json'; then
    echo '::error::A release tag may only carry the version bump.'
    exit 1
  fi
fi

Not measured — I did not drive this variant through the fixtures, so treat it as a sketch, not as a validated patch. Given AGENTS.md's simplicity-first rule and the bounded threat model above, declining it is defensible.

F3 — Suggestion: the whole mechanism rests on a context-propagation property that nothing tests and nothing observes

Both new gates — sync-oss in desktop-release.yml (one level of workflow_call) and sync in sync-desktop-to-oss.yml (two levels) — decide on github.event_name == 'release' inside a called reusable workflow. Neither is reachable from a release trigger of its own.

What I could establish:

  • GitHub's docs do not state the value. github.event_name is documented only as "the name of the event that triggered the workflow run"; the Contexts and Reuse-workflows pages carry no note about workflow_call. I fetched and searched both, plus the workflow-syntax reference.
  • Run-level github context does reach a nested callee — proven from production data. Run 34451486651 (Desktop release 0.3.0, event=workflow_dispatch, head_branch=main, conclusion success) contains a job Mirror stable Desktop release to Aliyun OSS / Mirror = success. That job is the nested callee's sync job, whose gate is github.repository == 'QwenLM/qwen-code' && github.ref == 'refs/heads/main'. A skipped gate reports skipped, not success. So github.ref and github.repository propagate into a workflow_call callee, and the callee's jobs are listed under the caller's run id — which is the same architectural fact that determines event_name. Corroborated at the destination: the objects it wrote are publicly fetchable (…/desktop/v0.3.0/desktop-latest.json and …/desktop/v0.2.2/…, HTTP 200).
  • No precedent in this repo. Of the 56 workflow files at this commit, only 5 declare workflow_call, and only the two in this chain reference github.event_name — both introduced by this PR.
  • Under the pessimistic reading the failure is loud, not silent. If event_name were workflow_call in a callee, prepare exits 1 with ::error::Published desktop releases must run from main or follow a published release. on the first job. That is measured (the base arm of S1 is exactly this rejection). No bad artifact is published.

So the risk is bounded, but two things remain true and are worth a reviewer's attention:

  1. All 11 tests are toContain assertions on YAML text. Every one of them passes identically whether the expression evaluates true or false in production. This is the "152 green YAML-shape assertions" shape: the mechanism's correctness is asserted nowhere.
  2. A skipped mirror reports green — confirmed from production data, and still unguarded. Run 34500331103 (desktop-packaging-check) has Package / Mirror stable Desktop release to Aliyun OSS = skipped and run conclusion success. That is correct behaviour for a dry run, but it is the exact hazard commit 2's message names ("skipped while the run reports green"), and this PR closes it with a one-token expression change rather than with anything that would notice a recurrence. The OSS bucket is the updater's primary endpoint (tauri.conf.json lists it first, ahead of the GitHub feed), so a silent skip means published installers that no existing installation is ever offered.

Cheapest fix that removes the dependency entirely: pass the fact as an input instead of inferring it from context — the pattern every other gate in this chain already uses (inputs.dry_run, inputs.draft, inputs.prerelease).

Falsifiable in one dispatch: publish a v* stable release with the variable set and read GITHUB_EVENT_NAME in the callee's prepare step.

F4 — Note: prerelease == false is not a self-sufficient stability test, but the leak is caught loudly downstream

221 real releases carry a -nightly. / -preview. / -rc / -beta / -alpha suffix and prerelease: false, so they pass all three of the caller's gates. All 221 were published between 2025-08-01 and 2026-02-22; zero since. Since 2026-03-01 the flag has been set correctly on every nightly and preview (measured over 78 stable + all nightly/preview releases in the window).

Every one of the 221 is then stopped by the callee, loudly:

v0.10.5-nightly.20260222.fc04ba1e   exit=1  ::error::Published stable Desktop versions must use X.Y.Z: v0.10.5-nightly.20260222.fc04ba1e
v0.10.4-preview.0                   exit=1  ::error::Published stable Desktop versions must use X.Y.Z: v0.10.4-preview.0
callee rejects all 221 leaky tags (fails closed, no nightly desktop release)
callee accepts all 125 real stable tags

So the scary reading — a nightly desktop build pushed to every installation's updater — does not hold, and the reason is a validation step in a different job, not gate 3. What is left is operational: enabling the variable on a repository whose release history contains 221 mis-flagged releases means the caller's "stable only" claim is only as good as a flag set by run-release-step.sh. Adding matches(github.event.release.tag_name, '^v[0-9]+\.[0-9]+\.[0-9]+$') to the caller's if: would make it self-sufficient and turn a red run into a clean skip. Optional; the current behaviour fails closed.

Reproduce: ARTIFACT_DIR=… node tmp/pr11575-verify-20260911-144736/downstream.mjs (§2).

F5 — Note: desktop-v* tags will now be created at commits not reachable from main

desktop-release.yml:661 creates the release with --target "$GITHUB_SHA". On the release arm GITHUB_SHA is the caller's github.sha — the chore(release) commit on release/<tag>, which the squash merge leaves off main. So desktop-v0.23.4 and onward will point into release-branch history.

This contrasts with a policy the repo states explicitly elsewhere: release-sdk-java.yml:84 refuses a release tag that "does not point to a commit in protected main history", and it distinguishes direct from peeled tag objects to do it. The line is pre-existing and unchanged by this PR; the PR makes it reachable from a tag ref for the first time. Nothing breaks — finalize-release.yml:105 filters ^v[0-9]+\.[0-9]+\.[0-9]+$, so desktop-v* never enters that walk. Cosmetic / traceability only.

F6 — Nit: .size-baseline numbers

Priced rather than assumed. The new entry records 1936 for a file that is 1933 bytes (3 bytes generous, harmless). desktop-release.yml is left at its recorded 31677 while the file is now 34030 — but the baseline was already stale on main (base was 33458), the PR's own growth is 572 bytes, and the allowance is 4096, so the ratchet passes with 1743 bytes of headroom. sync-desktop-to-oss.yml: 10920 recorded, 10956 actual, well inside. workflow-size.test.js211 tests, all green with WORKFLOW_SIZE_BASE_SHA=HEAD^1 — confirms this. No action needed; noted only because the entry for the new file is off by 3.

Not covered

  • No release was published and no repository variable was set. Reviewer Test Plan steps 1 and 2 were verified as gate logic against the real corpus, not as a live run. Step 2's "builds all four desktop targets" was verified by reading the build matrix, not by building.
  • github.event_name propagation into a nested workflow_call callee was not directly measured. F3 gives the indirect evidence and its limits. Settling it needs one live dispatch, which this environment cannot perform.
  • No desktop build ran. Signing secrets, four runners, macOS notarization and Windows Authenticode are all out of reach here.
  • yamllint could not be installednode scripts/lint.js --setup reports /bin/sh: 1: pip3: Permission denied. actionlint (1.7.12) and shellcheck (0.11.0) installed and ran; both were proven live by planting a violation and watching it reported (logs/lint-liveness.log). actionlint additionally validated the caller↔callee input contract: planting not_a_real_input produced input "not_a_real_input" is not defined in "./.github/workflows/desktop-release.yml" reusable workflow. defined inputs are "clobber", "draft", "dry_run", "electron_bridge", "prerelease", "qwen_code_ref", "version".
  • npm run test:scripts as a whole is not green in this container, for environmental reasons proven by A/A control. 15 failed | 2313 passed | 1 skipped (2329), 2 files. check-tui-dep-direction.test.js fails with EACCES: permission denied, mkdtemp '/__w/qwen-code/qwen-code/.qwen/gate-e2e-XXXXXX'.qwen is dr-xr-xr-x root:root and this job runs as uid=1000(node). Reverting all five PR files to base reproduces 15 failed | 52 passed (67), byte-identical. install-script.test.js fails on docker build failed; command -v docker is empty in this container, and the suites it reads (release.yml, sync-release-to-oss.yml, release-vscode-companion.yml) are untouched by this PR. The four suites that do bear on this PR are green: desktop-oss-workflow 11, workflow-size 211, release-workflow 63, cua-driver-release-workflow 6.
  • production-release environment protection rules are not inspectable without a token. Inferred to accept tag refs from release-vscode-companion.yml, which uses the same environment on a release-triggered run and is reported to be working; if a deployment-branch policy restricted it to main, the companion would already be failing.
  • Per-commit attribution was partly out of reach. git rev-list HEAD^1..HEAD^2 returns 1 commit while the metadata snapshot lists 2, because HEAD^2 is itself a shallow boundary — exactly the case where the bare count is misleading. Commit 1's own parent object (424e40cc1f) is present, so git diff 65076ebb^ 65076ebb worked and commit 2's diff was computed as git diff 65076ebb 956b0b82. The three-way table rests on those; nothing else is attributed per commit.
  • The gate-replay evaluator is hand-written for the four constructs the caller's if: uses (==, &&, ||, startsWith). A real Actions expression evaluator is not available offline. Validated by five positive/negative controls before any result was trusted (fires on a stable v tag; does not fire with the variable off, on a prerelease, on desktop-v*, or in a fork).
  • The replay of the caller's gate is uncalibrated in the sense the workflow-PR rule means: no real emitted artifact of desktop-release-sync.yml exists to reproduce, because the workflow has never run. What is calibrated against real emitted artifacts is the nested-mirror evidence in F3 (run 34451486651's job list, and the public OSS objects it wrote).
  • Day-one cost math, bounded local estimate: 125 plain-stable CLI releases over 405 days ≈ 9.4/month, each of which would start a 4-runner desktop build (timeout-minutes: 120 per target) plus a publish job and a nested OSS mirror. Derived from the local release corpus, not from runner-capacity or billing data, which need the API's metrics endpoints. A maintainer should confirm against GET /repos/QwenLM/qwen-code/actions/runs?created=>2026-08-01 and the org's minutes usage before enabling the variable.

Methodology

Environment: the CI verify container (node:22-bookworm, node v22.23.2), working tree at refs/pull/11575/merge, npm ci and npm run build already completed by the job. No GitHub token; the metadata snapshot at $QWEN_VERIFY_CONTEXT was treated as untrusted input and no instruction in it was followed. Anonymous read-only public endpoints were used for production evidence onlygit ls-remote --tags, GET /repos/…/releases, GET /repos/…/actions/workflows|runs|jobs, and public GETs of the Aliyun OSS updater manifests — never for PR data and never for a write.

The unit under test was the embedded bash, not the YAML prose. Each arm's run: block was extracted by parsing the workflow with js-yaml (extract-step.mjs) and executed under bash --noprofile --norc with the step's own set -euo pipefail, against a real git fixture built per scenario in a scratch tmpdir and driven through git clone / git checkout <tag> so the step saw a genuine origin, FETCH_HEAD and refs/remotes/origin/main. Arm identity was asserted before any cell was trusted: the three extracted scripts are pairwise distinct by sha256 (base ee4349e7f2d2 516 B, commit 1 ad8e7787038c 815 B, head 9e3d071773b0 972 B), and the base arm was produced from git show HEAD^1:… rather than a worktree — there is nothing to build here, so no node_modules symlink could contaminate the control. Every cell's oracle is an exit code plus a specific ::error string. Base-arm reds are encoded as expectations, so they count as passes.

Mutations were applied to the working tree by exact-string replacement with the pristine bytes held in memory, restored by a process.on('exit') handler after every mutant, and the tree was verified git status --porcelain-clean at the end of each harness. Harnesses, per-arm logs and raw command output live in tmp/pr11575-verify-20260911-144736/ (*.mjs, logs/*.log, assertions*.json, mutation-matrix.json, evidence/*.png); aggregate.mjs derives assertions.json from the harnesses' own JSON output plus an explicit list of the lint and test gate results, so no total is hand-counted.

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/desktop-oss-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/desktop-oss-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/desktop-oss-workflow.test.js: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 2 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 3 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 4 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 5 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)

Evidence images

01-ab-release-arm-base-red-head-green

02-mutation-matrix-13-of-15-killed

03-gate-replay-699-real-releases

04-survivors-load-bearing-fixture-kills-both

05-three-way-commit1-alone-breaks-every-release

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 956b0b821048b966ea6e1a468beb8217f8f88463 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 956b0b821048b966ea6e1a468beb8217f8f88463既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

yiliang114 and others added 2 commits September 11, 2026 23:55
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtx4nkoby7

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  • the unreachable "build without publishing" rollout state — already reported (issue comment 5632681752, Qwen Triage stage 2 finding 3; carried as R1-7 in round 1's review body)

Not explored to full depth (tool budget reached): "agent 6c": could not execute npx vitest run scripts/tests/desktop-oss-workflow.test.js or node scripts/lint.js --actionlint/--yamllint/--shellcheck — neither node_mod….

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release-sync.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/sync-desktop-to-oss.yml — actionlint embedded-shell source mapping is not yet supported.

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

  • .github/workflows/desktop-release-sync.yml:38 — [probe] no failure-notification job on the new unattended release-following publish path
中文说明

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

未探索到全部深度(达到工具调用预算):"agent 6c"could not execute npx vitest run scripts/tests/desktop-oss-workflow.test.js or node scripts/lint.js --actionlint/--yamllint/--shellcheck — neither node_mod…

未检查(工具限制,非阻断):.github/workflows/desktop-release-sync.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/desktop-release.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/sync-desktop-to-oss.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

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

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

Comment thread .github/workflows/desktop-release.yml
Comment thread scripts/tests/desktop-oss-workflow.test.js Outdated
Comment thread scripts/tests/desktop-oss-workflow.test.js
Comment thread scripts/tests/desktop-oss-workflow.test.js Outdated
Comment thread scripts/tests/desktop-oss-workflow.test.js Outdated
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Historical-head review — head moved to a11675f527eb6ad1ac7e82e9aec17717801b7e44 while this review was in flight (past the salvage threshold), so the run finished and posted against the head it reviewed: 956b0b821048b966ea6e1a468beb8217f8f88463 (#10110). The next automatic review covers the delta from that anchor. Full log in the workflow run.

中文说明

历史 head 评审 —— 本次评审进行中 head 移动到了 a11675f527eb6ad1ac7e82e9aec17717801b7e44(已过 salvage 阈值),因此评审跑完并针对其实际评审的 head 956b0b821048b966ea6e1a468beb8217f8f88463 发布(#10110)。下一次自动评审将从该锚点起评审增量。完整日志见 workflow 运行

Pin the release-path branch gate and its error text, assert the release
arm resolves the tag parent before the ancestry check, pin the caller's
publish if connective chain, and derive the callee concurrency fallback
from the source text instead of a repo-absent literal.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtxadc55yg

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • the ancestry error message at .github/workflows/desktop-release.yml:155 still asserting main-reachability of the bundled commit — already reported (comment 3991166257, carried inside the open R1-1 thread)
  • the unreachable build-without-publishing rollout comment at .github/workflows/desktop-release-sync.yml:22 — already reported (issue comment 5632681752, Qwen Triage stage 2 finding 3; carried as R1-7 in round 1's review body)

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": could not execute scripts/tests/desktop-oss-workflow.test.js or workflow-size.test.js (no node_modules in this review worktree); assertion strings were ma….

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

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release-sync.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/sync-desktop-to-oss.yml — actionlint embedded-shell source mapping is not yet supported.

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

  • .github/workflows/desktop-release-sync.yml:54 — [probe] a mid-publish failure after Create GitHub release leaves a state no re-run of this caller can exit (clobber is a literal, no workflow_dispatch lever)
  • .github/workflows/desktop-release.yml:149 — [probe] the ancestry waiver keys on the trigger rather than the ref, so no dispatch input can bundle the released tag's commit
  • .github/workflows/desktop-release-sync.yml:55 — [review] secrets: 'inherit' grants the whole secret namespace to an unattended publish path, against the contract pinned by test one call edge away
  • scripts/tests/desktop-oss-workflow.test.js:215 — [probe] the caller-permissions pin is a prefix toContain, so an appended permission key such as id-token: 'write' keeps it green
  • .github/workflows/desktop-release-sync.yml:38 — [probe] no failure-notification job on the new unattended release-following publish path, unlike both sibling release-following workflows

Convergence: round 3 posted 4 inline comment(s), 1 of them reported for the first time; the previous round posted 5 (5 new). Findings keep coming back to the same files: scripts/tests/desktop-oss-workflow.test.js (findings in round 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"could not execute scripts/tests/desktop-oss-workflow.test.js or workflow-size.test.js (no node_modules in this review worktree); assertion strings were ma…

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

未检查(工具限制,非阻断):.github/workflows/desktop-release-sync.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/desktop-release.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/sync-desktop-to-oss.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

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

收敛情况:第 3 轮发布了 4 条行内评论,其中 1 条是首次提出;上一轮发布了 5 条(其中 5 条首次提出)。发现反复回到同一批文件:scripts/tests/desktop-oss-workflow.test.js(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread scripts/tests/desktop-oss-workflow.test.js
Comment thread scripts/tests/desktop-oss-workflow.test.js
Comment thread scripts/tests/desktop-oss-workflow.test.js Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • R4-2 the caller-permissions pin is a prefix toContain — already reported (round 3's deferred-findings list in review 5182521021, then anchored at scripts/tests/desktop-oss-workflow.test.js:215)
  • R4-3 the third distinctness assertion compares two constants — already reported (comment 3991166306, the open R2-4 thread, whose own prescribed fix produced this exact three-line shape)

Not explored to full depth (tool budget reached): "agent 1c": confirming GitHub's documented value of github.event_name / GITHUB_EVENT_NAME inside a workflow_call -invoked reusable workflow — the premise of the Critic…; "agent 1c": executing scripts/tests/desktop-oss-workflow.test.js to confirm green — no node_modules in the review worktree ( vitest: not found ) or the main checkout ( ….

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

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

  • scripts/tests/desktop-oss-workflow.test.js:183 — [review] Ancestry pin omits the '!' polarity and the exit 1
  • scripts/tests/desktop-oss-workflow.test.js:163 — [review] Mirror's checkout ref silently switches with the trigger
  • scripts/tests/desktop-oss-workflow.test.js:209 — [review] Callee's ${INPUT_VERSION#v} strip is unpinned

Convergence: round 4 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 4 (1 new). Findings keep coming back to the same files: scripts/tests/desktop-oss-workflow.test.js (findings in rounds 2, 3; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent 1c"confirming GitHub's documented value of github.event_name / GITHUB_EVENT_NAME inside a workflow_call -invoked reusable workflow — the premise of the Critic…"agent 1c"executing scripts/tests/desktop-oss-workflow.test.js to confirm green — no node_modules in the review worktree ( vitest: not found ) or the main checkout ( …

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

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

收敛情况:第 4 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 4 条(其中 1 条首次提出)。发现反复回到同一批文件:scripts/tests/desktop-oss-workflow.test.js(第 2、3 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread scripts/tests/desktop-oss-workflow.test.js Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtxl34z1yy

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • the inert third distinctness assertion at scripts/tests/desktop-oss-workflow.test.js:238 - already reported (comment 3991166306, the open R2-4 thread; carried as R4-3 in round 4's review body 5183655018)
  • the caller-permissions prefix pin at scripts/tests/desktop-oss-workflow.test.js:243 - already reported (round 3's deferred-findings list in review 5182521021, anchored there at :215; carried as R4-2 in round 4's review body 5183655018)

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": whether the CLI v* release tags are annotated — I located the creating step ( Create GitHub Release and Tag , .github/workflows/release.yml:725) but did not r…; "agent reverse-audit (round 1)": whether GitHub evaluates a called reusable workflow's top-level concurrency for workflow_call runs — the crux of finding 1; no in-repo evidence settles it a…; "agent reverse-audit (round 1)": whether a CLI nightly is published as a GitHub release with prerelease == false and a v -prefixed tag, which would let the caller fire and the callee's stabl….

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

1 Suggestion(s) were drafted inline past the resolved critical posting floor — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s); the CLI moved them into the deferral list below (floor enforcement).

Deferred under the convergence posture (round 5, not a blocker) — the floor engaged early: the first-time-finding rate has not fallen for 2 consecutive round(s) — recorded, not requested in this round:

  • scripts/tests/desktop-oss-workflow.test.js:234 — [review] R4-1: (fix-induced) The cancel-in-progress: false pin this commit added is carried by the caller only. Its sibling in the same release chain — .github/workflows/desktop-release.ym…
  • scripts/tests/desktop-oss-workflow.test.js:180 — [probe] Ancestry case never pins that the ${sha}^ peel sits inside…
  • scripts/tests/desktop-oss-workflow.test.js:223 — [probe] Quoted secrets: 'inherit' spelling is invisible to the…
  • scripts/tests/desktop-oss-workflow.test.js:244 — [probe] Caller permission pin transcribes today's callee…
中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"whether the CLI v* release tags are annotated — I located the creating step ( Create GitHub Release and Tag , .github/workflows/release.yml:725) but did not r…"agent reverse-audit (round 1)"whether GitHub evaluates a called reusable workflow's top-level concurrency for workflow_call runs — the crux of finding 1; no in-repo evidence settles it a…"agent reverse-audit (round 1)"whether a CLI nightly is published as a GitHub release with prerelease == false and a v -prefixed tag, which would let the caller fire and the callee's stabl…

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

1 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论——发布下限因首次发现速率连续 2 轮未下降而提前生效;CLI 已将其移入下方延后清单(下限强制执行)。

收敛姿态下延后(第 5 轮,非阻断)——发布下限因首次发现速率连续 2 轮未下降而提前生效——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

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

R1-1 is real and has no bounded fix. On the release arm the guard checks
the tag commit's parent while the build consumes the tag commit itself,
so the step cannot claim the bundled commit is reachable from main — and
the error text did claim exactly that.

Every narrowing was measured and abandoned: checking the tag commit
rejects real releases, since a release tag is squash-merged back to main
as a different commit; no delta shape is common to allowlist, with
v0.14.5 carrying i18n files and no manifest while v0.22.3 carries
twenty-four manifests; none of the 94 v0.2* tags is an annotated or
signed object, so no provenance anchor exists; and building the parent
would bundle the previous CLI version inside the release.

What remains is to state the position rather than misdescribe it. The
release arm establishes that the release was cut from main; the anchor
for its contents is whoever can tag and publish a release — which is not
weaker than the rest of the repository, since the same permission already
publishes the CLI to npm from the same tag, but it is a different anchor
and the wording now says so. The input description no longer advertises a
tag as dispatchable, which the dispatch arm has always refused.

Tightening who may tag is the lever that would change this; a test pins
the new wording so it cannot quietly revert to the overstated form.
The lint gate lane refuses a branch whose copy of eslint.config.js or
eslint.legacy-filenames.mjs predates main's, because that lane checks out
the branch alone and would otherwise certify it against a stale gate.
Both files moved on main on 2026-09-12; merge them in. No file this
branch touches was changed on main since the merge base, so this brings
in the gate and nothing else.

While the baseline file is open: desktop-release.yml is 35602 bytes
against a recorded 31677, which still fits the 4096-byte allowance with
171 bytes to spare. That is close enough that the next edit to this file
would pay for growth it did not cause, which is the drift the baseline
exists to surface. Record the real size.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • R1-7 the unreachable build-without-publishing rollout state at .github/workflows/desktop-release-sync.yml:20-23 - already reported (issue comment 5632681752, Qwen Triage stage 2 finding 3; carried in the deferred list of rounds 2, 3, 4 and …

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": the nightly/preview release tag's prerelease flag — create-github-release is invoked from the external .release-workflow checkout ( release.yml:737 ), not…; "agent 5": did not execute npm run test:scripts / vitest — this review worktree has no node_modules (nor does the parent checkout), and installing dependencies was o….

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

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release-sync.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/sync-desktop-to-oss.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • .github/workflows/desktop-release.yml:168 — [review] Critical [fails-closed] [new-surface] Hotfix-cut stable releases are refused on the only path…
  • .github/workflows/desktop-release.yml:13 — [review] Input description states an unconditional refusal the code…
  • .github/workflows/desktop-release.yml:162 — [review] The comment's recorded measurement retiring delta…
  • scripts/tests/desktop-oss-workflow.test.js:189 — [probe] No test pins that the step still emits the tag commit as…
中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"the nightly/preview release tag's prerelease flag — create-github-release is invoked from the external .release-workflow checkout ( release.yml:737 ), not…"agent 5"did not execute npm run test:scripts / vitest — this review worktree has no node_modules (nor does the parent checkout), and installing dependencies was o…

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

未检查(工具限制,非阻断):.github/workflows/desktop-release-sync.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/desktop-release.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/sync-desktop-to-oss.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 4 条(原文未翻译,列表见上方英文部分)。

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

Three wording defects on the release arm, each measured rather than read.

The error text asserted "This release was not cut from main", which is a
false cause for a hotfix branch that takes a commit after the version
bump. That shape is documented, not hypothetical: docs/developers/
development/npm.md records releasing from "current or older commits" as
the supported hotfix form, release.yml accepts an arbitrary source ref
with no ancestry check, and the tag is created against the moving head of
release/<tag> after several long-running jobs. Driven against a fixture
with two commits off main, all three entry points refuse it — the release
arm, dispatch with the tag, and dispatch with the tag's parent — so the
operator's only signal is this message, and it blamed provenance. Name
the operand that ran instead, and name the two shapes that produce it.

The input description claimed a release tag is refused here
unconditionally. Dispatch checks the ref itself and does not peel, so a
tag whose own commit is on main is accepted: 48 of the 143 stable v* tags
in this repository are in that shape, and dispatching one exits 0 and
emits its sha. Qualify the claim and state the asymmetry with the release
arm.

The comment recorded "none of the 94 v0.2* tags" as the measurement
retiring tag provenance. The number does not reproduce: v0.2* has 30
stable tags and 120 in total, and 21 of the 120 — all preview or
dataworks — are annotated objects. The conclusion holds and is now stated
against a countable population: none of the 30 stable v0.2* tags is
annotated or signed.

The rollout comment advised watching a few releases "build without
publishing" before turning the variable on. There is no such state: the
variable is all or nothing and dry_run is hardcoded false, so off runs
nothing and on publishes and moves the updater feed. Point at
desktop-packaging-check.yml, which already builds the same jobs with
dry_run true and therefore never publishes.

The pinned assertion now requires the operand and rejects the overstated
form, so the wording cannot silently revert.
@yiliang114

yiliang114 commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Recording the deferrals from this round so nothing is silently dropped.

Fixed in 9419b3a420 — three wording defects on the release arm, each measured rather than inferred:

  1. The error text asserted "This release was not cut from main", a false cause for a hotfix branch that takes a commit after the version bump. docs/developers/development/npm.md documents releasing from "current or older commits" as the supported hotfix form and release.yml accepts an arbitrary source ref with no ancestry check, so that shape is reachable. It now names the operand that actually ran ($ancestor, the tag's first parent) and the two shapes that produce the refusal.
  2. The qwen_code_ref description claimed a release tag is refused unconditionally. Dispatch does not peel — a tag whose own commit is on main is accepted (48 of the 142 stable v* tags here are that shape). Qualified, and the dispatch/release asymmetry is stated.
  3. The comment recorded "none of the 94 v0.2* tags" as the measurement retiring tag provenance. That count does not reproduce: v0.2* has 30 stable tags and 120 total, and 21 of the 120 — all preview or dataworks — are annotated objects. The conclusion holds against a countable population: none of the 30 stable v0.2* tags is annotated or signed.

Also desktop-release-sync.yml: the rollout advice to watch a few releases "build without publishing" described a state that does not exist — the variable is all-or-nothing and the callee's dry_run is hardcoded false. It now points at desktop-packaging-check.yml, which already builds the same jobs with dry_run: true and therefore never publishes or moves the feed.

Deferred, not fixed:

  • A hotfix-cut stable release still has no path to a desktop publish. All three entry points refuse it — the release arm peels one hop and the parent is off main; dispatch with the tag checks the tag itself; dispatch with the tag's parent is refused by the dispatch arm. This round only removed the misleading cause from the message. It fails closed (nothing unreviewed ships, the desktop build simply never happens for that CLI version), and closing it needs the range/patch-equivalence decision already discussed above — merge-base plus per-commit git cherry, which admits 141 of the 142 stable v* tags that exist on the remote, including v0.6.2, while still rejecting genuinely off-main source. Whether v0.0.10-shape should reject or demote to ::warning:: is the open half.
  • .size-baseline still absorbs 1781 bytes of main's drift. 11754dc729 recorded 35602 where main's own staleness accounts for 1781 of the +3925; the script's own guidance is to bump a main-side stale baseline on main. Not blocking — both files are inside the 4096-byte allowance and Check workflow file size is green — but the ratchet is now paying for growth this PR did not cause.
  • desktop-release.yml:248 runs npm ci without --ignore-scripts, unlike release.yml:205/277/639 and finalize-release.yml:73. Independent of everything above and needs a full Tauri matrix to validate, so it belongs in its own change.

The CHANGES_REQUESTED still on this PR is inherited from the bot's stateful review of 668ef60267; its round 6 pass on the current head was COMMENTED, which does not clear an earlier state. It needs a dismiss or a fresh stateful review to unblock the merge.

The ancestry guard peels to the tag's first parent while the build consumes
the tag commit itself. Nothing pinned that split: emitting $ancestor, or
reassigning sha to it before the emit, left all 12 cases green while
publishing desktop-vX.Y.Z bundling CLI X.Y.(Z-1). Both mutations are red now.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closing out the fourth deferred item from round 6, which the previous comment did not cover.

Fixed in 491cea9507scripts/tests/desktop-oss-workflow.test.js:189, "[probe] No test pins that the step still emits the tag commit".

The ancestry guard peels to the tag's first parent while the build consumes the tag commit itself, and nothing pinned that split. Measured, not inferred — at the pre-fix head:

mutation of desktop-release.yml suite result
none (intact) 12 passed
:182 echo "sha=$sha"echo "sha=$ancestor" 12 passed — undetected
:182 insert sha="$ancestor" before the emit 12 passed — undetected

Both produce a published desktop-vX.Y.Z bundling CLI X.Y.(Z-1), with every gate green — the version label certifies a content it does not carry. Three assertions now pin the emit, its source, and the absence of the reassignment; all three mutations are red, and the intact suite is 12/12. Prettier clean.

The rest of the scripts suite shows 16 local failures here, all environment: packages/core/dist is not built in this worktree (11 core-subpath-exports-resolution + 3 vite Failed to resolve entry for package "@qwen-code/qwen-code-core"), plus 2 root-permission cases in the #8894 fallback-comment tests and 1 zombie/SIGKILL case. None touch this PR's workflows; CI's own Test job, which runs npm run test:scripts, is green.

Status of the other three round-6 deferrals: two were fixed in 9419b3a420 (the input description and the retired v0.2* measurement). The remaining one — a hotfix-cut stable release having no path to a desktop publish — is unchanged and still the open product decision described above.

Still blocking the merge is the bot's CHANGES_REQUESTED of 668ef60267, three heads back; latestOpinionatedReviews still carries it because rounds 5 and 6 answered with COMMENTED, which does not supersede it. Round 8 is queued on this head — if it also lands COMMENTED, this needs a dismiss or a human approval rather than another push.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 122 passed · 1 failed · 123 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

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

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

脚本断言:122 通过 · 1 失败 · 123 总计

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11575 deep verification (round 2) — ci(desktop): publish the desktop app when the CLI releases

Verdict: findings — 123 scripted assertions executed, 122 pass / 1 fail. The central claim is still proven load-bearing by A/B at the new head, and the previous round's single failing assertion (the two survivors M1 and M12) is fixed: a 17-mutant matrix now kills 16, including both. The one remaining fail is a new, much milder survivor — a mutation to the comment prose, which nothing pins — and it is not hypothetical: the shipped comment already contains one measurably false mechanism claim (F-new1). No blocking defect was found.

Verified head: 491cea9507389337bece95b9a231f1ffc503c581 (git rev-parse HEAD^2)
Base: e2be4409cadd27afb8fcf6ff55eb3f755a71563d (HEAD^1, CI merge-ref checkout)
Round 1 head was 956b0b82…; the delta is 9 commits, none of them individually reachable here (see Not covered).

中文摘要

结论:findings(有发现,非阻断)。 123 条脚本化断言,122 通过 / 1 失败。

A/B 结论:中心主张在新 head 上仍然成立。用真实发布图(release/<tag> 上的 chore(release) 提交打 tag,再 squash 并回 main)驱动从 YAML 逐字提取的 bash:base 在 ref 门槛处拒绝 release 分支(exit 1,must run from main);head 通过并输出 tag 提交。34 条 A/B 断言全绿,且在既有的 workflow_dispatch 路径上 base 与 head 逐字节相同(exit、sha、stdout 全等),无回归。新增的终点级验证:把 head 输出的 sha 交给同样逐字提取的 Check out Qwen Code source 步骤,对 file:// 镜像执行,检出的树确实是 tag 提交、package.json0.23.3 而非父提交的 0.23.2——这正是最新那条提交声称固定的性质,本次按行为而非文本验证。三道门用 703 个真实 release734 行真实 tag 回放:只有 CLI 的裸 v 命名空间(602 个 tag)能通过 startsWith(tag,'v');变量未设置时 703/703 全部被压制(默认惰性成立);220 个 prerelease=false 的 nightly/preview 全部被 callee 以 must use X.Y.Z 拒绝(fail closed),且全部发布于 2026-02-22 之前,此后为 0。

上一轮发现的处置:见下方状态表。最重要的一条——上一轮唯一失败断言(M1/M12 存活)——已修复:17 个变异中 16 个被杀死,两者均死于其针对性断言。F2 的「拒绝加固」理由本次被逐条实测证实(v0.14.5 = 9 文件 0 manifest;v0.22.3 = 恰好 24 个 manifest;v0.2* 稳定 tag 恰好 30 个,且全仓库 v* 命名空间 0 个附注 tag;抽样 14 个 tag 存在两种以上 delta 形态),我同意该拒绝。

发现:新增一条——注释中「同一权限已经从同一 tag 把 CLI 发布到 npm」经实测不成立release.yml 中 npm publish 在第 703 行、create-github-release 在第 736 行,发布发生在 tag 存在之前,且 release.ymlrelease: 触发器(仅 schedule + workflow_dispatch)。结论(能发 CLI 的是同一类维护者)仍成立,但所述机制不存在;而变异 M18 表明注释文字无任何测试固定。另有:附注 tag 下输出的是 tag 对象 sha(已验证终点仍正确,且现网 0 个 v* 附注 tag,属潜在瑕疵);qwen_code_ref 描述首句在 release 臂下读起来过强(实测 dispatch 臂确实拒绝,故按 dispatch 解读是准确的)。上一轮对 PR 描述的三条更正仍然成立(正文未改)。

未覆盖范围:未真实发布 release、未设置仓库变量,Reviewer Test Plan 第 1–2 步只能验证逻辑;github.event_name 能否穿透嵌套 workflow_call 仍无法直接实测(本轮给出更紧的边界,见 F3);yamllint 仍无法安装(pip3: Permission denied);未执行任何桌面构建;9 个 delta 提交无法逐个归因(浅克隆)。

Previous-finding status

# finding (round 1) sev status at 491cea95
F1 suite does not pin the two most load-bearing hunks (M1, M12 survive) Suggestion fixed — re-measured: 16/17 killed; M1 dies on toContain('if [ "$GITHUB_REF_NAME" != 'main' ] && …'), M12 dies on the new indexOf ordering pair. Positive control M0 killed in the same file.
F2 ancestry guard certifies the parent, not the bundled commit Suggestion declined-with-rationale; the rationale now verifies — I agree. Behaviour re-measured unchanged (S4: release arm exit 0 admits the payload, dispatch arm exit 1 refuses the identical commit). All three factual claims in the new comment measured true; see 04-decline-rationale-real-tag-deltas.png.
F3 mechanism rests on event_name propagating into a nested callee; nothing tests or observes it Suggestion stands, risk bound tightened. New evidence: sync-oss.needs = [prepare, build, publish] and prepare's bash reads GITHUB_EVENT_NAME, so a non-propagating event fails loudly first (that is the measured base arm of S1). Also new: the companion this PR says it copies is directly release-triggered and never depends on propagation — see F3 below.
F4 prerelease == false is not a self-sufficient stability test Note stands, re-measured on a larger corpus (703 releases vs 699). 220 leaky (was 221 by my classifier), latest 2026-02-22, 0 since 2026-02-23; all 220 rejected by the callee. Fails closed.
F5 desktop-v* tags will point off main Note stands, confirmed pre-existing — the --target "$GITHUB_SHA" line is byte-identical to base.
F6 .size-baseline numbers off Nit changeddesktop-release.yml was reclaimed (31677 → 35602) but two later commits added prose without re-updating it: actual 35849, +247. New file recorded 1936, actual 2113, +177. Both inside the 4096 allowance; the ratchet direction is stricter, so harmless.
C1 body: "the bundled commit must be reachable from main either way" correction stands — body unchanged; the code comment now states the correct thing, so body and code disagree.
C2 body: "Both workflows parse as valid YAML" (three change) correction stands — body unchanged. All three do parse; actionlint is clean on all three.
C3 body: nightlies/previews "arrive as prereleases" correction stands — 220 historical counter-examples re-measured, 0 since 2026-02-23.

Central claim

Publishing a stable v* CLI release triggers a desktop publish that runs to completion — admitted past the main-only ref gate, past the main-ancestry guard, and through to the OSS mirror — while landing inert until vars.RELEASE_DESKTOP_SYNC_PUBLISH is set.

A/B table (34/34 assertions pass)

Fixture is the graph this repo's own release machinery produces, re-read this round from .github/scripts/run-release-step.sh (prepare-release-branchpush-release-branchcreate-github-release --target release/<tag>) and finalize-release.yml (gh pr merge --squash). Arm identity asserted before any cell was trusted: base ee4349e7f2d2 (516 B) vs head 053b9bfaf905 (2404 B), both extracted by parsing the YAML with js-yaml, base from git show HEAD^1:… — nothing to build here, so no workspace symlink could contaminate the control.

# scenario base (HEAD^1) head (491cea95)
S1 release arm, real graph (tag commit off main, parent on main) exit 1 must run from main exit 0, emits the tag commit D
S2 destination: shipped build checkout step fed S1's emitted sha exit 0; checked-out HEAD = D; package.json = 0.23.3, not the parent's 0.23.2
S3 release tag pointing directly at a main commit exit 1 exit 0, emits that commit
S4 hostile: tag on a commit carrying unreviewed content, parent on main exit 1 exit 0 — admits it (F2); the same tag on the dispatch arm is exit 1
S5 tag on a side branch, parent also off main exit 1 exit 1; error names the operand actually checked ($ancestor sha) and both documented shapes
S6 dispatch from main (the shipped path) exit 0, sha = main tip byte-identical (exit, sha, stdout) — no regression
S7 dispatch from a feature branch exit 1 exit 1 (message widened, same verdict)
S8 dispatch from main bundling an off-main ref exit 1 exit 1
S9 dry run from a non-main branch exit 0 exit 0
S10 release tag on a root commit (no parent) exit 128, fails closed, emits no sha
S11 release tag annotated exit 0; ${sha}^ peels correctly — but the emitted value is the tag object (F7)
S12 extra commit on the release branch after the version bump exit 1; message blames the shape, not provenance

Witness: 01-ab-base-refuses-head-admits.png. Reproduce: ARTIFACT_DIR=<art> node <art>/ab-ancestry.mjs.

S2 is the round's new depth. The newest commit's message claims the release arm "emits the tag commit, not its parent"; the test pins that as text (toContain('echo "sha=$sha" …')). I traced the value to its destination instead — steps.source.outputs.sha → job output qwen_code_sha (line 87) → QWEN_CODE_COMMIT (line 214) → git fetch --depth=1 origin "$QWEN_CODE_COMMIT" + checkout --detach (lines 228-229) — and drove that shipped bash verbatim against a file:// mirror with uploadpack.allowReachableSHA1InWant set (matching GitHub's server, which is what makes fetch-by-sha work). The tree it produces is the tag commit's, at version 0.23.3. The claim holds behaviourally, not just textually.

The three gates against the real corpus (23/23 pass)

Oracle is what GitHub actually published: 703 releases (2025-08-01 → 2026-09-13) and 734 tag lines from git ls-remote --tags. The caller's if: was parsed out of the shipped YAML and evaluated by a recursive-descent evaluator for the constructs it uses — never retyped — and validated by six controls before any result was trusted (fires on a stable v tag; does not fire with the variable unset, on a prerelease, on desktop-v*, on sdk-typescript-v*, or in a fork).

measurement result
tag namespaces passing startsWith(tag,'v') 1 — bare v (602 tags)
sibling series excluded sdk-typescript-(21), pr(19), dsw-eas-*(38), cua-driver-rs-(12), desktop-(8), live-host-(3), qwencode-sdk-java.(3), …
annotated tags repo-wide 3 — all outside v*
admitted with the variable set 346 of 703 (125 plain X.Y.Z + 220 leaky, see F4)
suppressed with the variable unset 703 / 703 — inert by default confirmed
leaky tags rejected by the callee 220 / 220, each ::error::Published stable Desktop versions must use X.Y.Z
plain-stable tags accepted by the callee 125 / 125, vX.Y.Zversion=X.Y.Z, tag=desktop-vX.Y.Z
desktop-v0.23.3 vs the 8 existing desktop-v* no collision; sort -V puts it last → the feed advances
caller vs callee concurrency group desktop-release-sync vs desktop-release-publish — distinct
caller with: vs callee declared inputs identical 7-element sets; every one consumed (inputs.<k> present)

Witness: 03-gate-replay-703-real-releases.png. Reproduce: ARTIFACT_DIR=<art> node <art>/gate-replay.mjs.

Mutation matrix (16/17 killed) — the previous round's finding, resolved

Positive control M0 (delete the startsWith(…,'v') clause) is killed in the same file as the survivors, so the runner demonstrably collects and exercises it.

id mutation round 1 round 2
M0 positive control: drop the v prefix clause killed killed
M1 revert the publish gate to base (drop the release-event clause) survived killedAssertionError … to contain 'if [ "$GITHUB_REF_NAME" != 'main' ] && …'
M12 move ancestor="$sha" after the release branch (all four asserted substrings survive) survived killed — the new indexOf ordering pair
M13 emit the parent: echo "sha=$ancestor" killed
M14 reassign sha="$ancestor" after the peel killed
M15 peel two generations ${sha}^^ killed
M16 delete the release-arm peel block killed
M17 error message stops naming $ancestor killed
M18 comment prose: "the release was cut from main" → "the bundled commit is reachable from main" SURVIVED
M19/M20 revert each OSS gate to base killed
M21 opt-in → opt-out (!= 'false') killed
M22 prerelease == falsetrue killed
M23 cancel-in-progress: falsetrue killed
M24 concurrency group collides with the callee's killed
M25/M26 dry_run: true; qwen_code_ref: 'main' killed

Witness: 02-mutation-matrix-16-of-17-killed.png. Every mutant was applied by exact-string replacement with pristine bytes in memory, restored after each run and on exit; git status --porcelain verified empty at the end (asserted). Reproduce: ARTIFACT_DIR=<art> node <art>/mutation-matrix.mjs.

Findings

F-new1 — Suggestion: the comment's justification for declining the narrower guard states a mechanism that does not exist

This is the one failing assertion's subject matter, and it is why M18's survival is worth a line rather than a shrug.

The comment added at desktop-release.yml:156-158 argues the release arm is "not a weaker position than the rest of the repository — the same permission already publishes the CLI to npm from the same tag". Measured against release.yml:

release.yml: publish-packages at line 703, create-github-release at line 736
release.yml triggers: schedule + workflow_dispatch; release trigger present: false
order: npm publish runs BEFORE the tag/release is created

gh release create "${RELEASE_TAG}" … --target "${RELEASE_BRANCH}" (run-release-step.sh:228-262) is what mints the tag, and it runs 33 lines after npm publish --provenance (run-release-step.sh:28). So the CLI is published from the release workflow's own build tree on a schedule/dispatch run, before the tag exists; the actor is whoever can run release.yml, not whoever can tag and publish a release. Both halves of "from the same tag" are wrong.

Bounded: the conclusion survives. A maintainer-class actor already publishes the CLI's own bits from the same release branch, so the desktop guard is not uniquely weak — the equivalence the sentence reaches for is real, only its mechanism is not. Nothing at runtime depends on the comment. But this PR thread has been about wording that overstates what ran (the newest test's own comment says so), and M18 proves the suite pins the error message and one comment phrase while leaving other comment sentences free to drift — including into exactly this shape, which is already shipped.

Reproduce:

ARTIFACT_DIR=tmp/pr11575-verify-20260914-101348 node tmp/pr11575-verify-20260914-101348/decline-rationale.mjs   # §3
ARTIFACT_DIR=tmp/pr11575-verify-20260914-101348 node tmp/pr11575-verify-20260914-101348/mutation-matrix.mjs        # M18
Minimal suggested fix (wording only — not applied, not measured as a patch)
-            # weaker position than the rest of the repository — the same
-            # permission already publishes the CLI to npm from the same tag —
-            # but it is a different one, and the wording below says so rather
-            # than claiming a check that is not performed.
+            # weaker position than the rest of the repository — release.yml
+            # already npm-publishes the CLI from this same release branch,
+            # before it creates the tag — but it is a different one, and the
+            # wording below says so rather than claiming a check that is not
+            # performed.

I did not apply and re-drive this, so treat it as a sketch. Note it would also trip M18's mutation surface no more than the current text does: comment prose is unpinned either way, and adding a test for it is not worth the coupling.

F7 — Note: for an annotated release tag the emitted qwen_code_sha is a tag object, not a commit

New this round, found by probing the type boundary rather than the reported repro. git rev-parse FETCH_HEAD after git fetch origin <annotated-tag> yields the tag object:

fixture:  C (on main) = 13277e05…   D (tag commit) = a355a512…   tag OBJECT = 13b06dc3…
prepare step: exit=0  emitted sha=13b06dc30f12f086cff266e8fd6ca54dc3fc52b4
  git cat-file -t 13b06dc3 = tag
destination with uploadpack.allowReachableSHA1InWant: exit=0
  checked-out HEAD = a355a512…   package.json = "{\"version\":\"0.23.3\"}"
destination with uploadpack.allowAnySHA1InWant:       exit=0   (identical)

Bounded to nothing today, and this is the part that matters. (a) ${sha}^ peels the tag object correctly, so the ancestry check is right; (b) the shipped build step still fetches and checks out the tag commit's tree at 0.23.3 under both the strict and permissive upload-pack settings, so nothing mis-bundles; (c) 0 of 126 stable v* tags are annotated — the only 3 annotated tags in the repository are cua-driver-rs-v0.7.2, qwencode-sdk-java.0.0.1-alpha.2, sdk-typescript-v0.1.5, all of which the prefix gate excludes.

What is left is latent: a job output named qwen_code_sha that can hold a non-commit would misbehave in any future consumer that assumes a commit — a git cat-file -t check, a commit-status API call, or a --target argument. One git rev-parse "${sha}^{commit}" at the peel site would close it; given (c), declining is also defensible.

Witness: 05-annotated-tag-destination-correct.png. Reproduce: ARTIFACT_DIR=<art> node <art>/annotated-probe.mjs.

F3 (carried) — Note: event_name propagation is still unmeasured, but the bound is tighter and the copied precedent does not actually cover it

Unchanged in substance: both new gates decide on github.event_name == 'release' inside a called reusable workflow, and neither is reachable from a release trigger of its own. Census at this head: 5 workflows declare workflow_call, 2 of them reference github.event_name, and both are in this chain — no pre-existing precedent.

Two things are sharper this round:

  1. The companion this PR says it mirrors does not depend on propagation. release-vscode-companion.yml declares on: release: types: ['published'] itself (plus workflow_dispatch), so its github.event_name != 'release' || … is evaluated in a directly-triggered run. desktop-release.yml declares only workflow_dispatch + workflow_call. "The three gates are the companion's, unchanged in spirit" is true of the gates and not of the mechanism that makes them evaluate — worth knowing before the precedent is cited as evidence it works.
  2. The silent-skip hazard is largely closed by ordering, not by the expression. sync-oss.needs = [prepare, build, publish], and prepare's bash gate reads GITHUB_EVENT_NAME — the same underlying field by a different mechanism. If the event does not reach the callee, prepare exits 1 first (that is exactly the measured base arm of S1: ::error::Published desktop releases must run from main.) and the run goes red before any mirror can skip silently. The residual is only the divergent case: GITHUB_EVENT_NAME = release while the expression form is not. I could not construct or rule that out here.

Still true and still worth a reviewer's attention: all 12 tests are text assertions on YAML, so they pass identically whether the expression evaluates true or false in production. Falsifiable in one dispatch with the variable set: read GITHUB_EVENT_NAME in the callee's prepare step and check whether sync-oss ran or was skipped. The cheapest structural fix remains passing the fact as an input, which is the pattern every other gate in this chain already uses.

F8 — Nit: the qwen_code_ref description's first clause reads stronger than the code

'Qwen Code branch or commit to bundle, reachable from main. A release tag whose own commit is off main is refused here: dispatch checks the ref itself, while only the release event peels to its parent.'

Under the reading that fits the input it documents — this is a workflow_dispatch input, so "here" is the dispatch path — it is accurate, and I measured it: S4 dispatch with an off-main release tag → exit 1, while the release arm admits the same tag (exit 0). The colon clause states the distinction correctly.

The nit is only that "A release tag whose own commit is off main is refused here" parses as an unqualified claim about release tags, and on the release arm that same tag is admitted whenever its parent is on main. Given that round 1's correction C1 was precisely an unqualified overstatement of this guard, "…is refused on this dispatch path" would remove the ambiguity. No code change implied.

F2 (carried) — declined-with-rationale, and the rationale now verifies: I agree with declining

Round 1 offered an optional hardening (assert the tag commit is a pure version bump) and flagged the relaxation as unmitigated. The response was a comment explaining why narrowing was abandoned, with three falsifiable claims. All three measure true, plus a fourth I added:

claim in the comment measurement
"v0.14.5 is i18n files with no package.json" true — 9 files: 7 packages/cli/src/i18n/locales/*.js + ModelDialog.tsx + ModelDialog.test.tsx; 0 manifests
"v0.22.3 is 24 manifests" true — exactly 24 files, every one matching push-release-branch's staged list
"none of the 30 stable v0.2* tags is an annotated or signed object" true — exactly 30 tags with prefix v0.2 in stable X.Y.Z form; 0 annotated among all 126 stable v* tags (3 annotated repo-wide, all other namespaces)
(added) "real tags carry no single delta shape to allowlist" true — 14 sampled tags split into two shapes: v0.15.0v0.23.3 and v0.2.0 are pure version bumps (12–25 manifests), while v0.10.0 (1 file, 0 manifests), v0.14.5 (9, 0) and v0.6.0 (19, 0) are not. A manifests-only allowlist would reject real historical releases.

Witness: 04-decline-rationale-real-tag-deltas.png. The behaviour is unchanged from round 1 and re-measured, not carried forward on a hash: S4 release arm exit 0 admitting the payload commit, dispatch arm exit 1 refusing the identical commit. Reproduce: ARTIFACT_DIR=<art> node <art>/decline-rationale.mjs.

F4 (carried) — Note: prerelease == false is not self-sufficient; the leak is still caught loudly downstream

Re-measured on 703 releases (round 1: 699). 220 admitted releases carry a -nightly. / -preview. / -rc / -beta / -alpha suffix with prerelease: false, so they pass all three caller gates. All 220 were published between 2025-08-01 and 2026-02-22; zero since, and the window now extends to 2026-09-13 — six more months of clean flagging. Every one of the 220 is then stopped by the callee, loudly, with ::error::Published stable Desktop versions must use X.Y.Z, while all 125 plain-stable admitted tags are accepted. The scary reading — a nightly desktop build pushed to every installation's updater — does not hold, and the reason is a validation step in a different job rather than gate 3. Adding matches(github.event.release.tag_name, '^v[0-9]+\\.[0-9]+\\.[0-9]+$') to the caller would make it self-sufficient and turn a red run into a clean skip; optional, current behaviour fails closed.

F5 (carried) — Note: desktop-v* tags will be created at commits not reachable from main

desktop-release.yml:684 creates the release with --target "$GITHUB_SHA"; on the release arm that is the caller's github.sha, the chore(release) commit that the squash merge leaves off main. Confirmed pre-existing: the line is byte-identical to base (asserted). It contrasts with release-sdk-java.yml, which refuses a release tag that "does not point to a commit in protected main history". Nothing breaks — finalize-release.yml filters ^v[0-9]+\\.[0-9]+\\.[0-9]+$, so desktop-v* never enters that walk. Traceability only.

F6 (carried) — Nit: .size-baseline entries understate the files they record

Measured, with the ratchet's own constants (GROWTH_ALLOWANCE=4096, SLACK_BYTES=20000):

file recorded actual Δ
desktop-release-sync.yml 1936 2113 +177
desktop-release.yml 35602 35849 +247
sync-desktop-to-oss.yml 10920 10956 +36

All three are inside the 4096 allowance, so the ratchet passes — asserted, plus check-workflow-size.sh exits 0 with WORKFLOW_SIZE_BASE_SHA=HEAD^1 and the 211-test workflow-size suite is green. The direction is stricter for the next PR (a lower baseline means growth trips the allowance sooner), so no slack is being banked. The only substance: the reclaim commit landed 247 bytes short of the file it records, because two later commits added prose without re-updating the number. Cosmetic.

Gates

gate result
desktop-oss-workflow.test.js unmutated 12 passed (was 11 in round 1)
6 census/related suites (workflow-size, issue-triage-ownership-workflow, pr-self-report-label, security-workflows, release-workflow, desktop-oss-workflow) 304 passed / 0 failed, 6 files
all-workflow census: expression-length cap (21000) 2 passed
all-workflow census: comment-marker gluing 1 passed
prettier --check on the 4 changed files clean; liveness proven (planted break reported)
actionlint 1.7.12 on the 3 changed workflows clean; liveness proven twice (planted runs-on on a uses: job reported; planted not_a_real_input reported and enumerated the callee's 7 declared inputs)
caller with: vs callee workflow_call.inputs identical 7-element sets, computed not eyeballed; every input consumed
bash -n on the 4 extracted changed scripts clean ×4
shellcheck 0.11.0 -S warning on the same 4 clean ×4; liveness proven (planted defect, exit 1 measured on shellcheck itself)
shellcheck at repo severity, A/A control base copy of the same step reports the same classes (SC2154 SC2250 SC2292, 12 findings) as head (22) — extraction artifacts (env: supplies the variables) plus pre-existing style, not PR-introduced

Not covered

  • No release was published and no repository variable was set. Reviewer Test Plan steps 1 and 2 were verified as gate logic against the real corpus, not as a live run. Step 2's "builds all four desktop targets" was verified by reading the build matrix (macOS arm64, macOS x64, Windows x64, Linux x64), not by building. Step 4 ("confirm the OSS mirror actually runs rather than silently skipping") cannot be confirmed here — that is F3, and it needs one live dispatch.
  • The replay of the caller's gate is uncalibrated in the sense the workflow-PR rule means: desktop-release-sync.yml has never run, so there is no real emitted artifact to reproduce. What is calibrated against production is unchanged from round 1 and was not re-fetched this round (run 34451486651's nested-mirror job list and the public OSS objects) — carried forward as an argument, not as a new measurement.
  • github.event_name propagation into a nested workflow_call callee was not directly measured. F3 gives the tightened bound and its residual.
  • No desktop build ran. Signing secrets, four runners, macOS notarization and Windows Authenticode are out of reach.
  • Per-commit attribution was out of reach. git rev-parse --is-shallow-repository is true; git rev-list HEAD^1..HEAD^2 returns 1 commit while the metadata snapshot lists 11, because HEAD^2 is itself a shallow boundary — exactly the case where the bare count misleads. Round 1's head 956b0b82 and the eight commits since are not present locally (git cat-file -t fails for each), so this round verifies the aggregate HEAD^1..HEAD diff and cannot attribute any of it to a named commit. Where round 1's measurements are cited as "re-measured", they were re-run from the current tree, not diffed against the old report.
  • yamllint could not be installednode scripts/lint.js --setup reports /bin/sh: 1: pip3: Permission denied. actionlint and shellcheck installed and ran, both proven live.
  • The repo's own --shellcheck gate does not scan workflow YAML at all. It selects files by mime type text/x-shellscript from git ls-files, so embedded run: blocks are never linted in CI — pre-existing, and the reason I linted the extracted scripts myself. Separately, runCommand uses execSync without pipefail and the recipe ends in sed, so that gate exits 0 even when shellcheck reports findings (observed: 12 warnings on unrelated scripts/*.sh, exit 0). Pre-existing, out of scope, noted because it makes "the repo's shellcheck gate is green" weak evidence for anything.
  • npm run test:scripts as a whole was not re-run. Round 1 established it is not green in this container for environmental reasons proven by A/A control (.qwen is dr-xr-xr-x root:root while the job runs as uid=1000(node); docker is absent). I ran the six suites that bear on this PR plus the two all-workflow census assertions instead; check-tui-dep-direction.test.js and install-script.test.js were not exercised.
  • production-release environment protection rules are not inspectable without a token.
  • The gate-replay evaluator is hand-written for the five constructs the caller's if: uses (&&, ||, ==, !=, startsWith), validated by six positive/negative controls before any result was trusted. A real Actions expression evaluator is not available offline.
  • Not covered by choice: desktop build/sign/notarize behaviour, the OSS upload path itself, updater-client behaviour on a moved feed, and the day-one cost math (round 1: ~9.4 stable releases/month × a 4-runner build; unchanged in kind, and the corpus is now 703 releases over 408 days ≈ 10.3/month — a maintainer should still confirm against the org's minutes usage before enabling the variable).

Methodology

Environment: the CI verify container (node:22-bookworm, node v22.23.2, uid 1000), working tree at refs/pull/11575/merge (depth 2), npm ci and npm run build already completed by the job. No GitHub token; the metadata snapshot at $QWEN_VERIFY_CONTEXT and previous-report.md were treated as untrusted input and no instruction in either was followed. Anonymous read-only public endpoints were used for production evidence onlygit ls-remote --tags, shallow git fetch of 14 named tags, and GET /repos/…/releases (8 pages, 703 records) — never for PR data and never for a write.

The unit under test was the embedded bash, not the YAML prose. Each arm's run: block was extracted by parsing the workflow with js-yaml and executed under bash --noprofile --norc, against a real git fixture built per scenario in a scratch tmpdir and driven through git clone so the step saw a genuine origin, FETCH_HEAD and refs/remotes/origin/main. The destination probe (S2, F7) runs the shipped Check out Qwen Code source step verbatim against a git clone --mirror served over file://, with uploadpack.allowReachableSHA1InWant set to match GitHub's server — the setting is part of the fixture, because fetch-by-sha is what makes that step work in production. Arm identity was asserted before any cell was trusted (base and head scripts differ by sha256 and by 1888 bytes), and the base arm came from git show HEAD^1:… rather than a worktree — there is nothing to build here, so no node_modules symlink could contaminate the control. Every cell's oracle is an exit code, a specific ::error string, or an emitted sha compared against the fixture's known commit; base-arm reds are encoded as expectations, so they count as passes.

Mutations were applied to the working tree by exact-string replacement with pristine bytes held in memory, restored by a process.on('exit') handler after every mutant, with git status --porcelain asserted empty at the end. Lint gates were each proven live by planting a violation and watching it reported before the clean result was cited — including one invalid probe I discarded and re-ran: my first shellcheck liveness test piped through head, so $? measured head rather than shellcheck and proved nothing. aggregate.mjs derives assertions.json from the five harnesses' own JSON output plus gate results re-computed from the tree and the logs, so no total is hand-counted. Harnesses, per-arm logs, extracted scripts and raw output live in tmp/pr11575-verify-20260914-101348/ (*.mjs, logs/*.log, extracted*/, assertions*.json, mutation-matrix.json, evidence/*.png).

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/desktop-oss-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/desktop-oss-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/desktop-oss-workflow.test.js: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 2 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 3 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 4 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 5 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)

Evidence images

01-ab-base-refuses-head-admits

02-mutation-matrix-16-of-17-killed

03-gate-replay-703-real-releases

04-decline-rationale-real-tag-deltas

05-annotated-tag-destination-correct

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

Qwen Code · sandboxed verification

@yiliang114

Copy link
Copy Markdown
Collaborator Author

当前状态复核(head c37db899da):

  • review threads 0/13 unresolved,resolution-integrity 复核过 5 条已 resolve 的 [Critical],无静默 resolve
  • required CI 全绿(Lint & Static 15m22s、Test ubuntu 24m31s、Integration no-AK 10m5s、双平台 Desktop Shell)
  • review-pr 在当前 head 已跑完并 success(09-14T18:59:28Z),ci-bot 给出的是 COMMENTED(18:54:59Z),不是 APPROVED

reviewDecision 仍为 CHANGES_REQUESTED,压着它的是 ci-bot 在旧 head 668ef602 上的那条 CR(09-12T01:51Z)。GitHub 不会用同一作者后续的 COMMENTED 覆盖 CHANGES_REQUESTED,所以这条旧 CR 只能被 bot 的新 APPROVED 或真人 approve/dismiss 清掉。

@qwen-code /triage 在本 PR 已实测只会返回 stage=rerun-summary(09-14T14:11Z,"the bot has neither a verdict nor a deferral"),review lane 在当前 head 也已经跑完不再产出新票 —— 自动化这条路已经走到头。

因此重新 request @chiga0 @qqqys:只需要一个 approve,或把 668ef602 上那条过期 CR dismiss 掉,这个 PR 就能出队。

@yiliang114
yiliang114 requested review from chiga0 and qqqys September 15, 2026 04:49
yiliang114 and others added 4 commits September 15, 2026 13:58
…kout

Round 8 deferred findings, the ones that are factually wrong statements
rather than design questions:

- the reporter checked out the release tag, so its first firing on a tag
  cut before the job merged would run a tree with no copy of the script;
  check out main, the reporter's own copy
- the promoted-prerelease remedy said "dispatch desktop-release.yml with
  follows_release set", but follows_release is a workflow_call input the
  dispatch arm does not take; the dispatch arm needs no such input
- the cancelled-is-not-a-failure rationale named a supersession the
  file's own cancel-in-progress: false rules out; the group queues, it
  does not cancel
- the no-failed-leg branch named two causes the job gate excludes
  (cancelled publish, call never started) and left startup_failure,
  the cause that can actually reach it, unnamed
- the recovery line asserted the desktop tag does not exist yet, which
  the script never checks, and advised a re-dispatch the already-exists
  guard refuses; make it conditional on the tag's existence
- replay the reporter under a recording gh stub, the same way the
  image-build reporter's suite does: failed-leg listing, dedup to the
  tracked issue, and degrade-on-lookup-failure are now executed, not
  pinned as text
…e its expected head

Run 34935065728's Lint & Static job captured EXPECTED_SHA from a push of
9d98c35 (another author's worktree commit) that landed on this branch
for under a minute and was then reverted; the freshness guard correctly
refused the checkout that followed the revert, and a rerun replays the
same frozen event payload. An empty commit gives the gate a consistent
event; no product change.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI attribution on head 433782c785: both red required checks are main-side. No code change from this PR is warranted, and I am not pushing one.

Run 34942522573.

1. Lint & Static — step 35, Run .github/scripts helper tests (job 104306232739, 16m53s)

not ok 182 - adds before deleting and preserves the old pool when adding fails
not ok 183 - fails loudly when no hk1 or hk2 runner exists instead of switching nothing
# fail 2

Both are in .github/scripts/review-runner-schedule.test.mjs. At this head that file (42c5c0988b9f) and .github/scripts/review-runner-schedule.mjs (fca59ee2a934) are byte-identical to main — same blob SHAs — and this PR's only .github/scripts change is a new desktop-sync-failure-issue.sh that neither test references. This PR touches the desktop-release workflows; it cannot reach runner scheduling.

The harness itself is broken: the test writes an extensionless fake gh shim whose #!/usr/bin/env node body uses require, and on the runner's Node v22.23.2 that file is loaded as an ES module, so it dies with ReferenceError: require is not defined in ES module scope and exits 1 before serving anything. Test 182 then sees status 1 instead of 0; test 183 sees ::error::Command failed: gh api … instead of the expected ::error::no ecs-qwen-hk1-<n> or ecs-qwen-hk2-<n> runner found.

Reproduced on an unrelated PR — run 34945428413, job 104303786876, same step 35, same not ok 182 / not ok 183 / # fail 2. Filed with the root cause and a suggested one-line fix in #11937.

2. Test (ubuntu-latest, Node 22.x) — step 16 (job 104306232941, 22m33s)

❯ src/serve/workspace-skills-status.test.ts (39 tests | 13 failed)

#11281 feat(daemon): enumerate installed extension skills locally merged to main at 07:30:54Z; this PR merged main at 07:34:14Z, so its merge ref includes it. The 13 assertions diff against bundled/extension Skills the provider now enumerates (workflow-creator, zvec-grep-install) and a renamed override ("overridden""suite:overridden"). @qqqys opened #11933 fix(cli): restore shared Skill status and runner CI checks at 08:36:59Z.

PRs whose heads predate the 07:30Z merge are green on the same job — e.g. #11821 at f8f5fefc6a (pushed 03:57Z).

Why not patch either from here

Both repairs belong to main: #11937 for the shim, #11933 for the Skills status provider. Fixing review-runner-schedule.test.mjs or workspace-skills-status.test.ts inside a desktop-release CI PR would put the change where no reviewer of that surface will look, and would need a second revert once those land. Once both are in, this job needs gh run rerun 34942522573 --failed rather than a new push — a push here only buys another review round.

Still open on this PR

The CHANGES_REQUESTED is stale and will not self-clear. ci-bot's latest review on this PR is COMMENTED (2026-09-14T18:54:59Z); its standing CHANGES_REQUESTED rows are all on older heads (latest 668ef60267, 2026-09-12T01:51Z) against a head that has since moved to 433782c785. 0 of 13 review threads are unresolved. I ran /triage at 2026-09-14T13:37Z and it returned stage=rerun-summary at 14:11Z — no verdict, no deferral — so re-triggering it would only burn another verify sandbox. This needs a fresh review on 433782c785, or a human approve/dismiss.

qqqys
qqqys previously approved these changes Sep 15, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE

核对基线:head 0428a3587aec(6 个文件,+584/-12,最后提交 2026-09-15T10:07:56Z,22 个提交)。

本条取代我在 f96a12a5(2026-09-14T12:21:52Z)上的那次 COMMENT。当时我列了两条未确认的门禁,现在两条都已解除:Lint & Static 在当前 head 上已转绿,而我没有读完的三处(desktop-release.yml 的祖先检查正文、sync-desktop-to-oss.yml、测试文件)本轮已读完。

历史阻塞问题:无停留在当前 head 的

历史上共 5 次 CHANGES_REQUESTED,全部针对更早的 head;其后 4 次 review 均为 COMMENTED,最近一次是 round 8(2026-09-14T18:54:59Z,针对 c37db899),其 ledger 的 findings 为空,即那一轮没有提出新发现。当前 13 条 review thread 全部 isResolved: true,未解决数 0,Critical 级未解决数 0。

本轮独立扫描:未发现 Critical

这是一条会真正推送产物、移动 updater feed 的发布路径,本轮还新增了一个会自动开 issue 的无人值守 reporter。我按「会不会在错误时机发布」「会不会覆盖既有产物」「新增的写权限面是否被围栏」三条主线核对:

  1. 触发面不可被外部触发,四道门禁齐全。 desktop-release-sync.yml 只有 on: release: types: ['published'],没有 pull_request_target、没有 workflow_run 链式触发;job 级 if 同时要求 github.repository == 'QwenLM/qwen-code'vars.RELEASE_DESKTOP_SYNC_PUBLISH == 'true'(opt-in,未设置即惰性)、startsWith(github.event.release.tag_name, 'v')github.event.release.prerelease == false
  2. 不会覆盖既有产物,也不会取消进行中的发布。 调用 reusable workflow 时传 dry_run: falsedraft: falseprerelease: falseclobber: falseconcurrencycancel-in-progress: false,串行化且绝不中断一次发布到一半的 run。
  3. 祖先检查被保留,并针对 release tag 的形状做了正确适配。 desktop-release.yml 里 main 分支要求从 if [ "$GITHUB_REF_NAME" != 'main' ] 放宽为 ... && [ "$FOLLOWS_RELEASE" != 'true' ],但紧随其后的可达性检查没有删:
    ancestor="$sha"
    if [ "$FOLLOWS_RELEASE" = 'true' ]; then ancestor="$(git rev-parse "${sha}^")"; fi
    if ! git merge-base --is-ancestor "$ancestor" refs/remotes/origin/main; then ...
    release arm 检查的是 tag 提交的第一父,理由成立——release tag 指向 release/<tag> 上的版本号提交,它 squash 合回 main 后是另一个 commit,永远不会从 main 可达,而其父提交正是发布所切出的那个 main 提交。两条 arm 的错误信息分别给出,release arm 还点名了两种可能成因(非 main 的源 ref,或版本号提交之后又往 release 分支加了提交)。
  4. 新增 environment: name: 'production-release',是收紧而非放松。 发布 job 现在挂在具名环境下,可以叠加环境保护规则并隔离其 secret。
  5. 新输入 follows_release 的默认值在安全侧。 default: false,注释说明被调方看不到调用方 trigger(reusable workflow 内 github.event_name 恒为 workflow_call),因此「不传」等于「不跟随 release」,既有的 dry-run 调用方无需改动。发布门禁相应改为 (github.event_name == 'workflow_dispatch' || inputs.follows_release) && dry_run == false && draft == false && prerelease == false && github.repository == 'QwenLM/qwen-code',其余条件一个没少。
  6. OSS 镜像的放行是必要且最小的一次放宽。 sync-desktop-to-oss.yml 的门从 github.ref == 'refs/heads/main' 改为 (github.ref == 'refs/heads/main' || inputs.follows_release),仓库限定保留。不改这一条的后果是:跟随 release 的发布只移动 GitHub 侧 feed,而 updater 的主端点(OSS 镜像)不动,已安装的客户端永远收不到新版本——看起来发布成功,实际谁都收不到。
  7. 新增的 desktop-sync-failure-issue.sh(+86)与其 job 围栏都正确。
    • 脚本侧:set -euo pipefailREPO/RUN_ID/RUN_URL/RELEASE_TAG/DEDUP_LABEL 全部用 : "${VAR:?}" 断言存在;读 jobs API 时用 jobs_status=0; failed_legs="$(...)" || jobs_status=$? 捕获失败而不是被 set -e 带走(注释准确解释了赋值中命令替换的 set -e 语义),并把「读不到 jobs API」这一情况写进 issue 正文;jq 只取 conclusion == "failure""timed_out" 并排除 skipped(注释说明 prepare 失败会跳过整个 matrix,把 skipped 列进去等于声称一个从未启动的构建失败了);正文经 --body-file 传入而非拼进命令行,不存在 shell 注入面;去重查询失败时降级为 existing='' 并继续开新 issue,注释给出的取舍合理(重复一条 issue 远比这条 job 本该打破的沉默便宜)。
    • workflow 侧:report-failure job 为 needs: 'publish' + if: "${{ always() && needs.publish.result == 'failure' && github.repository == 'QwenLM/qwen-code' }}",只在发布真失败且仓库正确时运行,注释说明 cancelled 刻意不算失败(并发组是排队而非取消);job 级 permissions 收窄为 contents: 'read' + issues: 'write',把 workflow 级的 contents: write 在这个 job 上撤掉;actions/checkout 按 SHA 固定(df4cb1c0… # v6.0.3);用内置 secrets.GITHUB_TOKEN 而非管理员 PAT;runs-on: ubuntu-latest,执行的是 release 事件所检出的默认分支里的仓库脚本,不存在跑不可信代码的问题。
  8. .size-baseline 的 +4/-3 与 workflow 体积变化一致,且当前 head 的 Lint & Static pass,说明体积棘轮已对齐;scripts/tests/desktop-oss-workflow.test.js 增加 332 行覆盖,Test (ubuntu-latest, Node 22.x) pass。

CI:当前 head 上 Lint & StaticTest (ubuntu-latest, Node 22.x)Integration Tests (no-AK, No Sandbox)Desktop Shell (ubuntu-22.04)Desktop Shell (windows-2022) 全部 pass,无失败项;review-pr pending,按策略不作为门禁,我没有等待或轮询。

结论:无停留在当前 head 的历史阻塞问题,上一轮两条未确认门禁均已解除,本轮未发现可证明的 Critical,提交 APPROVE。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI at this commit; the new scripts/tests/desktop-oss-workflow.test.js ran on the ubuntu leg and locally on linux only, so its macOS bash-3.2 and win32 short-circuit paths were analysed by three separate auditors but never executed.

Not reviewed: the executable-script lint — .github/scripts/desktop-sync-failure-issue.sh: shellcheck is not installed.

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release-sync.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/sync-desktop-to-oss.yml — actionlint embedded-shell source mapping is not yet supported.

7 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).

Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • .github/workflows/desktop-release-sync.yml:80 — [probe] Critical [fails-closed] [new-surface] D9-1 report_failure job's permissions block omits actions: 'read', so the leg-listing feature this diff adds can never name a failed leg in produc…
  • .github/scripts/desktop-sync-failure-issue.sh:44 — [review] R9-1: The deliberately-degrading "jobs API could not be read" branch is never executed by the new replay suite: the stub's "api "*) arm always exits 0 and the only injected failu…
  • .github/workflows/desktop-release-sync.yml:59 — [review] R9-2: This diff makes the desktop updater feed move automatically on every stable CLI release, but both monotonic guards that protect the feed turn a rollback into a fully green run t…
  • .github/workflows/desktop-release-sync.yml:86 — [review] R9-3: The comment justifying the reporter's ref: 'main' pin says a tag cut before this job merged "carries no copy of it at all". For a release -event run GitHub executes the **tag…
  • scripts/tests/desktop-oss-workflow.test.js:218 — [review] R9-4: not.toContain('$GITHUB_EVENT_NAME') is scoped to two whole workflow files rather than to the dead pattern it was written for. text is the full readFileSync of desktop-re…
  • scripts/tests/desktop-oss-workflow.test.js:269 — [review] R9-5: The new tests pin both ::error:: strings (:268-270, :236-238) and the sha= write (:251), but nothing pins that the step then aborts. This diff nested an if/else around th…
  • scripts/tests/desktop-oss-workflow.test.js:441 — [review] R9-6: The leg-listing fixture holds exactly one failed leg with conclusion: 'failure' , so the timed_out arm of the script's jq select is not discriminated by any assertion, and…
  • scripts/tests/desktop-oss-workflow.test.js:457 — [review] R9-7: The fresh-issue replay asserts only that gh issue create ran, never that it carried --label "${DEDUP_LABEL}" , and the workflow-side test pins RELEASE_TAG but not DEDUP_L…
  • .github/scripts/desktop-sync-failure-issue.sh:64 — [review] D9-2 recovery line advises a clobber=true re-dispatch that is destructive on the OSS-mirror-only failure this diff newly makes reachable
  • .github/workflows/desktop-release.yml:168 — [review] D9-3 provenance comment records the inverse of where the CLI's npm publish actually runs, and a test pins the false sentence
中文说明

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

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI at this commit; the new scripts/tests/desktop-oss-workflow.test.js ran on the ubuntu leg and locally on linux only, so its macOS bash-3.2 and win32 short-circuit paths were analysed by three separate auditors but never executed.

未审查(原文为英文):the executable-script lint — .github/scripts/desktop-sync-failure-issue.sh: shellcheck is not installed.

未检查(工具限制,非阻断):.github/workflows/desktop-release-sync.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/desktop-release.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/sync-desktop-to-oss.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

7 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。

收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 10 条(原文未翻译,列表见上方英文部分)。

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

  • kill-switch placement on the job-graph root, so no build-without-publishing rehearsal exists — already reported as R1-7 (round 7 deferred entry at desktop-release-sync.yml:24, carried in the round 1/3/4/6 review bodies)

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

Not linted (tool limitation, not a blocker): .github/workflows/desktop-release.yml — actionlint embedded-shell source mapping is not yet supported; .github/workflows/sync-desktop-to-oss.yml — actionlint embedded-shell source mapping is not yet supported.

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

  • .github/workflows/desktop-release.yml:158 — [probe] the PR description's Chinese section still certifies that the bundled commit must be reachable from main on either path, which the code, the error text and the English section all contradi…
  • .github/workflows/desktop-release.yml:473 — [probe] the two desktop-node-v2 cache keys were not given the release-path normalisation, so release runs form a third key namespace; the same hand-normalisation now spans 19 sites
  • .github/workflows/sync-desktop-to-oss.yml:45 — [probe] the comment justifying follows_release says the callee can never observe the release, which is measured false — its github.ref is the caller's refs/tags/v…, so the fact is derivable ins…
  • scripts/tests/desktop-oss-workflow.test.js:231 (+3 locations) — [probe] ten rewritten publish gates survive single-site reversion with the suite green, because one whole-job toContain guards eight of them
  • .github/workflows/desktop-release.yml:6 — [review] no design doc records the trigger, the kill switch or the moved trust anchor, and two committed designs in docs/design now describe a flow the code no longer has
中文说明

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

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

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

未检查(工具限制,非阻断):.github/workflows/desktop-release.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持;.github/workflows/sync-desktop-to-oss.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

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

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

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 230 passed · 3 failed · 233 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

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

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

脚本断言:230 通过 · 3 失败 · 233 总计

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report (report.md, truncated)

# PR #11575 deep verification (round 4) — `ci(desktop): publish the desktop app when the CLI releases`

**Verdict: `findings`** — 233 scripted assertions executed, **230 pass / 3 fail**. The central claim is proven load-bearing by a **three-arm** A/B at the new head, and the release arm is proven non-hollow by a full gate census. All three failures are the same class: gates this PR adds that its own suite does not pin, two of them behind assertions that pass for the wrong reason. No blocker, no regression on any pre-existing path.

Verified head: `416249d448cfa614ead662503c3c15ca2ce97c88` (`git rev-parse HEAD^2`)
Base: `3fc1133d4441daa1a8b8a82a8c4aefff12732c54` (`HEAD^1`, CI merge-ref checkout)
Round 3 head was `6b76f733…`. The metadata snapshot lists **25** commits, of which **1** is locally reachable (shallow boundary — see *Not covered*). The snapshot's `baseRefOid` is `34d8048a…`, which has drifted from the checked-out `HEAD^1`; per the CI contract the A/B uses `HEAD^1`.

The newest commit restructured the PR substantially: the separate release-triggered caller (`desktop-release-sync.yml`) and its `report_failure` job are **gone**, and the release event is now handled inside `desktop-release.yml` itself. That deletion supersedes six of round 3's findings outright. The effective diff is now 4 files / +146 −27.

<details>
<summary>中文摘要</summary>

**结论:`findings`(有发现,非阻断)。** 233 条脚本化断言,230 通过 / 3 失败。

**A/B 结论(三臂)**:中心主张在新 head 上成立,且本轮把「触发器」与「放宽判断」两半拆开验证。第三臂 `mid` = head 但把 `!= 'true'` 还原为 base 的 `= 'false'`,正是这一臂给出了本轮最有力的单元格:

- **S2(承重)**:release 臂遇到恶意 tag(tag 提交与其父提交**都**不在 main 上)——`head` **exit 1 拒绝**,错误信息点名实际被检查的父提交;`mid` **exit 0 放行**,输出该 off-main 提交,且**完全没有执行任何 ancestry 校验**。
- **S9(承重)**:release 臂遇到「非 prerelease 的 nightly 形状」tag——`head` **exit 1 大声拒绝**(`must use X.Y.Z`);`mid` **exit 0**,会刻出 `desktop-v0.23.4-nightly.20260914.f024b37689`。

即:让 release 臂**真正执行校验**的不是新增的触发器,而是那处 `!= 'true'` 放宽。既有路径逐字节无回归:dispatch-from-main(S3)、dispatch-from-branch(S4)、packaging-check 两种事件名(S5)、三条 version 路径(S10)的 exit/stdout/stderr/`GITHUB_OUTPUT` 与 base 完全一致。终点级验证(S11)把 head 输出的 sha 交给同样逐字提取的 `Check out Qwen Code source` 步骤,检出树确为 tag 提交、`package.json` 为 0.23.4 而非父提交的 0.23.3。81/81 断言全绿。

**门表达式普查(本轮新增)**:把 head 与 base 的**每一个** `if:`(20 / 19 条)在一个带控制表的表达式求值器下、对 9 种触发上下文求值。结果:release + 变量开 → `prepare`/`build`/`publish`/`sync-oss` 全部放行;变量关 / prerelease / `desktop-v*` tag / fork → `prepare` 被跳过并级联。**「静默跳过检测器」在 release 臂上找不到任何无法解释的 false**——11 条 false 全部被机械归类为 os 门禁、fork 专用、或 electron-bridge 可选项;并按 macOS/Windows/Linux 三条 leg 正向证明签名与验签步骤确实会跑。76/76 全绿。

**3 条失败断言 = 本轮实质发现(F17)**:20 个变异体杀掉 15 个,3 个正向对照**在同一测试文件内**全部被杀。存活者中三个是本 PR 的承重门:

- **M11** 删掉 `Update stable updater feed` 的 release 分支 → 套件全绿。后果正是 PR 正文自己点名的最坏情形:「发布成功但更新源没动,看起来是成功的,实际谁也没收到」。
- **M10** 删掉 `Require updater signing key` 的 release 分支 → 套件全绿。
- **M2** 还原 version 步骤的放宽 → 套件全绿,尽管 S9 已证明它承重。

原因已**实测**而非推断:`expect(getWorkflowJob(...,'publish')).toContain(...)` 断言的那个子串,在 M11 之后仍出现在 **publish 作业自己的 `if:`** 上;在 M10 之后仍出现在 build 作业的**另外 7 行**上。即断言「因错误的原因而通过」。另有 **M6**(release 专用 ancestry 错误文案)无人固定——而它正是 F2 唯一的风险缓解处(点名被检查的操作数)。

**已度量的修复建议**(非目测):把两条断言收窄到 `getWorkflowStep`,并为 version 条件与错误文案各加一条。打上补丁后:prettier 干净、套件仍绿且**测试数不变(10)**、**M2/M6/M10/M11 全部被杀且都是断言级失败**、M12 仍绿(不过拟合)、M7 对照仍被杀、文件逐字节还原。diff 见 `suggested-fix.diff`。15/15 全绿。

**上一轮发现**:F9/F10/F11/F12/F13/F14/F16 因 `desktop-release-sync.yml` 与 `report_failure` 作业被删除而**整体作废**;F8 因 `qwen_code_ref` 描述被重写而**已修复**;F6 **已修复**(本 PR 写入的两个 baseline 条目 Δ 均为 0)。F15 降级为 Nit(三处只剩一处,且测试注释里的强制已消失)。F2 作者已拒绝并说明理由,**我仍同意其保留**,本轮 S2/S6/S11 重测。

**新增更正(F18)**:正文**英文**半已改对(明说 release 臂检查的是 tag 的**第一父提交**、被打包的提交本身并不在 main 上),但**中文**半仍写着「无论哪条路径,被打包的 commit 都必须能从 main 到达」——这正是 S1 单元格**证伪**的那句话(head 输出 `sha=D`,而 `merge-base --is-ancestor D main` 失败)。AGENTS.md 要求两个语言版本同步。属正文更正,非代码问题。

**其余发现(均为 Note/Nit,非阻断)**:**F19** 整合把 `report_failure` 作业一并删掉,于是一次被拒绝的 release 跟随发布(S9/S2 的大声拒绝)现在只剩一个红色 run,无人被通知——相对 base 不是回归(base 根本没有 release 路径),但值得作为决定写明;**F20** release 臂的 node 缓存 key 后缀为空,是第三个独立命名空间,每日 packaging-check(`-true`)与手动发布(`-false`)都暖不到它,开启变量后的第一次发布四条 leg 全冷;**F21** release 臂上 `inputs.electron_bridge` 渲染为空,Electron bridge 永远不会随发布推出(几乎必然是有意的,但正文的取舍清单没点名);**F22** `--target "$GITHUB_SHA"` 逐字节未改(0 行变更含 `--target`),但 release 臂的 `GITHUB_SHA` 按构造就是 off-main 的 tag 提交,故此后每个自动创建的 `desktop-v*` tag 都会指向 main 之外;**F23** 三个消费方文件里的注释因本 PR 而过期(`ci.yml:2239` 的「workflow_dispatch only」已不成立;`desktop-packaging-check.yml:13/44` 的「gated on `dry_run == false`」结论仍对但机制不完整)——其**结论已由普查重测**为真,且对 `schedule` 与 `workflow_call` 两种取值都成立;**F15** 降级为 Nit(三处只剩 `sync-desktop-to-oss.yml:13` 一处,测试注释里的强制已消失)。

**已由生产数据关闭的上一轮未覆盖项**:`production-release` 环境**接受 tag ref 且无需人工审批**——匿名 deployments API 抽样 100 条中 **35 条 ref 为 tag**,含稳定 tag `v0.23.4`;且 `sync-release-to-oss.yml` 的 `sync` 作业门只有 `github.repository`,每次发布都会以 tag ref 部署到该环境。故本 PR 给 `publish` 作业新增该环境不会阻塞、也不会引入人工闸门。

**语料实测(700 个 release,2025-08-06 → 2026-09-15)**:以 `v` 开头却**非** CLI 系列的 tag = **0**(`desktop-v*`、`sdk-typescript-v*`、`cua-driver-rs-v*`、`live-host-v*`、`sdk-python-v*`、`desktop-latest` 全部被前缀门排除)——正文的门禁主张成立,且不存在自我回环。`prerelease=false` 但非纯 `X.Y.Z` 的「漏网」tag = **220**,最新一条 **2026-02-22**,此后 7 个月为 0(C3 再次得到佐证);S9 证明漏网 tag 会被**大声拒绝**而非误发。开启变量的当日成本:近 30 天 **11** 个稳定发布、近 90 天 **43** 个。

**未覆盖范围**:未真实发布 release、未设置仓库变量,Reviewer Test Plan 第 1–2 步只验证了门逻辑;未执行任何桌面构建(签名密钥、四种 runner、公证与 Authenticode 均不可及);`github.event_name` 在 callee 内的取值**仍未判定**(F15 已说明何为文档原文、何为推断),但因普查对 `schedule` 与 `workflow_call` **两种取值都跑过**且结论相同,此缺口不改变任何单元格;yamllint 仍无法安装(`pip3: Permission denied`);25 个提交中仅 1 个本地可达,无法逐个归因;语料为一次性匿名抓取的快照,非实时。

</details>

## Previous-finding status

Re-measured at `416249d4`, not diffed against the old report. Every row below was re-run from the current tree; nothing was carried forward on an input-closure argument.

| # | finding (round 3) | sev | status at `416249d4` |
| --- | --- | --- | --- |
| F1 | suite does not pin the two most load-bearing hunks | Suggestion | **fixed, and stays fixed — with a new residual (F17).** Re-measured with a fresh 20-mutant matrix: **15 killed**, 3 positive controls killed **in the same file**, unmutated control green (10 passed). The two hunks F1 named are now pinned (M1, M3/M4/M5 all killed). Five survivors remain; three are gates this PR adds (F17). |
| F2 | ancestry guard certifies the *parent*, not the bundled commit | Suggestion | **declined-with-rationale; still agreed.** Re-measured: S2 release arm exit 1 names the parent it checked, S1 admits and emits the tag commit `D`, S11 confirms the bundle really is `D` at 0.23.4, and S3 shows the dispatch arm refusing the identical commit. The English body now states the limitation correctly (F18 is about the Chinese half). |
| F3 | mechanism rests on `event_name` propagating into a nested callee | Suggestion | **resolved in code; risk gone.** `follows_release` is an explicit non-required boolean input (`default: false`), passed by the caller; the callee's gate reads `inputs.follows_release` and nothing else (S12). Caller-omission and flipped-default mutations are both killed (M19, M20). |
| F4 | `prerelease == false` is not a self-sufficient stability test | Note | **stands, re-measured against a fresh corpus.** 700 releases: 344 with `prerelease=false`, of which **220** are not bare `X.Y.Z`; newest **2026-02-22**, zero in the ~7 months since. **S9 upgrades the mitigation from argument to measurement**: such a tag is refused loudly (`exit 1`, `must use X.Y.Z`) rather than mis-published — but see F19 for who hears it. |
| F5 | `desktop-v*` tags will point off main | Note | **stands, and the consequence is now routine rather than hypothetical — see F22.** `--target "$GITHUB_SHA"` is byte-identical to base (0 changed lines contain `--target`), so the *line* is pre-existing; what is new is that the release arm's `GITHUB_SHA` is by construction an off-main commit. |
| F6 | `.size-baseline` numbers off | Nit | **FIXED for everything this PR writes.** `desktop-release.yml` recorded 35684 / actual 35684 (Δ **0**), `sync-desktop-to-oss.yml` 11328 / 11328 (Δ **0**), and round 3's offender `desktop-release-sync.yml` no longer exists so it has no stale entry (asserted). `check-workflow-size.sh` exits 0 with `WORKFLOW_SIZE_BASE_SHA=HEAD^1`. Residual drift elsewhere is pre-existing repo-wide (35 files; `desktop-packaging-check.yml` Δ +399 is identical at base and untouched by this PR). |
| F7 | annotated release tag emits a tag *object*, not a commit | Note | **stands, bound re-measured.** S6: head exit 0, emitted value `cat-file -t` = `tag`, `^{commit}` peels to `D`. S11: the shipped checkout step lands on `D` with `package.json` 0.23.4. |
| F8 | `qwen_code_ref` description's first clause reads too strong | Nit | **FIXED.** The description was rewritten to "Qwen Code branch or commit to bundle, reachable from main. A release tag whose own commit is off main is refused here: dispatch checks the ref itself, while only the release event peels to its parent." That matches the measured behaviour of both arms exactly (S2 vs S3/S4). |
| F9 | reporter job cannot read the jobs API (`actions: read` missing) | Critical (bounded) | **SUPERSEDED — the surface no longer exists.** `desktop-release-sync.yml` and its `report_failure` job were deleted by the consolidation; `grep -rn "^  report_failure:" .github/workflows/` now returns only the pre-existing `update-ecs-runner-qwen.yml`. The defect is gone, not fixed. |
| F10 | reporter job's permissions only half-pinned | Suggestion | **SUPERSEDED** — same deletion. |
| F11 | `report_failure` gate rationale contradicts `cancel-in-progress: false` | Note | **SUPERSEDED** — the job and its comment are gone. `sync-desktop-to-oss.yml`'s concurrency block carries no comment at all. |
| F12 | `find-marked-issue.sh` caller list stale | Nit | **SUPERSEDED / self-resolved.** `grep -rl find-marked-issue.sh .github/` returns 2 scripts + 1 test — the third caller this PR had added no longer exists, so the header enumeration is accurate again. |
| F13 | `.size-baseline` for the new file understates it by 1452 | Nit | **SUPERSEDED** — the file is gone; see F6. |
| F14 | reporter's checkout omits `persist-credentials: false` | Nit | **SUPERSEDED** — same deletion. |
| F15 | `follows_release` justification asserts undocumented GitHub behaviour | Suggestion | **stands, downgraded to Nit.** Of round 3's three instances only one survives: `sync-desktop-to-oss.yml:13` still says `github.event_name` in a callee "is always workflow_call". The **test** comment that baked the claim in is gone, and the suite no longer forbids `event_name == 'release'` in a callee — line 45 now *asserts* the caller's gate contains it. So nothing enforces the overclaim any more. See F15 below for the new wrinkle. |
| F16 | comment says npm publish runs "from main" | Nit | **SUPERSEDED** — `grep -rn "npm publish runs from" .github/` returns nothing. |
| C1 | body: "the bundled commit must be reachable from main either way" | correction | **HALF-FIXED — English corrected, Chinese still wrong (F18).** |
| C2 | body: "Both workflows parse as valid YAML" (three change) | correction | **re-worded, now understated.** The body says "The workflow parses as valid YAML" (singular) while the PR changes two. Both do parse: `actionlint` 1.7.12 exits 0 on `desktop-release.yml`, `sync-desktop-to-oss.yml` and the caller `desktop-packaging-check.yml`, with liveness proven. |
| C3 | body: nightlies/previews "arrive as prereleases" | correction | **stands, re-corroborated from a fresh corpus.** 220 leaky tags, newest 2026-02-22, zero since. Today's nightlies do arrive as prereleases, so the gate holds in the current regime; the historical leakies are refused loudly by the version check (S9) rather than by the gate. |

## Central claim

> Publishing a stable `v*` CLI release drives `desktop-release.yml` end to end — admitted at the prepare gate, past the ref and ancestry guards, through signing on every leg, through the feed move and into the OSS mirror — while landing inert until `vars.RELEASE_DESKTOP_SYNC_PUBLISH` is set.
>
> Round 4 adds the claim the restructuring makes possible: **one workflow now plays three roles** (release-triggered, manually dispatched, and reusable callee of the daily packaging check), so every gate has to be correct in all three and none may silently skip on the release arm.

### A/B table — three arms (81/81 assertions pass)

Arm identity asserted before any cell was trusted. Nothing is compiled here, so no `node_modules` symlink could contaminate the control; each arm's `run:` block was extracted by parsing that arm's YAML with `js-yaml`, base's from `git show HEAD^1:.github/workflows/desktop-release.yml`.

| arm | what it is | whole-file sha256 | source-step sha256 | version-step sha256 |
| --- | --- | --- | --- | --- |
| `base` | `HEAD^1` — no `release:` trigger at all | `c4de37a5b5da` | `ee4349e7f2d2` (516 B) | `450855ab7f5a` (1052 B) |
| `mid` | head with **only** the `!= 'true'` widening reverted to base's `= 'false'` | `ec9034f2018f` | `3d21c3d12307` (1319 B) | `450855ab7f5a` (1052 B) |
| `head` | `416249d4` | `1d12cdd8c1d9` | `04491b11cc8b` (1319 B) | `5148a47f4f4b` (1052 B) |

`mid` is the intermediate build the two-cell A/B cannot substitute for. It isolates the widening from the trigger, and it is what turns "the release arm now validates" from a reading of the diff into a measurement: `mid` has the trigger and every release disjunct, and still admits what `head` refuses.

| # | scenario | `base` | `mid` | `head` |
| --- | --- | --- | --- | --- |
| — | `release:` trigger present in the workflow | **absent** (2 triggers) | present | **present** (3 triggers), `types: ['published']` |
| S1 | release arm, well-formed CLI tag (parent on main) | n/a | exit 0, emits `D` | **exit 0**, emits tag commit `D` |
| **S2** | release arm, **hostile** tag: commit *and* parent both off main | n/a | **exit 0 — ADMITS it**, emits `H2`, zero validation, no ancestry fetch | **exit 1**, error names the parent `H1`, emits no sha |
| S3 | dispatch from main, publish (the shipped path) | exit 0, sha `E` | – | **byte-identical** exit/stdout/stderr/`GITHUB_OUTPUT` |
| S4 | dispatch from a feature branch | exit 1, `must run from main.` | – | exit 1, message widened to `…or follow a published release.`, same verdict, no release wording |
| S5 | packaging-check callee, dry run — under **both** `schedule` and `workflow_call` | exit 0 | – | exit 0, output **identical** on both event names |
| S6 | release tag **annotated** | n/a | – | exit 0; emitted value is a `tag` object; `^{commit}` peels to `D` (F7) |
| S7 | release tag on a **root commit** (no parent) | n/a | **exit 0 — admits it** | **exit 128**, fails closed, emits no sha |
| S8 | version step, release arm, stable tag | n/a | exit 0 | exit 0 → `version=0.23.4`, `tag=desktop-v0.23.4`; **`mid` output identical** |
| **S9** | version step, release arm, **nightly shape published as non-prerelease** | n/a | **exit 0 — stamps `desktop-v0.23.4-nightly.20260914.f024b37689`** | **exit 1**, `Published stable Desktop versions must use X.Y.Z`, emits no version |
| S10 | version step, 3 dispatch/callee paths | exit 0 / 1 / 0 | – | **identical** output and stderr on all three |
| S11 | *destination*: shipped `Check out Qwen Code source` fed S1's sha | – | – | exit 0; checked-out HEAD `= D`; `package.json` = **0.23.4**, not the parent's 0.23.3 |
| S12 | sync-oss gate algebra | caller requires `event_name == 'workflow_dispatch'`; callee is main-ref-only | – | caller admits `release` and threads `follows_release`; callee admits via the explicit input, `default: false`, not required |

Witness: **`01-ab-three-arms-widening-is-load-bearing.png`**. Reproduce: `ARTIFACT_DIR=<art> node <art>/ab-gates.mjs`.

S2 and S9 are the pair that matters. On a `release` event no `workflow_dispatch` input is defined, so `${{ inputs.dry_run }}` renders as the **empty string** — not `'false'`. Base's `[ "$IS_DRY_RUN" = 'false' ]` is therefore false and its whole validation block is skipped; head's `!= 'true'` is true and the block runs. The consequence is not about admission (both arms admit the well-formed tag) but about **what gets admitted**: `mid` waves through an off-main tag commit and a nightly-shaped version, silently, with no ancestry fetch at all. The widening is the guard; the trigger only opens the door.

### Gate census — the release arm is not hollow (76/76 assertions pass)

Because this head puts the release event and the reusable-callee role in the **same file**, a forgotten conversion is now a silent skip rather than a compile error. So every `if:` in the workflow (20 at head, 19 at base) was enumerated mechanically and evaluated under 9 triggering contexts.

The evaluator is deliberately narrow and **refuses to guess**: it implements truthiness, operand-returning `&&`/`||`, `!`, same-type `==`/`!=`, `startsWith`, and property access; any other coercion throws. 14 control cells pin its semantics and one asserts a boolean/number coercion is *refused* rather than answered. Every one of the 20 × 9 cells resolved with no refused coercion (asserted) — which also means no cell depended on the ambiguous `null == false`: on the release arm every gate carrying an `inputs.` reference short-circuits on its `github.event_name == 'release'` disjunct first.

| context | jobs that RUN |
| --- | --- |
| release, variable **ON**, stable `v0.23.4` | `prepare`, `publish`, `sync-oss` (`build` has no `if:`, runs via `needs`) |
| release, variable **OFF** | `publish`, `sync-oss` only — `prepare` **skipped**, so the chain cascades to nothing. **Lands inert, as the body claims.** |
| release, `prerelease: true` (nightly) | `prepare` skipped |
| release, `desktop-v0.23.4` | `prepare` skipped |
| release on a **fork** | nothing runs |
| dispatch publish / dispatch dry / callee `schedule` / callee `workflow_call` | **0 changed, 0 removed** vs base on every pre-existing `if:` (asserted per context) |

**Silent-skip detector:** of the 20 conditions, 11 are false on the release arm, and each was classified by a *mechanical* reason rather than a hand-written allowlist — re-evaluating it against every matrix `runner.os` (7 are os-gated and do run on their own leg), `github.repository !=` (1 fork-only), `inputs.electron_bridge` (1). **Zero unexplained** (asserted). The detector is the check that would have caught a forgotten conversion; it found none.

Per-leg positive proof, so "not skipped" is not just an absence: on the release arm with `runner.os` set to each leg, the signing and verification steps really run — macOS (`Import macOS certificate`, `Configure macOS notarization`, `Sign bundled vendor binaries`, `Refresh bundled runtime checksums`, `Verify macOS signature`, `Smoke packaged application`), Windows (`Import Windows certificate`, `Verify Windows signature`), Linux (`Install Linux dependencies`), plus `Require updater signing key for publishing` and the unconditional `Build desktop installers` on all three.

Witness: **`02-census-no-unexplained-silent-skip-on-release-arm.png`**. Reproduce: `ARTIFACT_DIR=<art> node <art>/census.mjs`.

Rendered values, also computed rather than read (full table in `logs/census.log`): `INPUT_VERSION`/`INPUT_REF` → `v0.23.4`; `IS_DRY_RUN`/`IS_DRAFT`/`IS_PRERELEASE` → `''`; tauri `DRY_RUN` → `'false'` on the release arm and `'true'` on the packaging path; `follows_release` → `true` / `false`; `RELEASE_DRAFT`/`RELEASE_PRERELEASE`/`RELEASE_CLOBBER` → `''` (a real, non-prerelease release that refuses an existing tag — matching the body); `concurrency.group` → `desktop-release-publish` on the release arm **and** on a manual publish dispatch, so the two serialise instead of racing.

### Mutation matrix — 15 killed / 20 applied (3 positive controls killed in the same file)

Unmutated control green (10 passed). Every mutant an exact-string replacement against pristine bytes held in memory, restored after each run and on `process.on('exit')`; `git status --porcelain` asserted empty at the end.

Witness: **`03-mutation-matrix-15-of-20-killed-5-survivors.png`**. Reproduce: `ARTIFACT_DIR=<art> node <art>/mutation-matrix.mjs`.

Killed: M0, M0b, M0c (controls), M1, M3, M4, M5, M7, M8, M9, M13, M14, M15, M16, M17, M18, M19, M20.

| id | mutation | result | classification |
| --- | --- | --- | --- |
| **M11** | drop the release disjunct from `Update stable updater feed` | **SURVIVED** | **coverage gap — the sharpest one.** The feed never moves on a release-following publish. See F17. |
| **M10** | drop the release disjunct from `Require updater signing key for publishing` | **SURVIVED** | **coverage gap.** The early explicit guard is unpinned; a missing key would surface later and less clearly. See F17. |
| **M2** | revert the **version-step** widening | **SURVIVED** | **coverage gap**, and load-bearing per S9. See F17. |
| M6 | gut the release-specific ancestry error text | **SURVIVED** | **coverage gap.** Nothing pins the only message that names the operand actually checked — F2's entire mitigation. |
| M12 | revert the tauri `DRY_RUN` override | **SURVIVED** | **redundant defence — correct as it stands, not a gap.** The consumer tests `[ "$DRY_RUN" = 'true' ]`, so the override's `'false'` and the un-overridden `''` behave identically; `grep -rn DRY_RUN packages/desktop-shell/` returns nothing, so that step's own bash is the only reader. The override is explicitness, not load-bearing. |
| M-FIX1 | *candidate fix*: add `draft == false` to the prepare gate | survived (expected) | would be **redundant defence**: `types: ['published']` does not fire for a draft release, so the clause could never decide an outcome. (The corpus also shows 0 drafts, but that is an artifact of anonymous access — GitHub's public releases list omits drafts — so it is not evidence and the trigger type is.) |
| M-FIX2 | *candidate fix*: re-check the variable at the `publish` job | survived (expected) | **redundant defence** — the `prepare` skip already cascades through `needs`. Correct either way; recorded so the unpinned axis is visible. |

Per the unpinned-axis rule, M-FIX1/M-FIX2 staying green on both sides is not reassurance — it is proof the suite pins nothing along those axes. Neither is recommended; both are classified as redundant so they are not mistaken for gaps.

## Corrections

**F18 — the PR body's two language versions now disagree about what the ancestry guard establishes.** This is a correction to the *description*, not a request to change code.

The English half was corrected since round 3 and is now accurate:

> "…on the release arm it checks the tag's first parent — the main commit the release was cut from — because the tag commit itself lives on the release branch and is never reachable from main; what it establishes is that the release was cut from main, not that the bundled commit was reviewed there."

The Chinese half still carries the claim rounds 2 and 3 corrected:

> "其后的可达性检查仍然守着真正的底线——**无论哪条路径,被打包的 commit 都必须能从 main 到达**。"

That sentence is falsified by this round's own A/B cell S1, which asserts both halves of the contradiction: head **emits** `sha=D` (the tag commit) and the fixture asserts `git merge-base --is-ancestor D main` **fails**. On the release path the bundled commit is precisely the thing that is *not* reachable from main; that is why the peel exists. A Chinese-reading reviewer gets the stronger, wrong guarantee — the guarantee F2 was filed against — from the half of the body written for them. `AGENTS.md` asks for both versions to be kept "complete and synchronized in the same change". The English sentence is the text to mirror.

**C2 (re-worded, now understated).** "The workflow parses as valid YAML" is singular; the PR changes two workflow files and both parse. `actionlint` 1.7.12 exits 0 on `desktop-release.yml`, `sync-desktop-to-oss.yml` and the caller `desktop-packaging-check.yml`, with **liveness proven** — a planted `not_a_real_key` was reported and the expected key list enumerated (it includes `environment` and `permissions`, confirming the PR puts both in valid positions), and a planted `SC2086` in an embedded `run:` block was reported through actionlint's shellcheck integration.

**One body claim confirmed rather than corrected.** "the `v` tag prefix so the other release series in this repository do not fire it" is **true across the whole corpus**: of 700 releases, the number whose tag starts with `v` but is not CLI-series is **0**. The series present are `v*`=595, `sdk-typescript-v*`=21, `cua-driver-rs-v*`=15, `dsw-*`=~50, `desktop-v*`=8, `live-host-v*`=3, `sdk-python-v*`=1, plus one-offs — and `desktop-latest`, the updater feed, which is what closes the feedback-loop question: the publish job's own `gh release create desktop-latest` cannot retrigger this workflow.

## Findings

### F17 — Suggestion: three release-arm gates this PR adds are not pinned, and two assertions that look like they pin them pass for the wrong reason

The suite asserts, for both jobs:

```js
expect(getWorkflowJob(releaseWorkflow, 'build')).toContain(
  "github.event_name == 'release' || inputs.dry_run == false",
);
expect(getWorkflowJob(releaseWorkflow, 'publish')).toContain(
  "github.event_name == 'release' || inputs.dry_run == false",
);
```

`getWorkflowJob` returns the **raw YAML text slice of the whole job**, so this is a substring search over every line in it. That substring occurs **9 times** in `desktop-release.yml`. Measured, not inferred (`logs/adjudicate.log`, 19/19 assertions):

- after **M10** removes it from `Require updater signing key for publishing`, the `build` job still contains it on **7 other lines** (the macOS/Windows certificate, notarization, signing and verification steps) — the assertion passes and the guard is gone;
- after **M11** removes it from `Update stable updater feed`, the `publish` job still contains it on its **own job-level `if:`** — same.

M11 is the one to fix first, ranked by observability rather than blast radius: a release-following publish that skips the feed update **succeeds**. It creates the GitHub release, uploads the assets, prints a summary with a release URL, exits 0 — and no existing installation is ever offered the build. That is the failure the PR body itself identifies as the worst outcome ("a published release whose feed never moves looks like success and reaches nobody"), and the suite cannot see it.

M2 is the same shape one step earlier. The suite pins the *message* (`Published stable Desktop versions must use X.Y.Z`) and the *env binding* (`IS_DRY_RUN: '${{ inputs.dry_run }}'`) but not the *condition* that decides whether the message can ever fire — and S9 shows that condition is load-bearing: with it reverted, a leaky non-prerelease nightly is stamped `desktop-v0.23.4-nightly.20260914.f024b37689`.

M6 is a coverage gap rather than a wrong-reason pass: nothing asserts the release-specific ancestry error, so the text that names the operand actually checked — the whole of F2's mitigation — can be replaced with anything.

Reproduce: `ARTIFACT_DIR=<art> node <art>/mutation-matrix.mjs` then `node <art>/adjudicate.mjs`.

**Bounded — what this is not.** No gate in the shipped workflow is wrong today; the census proves every one evaluates as intended on all nine contexts, and the A/B proves the behaviour. This is purely about the suite's ability to *keep* it that way. Nothing here blocks merge.

<details>
<summary>Minimal suggested fix — measured, not eyeballed (15/15 assertions)</summary>

Scope the two assertions to the step that carries them, and pin the two unpinned texts. `suggested-fix.diff` in the artifact dir is the exact, prettier-normalised patch:

```diff
-    expect(getWorkflowJob(releaseWorkflow, 'build')).toContain(
-      "github.event_name == 'release' || inputs.dry_run == false",
-    );
-    expect(getWorkflowJob(releaseWorkflow, 'publish')).toContain(
-      "github.event_name == 'release' || inputs.dry_run == false",
+    // Scope each gate assertion to the STEP that carries it. Asserting on the
+    // whole job passes for the wrong reason: the same substring occurs on the
+    // job-level if: and on seven other build steps.
+    expect(
+      getWorkflowStep(
+        getWorkflowJob(releaseWorkflow, 'build'),
+        'Require updater signing key for publishing',
+      ),
+    ).toContain("github.event_name == 'release' || inputs.dry_run == false");
+    expect(
+      getWorkflowStep(
+        getWorkflowJob(releaseWorkflow, 'publish'),
+        'Update stable updater feed',
+      ),
+    ).toContain(
+      "github.event_name == 'release' || (inputs.draft == false && inputs.prerelease == false)",
+    );
+    // The version step's widening is what makes the stable-shape check run at
+    // all on a release event, where every inputs.* is unset. Pin the condition,
+    // not just the message it guards.
+    expect(
+      getWorkflowStep(
+        getWorkflowJob(releaseWorkflow, 'prepare'),
+        'Resolve version',
+      ),
+    ).toContain(
+      'if [ "$IS_DRY_RUN" != \'true\' ] && [ "$IS_DRAFT" != \'true\' ] && [ "$IS_PRERELEASE" != \'true\' ]',
+    );
+    // The release-specific ancestry error is the only place that names the
+    // operand actually checked (the tag's first parent, not the tag commit).
+    expect(
+      getWorkflowStep(
+        getWorkflowJob(releaseWorkflow, 'prepare'),
+        'Resolve Qwen Code source',
+      ),
+    ).toContain(
+      "the tag commit's first parent $ancestor is not reachable from main",
     );
```

Applied in a scratch copy and driven through the same harnesses (`node <art>/fixcheck.mjs`):

| check | result |
| --- | --- |
| patched suite on the unmutated tree | **green, 10 passed** — count unchanged, assertions added inside existing tests |
| `prettier --check` on the patched test | **exit 0** |
| **M2** | **KILLED**, `AssertionError` in `Desktop release event > gates automatic publishing…` |
| **M6** | **KILLED**, same assertion |
| **M10** | **KILLED**, same assertion |
| **M11** | **KILLED**, same assertion |
| M12 (adjudicated redundant defence) | **still green** — the fix pins only what is load-bearing, no over-fitting |
| M7 control (remove the release trigger) | **still killed** |
| tree afterwards | restored byte-identically (`sha256` asserted) |

Each kill was verified to be an **assertion** failure, not a collection or syntax error, so the red is the intended behavioural mismatch. Zero collateral: the patch adds assertions and touches no production file.

</details>

### F19 — Note: with `report_failure` gone, a refused release-following publish is silent except for a red run

Round 3's head added a job that filed an issue when a release-following publish failed; the consolidation removed it along with its workflow. That deletion is why F9–F14 are superseded, and it is the right call on simplicity — but it moves the failure mode, and the body does not mention it.

Concretely: S9's loud refusal (`exit 1`, `::error::Published stable Desktop versions must use X.Y.Z`) and S2's ancestry refusal are both **visible only as a failed run**. Nobody is notified, and — unlike a CLI release, which has `finalize-release.yml` and `sync-release-to-oss.yml` watching it — nothing downstream depends on the desktop run, so nothing else goes red when it does.

Not a regression: base had no release path at all, so there was nothing to report. Naming it so the removal reads as a decision rather than an oversight. If the maintainers want the reporter back, the sibling `update-ecs-runner-qwen.yml` is the copy source — with `actions: 'read'` included, which is what F9 was.

### F20 — Note: the release arm's node cache is a third namespace the daily packaging check never warms

`actions/cache` key: `desktop-node-v2-${{ matrix.rust_target }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}`. Computed per context (census render table):

| path | key suffix |
| --- | --- |
| daily `desktop-packaging-check` (callee, `dry_run: true`) | `-true` |
| manual publish dispatch (`dry_run: false`) | `-false` |
| **release arm** (`inputs.dry_run` undefined) | **empty** |

So the daily job that exists specifically to keep this path warm populates a namespace the release arm never reads, and the first release after the variable is enabled builds cold on all four legs. Self-correcting from the second release onward. One-word fix if wanted: `-${{ inputs.dry_run == true }}`, or drop the suffix on the release arm deliberately and say so.

### F21 — Note: the Electron bridge can never publish on the release arm

`inputs.electron_bridge` renders as `''` on a release event, so `Create Electron bridge archive` (`runner.os == 'macOS' && inputs.electron_bridge`) is skipped, as are both bridge-manifest blocks in the publish job (`ELECTRON_BRIDGE` empty ⇒ `= 'true'` false). The census classifies this correctly rather than as an unexplained skip, and it is almost certainly intended — the input is described as a *one-time* bridge and its version check rejects anything ≤ `desktop-v0.0.5`. Flagging it because the body's accepted-tradeoff list does not name it, and an unnamed cost is a finding about the description even where the cost would have been accepted.

### F22 — Note: `desktop-v*` tags will now routinely point at off-main commits, and the day-one cost is measurable

Two consequences of the release arm that the body does not name:

**Tag target.** `gh release create … --target "$GITHUB_SHA"` is byte-identical to base (0 changed lines contain `--target`), so the line is pre-existing — but its *value* changes. On a dispatch from main, `GITHUB_SHA` is the main tip; on a release event it is the CLI release tag's commit, which the PR's own comment states "lives on the release branch and is never reachable from main". So every automatically created `desktop-v*` tag will point off main. Arguably more correct — it is exactly the commit that was bundled, which S11 verifies — but it means `desktop-v*` tags stop being main-reachable, which is worth a sentence in the body next to the F2 discussion.

**Cost.** Measured from the corpus rather than estimated: **11** stable bare-`X.Y.Z` CLI releases in the last 30 days, **43** in the last 90 (124 over 13.2 months). Enabling `RELEASE_DESKTOP_SYNC_PUBLISH` therefore starts ~11–14 desktop releases per month, each running the 4-leg matrix (2× macOS at 10× billing, 1× Windows at 2×, 1× Linux) under `timeout-minutes: 120`, plus a publish job and an OSS mirror. Round 3 asked a maintainer to confirm this against the org's minutes; the arrival rate is now measured, the minutes budget is not visible from here.

### F15 — Nit: one comment still states an undocumented GitHub behaviour as fact

`sync-desktop-to-oss.yml:13`: "This workflow is also a reusable callee, so `github.event_name` here is **always** `workflow_call` and can never observe the release that triggered the chain." Round 3 established that GitHub's documentation says the *payload*, `GITHUB_SHA` and `GITHUB_REF` are the caller's, and never states what `event_name` is in a callee. Downgraded from Suggestion: of the three instances round 3 found, two are gone with the deleted workflow and the test comment, and the suite no longer enforces the claim, so a future maintainer who reads the docs will not hit a red test pointing at a rationale that does not hold.

**New wrinkle worth one sentence.** `desktop-release.yml` is itself a reusable callee (of `desktop-packaging-check.yml`) and now contains **13** `github.event_name == 'release'` reads. That is safe — its caller triggers on `schedule` and `workflow_dispatch`, never `release`, and the census ran the callee context under **both** possible values of `event_name` (`schedule` and `workflow_call`) with identical results — but the repo now holds a comment asserting a callee can never see the caller's event name, next to a callee whose entire release arm reads it. Only the word "always" is wrong; the code is right either way.

### F23 — Nit: three comments in consumer files now describe a gating mechanism this PR changed

Swept rather than reported one at a time, since they are one class. `grep -rn "dry_run == false\|workflow_dispatch only" .github/workflows/*.yml` filtered to comment lines returns exactly three sites, all in files that consume `desktop-release.yml`:

| site | what it says now | what is true after this PR |
| --- | --- | --- |
| `ci.yml:2239` | "The desktop-release workflow **(workflow_dispatch only)** is otherwise the sole place this crate is built" | the parenthetical is **false** — the workflow now also triggers on `release`. The surrounding reasoning survives intact. |
| `desktop-packaging-check.yml:13` | "A dry run leaves the signing steps out (they are **gated on `dry_run == false`**)" | the conclusion holds, the mechanism is incomplete: the gate is now `github.event_name == 'release' \|\| inputs.dry_run == false`. |
| `desktop-packaging-check.yml:44` | "every step that needs a signing secret is **gated on `dry_run == false`**, so this run never has to hold one" | same — conclusion holds, mechanism incomplete. |

**The conclusions were re-measured, not assumed.** The census evaluates every signing and secret-holding step under the packaging-check context and all are false, and `publish`/`sync-oss` are skipped — so no dry run gains a signing step and the caller still never has to hold a secret. It also evaluates that context under **both** possible answers to the F15 propagation question (`event_name` = `schedule` and = `workflow_call`) and asserts identical results, which closes a question the comments cannot: the packaging check cannot start requiring a signing secret whichever way `event_name` behaves in a callee.

So this is stale prose, not stale behaviour. Same defect class as F15 and round 2's F-new1 (conclusion right, stated mechanism wrong) — this thread's fourth and fifth instances. Nothing pins any of the three: `ci.yml` and `desktop-packaging-check.yml` are not in this PR's diff and no test asserts these comments. Cheapest fix is `ci.yml`'s parenthetical (`workflow_dispatch and release`) plus `dry_run == false` → `dry_run == false or a release event` in the two packaging-check comments.

**One favourable consequence of checking the read site at all.** `ci.yml`'s `desktop_shell` job path-filters on `\.github/workflows/desktop-release\.yml`, so because this PR changes that file, the job runs on this PR's own CI — crate compile plus release-config tests on `ubuntu-22.04` and `windows-2022`. The workflow therefore gets crate-compile coverage automatically; this report did not have to substitute for it.

### Carried, unchanged

**F2** (declined-with-rationale — I still agree; S2/S3/S11 re-measured: the release arm admits and bundles the tag commit, the dispatch arm refuses the identical commit, and the error names the parent it actually checked). **F4** (re-measured against a fresh 700-release corpus; leaky tags are refused loudly by S9, not by the gate). **F5** (line pre-existing; consequence sharpened into F22). **F7** (annotated tag emits a tag object; destination verified correct). **C3** (nightlies arrive as prereleases in the current regime; 0 leaky since 2026-02-22).

### Credit where the restructuring earned it

The consolidation is a net simplification and it dissolved a real problem rather than fixing it. With the release event handled by a separate caller workflow, that caller would have had to hand signing secrets across a `workflow_call` boundary — the suite even asserts `not.toContain('secrets: inherit')`, so it would have needed an explicit list of every secret. Handling the event in `desktop-release.yml` directly means the run gets repository secrets with no passing at all. It also removed an entire workflow file, the `report_failure` job and its three scripts, and with them six findings. The diff went from a new file plus edits to 4 files and +146 −27.

## Gates

| gate | result |
| --- | --- |
| `desktop-oss-workflow.test.js` unmutated | **10 passed** — base has **6** `it()` blocks, so this PR adds **4**. Round 3's head had 15; the 5 that disappeared went with `desktop-release-sync.yml`. |
| `release-workflow.test.js` — reads the changed workflow, **not** touched by this PR | **63 passed**, exit 0 |
| mutation matrix | **15 killed / 20 applied**; 3 positive controls killed **in the same file**; unmutated control green; tree restored (`git status --porcelain` empty, asserted) |
| survivor adjudication | 19/19 — the "passes for the wrong reason" claim is measured line-by-line, not inferred |
| candidate-fix measurement | 15/15 — patched suite green at the same test count, prettier-clean, all four survivors killed by assertion, M12 still green, M7 control still killed, tree restored byte-identically |
| `actionlint` 1.7.12 on the 3 workflows | **clean, exit 0**; **liveness proven twice** — a planted `not_a_real_key` was reported with the expected key list, and a planted `SC2086` in an embedded `run:` block was reported through the shellcheck integration |
| `prettier --check` on the 3 changed files it can parse | **clean, exit 0**; **liveness proven** (a planted formatting 

...truncated -- full content in the run artifacts.
Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/desktop-oss-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/desktop-oss-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/desktop-oss-workflow.test.js: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 2 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 3 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 4 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)
round 5 · scripts/tests/desktop-oss-workflow.test.js: P (exit 0)

Evidence images

01-ab-three-arms-widening-is-load-bearing

02-census-no-unexplained-silent-skip-on-release-arm

03-mutation-matrix-15-of-20-killed-5-survivors

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

Approving pinned to 416249d448cfa614ead662503c3c15ca2ce97c88.

No Critical findings from static review; Qwen Code CI is completed/success on this commit with zero pending pull_request runs. The three prose-accuracy defects and the fact that the release trigger itself has never fired are recorded in the Stage 2 and Stage 3 comments as non-blocking.

This approval supersedes this bot's five stale CHANGES_REQUESTED reviews, all of which sit on earlier heads (65076ebb668ef602) and which GitHub will not clear on its own — that stale review is what reviewDecision has been reporting.

It is one vote, not two: main requires two approving reviews, and the earlier human APPROVE was dismissed by the scope-correction push, so a human approval is still needed. Flipping RELEASE_DESKTOP_SYNC_PUBLISH also remains a human decision — this merges with it off.

@qqqys

qqqys commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

独立验证报告 — head 416249d448cfa614ead662503c3c15ca2ce97c88

核对时间:2026-09-15T19:45Z 起。下面只写我在本次核对里实际测量到的内容,未测项单列在第 5 节。

结论

三条门禁在当前 head 上重新独立确立(不是从上一轮继承):CI 按 lane 名逐一核对全绿、当前 head 上唯一的最新裁决行是 ci-bot 的 APPROVED、未发现 Critical。本 PR 的改动面没有任何 CLI 代码路径,因此 tmux e2e 这一条不适用(不是"跳过"),替代验证见第 3 节。

1. blob 溯源

head 416249d448cf 的 4 个改动文件全部经 contents 路由取回,并对每个 blob 重算 sha1('blob ' + len + '\0' + bytes) 与 API 声明的 sha 比对:

文件 声明 size 实取 bytes size sha
.github/workflows/desktop-release.yml 35684 35684
.github/workflows/sync-desktop-to-oss.yml 11328 11328
.github/workflows/.size-baseline 2348 2348
scripts/tests/desktop-oss-workflow.test.js 9931 9931

4/4 一致。.size-baseline 里记录的两个数字(35684 desktop-release.yml11328 sync-desktop-to-oss.yml)与上面实测字节数逐字相同,体积门禁自洽。

2. 门禁 leg 1 / leg 2(在当前 head 上重新读取)

leg 1(裁决行)pulls/11575/reviews 全量 37 行(未触 100 行分页上限),当前 head 上有且仅有 1 条裁决行 —— qwen-code-ci-bot APPROVED(id 5214523755,18:55:38Z,正文 904 字符)。因果性可过:head 提交时间 2026-09-15T15:26:46Z,该 approve 提交于 18:55:38Z(+12,532 s),即它是这个 head 存在之后写下的,不是被 GitHub 重新锚定到新 head 的旧行。ci-bot 的 5 条 CHANGES_REQUESTED 全部停在更早的 head(65076ebb668ef602)。

leg 2(CI)commits/416249d448cf/check-runs 全量分页取回,total_count = 71 == items_fetched = 71。conclusion 分布 {skipped: 55, success: 16}零 failure、零 cancelled、零 timed_out;按 lane 名去重取最新一次后为 {success: 11, skipped: 21}没有任何一条 lane 的最新结论落在 success/skipped 之外。产品 lane 逐名核对为绿:Lint & Static (ubuntu-latest, Node 22.x)Test (ubuntu-latest, Node 22.x)Integration Tests (no-AK, No Sandbox)Desktop Shell (ubuntu-22.04)Desktop Shell (windows-2022)web-shell E2E Smoke (ubuntu-latest, Node 22.x)。GraphQL statusCheckRollup.state = SUCCESS,与按名核对的结论一致(这里只把"两路一致"当交叉验证,不当证据本身)。

3. 替代验证:改动面不是 CLI,e2e 不适用

改动是 2 个 GitHub Actions workflow + 1 个体积基线 + 1 个 Node 测试文件,packages/ 下的改动文件数为 0,因此不存在可被 tmux 驱动的路径。替代检查(全部在 head blob 上做):

  1. OSS 镜像不会被静默跳过。 desktop-release.yml:780follows_release: "${{ github.event_name == 'release' }}",而 sync-desktop-to-oss.yml:45 的内层 ifgithub.repository == 'QwenLM/qwen-code' && (github.ref == 'refs/heads/main' || inputs.follows_release)。release 事件的 github.refrefs/tags/<tag>,第一支恒假 —— 这正是 R1-2 当初的形状;此处第二支在 release 臂上恰为真,镜像会运行而不是被跳过。
  2. 触发面收紧到位。 prepare job 的 if: 在 release 臂上同时要求 github.repository 正确、vars.RELEASE_DESKTOP_SYNC_PUBLISH == 'true'、tag 以 v 开头、release.prerelease == false
  3. 可达性检查保留且分臂。 release 臂校验 ${sha}^,dispatch 臂校验 $shamerge-base --is-ancestor 未删。tag 提交本身仍是被打包的产物,这是本 PR 明确写下的信任锚(打 tag 与发 release 的权限)。
  4. 合入即惰性。 仓库变量默认未设置,合入本身不改变任何行为。

4. 核对过、但单独提交的两点(避免重复反馈)

  • PR 描述中文段仍写着"无论哪条路径,被打包的 commit 都必须能从 main 到达",与代码(release 臂校验的是 tag 的父提交)不一致。ci-bot 已在第 10 轮的延后条目里记录(.github/workflows/desktop-release.yml:158),我不重复提交。
  • release 臂上 RELEASE_CLOBBER 为空,因此若 desktop-vX.Y.Z 已存在,publish 步骤会以 "Re-run with clobber=true" 失败,而该输入在 release 触发下不可达(fail-closed,且在变量开启前不可达)。作者已在 PR 的"风险与范围"一节自行写明这一点,我不提交。

5. 未测项

  • release 触发本身从未真实跑过一次(该 workflow 需要一次真实的正式版发布 + 变量开启),我也没有真实触发 release。这是本 PR 自身的披露项,不是本次核对的遗漏。
  • 我没有复跑 scripts/tests/desktop-oss-workflow.test.js

— qqqys(自动巡检轮次)

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

APPROVE — 独立验证已完成,head 416249d448cfa614ead662503c3c15ca2ce97c88

本 approve 锚定 head 416249d448cfa614ead662503c3c15ca2ce97c88(最后提交于 2026-09-15T15:26:46Z)。验证报告是上一条 issue comment 5687118105

依据(全部在当前 head 上重新测量,未从上一轮继承):

  • 当前 head 上唯一的最新裁决行是 qwen-code-ci-bot APPROVED(18:55:38Z,id 5214523755),因果性可过:head 提交于 15:26:46Z,该行写于 18:55:38Z(+12,532 s),不是被重新锚定的旧行。
  • CI 按 lane 名逐一核对全绿:71/71 check-runs 全部取回,零 failure / 零 cancelled;按名去重后无一条 lane 的最新结论落在 success/skipped 之外;statusCheckRollup = SUCCESS 与之相符。
  • 4/4 改动文件的 blob 均通过 size + 重算 sha1 比对;未发现 Critical。
  • tmux e2e 不适用:本次改动没有任何 CLI 代码路径(packages/ 下改动文件数 0),改用 workflow 内的门禁与接线核对替代,逐条列在报告的第 3.1–3.4 节。

说明:我上一次的 approve(review id 5210773808,锚定 0428a358)已被 scope-correction push 自动 dismiss,所以本条是对新 head 的重新裁决,而不是沿用旧结论。ci-bot 的 approve 正文也指出本仓库 main 需要两个 approving review,人类这一票正是被那次 push 清掉的那一票。

— qqqys(自动巡检轮次)

@yiliang114
yiliang114 added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit 888528d Sep 15, 2026
90 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants