Repository navigation
feat(desktop): add a linux-aarch64 leg to the Desktop release matrix - #12833
Conversation
ARM64 Linux has no Desktop artifact: the build matrix stops at x86_64-unknown-linux-gnu, so desktop-latest.json carries four platform keys and an ARM64 installer that trusts the feed falls back to the x86_64 AppImage, which cannot execute (#12806). The matrix row is not the whole change. Both manifest scripts select a Linux AppImage by extension alone and require exactly one match, so a second Linux leg breaks publish *after* every build succeeded: Expected one updater artifact for linux-x86_64, found 2 The Linux selection is now arch-explicit in create-desktop-update-manifest.mjs (which gains the linux-aarch64 key), in create-electron-bridge-manifest.mjs, and in the Electron bridge's release-assets glob. Tauri's AppImage arch tokens are `_amd64`/`_aarch64` while the .deb beside them uses Debian's `_amd64`/`_arm64`, so the two Linux legs cannot be told apart by the Debian token alone. The arm64 leg builds on ubuntu-22.04-arm rather than ubuntu-24.04-arm so both Linux artifacts keep one glibc floor (2.35); 24.04 would need 2.39 and drop Ubuntu 22.04 / Debian 12 arm64, which the x64 artifact supports. Re-mirroring an already-published release through sync-desktop-to-oss.yml has to reproduce the feed that release actually shipped, so that path passes --allow-missing-platform linux-aarch64. The fresh-build path stays strict: a leg that failed to upload must fail the mirror, not be published as missing. test-release.js gains a check that the matrix's rust targets and the manifest's platform keys stay in sync — a leg nobody taught the manifest about ships silently, which is the failure this issue reports. Fixes #12806 Co-authored-by: Qwen-Coder <[email protected]>
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed at 9b920a22. No P0/P1 — review record for a non-author maintainer vote (own PR).
Verified the load-bearing details:
- Arch tokens are correct: the manifest matches Tauri's real AppImage suffixes (
_amd64/_aarch64), not the Debian ones — and neither pattern can match the other's artifact, so the exactly-one-match invariant holds with both legs present. - glibc floor rationale is real: the arm64 leg pins
ubuntu-22.04-arm(glibc 2.35, shared with the x64 leg) rather than 24.04 (2.39), which would have produced an artifact that refuses to start on Ubuntu 22.04 / Debian 12 arm64. - The
--allow-missing-platformescape hatch is scoped exactly right: only the OSS re-mirror-of-an-existing-release path passes it (SOURCE = 'release'), fresh builds stay strict, and the tests pin all three shapes (absent without the flag fails; wrong platform name still fails; right name succeeds and omits the leg). - The matrix↔feed cross-check test (
testReleaseMatrixCoversUpdaterPlatforms) closes the original #12806 failure mode — a leg nobody taught the manifest about can no longer ship silently. - Electron bridge correctly pins its x64-only AppImage selection in both the manifest script and the workflow glob.
CI green at this head (13 pass, 0 fail).
chiga0
left a comment
There was a problem hiding this comment.
No confirmed blocking findings.
Approval withheld: arm64 build has never been executed (structural — PR CI cannot run the release matrix).
Scope: All 7 changed files reviewed in full. Not reviewed: arm64 build execution (requires a live release-matrix run; outside reviewer reach by construction).
What I verified:
-
All three exact-one-match sites updated correctly. On
main, three code paths assert exactly one*.AppImagehit. The diff patches all three: (a)create-desktop-update-manifest.mjsswitches from/.AppImage$/ito arch-specific/_amd64.AppImage$/iand/_aarch64.AppImage$/i; (b)create-electron-bridge-manifest.mjsswitches to/_amd64.AppImage$/i(bridge is x64-only by design); (c) thedesktop-release.ymlfeed-step glob switches to*_amd64.AppImage. The remaining consumers —Collect artifacts(arch-agnostic allow-list), OSS mirror asset checks (existence-onlyfind … -print -quit),sha256sum, andgh release create— are count-agnostic and need no change. -
allowMissingPlatformsparsing is safe. When--allow-missing-platformis absent,options['allow-missing-platform']isundefined, falls back to'', splits to[''], trims, andfilter(Boolean)removes the empty string — emptySet, strict mode. No silent opt-out. -
The
if (!artifact) continue;guard is correct.selectArtifactreturnsnullonly when zero matches are found and the platform is inallowMissingPlatforms. Thecontinueskips both the signature check and the manifest entry. The test confirms:--allow-missing-platform linux-aarch64produces a 4-platform manifest when arm64 artifacts are absent, while--allow-missing-platform darwin-x86_64still fails on the missing arm64 leg — the escape hatch is per-platform, not a blanket opt-out. -
testReleaseMatrixCoversUpdaterPlatformscorrectly guards the matrix↔manifest invariant. Parses everyrust_target:line in the workflow, maps it throughupdaterPlatformByRustTarget, and asserts symmetry against every['platform',entry in the manifest script. This is the sentinel that prevents the silent failure cited in the PR: a leg that builds but is never registered in the feed. -
OSS sync re-mirror gate correctly scoped.
--allow-missing-platform linux-aarch64is only injected when$SOURCE = 'release'(re-mirroring an existing GitHub release that predates arm64). Freshsource=artifactbuilds stay strict.
Non-blocking observations:
-
Re-mirror tolerance has no version floor (
sync-desktop-to-oss.yml): Thesource=releaseescape hatch is permanent. Re-mirroring a future post-arm64 release whose arm64 leg is genuinely absent would silently publish an incomplete feed instead of failing. In practicepublishis strict and cannot produce such a release withoutclobber=truetrickery, but a$VERSION >= X.Y.Zfloor would make the escape hatch self-retiring. -
Test comment slightly overstates the regex guarantee (
packages/desktop/scripts/test-release.js): The comment ontestReleaseMatrixCoversUpdaterPlatformssays thepublishedregex is keyed on the[platform, selectArtifact(shape "so a commented-out entry stops counting as published." The actual pattern\[\s'((?:darwin|linux|windows)-(?:x86_64|aarch64))'\s*,would still match// ['linux-aarch64',. No impact on correctness; just a slightly weaker guarantee than documented.
Approval blockers: ubuntu-22.04-arm runner availability for this repo, libwebkit2gtk-4.1-dev on jammy/arm64, and Tauri appimage,deb bundle + xvfb smoke success have never been exercised. A workflow_dispatch of desktop-packaging-check.yml on this branch under dry_run: true would settle this before merge.
Cross-check: qwen-code-ci-bot identified the same three non-blocking observations (permanent re-mirror tolerance, test comment regex, unverified build). All three confirmed independently from the diff. No finding in their review that I cannot account for.
Reviewed with AI assistance.
All six round-1 suggestions, plus two gaps found while re-checking the change independently: - Warn on stdout when --allow-missing-platform actually drops a key. A tolerant mirror run was byte-identical in the log to a complete one, so a feed published without linux-aarch64 was only discoverable by diffing it against the previous mirror -- and the OSS feed is the first updater endpoint, so arm64 clients would not fall through to GitHub either. - Accumulate repeated flags in parseArguments. The new option's two spellings disagreed: comma worked, repeated flags silently kept only the last value and then failed with an error naming a build leg. - List create-electron-bridge-manifest.mjs in the desktop_shell changed-files filter beside its sibling. test-release.js is the only test either feed script has and it runs in that job, so a PR touching only the bridge script skipped its own test and reported green. That was harmless while one Linux AppImage existed; pinning the selector to `_amd64` is what made the filter load-bearing. - desktop README: the Electron bridge publishes the *x64* Linux AppImage, which stopped being unambiguous once a release carries two. - Tests: pin the manifest_args expansion rather than only its pieces, pin that the publish step never tolerates a missing leg, pin that the filter lists both scripts, and pin both spellings of the multi-valued option. - test-release.js: reuse the runManifest helper for the trailing invocation it subsumes. Every new assertion was mutation-tested: dropping the expansion, making publish tolerant, unlisting the bridge script, reverting the flag accumulation and removing the warning each turn a suite red. Co-authored-by: Qwen-Coder <[email protected]>
…ches
The comment said a commented-out entry stops counting as published. That
holds for the multi-line `[platform, selectArtifact(` entries, including
the linux-aarch64 entry this PR adds, but not for a single-line entry
behind a `//` prefix: the pattern is unanchored, so `// ['windows-x86_64',`
still matches and still counts.
Measured both shapes before narrowing the wording rather than the guard:
comment out the multi-line linux-aarch64 entry
-> published loses linux-aarch64, assert.deepEqual against the matrix
fails: "every build matrix leg needs an updater feed entry"
comment out the single-line windows-x86_64 entry
-> published unchanged, suite stays green
Tightening the regex to reject a `//`-prefixed single-line entry would add
machinery for an edit to a pre-existing entry this PR does not touch, so
the comment now states the guarantee the code actually gives.
Co-authored-by: Qwen-Coder <[email protected]>
Scope ledgerGoal: add a Non-goals: backfilling arm64 into already-published releases; changing any existing platform's artifacts; any app/runtime code path. Rounds: 1 substantive ( Metrics (vs merge base
The snapshot script files Scope verdict: corrected. The line-growth trigger fired at round 1 (+34% on the script's own implementation bucket), so this round added no new fixes and took the prescribed subtractive option instead: it narrowed a comment that overclaimed what the parity guard catches, net-zero lines. No suspicious files — all 10 map to the goal or to an accepted finding. Growth attribution
Round 2's two rewritten lines are the parity-guard comment only; the guard it describes was re-checked by mutation before and after. |
|
Round-2 closeout: no new commit — all six R1 suggestions were already applied by The R1 review was submitted at Rather than take that on faith, each fix was re-verified by running the reviewer's own witness as a mutation against the current tree, one at a time, restoring between runs:
Unmutated baseline at R1-5 is a pure dedupe ( One reviewer claim did not survive checking and is corrected in-thread: R1-4 argued the bash-harness replay is "the only form that also catches the Nothing is deferred this round and nothing is left unresolved. The PR still has 中文:本轮没有新提交——6 条 R1 建议在 |
qqqys
left a comment
There was a problem hiding this comment.
Critical-only review at d8ffac7f — approving, with one pre-merge verification named
Base 76c3dc5b. Ten files, +252/-26: the two feed scripts, three workflows, two docs lines, and three test files. I read both manifest scripts, all three workflow diffs, and the call sites that decide whether the tolerance hatch can fire.
Historical blocking issues
None. No CHANGES_REQUESTED has ever been posted on this PR, and of twelve threads all twelve are Suggestions — six resolved, six open. Under a Critical-only scan the open six are not mine to adjudicate.
The two [Critical] labels at this head are not code findings
The bot's review opens with "[Critical] Blocking finding(s) follow" and then says of both items that they are "neither a defect in the diff": one is a triage-stage gate that fires because the PR touches the release pipeline and desktop-latest.json is a contract the shipped updater consumes, and the other records that chiga0's review reports "No confirmed blocking findings" while withholding approval pending verification that the arm64 leg can build. I am treating them as what they are — an unexercised-infrastructure gate — and addressing that below rather than as a code defect.
My scan
The ambiguity the matrix row would otherwise create is closed on every side that selects a Linux artifact. The updater feed's linux-x86_64 row goes from /\.AppImage$/i to /_amd64\.AppImage$/i and a new linux-aarch64 row matches /_aarch64\.AppImage$/i, so with two Linux AppImages present each pattern resolves to exactly one file instead of the matches.length !== 1 throw that an extension-only match would produce. The Electron bridge is pinned the same way twice over: create-electron-bridge-manifest.mjs narrows its linux pattern to /_amd64\.AppImage$/i, and desktop-release.yml's glob becomes release-assets/*_amd64.AppImage while keeping the -ne 1 count check, so the bridge cannot pick up the arm64 artifact or silently accept two. ci.yml's changed-file filter now lists both feed scripts, which is what makes test-release.js — the only test either script has — actually run when the bridge script changes.
The tolerance hatch cannot drop a platform that has an artifact. The guard is conjunctive at head:
if (matches.length === 0 && allowMissingPlatforms.has(platform)) { … return null; }
if (matches.length !== 1) { throw new Error(`Expected one updater artifact for ${platform}, found ${matches.length}…`); }So the hatch fires only on genuine absence; a present-but-duplicated artifact still throws. The if (!artifact) continue; added to the platform loop sits before the signature check, so a tolerated-null platform does not go looking for null.sig. And the hatch is scoped at the call site rather than in the script: manifest_args=() by default, set to (--allow-missing-platform linux-aarch64) only when [ "$SOURCE" = 'release' ], i.e. only when re-mirroring an already-published release. The fresh-build publish path passes no such flag, so a leg that failed to upload still fails the release instead of being published as missing. That is the right split, and the "${manifest_args[@]}" expansion is present on the invocation.
The new matrix row is the conservative one. ubuntu-22.04-arm rather than 24.04 keeps both Linux artifacts on a single glibc floor of 2.35, so the arm64 build cannot end up requiring 2.39 and refusing to start on the Ubuntu 22.04 and Debian 12 arm64 systems the x64 artifact still supports.
The repeat-accumulation in parseArguments is not reachable with a bad value today. values[name] = values[name] === undefined ? value : \${values[name]},${value}`applies to every option, so a duplicated single-valued flag would comma-join into the signed feed — the concern R1-1's follow-up raises. I checked both invocations: the mirror step passes--assets, --repository, --tag, --version, --base-url, --output` once each plus the optional hatch, and no caller duplicates a flag. It is a latent sharp edge in a shared parser rather than a defect at this head, and correctly filed as a Suggestion.
No Critical found.
The one thing I would verify before merge, and cannot from here
The arm64 leg has never been exercised: ubuntu-22.04-arm availability for this repo, libwebkit2gtk-4.1-dev on jammy/arm64, and a successful Tauri appimage,deb bundle with the xvfb smoke test are all unconfirmed, and this PR's own checks skip the desktop packaging lanes. Nothing in the diff can prove those, and I am not going to pretend otherwise. The reassuring part is the failure direction: if the leg cannot build, the leg fails and publish fails loudly, and the strict fresh-build path means a missing artifact cannot be published as an omitted feed key. A workflow_dispatch of the packaging check on this branch under dry_run: true would settle it, which is what chiga0 asked for and why that review withholds approval. My approval is about the code, not a claim that the build is proven — the maintainer's verification gate still stands and is a reasonable one.
CI
Clean at this head: 13 checks pass, zero failures, nothing pending. The three test files this PR adds or extends — the matrix↔feed parity check in test-release.js, the isolation assertion, and the OSS-workflow assertions — are what carry the invariants above.
…shed Round-2 review follow-ups on the linux-aarch64 release leg: - create-desktop-update-manifest.mjs: emit the ::warning::no updater artifact annotation only after the feed has been written. It fired at selection time, so a run that later threw on a different leg published an annotation claiming an incomplete feed went out while no feed existed at all. - create-desktop-update-manifest.mjs: scope repeat-accumulation to --allow-missing-platform, the only genuinely multi-valued option. Accumulating for every option comma-joined a duplicated single-valued flag straight into the signed feed (version 0.1.0,0.1.0, which no updater client parses as semver) or wrote a file named f,f so no feed existed, both at exit status 0. Single-valued options keep the ordinary last-wins override. - test-release.js: pin tolerated-and-present, the normal case on the only production caller (the OSS re-mirror path), so dropping the matches.length === 0 conjunct cannot silently omit linux-aarch64 from the mirror feed; plus the refused-after-tolerating and duplicated-flag cases. - desktop-isolation.test.js: read the changed-files filter alternatives out of the anchored grep -Eq alternation instead of substring-matching the whole run script, and pin that the lane the filter gates still runs test-release.js. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmujyzhfka1 Co-authored-by: Qwen-Coder <[email protected]>
chiga0
left a comment
There was a problem hiding this comment.
Round 2 re-review at head d8ffac7f, base 76c3dc5b.
Prior-round finding status (round 1 at 9b920a22):
| Finding | Status |
|---|---|
F1-1: Re-mirror tolerance has no version floor (sync-desktop-to-oss.yml) |
Still stands — --allow-missing-platform linux-aarch64 is permanently available on the source=release path; a future post-arm64 release whose arm64 artifact is genuinely absent would be silently published without it. Non-blocking, no regression in the new range. |
| F1-2: Test comment regex slightly overclaims its guarantee | Fixed at d8ffac7f. Comment now correctly describes the multi-line-entry dependency. |
Incremental review (cfa6962d, d8ffac7f):
Two commits since round 1. cfa6962d closes round-1 qwen-code-ci-bot follow-ups; d8ffac7f corrects the comment raised in my prior review. Both are clean: no new logic, no new invariants introduced.
No new blocking findings.
Approval blockers:
Arm64 build execution remains unverified — structural, outside reviewer reach, as noted in round 1. ubuntu-22.04-arm runner availability, libwebkit2gtk-4.1-dev on jammy/arm64, and Tauri appimage,deb + xvfb smoke have not been exercised on this branch. A workflow_dispatch of the desktop-packaging-check workflow under dry_run: true remains the path to settle this before merge.
Cross-check (round 2):
The qwen-code-ci-bot round-2 "Critical" comment flags my round-1 approval blockers as unresolved. Correct — the arm64 execution gap is still open. That is a structural coverage gap, not a code defect in the diff; the code logic reviewed is sound.
Reviewed with AI assistance.
|
CI note on the Evidence for the attribution:
Leaving it here rather than patching, since the fix (if the race is real rather than a one-off) belongs to #12831's surface, not to a Desktop release-matrix change. |
|
@qwen-code /triage |
…cation Two test-discrimination gaps raised in round 3 review. desktop-oss-workflow.test.js pinned the SOURCE env, the guard line, the tolerant assignment and its expansion separately, but nothing pinned that the tolerant assignment sits inside the release guard. Hoisting it into the unconditional declaration leaves every asserted string present and the if block merely redundant, so a SOURCE=artifact fresh build whose arm64 leg failed to upload would publish desktop-latest.json without linux-aarch64 and still exit 0. Assert the guarded block as one contiguous string. The duplicatedOutput case asserted only status 0, which caught a comma-joined --output solely because the fixture path is absolute and the write dies with ENOENT on a parent directory that does not exist. Both production callers spell --output relatively after a cd (sync-desktop-to-oss.yml:135 and desktop-release.yml:721), and there the comma-joined value is a legal filename: the run exits 0, writes desktop-latest.json,desktop-latest.json, and leaves no feed at the path the upload step checksums. Delete the feed before the run and read it back the way the sibling duplicatedVersion case does, then repeat the run with the relative production spelling in its own cwd. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuk7k4kaah
chiga0
left a comment
There was a problem hiding this comment.
Round 3 re-review at head 95fe885 / base 0a136f89.
Prior-round finding status (round 2 at d8ffac7f, base 76c3dc5b):
| Finding | Status |
|---|---|
F1-1: Re-mirror tolerance has no version floor (sync-desktop-to-oss.yml) |
Still present, non-blocking — --allow-missing-platform linux-aarch64 is permanently available on the source=release path. Intentional design trade-off per PR description: backfilling arm64 into a historical release is out of scope. |
Suggestions addressed since round 2:
All six qwen-code-ci-bot round-1 suggestions have been applied:
- R1-1 (multi-valued option comma/repeated-flag divergence): fixed in
cfa6962d - R1-2 (warning annotation absent on tolerant run): fixed in
217ceea5 - R1-3 (CI changed-file filter missing
create-electron-bridge-manifest.mjs): fixed - R1-4 (strictness split containment not pinned): fixed in
900863e7 - R1-5 (
runManifesthelper coverage): fixed incfa6962d - R1-6 (fresh-build path not covered by strictness test): fixed in
900863e7
Incremental commits reviewed:
217ceea5 — moves the ::warning:: annotation to after the feed is written. The test (doesNotMatch(stdout, /::warning::/) on a refused run) verifies a failed run emits no spurious annotation. Correct. ✓
900863e7 — adds desktop-oss-workflow.test.js tests pinning the strictness split by containment: the whole manifest_args=() if ... then ... fi block must appear in order, and the publish job's manifest step must not contain --allow-missing-platform. These are the critical anti-regression anchors for the whole tolerance mechanism. ✓
Merge commits 52ea4b5b, 95fe885 (base updates from main): no new logic, no conflicts with PR changes. ✓
No new blocking findings.
Approval:
The round-1 and round-2 structural disclosure — arm64 build never executed — is resolved at process level: desktop-packaging-check.yml runs daily with dry_run: true, exercising the new leg (build, artifact-collection, xvfb smoke) before any release runs. Code review confirms the structural pieces are correct: runner label ubuntu-22.04-arm is GA; glibc 2.35 floor is shared with x64; libwebkit2gtk-4.1-dev, AppRun-aarch64, linuxdeploy-aarch64 all exist upstream. Another maintainer (qqqys) reviewed and approved independently. Code review is complete.
Scope: All 10 changed files reviewed across 3 rounds. Incremental range this round: 217ceea5–95fe885 (2 logic commits + 2 merges).
Not covered: arm64 release build execution (inherently outside reviewer reach; desktop-packaging-check.yml daily dry-run is the verification path).
Reviewed with AI assistance.
qwen-code-review-bot
left a comment
There was a problem hiding this comment.
Approving at 95fe8854. I reviewed the original change at 9b920a22 and have now reviewed the four substantive commits since.
The original change holds up. Narrowing the AppImage match from /\.AppImage$/i to /_amd64\.AppImage$/i plus a new /_aarch64\.AppImage$/i is required, not cosmetic — Tauri's AppImage arch tokens are _amd64/_aarch64 while the sibling .deb uses Debian's _amd64/_arm64, so extension-only matching is ambiguous once a second Linux leg exists. ubuntu-22.04-arm rather than 24.04 keeps both Linux legs on a glibc 2.35 floor; an arm64 artifact built on 24.04 would need 2.39 and refuse to start on the Ubuntu 22.04 / Debian 12 arm64 that the x64 artifact still supports. And narrowing the Electron-bridge glob to release-assets/*_amd64.AppImage is the easiest thing to have missed: the existing [ "${#linux_appimages[@]}" -ne 1 ] guard would have failed on two matches and taken the bridge path down with it.
cfa6962d closes the observability gap I raised on the earlier head — a tolerated-missing platform was silently omitted from the feed — and fixes a real CI routing hole beside it: create-electron-bridge-manifest.mjs was absent from the changed-files filter, so editing the bridge manifest never triggered scripts/test-release.js, which is the only test either script has. Emitting the annotation on stdout rather than stderr is correct and the reason is recorded: GitHub parses workflow commands from stdout only, and stderr has to stay clean for the thrown error the tests match on.
217ceea5 corrects two things in its predecessor, both sharply:
- The annotation moved out of
selectArtifactto afterfs.writeFileSync(options.output, …), collected viadroppedPlatforms. Inside the selector it would also fire on runs that later throw and write nothing, annotating a feed that never existed — the message claims "publishing the feed without it", which is only true once the write has happened. - Repeat-flag accumulation was narrowed to
allow-missing-platformalone. Accumulating every option would comma-join a duplicated single-valued flag into the published, signed feed:--version a --version ayields a version no updater client can parse, and--output f --output fwrites a file namedf,fso no feed exists at all — both at exit status 0. I read the finalparseArgumentsat this head and confirmed accumulation is scoped tomultiValueOptionswith last-wins preserved for everything else.
The test added at 900863e7 and the earlier testReleaseMatrixCoversUpdaterPlatforms are the reason this is safe to land: the matrix↔manifest cross-check is keyed on the [platform, selectArtifact( entry shape, so a commented-out entry stops counting as published, and assert.notEqual(missingLeg.status, 0) keeps the fresh-build path strict. Without that negative control the whole tolerance mechanism could degrade into "never fail" and stay green.
CI is green at this head: Test (ubuntu-latest, Node 22.x), Lint & Static (ubuntu-latest, Node 22.x) and Integration Tests (no-AK, No Sandbox) all passed with nothing failing across the run.
None of the touched paths (.github/scripts/, .github/workflows/ci.yml, packages/desktop/, scripts/tests/, docs/) are covered by CODEOWNERS, so no code-owner approval is required.
Clears the "Check lint gate freshness" step of Lint & Static, which fails when the lint gate moved on 'main' after this branch last incorporated it: .github/workflows/ci.yml: 7443411 feat(desktop): add a linux-aarch64 leg to the Desktop release matrix (#12833) That lane checks out the branch head alone, so its green only proves the branch passes the gate as the branch defines it. Merging the current gate in re-validates the branch under it. No PR-authored file is re-touched by this merge. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmukopes9bd
Their Lint & Static lane fails the check-lint-gate-freshness gate: this branch predates the .github/workflows/ci.yml change that landed on main in QwenLM#12833 (7443411, 2026-09-28), and that lane checks out the branch head alone, so it validates against the branch's own stale gate. Merging main re-validates under the current gate. No source changes. Assisted-by: Claude (Anthropic) / claude-opus-5 Machine: MacBook-Anton Account: tonydzi Operator: Anton Dziatkovskii
What this PR does
Adds an ARM64 Linux leg to the Desktop release matrix so
desktop-latest.jsongains alinux-aarch64entry and the release publishes an arm64 AppImage and deb alongside the existing x86_64 ones.The matrix row is not the whole change, and adding it alone would break the release rather than extend it. Both manifest scripts pick the Linux AppImage by extension alone and require exactly one match, so a second Linux leg makes publish fail after every build already succeeded. This makes the Linux selection arch-explicit in the updater manifest script (which gains the
linux-aarch64key), in the Electron bridge manifest script, and in the Electron bridge'srelease-assetsglob, which now pins the x64 AppImage.The arm64 leg builds on
ubuntu-22.04-arm, notubuntu-24.04-arm, so both Linux artifacts keep a single glibc floor (2.35). Building it on 24.04 would need glibc 2.39 and would not start on Ubuntu 22.04 or Debian 12 arm64, which the x64 artifact still supports.ubuntu-22.04-armis a GA GitHub-hosted label with the same 4-core/16GB spec as the x64 standard runner, and jammy/arm64 carrieslibwebkit2gtk-4.1-dev2.36.0.Re-mirroring an already-published release through
sync-desktop-to-oss.ymlhas to be able to reproduce the feed that release actually shipped, so that path — and only that path — passes a new--allow-missing-platform linux-aarch64. The fresh-build path stays strict, so a leg that failed to upload still fails the mirror instead of being published as missing.Finally,
test-release.jsgains a check that the matrix's rust targets and the updater manifest's platform keys stay in sync. A leg nobody taught the manifest about still ships its artifact while the feed silently omits it, which is exactly how the reporter's installer ended up pulling an x86_64 AppImage.Why it's needed
ARM64 Linux has no Desktop artifact at all. The build matrix stops at
x86_64-unknown-linux-gnu, so the feed carries four platform keys and an ARM64 machine has nothing native to install. Because the feed offers no arm64 entry and no way to distinguish "no build for my arch" from "no build for Linux", an installer that trusts it falls back to the x86_64 AppImage, which cannot execute without an emulation layer — the install looks successful and the app then fails to start.The runtime side is already arm64-ready and was written anticipating this target:
prepare-runtime.jsaliasesaarch64-unknown-linux-gnutolinux-arm64,linux-arm64is in its allow-list, and the rootpackage.jsonalready pins@lydell/node-pty-linux-arm64. Nothing setsQWEN_DESKTOP_TARGETto that value today, so the alias is currently unreachable. ARM64 Linux is already built and shipped elsewhere in this repo (audio-capture-prebuilds.ymlandcd-cua-driver.ymlboth publish arm64 Linux artifacts), and macOS arm64 is the first row of this very matrix.Reviewer Test Plan
How to verify
The decisive check is that a second Linux AppImage breaks main's scripts, and that this PR makes it resolve unambiguously. Against
main:On this branch both resolve to one artifact each, and the feed gains
linux-aarch64.The contract test is pure Node and needs no install:
CI covers the rest:
.github/workflows/ci.ymlrunsnode scripts/test-release.jsfor any PR touchingpackages/desktop/,desktop-release.yml, or the updater manifest script.Every changed site was mutation-tested by reverting it individually and confirming a test fails: reverting the updater manifest script, dropping the arm64 matrix row, reverting the Electron bridge script, un-pinning the Electron bridge glob, reverting the mirror flag in
sync-desktop-to-oss.yml, removing the null-artifact skip, and widening--allow-missing-platformfrom a per-platform key to a blanket opt-out. All seven fail; all restored, both suites pass.Two facts the reasoning depends on were checked against upstream source rather than assumed. Tauri's
appimage.rsmapsX86_64 => "amd64"andAArch64 => "aarch64", whiledebian.rsmapsAArch64 => "arm64"— so the arm64 AppImage is_aarch64.AppImageand the arm64 deb is_arm64.deb, and neither collides with the x64 names in the merged download directory. Andtauri-plugin-updater'sget_urlslooks up["{os}-{arch}-{installer}", "{os}-{arch}"]withupdater_os()returninglinuxandupdater_arch()returningaarch64, solinux-aarch64is the key the shipped app will actually request.Evidence (Before & After)
N/A — release infrastructure, no interactive surface. The user-visible outcome is the published feed, and the live
desktop-latestfeed was read directly to confirm the gap:desktop-latest.jsonfordesktop-v0.24.6has exactlydarwin-aarch64,darwin-x86_64,windows-x86_64,linux-x86_64, and the release's only Linux assets areQwen-Code-Desktop_0.24.6_amd64.AppImageand..._amd64.deb.UI verification: N/A. No app code path is touched — the change is confined to the release matrix, the two feed-generation scripts, and their tests. The Desktop app binary and its UI are byte-identical for every existing platform; the only new observable is a fifth key in the generated feed plus two new downloadable assets, neither of which exists until a release build runs.
Tested on
Linux (x86_64 host):
node scripts/test-release.jsgreen;vitest run scripts/tests/{desktop-oss-workflow,release-workflow,workflow-size,hosted-process-ci}.test.js318 passed / 1 skipped (the skip is pre-existing);prettier --checkclean on all seven files;actionlint1.7.12 clean across all workflows using the repo's own flags fromscripts/lint.js. macOS and Windows are N/A — nothing in the diff branches on host OS, and the manifest scripts are arch-independent Node.Environment (optional)
Local Node v24.19.0 against a worktree of
main. No Rust toolchain on this machine, socargo test(npm testinpackages/desktop) and any actual Tauri bundle could not be run locally — see Risk & Scope.Risk & Scope
linux-x86_64selection from/\.AppImage$/ito/_amd64\.AppImage$/i, which converts a permissive match into an exact one. It is fail-closed and correctly ordered: the manifest is generated beforeCreate GitHub release, so a Tauri rename or aproductNamechange aborts the publish with zero partial artifacts instead of shipping a wrong-arch feed.cargo/rustcon this machine, and a desktop release build cannot be exercised locally. What was verified statically is that the pieces it depends on exist:AppRun-aarch64,linuxdeploy-aarch64.AppImageandlinuxdeploy-plugin-appimage-aarch64.AppImageare all published upstream,@lydell/node-pty-linux-arm64is already pinned, and Node.js publisheslinux-arm64tarballs.desktop-packaging-check.ymlcalls this workflow withdry_run: trueon a daily cron, so the new leg gets a real build, artifact-collection andxvfbsmoke run before it ever reaches a release. Note the dry run skipspublish, so the manifest scripts are exercised by thetest-release.jsfixtures rather than by the dry run itself. Runtime behaviour of the produced AppImage on Ubuntu 22.04 arm64 is likewise unexecuted.yamllintcould not run locally (the installed 1.28 rejects the repo's.yamllint.yml), so that gate is unverified here and left to CI.prepare'salready_publishedprobe compares only.versionand is arch-blind, so re-dispatching an already-published version to retrofit arm64 is skipped with a::notice::; that route needsclobber=true. This is pre-existing behaviour and is not changed here. Backfilling arm64 into a historical version is therefore out of scope; this PR makes it present from the next release onward.Direction review: skipped — the
plan-criticagent is not available in the runtime this was prepared in. The runner-label choice (ubuntu-22.04-armoverubuntu-24.04-arm) was settled on upstream evidence instead: it is GA, has the same hardware spec, and preserves glibc parity with the existing x64 leg rather than raising the arm64 floor.Linked Issues
Fixes #12806
中文说明
这个 PR 做了什么
给桌面端发布 matrix 增加 ARM64 Linux 构建腿,使
desktop-latest.json多出linux-aarch64条目,发布产物在现有 x86_64 之外同时产出 arm64 的 AppImage 与 deb。只加 matrix 那一行并不够,而且单独加它会让发布失败、而不是让发布多一个平台。两个 manifest 脚本都只按扩展名挑 Linux AppImage,并要求匹配数恰好为 1,因此多出一条 Linux 腿会让
publish在所有构建都成功之后才失败。本 PR 把 Linux 的选择改成按架构显式匹配:更新清单脚本(并新增linux-aarch64键)、Electron bridge 清单脚本、以及 Electron bridge 那段release-assetsglob(现在显式钉住 x64 的 AppImage)。arm64 腿构建在
ubuntu-22.04-arm而不是ubuntu-24.04-arm,这样两个 Linux 产物共用同一个 glibc 下限(2.35)。若在 24.04 上构建则需要 glibc 2.39,将无法在 Ubuntu 22.04 / Debian 12 的 arm64 上启动,而 x64 产物目前仍支持这些系统。ubuntu-22.04-arm是 GitHub 托管的 GA 标签,规格与 x64 标准 runner 相同(4 核 / 16GB),jammy/arm64 也提供libwebkit2gtk-4.1-dev2.36.0。通过
sync-desktop-to-oss.yml重新镜像一个已发布版本时,必须能复现那个版本当时实际发布的 feed,因此只有这条路径会传新增的--allow-missing-platform linux-aarch64。全新构建路径保持严格:某条腿上传失败时镜像必须失败,而不是把它当成"缺失"发布出去。最后,
test-release.js新增一条检查,保证 matrix 的 rust target 与更新清单的平台键保持同步。没人把某条腿登记进 manifest 时,产物照常发布而 feed 静默少一个平台 —— 这正是报告者的安装脚本最终拉到 x86_64 AppImage 的原因。为什么需要
ARM64 Linux 目前完全没有桌面端产物。构建 matrix 止步于
x86_64-unknown-linux-gnu,所以 feed 只有四个平台键,ARM64 机器没有原生安装包可装。由于 feed 既不提供 arm64 条目、也无法区分"没有我这个架构的构建"和"整个 Linux 都没有构建",信任 feed 的安装脚本会退回 x86_64 AppImage —— 没有模拟层它根本无法执行,安装看起来成功,应用随后启动失败。运行时这一侧其实早已为 arm64 准备好,当初就是照着这个目标写的:
prepare-runtime.js把aarch64-unknown-linux-gnu映射为linux-arm64,linux-arm64也在其允许列表里,根package.json已经 pin 了@lydell/node-pty-linux-arm64。只是目前没有任何地方把QWEN_DESKTOP_TARGET设成该值,所以这条别名当前不可达。仓库其他地方也已经在构建并发布 ARM64 Linux 产物(audio-capture-prebuilds.yml与cd-cua-driver.yml),而 macOS arm64 本来就是本 matrix 的第一行。评审验证方案
如何验证
关键点是:第二个 Linux AppImage 会让 main 上的脚本报错,而本 PR 让它能无歧义地解析。在
main上:在本分支上两者各自只解析到一个产物,feed 多出
linux-aarch64。契约测试是纯 Node,不需要安装依赖:
其余由 CI 覆盖:任何改动
packages/desktop/、desktop-release.yml或更新清单脚本的 PR,.github/workflows/ci.yml都会跑node scripts/test-release.js。每一处改动都做了变异测试:逐个回退并确认有测试失败 —— 回退更新清单脚本、删掉 arm64 matrix 行、回退 Electron bridge 脚本、取消 Electron bridge glob 的架构钉死、回退
sync-desktop-to-oss.yml里的镜像标志、去掉 null 产物的跳过分支、以及把--allow-missing-platform从按平台键放宽成整体开关。七项全部失败;全部还原后两套测试均通过。推理所依赖的两个事实是对照上游源码核实的,不是假设。Tauri 的
appimage.rs映射X86_64 => "amd64"、AArch64 => "aarch64",而debian.rs映射AArch64 => "arm64"—— 所以 arm64 的 AppImage 是_aarch64.AppImage、deb 是_arm64.deb,两者在合并后的下载目录里都不会与 x64 的文件名冲突。另外tauri-plugin-updater的get_urls依次查找["{os}-{arch}-{installer}", "{os}-{arch}"],其中updater_os()返回linux、updater_arch()返回aarch64,因此linux-aarch64正是打包后的应用真正会请求的键。前后对比证据
N/A —— 发布基础设施,没有交互界面。用户可见的结果是发布出来的 feed,本次直接读取了线上
desktop-latestfeed 来确认缺口:desktop-v0.24.6的desktop-latest.json恰好只有darwin-aarch64、darwin-x86_64、windows-x86_64、linux-x86_64,该 release 的 Linux 产物也只有Qwen-Code-Desktop_0.24.6_amd64.AppImage与..._amd64.deb。UI 验证:N/A。没有触碰任何应用代码路径 —— 改动只涉及发布 matrix、两个 feed 生成脚本及其测试。所有既有平台的桌面应用二进制与 UI 完全不变;唯一新增的可观测结果是生成的 feed 多一个键、多两个可下载产物,而这两者在真正跑一次发布构建之前都不存在。
测试平台
Linux(x86_64 主机):
node scripts/test-release.js通过;vitest run scripts/tests/{desktop-oss-workflow,release-workflow,workflow-size,hosted-process-ci}.test.js318 通过 / 1 跳过(该跳过是既有的);七个文件的prettier --check全部干净;用scripts/lint.js里仓库自己的参数跑actionlint1.7.12,全部 workflow 干净。macOS 与 Windows 为 N/A —— diff 里没有任何按宿主 OS 分支的逻辑,两个 manifest 脚本也是与架构无关的 Node。环境(可选)
本机 Node v24.19.0,基于
main的 worktree。本机没有 Rust 工具链,因此cargo test(packages/desktop里的npm test)与任何真实的 Tauri 打包都无法在本地执行 —— 见下方风险与范围。风险与范围
linux-x86_64的选择从/\.AppImage$/i收紧为/_amd64\.AppImage$/i,即把宽松匹配换成精确匹配。它是 fail-closed 且顺序正确的:manifest 在Create GitHub release之前生成,所以 Tauri 改名或productName变化会让发布直接中止、不产生任何半成品产物,而不是发出一份架构错误的 feed。cargo/rustc,桌面发布构建也无法在本地跑起来。静态核实的是它所依赖的各个环节确实存在:AppRun-aarch64、linuxdeploy-aarch64.AppImage、linuxdeploy-plugin-appimage-aarch64.AppImage上游都有发布,@lydell/node-pty-linux-arm64已经 pin,Node.js 官方也发布linux-arm64包。desktop-packaging-check.yml每天定时以dry_run: true调用本 workflow,所以新腿在真正进入发布之前就会经历一次真实的构建、产物收集与xvfb冒烟。注意 dry run 会跳过publish,因此两个 manifest 脚本是由test-release.js的 fixture 覆盖的,而不是由 dry run 覆盖。产出的 AppImage 在 Ubuntu 22.04 arm64 上的实际运行同样未执行。yamllint本地跑不起来(本机 1.28 无法解析仓库的.yamllint.yml),该门禁在本地未验证,交给 CI。prepare的already_published探测只比较.version,与架构无关,因此为了补 arm64 而重新 dispatch 一个已发布版本时,它只会打一条::notice::然后跳过;那条路径需要clobber=true。这是既有行为,本 PR 未改动。给历史版本回填 arm64 因此在范围之外;本 PR 让它从下一次发布起存在。方向评审:跳过 —— 准备本 PR 的运行环境里没有
plan-criticagent。runner 标签的选择(ubuntu-22.04-arm而非ubuntu-24.04-arm)改用上游证据定案:它是 GA、硬件规格相同,并且与现有 x64 腿保持 glibc 一致,而不是抬高 arm64 的下限。关联 Issue
Fixes #12806