From 3d22a1a42a2bcdcdd1e15aeb372b9cbaf03efeaf Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Mon, 3 Aug 2026 01:00:45 +0800 Subject: [PATCH 1/3] feat(desktop): bridge Electron updates to Tauri --- .../create-electron-bridge-manifest.mjs | 67 ++++++++++ .github/workflows/desktop-release.yml | 122 +++++++++++++++--- ...desktop-electron-to-tauri-update-bridge.md | 66 ++++++++++ packages/desktop-shell/README.md | 8 ++ .../desktop-shell/scripts/smoke-packaged.js | 7 +- .../desktop-shell/scripts/test-release.js | 108 ++++++++++++++++ .../desktop-shell/src-tauri/tauri.conf.json | 4 +- 7 files changed, 358 insertions(+), 24 deletions(-) create mode 100644 .github/scripts/create-electron-bridge-manifest.mjs create mode 100644 docs/design/desktop-electron-to-tauri-update-bridge.md diff --git a/.github/scripts/create-electron-bridge-manifest.mjs b/.github/scripts/create-electron-bridge-manifest.mjs new file mode 100644 index 00000000000..46ca88d8008 --- /dev/null +++ b/.github/scripts/create-electron-bridge-manifest.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +const options = parseArguments(process.argv.slice(2)); +const assets = fs.readdirSync(options.assets).sort(); +const names = [ + 'Qwen-Code-Desktop-arm64.zip', + 'Qwen-Code-Desktop-x64.zip', + 'Qwen-Code-Desktop-arm64.dmg', + 'Qwen-Code-Desktop-x64.dmg', +]; +const artifacts = names.map((name) => readArtifact(assets, name)); +const primary = artifacts[0]; +const releaseDate = options['release-date'] ?? new Date().toISOString(); + +const lines = [ + `version: ${options.version}`, + 'files:', + ...artifacts.flatMap((artifact) => [ + ` - url: ${artifact.name}`, + ` sha512: ${artifact.sha512}`, + ` size: ${artifact.size}`, + ]), + `path: ${primary.name}`, + `sha512: ${primary.sha512}`, + `releaseDate: '${releaseDate}'`, +]; +fs.writeFileSync(options.output, `${lines.join('\n')}\n`); + +function readArtifact(assets, name) { + if (!assets.includes(name)) { + throw new Error(`Missing Electron bridge artifact: ${name}`); + } + const file = path.join(options.assets, name); + return { + name, + sha512: crypto + .createHash('sha512') + .update(fs.readFileSync(file)) + .digest('base64'), + size: fs.statSync(file).size, + }; +} + +function parseArguments(args) { + const values = {}; + for (let index = 0; index < args.length; index += 2) { + const name = args[index]?.replace(/^--/, ''); + const value = args[index + 1]; + if (!name || value === undefined) throw new Error('Invalid arguments.'); + values[name] = value; + } + for (const required of ['assets', 'version', 'output']) { + if (!values[required]) throw new Error(`Missing --${required}`); + } + if ( + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test( + values.version, + ) + ) { + throw new Error(`Invalid --version: ${values.version}`); + } + return values; +} diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index d2040e0e12e..c485b1e9d79 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -14,6 +14,11 @@ on: required: true default: 'main' type: 'string' + electron_bridge: + description: 'Publish the one-time macOS Electron-to-Tauri update bridge.' + required: true + default: false + type: 'boolean' dry_run: description: 'Build unsigned installers without publishing.' required: true @@ -64,6 +69,7 @@ jobs: id: 'version' shell: 'bash' env: + ELECTRON_BRIDGE: '${{ inputs.electron_bridge }}' INPUT_VERSION: '${{ inputs.version }}' run: | set -euo pipefail @@ -72,6 +78,14 @@ jobs: echo "::error::Desktop version must be valid SemVer: $INPUT_VERSION" exit 1 fi + if [ "$ELECTRON_BRIDGE" = 'true' ]; then + core="${version%%[-+]*}" + IFS='.' read -r major minor patch <<< "$core" + if [ "$major" -eq 0 ] && [ "$minor" -eq 0 ] && [ "$patch" -le 5 ]; then + echo '::error::The Electron bridge version must be newer than desktop-v0.0.5.' + exit 1 + fi + fi echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=desktop-v$version" >> "$GITHUB_OUTPUT" @@ -109,18 +123,22 @@ jobs: include: - name: 'macOS arm64' os: 'macos-latest' + legacy_arch: 'arm64' rust_target: 'aarch64-apple-darwin' tauri_args: '--target aarch64-apple-darwin --bundles app,dmg' - name: 'macOS x64' os: 'macos-15' + legacy_arch: 'x64' rust_target: 'x86_64-apple-darwin' tauri_args: '--target x86_64-apple-darwin --bundles app,dmg' - name: 'Windows x64' os: 'windows-latest' + legacy_arch: '' rust_target: 'x86_64-pc-windows-msvc' tauri_args: '--target x86_64-pc-windows-msvc --bundles nsis' - name: 'Linux x64' os: 'ubuntu-22.04' + legacy_arch: '' rust_target: 'x86_64-unknown-linux-gnu' tauri_args: '--target x86_64-unknown-linux-gnu --bundles appimage,deb' env: @@ -195,21 +213,32 @@ jobs: env: APPLE_CERTIFICATE: '${{ secrets.APPLE_CERTIFICATE }}' APPLE_CERTIFICATE_PASSWORD: '${{ secrets.APPLE_CERTIFICATE_PASSWORD }}' + LEGACY_APPLE_CERTIFICATE: '${{ secrets.MAC_CSC_LINK }}' + LEGACY_APPLE_CERTIFICATE_PASSWORD: '${{ secrets.MAC_CSC_KEY_PASSWORD }}' KEYCHAIN_PASSWORD: '${{ secrets.APPLE_KEYCHAIN_PASSWORD }}' run: | set -euo pipefail - for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD; do - if [ -z "${!name}" ]; then echo "::error::$name is required for published macOS releases."; exit 1; fi - done + if [ -n "$APPLE_CERTIFICATE" ] && [ -n "$APPLE_CERTIFICATE_PASSWORD" ]; then + certificate_data="$APPLE_CERTIFICATE" + certificate_password="$APPLE_CERTIFICATE_PASSWORD" + elif [ -n "$LEGACY_APPLE_CERTIFICATE" ] && [ -n "$LEGACY_APPLE_CERTIFICATE_PASSWORD" ]; then + certificate_data="$LEGACY_APPLE_CERTIFICATE" + certificate_password="$LEGACY_APPLE_CERTIFICATE_PASSWORD" + else + echo '::error::A complete APPLE_CERTIFICATE pair or MAC_CSC pair is required for published macOS releases.' + exit 1 + fi + keychain_password="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 32)}" certificate="$RUNNER_TEMP/qwen-code-desktop.p12" keychain="$RUNNER_TEMP/qwen-code-desktop.keychain-db" - printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate" - security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" + certificate_data="${certificate_data#*base64,}" + printf '%s' "$certificate_data" | base64 --decode > "$certificate" + security create-keychain -p "$keychain_password" "$keychain" security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" - security import "$certificate" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$certificate" -P "$certificate_password" -A -t cert -f pkcs12 -k "$keychain" security list-keychains -d user -s "$keychain" login.keychain-db - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$keychain" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)" if [ -z "$identity" ]; then echo '::error::Developer ID Application identity was not found.'; exit 1; fi echo "APPLE_SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" @@ -221,17 +250,29 @@ jobs: APPLE_API_ISSUER: '${{ secrets.APPLE_API_ISSUER }}' APPLE_API_KEY: '${{ secrets.APPLE_API_KEY }}' APPLE_API_KEY_P8: '${{ secrets.APPLE_API_KEY_P8 }}' + LEGACY_APPLE_API_ISSUER: '${{ secrets.APPLE_NOTARY_ISSUER_ID }}' + LEGACY_APPLE_API_KEY: '${{ secrets.APPLE_NOTARY_KEY_ID }}' + LEGACY_APPLE_API_KEY_P8: '${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}' run: | set -euo pipefail - for name in APPLE_API_ISSUER APPLE_API_KEY APPLE_API_KEY_P8; do - if [ -z "${!name}" ]; then echo "::error::$name is required for macOS notarization."; exit 1; fi - done - key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" - printf '%s' "$APPLE_API_KEY_P8" > "$key_path" + if [ -n "$APPLE_API_ISSUER" ] && [ -n "$APPLE_API_KEY" ] && [ -n "$APPLE_API_KEY_P8" ]; then + api_issuer="$APPLE_API_ISSUER" + api_key="$APPLE_API_KEY" + key_path="$RUNNER_TEMP/AuthKey_${api_key}.p8" + printf '%s' "$APPLE_API_KEY_P8" > "$key_path" + elif [ -n "$LEGACY_APPLE_API_ISSUER" ] && [ -n "$LEGACY_APPLE_API_KEY" ] && [ -n "$LEGACY_APPLE_API_KEY_P8" ]; then + api_issuer="$LEGACY_APPLE_API_ISSUER" + api_key="$LEGACY_APPLE_API_KEY" + key_path="$RUNNER_TEMP/AuthKey_${api_key}.p8" + printf '%s' "$LEGACY_APPLE_API_KEY_P8" | base64 --decode > "$key_path" + else + echo '::error::A complete APPLE_API notarization set or APPLE_NOTARY set is required.' + exit 1 + fi { echo "APPLE_API_KEY_PATH=$key_path" - echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" - echo "APPLE_API_KEY=$APPLE_API_KEY" + echo "APPLE_API_ISSUER=$api_issuer" + echo "APPLE_API_KEY=$api_key" } >> "$GITHUB_ENV" - name: 'Import Windows certificate' @@ -302,6 +343,22 @@ jobs: $signature = Get-AuthenticodeSignature $installer.FullName if ($signature.Status -ne 'Valid') { throw "Invalid Authenticode signature: $($signature.Status)" } + - name: 'Create Electron bridge archive' + if: "runner.os == 'macOS' && inputs.electron_bridge" + shell: 'bash' + env: + LEGACY_ARCH: '${{ matrix.legacy_arch }}' + run: | + set -euo pipefail + app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name 'Qwen Code Desktop.app' -print -quit)" + if [ -z "$app" ]; then + echo '::error::The legacy-named macOS app bundle was not produced.' + exit 1 + fi + destination="packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/electron-bridge" + mkdir -p "$destination" + ditto -c -k --sequesterRsrc --keepParent "$app" "$destination/Qwen-Code-Desktop-$LEGACY_ARCH.zip" + - name: 'Smoke packaged application' if: "runner.os == 'macOS'" working-directory: 'packages/desktop-shell' @@ -329,6 +386,8 @@ jobs: - name: 'Collect artifacts' shell: 'bash' + env: + LEGACY_ARCH: '${{ matrix.legacy_arch }}' run: | set -euo pipefail destination="$RUNNER_TEMP/desktop-artifacts" @@ -342,14 +401,17 @@ jobs: case "$name" in *.app.tar.gz.sig) stem="${name%.app.tar.gz.sig}"; extension='.app.tar.gz.sig' ;; *.app.tar.gz) stem="${name%.app.tar.gz}"; extension='.app.tar.gz' ;; - *.dmg) stem="${name%.dmg}"; extension='.dmg' ;; + *.dmg) name="Qwen-Code-Desktop-$LEGACY_ARCH.dmg" ;; + Qwen-Code-Desktop-*.zip) ;; *) continue ;; esac - name="${stem}-${{ matrix.rust_target }}${extension}" + if [ -n "$extension" ]; then + name="${stem}-${{ matrix.rust_target }}${extension}" + fi fi name="${name// /-}" cp "$artifact" "$destination/$name" - done < <(find "$bundle_root" -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.exe' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0) + done < <(find "$bundle_root" -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.exe' -o -name '*.zip' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0) if [ -z "$(find "$destination" -type f -print -quit)" ]; then echo '::error::No desktop artifacts were produced.' exit 1 @@ -371,7 +433,7 @@ jobs: publish: name: 'Publish GitHub release' - if: '${{ inputs.dry_run == false && github.repository == ''QwenLM/qwen-code'' }}' + if: "${{ inputs.dry_run == false && github.repository == 'QwenLM/qwen-code' }}" needs: - 'prepare' - 'build' @@ -390,6 +452,7 @@ jobs: - name: 'Generate checksums and updater manifest' shell: 'bash' env: + ELECTRON_BRIDGE: '${{ inputs.electron_bridge }}' GH_REPO: '${{ github.repository }}' RELEASE_TAG: '${{ needs.prepare.outputs.tag }}' RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' @@ -404,6 +467,12 @@ jobs: --tag "$RELEASE_TAG" \ --version "$RELEASE_VERSION" \ --output desktop-latest.json + if [ "$ELECTRON_BRIDGE" = 'true' ]; then + node ../.github/scripts/create-electron-bridge-manifest.mjs \ + --assets . \ + --version "$RELEASE_VERSION" \ + --output latest-mac.yml + fi sha256sum -- * > SHA256SUMS.txt - name: 'Create GitHub release' @@ -439,14 +508,25 @@ jobs: - name: 'Update stable updater feed' if: '${{ inputs.draft == false && inputs.prerelease == false }}' env: + ELECTRON_BRIDGE: '${{ inputs.electron_bridge }}' GH_TOKEN: '${{ github.token }}' FEED_TAG: '${{ env.DESKTOP_FEED_TAG }}' run: | set -euo pipefail + feed_assets=(release-assets/desktop-latest.json) + if [ "$ELECTRON_BRIDGE" = 'true' ]; then + feed_assets+=( + release-assets/latest-mac.yml + release-assets/Qwen-Code-Desktop-arm64.zip + release-assets/Qwen-Code-Desktop-x64.zip + release-assets/Qwen-Code-Desktop-arm64.dmg + release-assets/Qwen-Code-Desktop-x64.dmg + ) + fi if gh release view "$FEED_TAG" >/dev/null 2>&1; then - gh release upload "$FEED_TAG" release-assets/desktop-latest.json --clobber + gh release upload "$FEED_TAG" "${feed_assets[@]}" --clobber else - gh release create "$FEED_TAG" release-assets/desktop-latest.json --title 'Qwen Code Desktop latest' --notes 'Stable desktop updater feed.' --latest=false + gh release create "$FEED_TAG" "${feed_assets[@]}" --title 'Qwen Code Desktop latest' --notes 'Stable desktop updater feed.' --latest=false fi - name: 'Publish release summary' diff --git a/docs/design/desktop-electron-to-tauri-update-bridge.md b/docs/design/desktop-electron-to-tauri-update-bridge.md new file mode 100644 index 00000000000..2d9abaa1dce --- /dev/null +++ b/docs/design/desktop-electron-to-tauri-update-bridge.md @@ -0,0 +1,66 @@ +# Electron-to-Tauri desktop update bridge + +## Context + +The last published desktop release, `desktop-v0.0.5`, is an Electron app named `Qwen Code Desktop` with bundle identifier `com.alibaba.qwen-code`. Its macOS updater reads `latest-mac.yml` from the fixed `desktop-latest` release and installs a ZIP archive. + +The new desktop shell is a Tauri app. It currently uses a different product name and bundle identifier and publishes `desktop-latest.json`, so the existing Electron app cannot discover or replace it. + +## Goals + +- Let signed macOS Electron `0.0.5` installations update directly to the first stable Tauri release. +- Preserve the existing macOS application identity so the updater replaces the installed app bundle. +- Keep Tauri's signed updater feed for all releases after the migration. +- Make the bridge opt-in and one-time; later releases must not need Electron build tooling. + +## Non-goals + +- Migrating Electron settings, sessions, or workspace state. The Tauri app may ask for a workspace on first launch. +- Bridging Windows or Linux Electron installations. +- Generating Electron differential blockmaps. Electron updater falls back to the checksum-verified full ZIP. + +## Compatibility contract + +The Tauri bundle uses the legacy macOS identity: + +- product name: `Qwen Code Desktop` +- bundle identifier: `com.alibaba.qwen-code` +- artifact prefix: `Qwen-Code-Desktop` +- signing identity: the existing Developer ID Application certificate + +The bridge release must be newer than `0.0.5`. It publishes two updater views over the same signed app bundles: + +1. `latest-mac.yml` points legacy Electron clients at `Qwen-Code-Desktop-arm64.zip` or `Qwen-Code-Desktop-x64.zip`. +2. `desktop-latest.json` points Tauri clients at the signed Tauri updater archives. + +The ZIP is created from the already signed and notarized `.app`; it is not rebuilt by Electron tooling. + +## Release flow + +`Desktop Release` gains an `electron_bridge` input, disabled by default. + +- All macOS builds continue to produce the Tauri app, DMG, updater archive, and updater signature. +- When `electron_bridge` is enabled, each macOS build also creates a legacy-compatible ZIP. +- The publish job generates `latest-mac.yml` from the two ZIPs and two DMGs. +- A stable bridge release uploads the legacy metadata and payloads to `desktop-latest` together with `desktop-latest.json`. +- Later stable releases leave `electron_bridge` disabled. Updating `desktop-latest.json` does not remove the bridge files, so Electron installations that return later can still cross to Tauri. + +Draft and prerelease runs may build and publish bridge artifacts for inspection, but they never update the stable feed. + +## Signing credentials + +The repository already stores the Electron-era Apple certificate and App Store Connect API key under `MAC_CSC_*` and `APPLE_NOTARY_*` secret names. The workflow accepts those names as fallbacks for the newer Tauri names, so the Developer ID identity remains unchanged. + +Tauri updater artifacts additionally require `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. The private key must match the public key in the Tauri configuration before the first published Tauri release. + +## Validation + +Automated release-helper tests verify: + +- the legacy application identity, +- exact bridge artifact selection, +- SHA-512 and size values in `latest-mac.yml`, +- failure when a required bridge artifact is missing, +- existing Tauri updater manifest and version synchronization behavior. + +Before the stable release, install the signed `desktop-v0.0.5` arm64 and x64 builds, point them at an isolated bridge feed, and verify both `0.0.5 -> Tauri bridge` and `Tauri bridge -> newer Tauri` updates. diff --git a/packages/desktop-shell/README.md b/packages/desktop-shell/README.md index 2d91ab16d0c..7c0b669a95f 100644 --- a/packages/desktop-shell/README.md +++ b/packages/desktop-shell/README.md @@ -24,3 +24,11 @@ npm run dev --workspaces=false ``` Use `QWEN_DESKTOP_WORKSPACE=/absolute/path` to choose the initial workspace. Without it, the app shows a workspace picker on first launch. + +## Releases + +The `Desktop Release` workflow builds signed updater artifacts when `dry_run` is disabled. Published releases require the Tauri updater private key. macOS releases also require Apple signing and notarization credentials. + +The first stable Tauri release may set `electron_bridge=true` to publish the macOS ZIPs and `latest-mac.yml` consumed by Electron `0.0.5`. Leave the input disabled for later releases; the fixed `desktop-latest` release retains the bridge assets while `desktop-latest.json` advances independently. + +The macOS workflow accepts either the Tauri-era `APPLE_*` certificate and notarization secrets or the existing `MAC_CSC_*` and `APPLE_NOTARY_*` secrets. `TAURI_SIGNING_PRIVATE_KEY` must match the public key in `src-tauri/tauri.conf.json`. diff --git a/packages/desktop-shell/scripts/smoke-packaged.js b/packages/desktop-shell/scripts/smoke-packaged.js index f7ec4e0b09d..751115203c1 100755 --- a/packages/desktop-shell/scripts/smoke-packaged.js +++ b/packages/desktop-shell/scripts/smoke-packaged.js @@ -22,7 +22,12 @@ const isolatedHome = path.join(workspace, 'home'); const isolatedState = path.join(workspace, 'state'); fs.mkdirSync(isolatedHome); fs.mkdirSync(isolatedState); -const appId = 'com.qwen.code.desktop'; +const appId = JSON.parse( + fs.readFileSync( + path.join(packageDir, 'src-tauri', 'tauri.conf.json'), + 'utf8', + ), +).identifier; const logRoot = process.platform === 'darwin' ? path.join(isolatedHome, 'Library', 'Logs', appId) diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index 9c710e4d894..a6f432ae30e 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { execFileSync, spawnSync } from 'node:child_process'; +import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -18,6 +19,12 @@ const manifestScript = path.join( 'scripts', 'create-desktop-update-manifest.mjs', ); +const electronBridgeScript = path.join( + repoRoot, + '.github', + 'scripts', + 'create-electron-bridge-manifest.mjs', +); const versionScript = path.join(packageDir, 'scripts', 'version.js'); const root = fs.mkdtempSync( @@ -25,13 +32,44 @@ const root = fs.mkdtempSync( ); try { testBootstrapBridgeConfiguration(); + testLegacyApplicationIdentity(); + testElectronBridgeWorkflow(); testUpdateManifest(path.join(root, 'manifest')); + testElectronBridgeManifest(path.join(root, 'electron-bridge')); testVersionSynchronization(path.join(root, 'version')); console.log('Desktop release helper checks passed.'); } finally { fs.rmSync(root, { recursive: true, force: true }); } +function testLegacyApplicationIdentity() { + const config = JSON.parse( + fs.readFileSync( + path.join(packageDir, 'src-tauri', 'tauri.conf.json'), + 'utf8', + ), + ); + assert.equal(config.productName, 'Qwen Code Desktop'); + assert.equal(config.identifier, 'com.alibaba.qwen-code'); +} + +function testElectronBridgeWorkflow() { + const workflow = fs.readFileSync( + path.join(repoRoot, '.github', 'workflows', 'desktop-release.yml'), + 'utf8', + ); + assert.match(workflow, /^ {6}electron_bridge:$/m); + assert.match(workflow, /create-electron-bridge-manifest\.mjs/); + for (const artifact of [ + 'Qwen-Code-Desktop-arm64.zip', + 'Qwen-Code-Desktop-x64.zip', + 'Qwen-Code-Desktop-arm64.dmg', + 'Qwen-Code-Desktop-x64.dmg', + ]) { + assert.match(workflow, new RegExp(artifact.replaceAll('.', '\\.'))); + } +} + function testBootstrapBridgeConfiguration() { const config = JSON.parse( fs.readFileSync( @@ -149,6 +187,76 @@ function testUpdateManifest(directory) { assert.match(failure.stderr, /Missing updater signature/); } +function testElectronBridgeManifest(directory) { + const assets = path.join(directory, 'assets'); + fs.mkdirSync(assets, { recursive: true }); + const artifacts = [ + 'Qwen-Code-Desktop-arm64.zip', + 'Qwen-Code-Desktop-x64.zip', + 'Qwen-Code-Desktop-arm64.dmg', + 'Qwen-Code-Desktop-x64.dmg', + ]; + for (const artifact of artifacts) { + fs.writeFileSync(path.join(assets, artifact), `contents:${artifact}`); + } + const output = path.join(directory, 'latest-mac.yml'); + execFileSync(process.execPath, [ + electronBridgeScript, + '--assets', + assets, + '--version', + '0.1.0', + '--release-date', + '2026-08-03T00:00:00.000Z', + '--output', + output, + ]); + const manifest = fs.readFileSync(output, 'utf8'); + assert.match(manifest, /^version: 0\.1\.0$/m); + assert.match(manifest, /^releaseDate: '2026-08-03T00:00:00\.000Z'$/m); + for (const artifact of artifacts) { + const contents = fs.readFileSync(path.join(assets, artifact)); + const sha512 = crypto + .createHash('sha512') + .update(contents) + .digest('base64'); + assert.match( + manifest, + new RegExp( + `^ - url: ${artifact.replaceAll('.', '\\.')}\\n sha512: ${sha512.replaceAll('+', '\\+')}\\n size: ${contents.length}$`, + 'm', + ), + ); + } + const arm64Contents = fs.readFileSync(path.join(assets, artifacts[0])); + const arm64Sha512 = crypto + .createHash('sha512') + .update(arm64Contents) + .digest('base64'); + assert.match(manifest, /^path: Qwen-Code-Desktop-arm64\.zip$/m); + assert.match( + manifest, + new RegExp(`^sha512: ${arm64Sha512.replaceAll('+', '\\+')}$`, 'm'), + ); + + fs.rmSync(path.join(assets, artifacts[1])); + const failure = spawnSync( + process.execPath, + [ + electronBridgeScript, + '--assets', + assets, + '--version', + '0.1.0', + '--output', + output, + ], + { encoding: 'utf8' }, + ); + assert.notEqual(failure.status, 0); + assert.match(failure.stderr, /Missing Electron bridge artifact/); +} + function testVersionSynchronization(directory) { fs.mkdirSync(path.join(directory, 'src-tauri'), { recursive: true }); fs.copyFileSync( diff --git a/packages/desktop-shell/src-tauri/tauri.conf.json b/packages/desktop-shell/src-tauri/tauri.conf.json index bd33d82601f..5be94effa72 100644 --- a/packages/desktop-shell/src-tauri/tauri.conf.json +++ b/packages/desktop-shell/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "Qwen Code", + "productName": "Qwen Code Desktop", "version": "0.0.1", - "identifier": "com.qwen.code.desktop", + "identifier": "com.alibaba.qwen-code", "build": { "frontendDist": "../bootstrap" }, From 3742e4bbfb228ead011c4c760a6efc733f42a87e Mon Sep 17 00:00:00 2001 From: qwen-code-ci-bot Date: Sun, 2 Aug 2026 18:22:53 +0000 Subject: [PATCH 2/3] test(desktop): cover parseArguments validation in electron bridge manifest (#8392) --- .../desktop-shell/scripts/test-release.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index a6f432ae30e..42dbaa9ef5a 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -255,6 +255,30 @@ function testElectronBridgeManifest(directory) { ); assert.notEqual(failure.status, 0); assert.match(failure.stderr, /Missing Electron bridge artifact/); + + const invalidVersion = spawnSync( + process.execPath, + [ + electronBridgeScript, + '--assets', + assets, + '--version', + '0.1', + '--output', + output, + ], + { encoding: 'utf8' }, + ); + assert.notEqual(invalidVersion.status, 0); + assert.match(invalidVersion.stderr, /Invalid --version/); + + const missingOutput = spawnSync( + process.execPath, + [electronBridgeScript, '--assets', assets, '--version', '0.1.0'], + { encoding: 'utf8' }, + ); + assert.notEqual(missingOutput.status, 0); + assert.match(missingOutput.stderr, /Missing --output/); } function testVersionSynchronization(directory) { From 01c47d6515cc42ee593023f086385249042c252f Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Mon, 3 Aug 2026 10:34:08 +0800 Subject: [PATCH 3/3] chore(desktop): address bridge review follow-ups --- .github/scripts/create-electron-bridge-manifest.mjs | 3 +-- docs/design/desktop-electron-to-tauri-update-bridge.md | 2 +- packages/desktop-shell/scripts/test-release.js | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/create-electron-bridge-manifest.mjs b/.github/scripts/create-electron-bridge-manifest.mjs index 46ca88d8008..e143d4bcc64 100644 --- a/.github/scripts/create-electron-bridge-manifest.mjs +++ b/.github/scripts/create-electron-bridge-manifest.mjs @@ -14,7 +14,6 @@ const names = [ ]; const artifacts = names.map((name) => readArtifact(assets, name)); const primary = artifacts[0]; -const releaseDate = options['release-date'] ?? new Date().toISOString(); const lines = [ `version: ${options.version}`, @@ -26,7 +25,7 @@ const lines = [ ]), `path: ${primary.name}`, `sha512: ${primary.sha512}`, - `releaseDate: '${releaseDate}'`, + `releaseDate: '${new Date().toISOString()}'`, ]; fs.writeFileSync(options.output, `${lines.join('\n')}\n`); diff --git a/docs/design/desktop-electron-to-tauri-update-bridge.md b/docs/design/desktop-electron-to-tauri-update-bridge.md index 2d9abaa1dce..bc3a98d6cc7 100644 --- a/docs/design/desktop-electron-to-tauri-update-bridge.md +++ b/docs/design/desktop-electron-to-tauri-update-bridge.md @@ -51,7 +51,7 @@ Draft and prerelease runs may build and publish bridge artifacts for inspection, The repository already stores the Electron-era Apple certificate and App Store Connect API key under `MAC_CSC_*` and `APPLE_NOTARY_*` secret names. The workflow accepts those names as fallbacks for the newer Tauri names, so the Developer ID identity remains unchanged. -Tauri updater artifacts additionally require `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. The private key must match the public key in the Tauri configuration before the first published Tauri release. +Tauri updater artifacts additionally require `TAURI_SIGNING_PRIVATE_KEY`; `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` is only needed for an encrypted private key. The private key must match the public key in the Tauri configuration before the first published Tauri release. ## Validation diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js index 42dbaa9ef5a..8f6476bcba3 100755 --- a/packages/desktop-shell/scripts/test-release.js +++ b/packages/desktop-shell/scripts/test-release.js @@ -206,14 +206,15 @@ function testElectronBridgeManifest(directory) { assets, '--version', '0.1.0', - '--release-date', - '2026-08-03T00:00:00.000Z', '--output', output, ]); const manifest = fs.readFileSync(output, 'utf8'); assert.match(manifest, /^version: 0\.1\.0$/m); - assert.match(manifest, /^releaseDate: '2026-08-03T00:00:00\.000Z'$/m); + assert.match( + manifest, + /^releaseDate: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'$/m, + ); for (const artifact of artifacts) { const contents = fs.readFileSync(path.join(assets, artifact)); const sha512 = crypto