ci(desktop): publish the desktop app when the CLI releases - #11575
Conversation
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.
|
Re-run on head Template complete ✓ — all nine headings present, including a real Reviewer Test Plan and an honest Problem: observed, not theoretical. The desktop app's own version line has drifted to Direction: aligned, and the strongest signal is that this isn't a new idea. Worth saying plainly, because this is a release path: merging this changes nothing. I traced the skip chain with the variable unset — Size: not applicable. No core paths — nothing under Approach: the scope feels right now, and the round-8 correction is why. The earlier shape carried a standalone Risk: no elevated risk signals — none of the four files match the high-risk path patterns. Moving on to code review. 🔍 中文说明应作者请求在 head 模板完整 ✓——九个标题齐全,包含一份真实的 Reviewer Test Plan,并且 Before & After 老实写了 问题:是已观测到的,不是理论性的。桌面应用自己的版本线已经漂到 方向:对齐,而且最有力的信号是这并不是一个新想法。 因为这是一条发布路径,有一点要说清楚:合并本 PR 不改变任何行为。 我顺着变量未设置时的跳过链走了一遍—— 规模:不适用。没有触及核心路径—— 方案:现在范围是合理的,而这正是第 8 轮修正的结果。早先的形态带了一个独立的 风险:无升级风险信号——四个文件都不匹配高风险路径模式。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewNo 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:
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:
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 APIPer 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.
The Sandboxed verification would settle what CI cannot: 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 Real-scenario tmux testing: N/A — this run is unattended CI ( 中文说明代码审查:没有 Critical 阻断项。 我专门去找"会不会在错误的时机发布""会不会发布错误的东西",静态能触及的每一处,失效即关闭的链条都是成立的。 我实际核对过、而不是想当然的几点:
非阻断项,三条在更早的轮次都已报告并被延后。我在这里重述只是为了让准备合并的维护者一次看全:
复用检查:本 PR 扩展了两个已存在的 workflow,而不是新加一个平行的;它加的那道门就是插件那道。这里没有该复用而未复用的新代码。 测试证据:本 PR 自己的 CI,通过 API 读取。 按 skill 规则我没有构建或执行本 PR 的任何代码,审查是静态的,下面的证据是 PR 自己在被审 commit 上的 CI。
对本 diff 真正有意义的是 沙箱验证能解决 CI 解决不了的部分: 未验证项及原因:release 触发从未真正点燃过,因为要验证它就得发布一次真实 release 并设置一个仓库变量——两者都超出评审者能从 diff 做到的范围。 真实场景 tmux 测试:N/A——本次是无人值守 CI 运行( — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
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 Is every change necessary? Yes. The one piece of churn is a single deleted blank line before 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 Two things a maintainer should weigh before merging, neither of which I can settle from the diff:
Mechanically, for whoever picks this up: my approval below is pinned to 中文说明信心度:4/5——技术上干净、已收敛、在被审 commit 上全绿,并且在有人打开变量之前是惰性的;扣掉的那一分在于触发器本身从未点燃过,以及行文中仍有三处准确性缺陷。 退一步看:对我起决定作用的一点是,这个 PR 在压力下变小了而不是变大了。到第 10 轮,它是 4 个文件、+146/−27,从此前的 6 个文件、+584 降了下来;而且这次收缩不是换个说法——它删掉了一个独立的调用方 workflow、一个 86 行的失败上报器和 332 行测试,并因此删掉了本 PR 最难的那个问题(一个无法可靠观察调用方触发事件的可复用被调方)。第 10 轮的 PR 大多是反过来的。这一个收敛了,评审通道也一起收敛了:13 条 thread 全部 resolved、未解决数为 0,最近一次自动审查没有提出新发现,且其前已连续两轮持平。 拿我自己的思路与 PR 的做法比——我会做同样的事,而且很可能先走一遍慢的那条路。在既有的 每一处改动都必要吗?是。唯一的杂音是 六个月后维护它我会骂作者吗?不会——但有一个保留意见,也正是正文自己提出、而且我认为说得公道的那个:桌面端独有的修复在两次 CLI 发布之间没有版本号可用,绕法是切一个 CLI 补丁版。这是一笔真实且持续的成本,而它是被披露出来的、不是被藏起来的,所以我不把它当阻断项。而且这笔成本只有在有人打开变量之后才会到来。 我确实不确定的地方,以及它为何不构成阻断:中心主张关乎一个评审者无法点燃的触发器。静态能查的我都查了—— 有两件事请维护者在合并前权衡,都不是我能从 diff 里判定的:
给接手的人一个机制上的说明:下面我这次的批准钉在 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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)
- 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
|
@qwen-code /triage |
|
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 reportPR #11575 deep verification —
|
| 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.mjsFull 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') |
1 — v (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.3 → desktop-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.
- "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. - "Both workflows parse as valid YAML" — three workflow files change (
desktop-release-sync.ymladded,desktop-release.ymlandsync-desktop-to-oss.ymledited), plus.size-baseline. All three do parse andactionlintis clean on all three; the count is just off by one. - "nightlies and previews … arrive as prereleases" — that is a property of
run-release-step.sh's--prereleaseflag, not an invariant of thev*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.mjsMeasured 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
fiNot 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_nameis documented only as "the name of the event that triggered the workflow run"; the Contexts and Reuse-workflows pages carry no note aboutworkflow_call. I fetched and searched both, plus the workflow-syntax reference. - Run-level
githubcontext does reach a nested callee — proven from production data. Run34451486651(Desktop release 0.3.0,event=workflow_dispatch,head_branch=main, conclusionsuccess) contains a jobMirror stable Desktop release to Aliyun OSS / Mirror= success. That job is the nested callee'ssyncjob, whose gate isgithub.repository == 'QwenLM/qwen-code' && github.ref == 'refs/heads/main'. A skipped gate reportsskipped, notsuccess. Sogithub.refandgithub.repositorypropagate into aworkflow_callcallee, and the callee's jobs are listed under the caller's run id — which is the same architectural fact that determinesevent_name. Corroborated at the destination: the objects it wrote are publicly fetchable (…/desktop/v0.3.0/desktop-latest.jsonand…/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 referencegithub.event_name— both introduced by this PR. - Under the pessimistic reading the failure is loud, not silent. If
event_namewereworkflow_callin a callee,prepareexits 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:
- All 11 tests are
toContainassertions 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. - A skipped mirror reports green — confirmed from production data, and still unguarded. Run
34500331103(desktop-packaging-check) hasPackage / 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.jsonlists 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.js — 211 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_namepropagation into a nestedworkflow_callcallee 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 installed —
node scripts/lint.js --setupreports/bin/sh: 1: pip3: Permission denied.actionlint(1.7.12) andshellcheck(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: plantingnot_a_real_inputproducedinput "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:scriptsas 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.jsfails withEACCES: permission denied, mkdtemp '/__w/qwen-code/qwen-code/.qwen/gate-e2e-XXXXXX'—.qwenisdr-xr-xr-x root:rootand this job runs asuid=1000(node). Reverting all five PR files to base reproduces 15 failed | 52 passed (67), byte-identical.install-script.test.jsfails ondocker build failed;command -v dockeris 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-workflow11,workflow-size211,release-workflow63,cua-driver-release-workflow6.production-releaseenvironment protection rules are not inspectable without a token. Inferred to accept tag refs fromrelease-vscode-companion.yml, which uses the same environment on arelease-triggered run and is reported to be working; if a deployment-branch policy restricted it tomain, the companion would already be failing.- Per-commit attribution was partly out of reach.
git rev-list HEAD^1..HEAD^2returns 1 commit while the metadata snapshot lists 2, becauseHEAD^2is itself a shallow boundary — exactly the case where the bare count is misleading. Commit 1's own parent object (424e40cc1f) is present, sogit diff 65076ebb^ 65076ebbworked and commit 2's diff was computed asgit 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 stablevtag; does not fire with the variable off, on a prerelease, ondesktop-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.ymlexists 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: 120per 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 againstGET /repos/QwenLM/qwen-code/actions/runs?created=>2026-08-01and 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 only — git 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
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-conflict/jmtx4nkoby7
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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)
|
⏳ Historical-head review — head moved to 中文说明⏳ 历史 head 评审 —— 本次评审进行中 head 移动到了 |
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
left a comment
There was a problem hiding this comment.
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 awayscripts/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)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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 1scripts/tests/desktop-oss-workflow.test.js:163 — [review] Mirror's checkout ref silently switches with the triggerscripts/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)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-pr-closeout/jmtxl34z1yy
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
|
Recording the deferrals from this round so nothing is silently dropped. Fixed in
Also Deferred, not fixed:
The |
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>
|
Closing out the fourth deferred item from round 6, which the previous comment did not cover. Fixed in 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:
Both produce a published The rest of the scripts suite shows 16 local failures here, all environment: Status of the other three round-6 deferrals: two were fixed in Still blocking the merge is the bot's |
|
@qwen-code /triage |
|
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 reportPR #11575 deep verification (round 2) —
|
| # | 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 | changed — desktop-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 untilvars.RELEASE_DESKTOP_SYNC_PUBLISHis 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-branch → push-release-branch → create-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.Z → version=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 | killed — AssertionError … 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 == false → true |
– | killed |
| M23 | cancel-in-progress: false → true |
– | 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 # M18Minimal 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:
- The companion this PR says it mirrors does not depend on propagation.
release-vscode-companion.ymldeclareson: release: types: ['published']itself (plusworkflow_dispatch), so itsgithub.event_name != 'release' || …is evaluated in a directly-triggered run.desktop-release.ymldeclares onlyworkflow_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. - The silent-skip hazard is largely closed by ordering, not by the expression.
sync-oss.needs = [prepare, build, publish], andprepare's bash gate readsGITHUB_EVENT_NAME— the same underlying field by a different mechanism. If the event does not reach the callee,prepareexits 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=releasewhile 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.0…v0.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.ymlhas 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 (run34451486651's nested-mirror job list and the public OSS objects) — carried forward as an argument, not as a new measurement. github.event_namepropagation into a nestedworkflow_callcallee 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-repositoryis true;git rev-list HEAD^1..HEAD^2returns 1 commit while the metadata snapshot lists 11, becauseHEAD^2is itself a shallow boundary — exactly the case where the bare count misleads. Round 1's head956b0b82and the eight commits since are not present locally (git cat-file -tfails for each), so this round verifies the aggregateHEAD^1..HEADdiff 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 installed —
node scripts/lint.js --setupreports/bin/sh: 1: pip3: Permission denied.actionlintandshellcheckinstalled and ran, both proven live. - The repo's own
--shellcheckgate does not scan workflow YAML at all. It selects files by mime typetext/x-shellscriptfromgit ls-files, so embeddedrun:blocks are never linted in CI — pre-existing, and the reason I linted the extracted scripts myself. Separately,runCommandusesexecSyncwithoutpipefailand the recipe ends insed, so that gate exits 0 even when shellcheck reports findings (observed: 12 warnings on unrelatedscripts/*.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:scriptsas a whole was not re-run. Round 1 established it is not green in this container for environmental reasons proven by A/A control (.qwenisdr-xr-xr-x root:rootwhile the job runs asuid=1000(node);dockeris absent). I ran the six suites that bear on this PR plus the two all-workflow census assertions instead;check-tui-dep-direction.test.jsandinstall-script.test.jswere not exercised.production-releaseenvironment 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 only — git 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
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
当前状态复核(head
因此重新 request @chiga0 @qqqys:只需要一个 approve,或把 |
…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.
|
CI attribution on head Run 34942522573. 1.
|
qqqys
left a comment
There was a problem hiding this comment.
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。我按「会不会在错误时机发布」「会不会覆盖既有产物」「新增的写权限面是否被围栏」三条主线核对:
- 触发面不可被外部触发,四道门禁齐全。
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。 - 不会覆盖既有产物,也不会取消进行中的发布。 调用 reusable workflow 时传
dry_run: false、draft: false、prerelease: false、clobber: false;concurrency配cancel-in-progress: false,串行化且绝不中断一次发布到一半的 run。 - 祖先检查被保留,并针对 release tag 的形状做了正确适配。
desktop-release.yml里 main 分支要求从if [ "$GITHUB_REF_NAME" != 'main' ]放宽为... && [ "$FOLLOWS_RELEASE" != 'true' ],但紧随其后的可达性检查没有删:release arm 检查的是 tag 提交的第一父,理由成立——release tag 指向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/<tag>上的版本号提交,它 squash 合回 main 后是另一个 commit,永远不会从 main 可达,而其父提交正是发布所切出的那个 main 提交。两条 arm 的错误信息分别给出,release arm 还点名了两种可能成因(非 main 的源 ref,或版本号提交之后又往 release 分支加了提交)。 - 新增
environment: name: 'production-release',是收紧而非放松。 发布 job 现在挂在具名环境下,可以叠加环境保护规则并隔离其 secret。 - 新输入
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',其余条件一个没少。 - OSS 镜像的放行是必要且最小的一次放宽。
sync-desktop-to-oss.yml的门从github.ref == 'refs/heads/main'改为(github.ref == 'refs/heads/main' || inputs.follows_release),仓库限定保留。不改这一条的后果是:跟随 release 的发布只移动 GitHub 侧 feed,而 updater 的主端点(OSS 镜像)不动,已安装的客户端永远收不到新版本——看起来发布成功,实际谁都收不到。 - 新增的
desktop-sync-failure-issue.sh(+86)与其 job 围栏都正确。- 脚本侧:
set -euo pipefail;REPO/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-failurejob 为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 事件所检出的默认分支里的仓库脚本,不存在跑不可信代码的问题。
- 脚本侧:
.size-baseline的 +4/-3 与 workflow 体积变化一致,且当前 head 的Lint & Staticpass,说明体积棘轮已对齐;scripts/tests/desktop-oss-workflow.test.js增加 332 行覆盖,Test (ubuntu-latest, Node 22.x)pass。
CI:当前 head 上 Lint & Static、Test (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
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI 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
left a comment
There was a problem hiding this comment.
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)
|
@qwen-code /triage |
|
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)Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
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 (65076ebb … 668ef602) 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.
独立验证报告 — head
|
| 文件 | 声明 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.yml、11328 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(65076ebb … 668ef602)。
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 上做):
- OSS 镜像不会被静默跳过。
desktop-release.yml:780是follows_release: "${{ github.event_name == 'release' }}",而sync-desktop-to-oss.yml:45的内层if是github.repository == 'QwenLM/qwen-code' && (github.ref == 'refs/heads/main' || inputs.follows_release)。release 事件的github.ref是refs/tags/<tag>,第一支恒假 —— 这正是 R1-2 当初的形状;此处第二支在 release 臂上恰为真,镜像会运行而不是被跳过。 - 触发面收紧到位。
preparejob 的if:在 release 臂上同时要求github.repository正确、vars.RELEASE_DESKTOP_SYNC_PUBLISH == 'true'、tag 以v开头、release.prerelease == false。 - 可达性检查保留且分臂。 release 臂校验
${sha}^,dispatch 臂校验$sha,merge-base --is-ancestor未删。tag 提交本身仍是被打包的产物,这是本 PR 明确写下的信任锚(打 tag 与发 release 的权限)。 - 合入即惰性。 仓库变量默认未设置,合入本身不改变任何行为。
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
left a comment
There was a problem hiding this comment.
APPROVE — 独立验证已完成,head 416249d448cfa614ead662503c3c15ca2ce97c88
本 approve 锚定 head 416249d448cfa614ead662503c3c15ca2ce97c88(最后提交于 2026-09-15T15:26:46Z)。验证报告是上一条 issue comment 5687118105。
依据(全部在当前 head 上重新测量,未从上一轮继承):
- 当前 head 上唯一的最新裁决行是
qwen-code-ci-bot APPROVED(18:55:38Z,id5214523755),因果性可过: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(自动巡检轮次)













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.0while the CLI is at0.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
vtag 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*orsdk-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
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
desktop-v0.3.0behind as the last of the old numbering.packages/desktop-shell/package.jsonstays at0.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 withoutclobber, which the automatic path keeps atfalse; 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 bareX.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.Linked Issues
Related to #8092.
中文说明
这个 PR 做了什么
让桌面应用跟随 CLI 发布,与 VS Code 插件现有的做法一致:当 Qwen Code 发布正式版时,桌面应用以相同版本号构建并发布。
它合入后不会生效。控制它的仓库变量默认关闭,因此在有人主动打开之前,合并本 PR 不改变任何行为。
为什么需要
桌面应用是 CLI 的一层外壳。"这是哪个版本"这个问题真正有意义的答案,是它内含的那个 CLI——这也正是它自己的版本线漂到
0.3.0、而 CLI 已在0.23.2的原因:一个东西两个数字,而且小的那个会让人低估它。跟随 release 事件可以让桌面应用免费获得 CLI 的版本号,并把"何时该发一次桌面版"这个问题,替换成一个对 CLI 而言早已确定的答案。三道门沿用插件的设计,精神不变:一个仓库变量,使其无需改代码即可停用;
vtag 前缀,使本仓库其他发布系列不会触发它;仅正式版,使 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 配置改动,无用户可见界面。
测试平台
该 workflow 能作为合法 YAML 解析,被修改的条件也有更新后的测试固定;但 release 触发只能通过实际发布一次 release 来验证。
风险与范围
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。