Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/scripts/create-desktop-update-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ const platforms = {};
const platformArtifacts = [
[
'darwin-aarch64',
selectArtifact(assets, /-aarch64-apple-darwin\.app\.tar\.gz$/i, 'darwin-aarch64'),
selectArtifact(
assets,
/-aarch64-apple-darwin\.app\.tar\.gz$/i,
'darwin-aarch64',
),
],
[
'darwin-x86_64',
selectArtifact(assets, /-x86_64-apple-darwin\.app\.tar\.gz$/i, 'darwin-x86_64'),
selectArtifact(
assets,
/-x86_64-apple-darwin\.app\.tar\.gz$/i,
'darwin-x86_64',
),
],
['windows-x86_64', selectArtifact(assets, /-setup\.exe$/i, 'windows-x86_64')],
['linux-x86_64', selectArtifact(assets, /\.AppImage$/i, 'linux-x86_64')],
Expand All @@ -25,8 +33,10 @@ for (const [platform, artifact] of platformArtifacts) {
throw new Error(`Missing updater signature for ${artifact}`);
}
platforms[platform] = {
signature: fs.readFileSync(path.join(options.assets, signatureFile), 'utf8').trim(),
url: `https://github.com/${options.repository}/releases/download/${options.tag}/${encodeURIComponent(artifact)}`,
signature: fs
.readFileSync(path.join(options.assets, signatureFile), 'utf8')
.trim(),
url: `${releaseBaseUrl(options)}/${encodeURIComponent(artifact)}`,
};
}

Expand All @@ -47,6 +57,12 @@ function selectArtifact(assets, pattern, platform) {
return matches[0];
}

function releaseBaseUrl(options) {
return options['base-url']
? options['base-url'].replace(/\/+$/, '')
: `https://github.com/${options.repository}/releases/download/${options.tag}`;
}

function parseArguments(args) {
const values = {};
for (let index = 0; index < args.length; index += 2) {
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,20 @@ jobs:
env:
ELECTRON_BRIDGE: '${{ inputs.electron_bridge }}'
INPUT_VERSION: '${{ inputs.version }}'
IS_DRAFT: '${{ inputs.draft }}'
IS_DRY_RUN: '${{ inputs.dry_run }}'
IS_PRERELEASE: '${{ inputs.prerelease }}'
run: |
set -euo pipefail
version="${INPUT_VERSION#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Desktop version must be valid SemVer: $INPUT_VERSION"
exit 1
fi
if [ "$IS_DRY_RUN" = 'false' ] && [ "$IS_DRAFT" = 'false' ] && [ "$IS_PRERELEASE" = 'false' ] && [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Published stable Desktop versions must use X.Y.Z: $INPUT_VERSION"
exit 1
fi
if [ "$ELECTRON_BRIDGE" = 'true' ]; then
core="${version%%[-+]*}"
IFS='.' read -r major minor patch <<< "$core"
Expand Down Expand Up @@ -614,3 +621,21 @@ jobs:
echo "Tag: $RELEASE_TAG"
echo "Release: $RELEASE_URL"
} >> "$GITHUB_STEP_SUMMARY"

sync-oss:
name: 'Mirror stable Desktop release to Aliyun OSS'
if: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false && github.repository == 'QwenLM/qwen-code' }}"
Comment thread
yiliang114 marked this conversation as resolved.
needs:
- 'prepare'
- 'build'
- 'publish'
permissions:
actions: 'read'
contents: 'read'
uses: './.github/workflows/sync-desktop-to-oss.yml'
with:
version: '${{ needs.prepare.outputs.version }}'
source: 'artifact'
secrets:
ALIYUN_OSS_ACCESS_KEY_ID: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}'
ALIYUN_OSS_ACCESS_KEY_SECRET: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}'
237 changes: 237 additions & 0 deletions .github/workflows/sync-desktop-to-oss.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
name: 'Sync Qwen Code Desktop to Aliyun OSS'

on:
workflow_call:
inputs:
version:
required: true
type: 'string'
source:
required: true
type: 'string'
secrets:
ALIYUN_OSS_ACCESS_KEY_ID:
required: true
ALIYUN_OSS_ACCESS_KEY_SECRET:
required: true
workflow_dispatch:
inputs:
version:
description: 'Stable Desktop version to mirror, for example 0.1.2 or v0.1.2.'
required: true
type: 'string'
source:
description: 'Download the assets from the matching GitHub release.'
required: true
default: 'release'
type: 'choice'
options:
- 'release'

concurrency:
group: 'sync-desktop-to-oss'
cancel-in-progress: false

jobs:
sync:
name: 'Mirror Qwen Code Desktop to Aliyun OSS'
if: "${{ github.repository == 'QwenLM/qwen-code' && github.ref == 'refs/heads/main' }}"
runs-on: 'ubuntu-latest'
timeout-minutes: 90
environment:
name: 'production-release'
permissions:
actions: 'read'
contents: 'read'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3

- name: 'Resolve release'
id: 'release'
env:
INPUT_SOURCE: '${{ inputs.source }}'
INPUT_VERSION: '${{ inputs.version }}'
run: |
set -euo pipefail
version="${INPUT_VERSION#v}"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Desktop OSS mirrors require a stable X.Y.Z version (got '$INPUT_VERSION')."
exit 1
fi
if [[ "$INPUT_SOURCE" != 'artifact' && "$INPUT_SOURCE" != 'release' ]]; then
echo "::error::Desktop mirror source must be artifact or release (got '$INPUT_SOURCE')."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "source=$INPUT_SOURCE" >> "$GITHUB_OUTPUT"

- name: 'Download release workflow artifacts'
if: "${{ steps.release.outputs.source == 'artifact' }}"
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1
with:
pattern: 'desktop-*'
path: 'dist/desktop'
merge-multiple: true

- name: 'Download GitHub release assets'
if: "${{ steps.release.outputs.source == 'release' }}"
env:
GH_TOKEN: '${{ github.token }}'
VERSION: '${{ steps.release.outputs.version }}'
run: |
set -euo pipefail
metadata="$(gh release view "desktop-v${VERSION}" --json isDraft,isPrerelease)"
if ! jq -e '.isDraft == false and .isPrerelease == false' <<<"$metadata" >/dev/null; then
echo "::error::desktop-v${VERSION} is not a published stable release."
exit 1
fi
mkdir -p dist/desktop
gh release download "desktop-v${VERSION}" --dir dist/desktop

- name: 'Verify and prepare mirror assets'
env:
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
Comment thread
yiliang114 marked this conversation as resolved.
VERSION: '${{ steps.release.outputs.version }}'
run: |
set -euo pipefail
cd dist/desktop
if [[ -f SHA256SUMS.txt ]]; then
sha256sum -c SHA256SUMS.txt
fi
for asset in Qwen-Code-Desktop-arm64.dmg Qwen-Code-Desktop-x64.dmg; do
test -f "$asset" || { echo "::error::Missing Desktop installer $asset"; exit 1; }
done
find . -maxdepth 1 -name '*_x64-setup.exe' -print -quit | grep -q . \
|| { echo "::error::Missing Desktop installer matching *_x64-setup.exe"; exit 1; }
find . -maxdepth 1 -name '*.AppImage' -print -quit | grep -q . \
|| { echo "::error::Missing Desktop installer matching *.AppImage"; exit 1; }
find . -maxdepth 1 -name '*.deb' -print -quit | grep -q . \
|| { echo "::error::Missing Desktop installer matching *.deb"; exit 1; }
rm -f desktop-latest.json SHA256SUMS.txt
node ../../.github/scripts/create-desktop-update-manifest.mjs \
--assets . \
--repository "$GITHUB_REPOSITORY" \
--tag "desktop-v${VERSION}" \
--version "$VERSION" \
--base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/desktop/v${VERSION}" \
--output desktop-latest.json
sha256sum -- * > SHA256SUMS.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] sha256sum glob with * may produce malformed checksums with filenames containing spaces

Failure scenario: If a future CI build change produces an artifact with a space in its filename, sha256sum -- * writes hash Qwen Code Desktop.exe into SHA256SUMS.txt. The verification loop while read -r _ asset would read asset="Qwen" — the truncated first word — and curl would fail with a 404. Versioned assets would already be on OSS but verification fails.

Suggested change
sha256sum -- * > SHA256SUMS.txt
find . -maxdepth 1 -type f -exec sha256sum {} + > SHA256SUMS.txt

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

中文说明

Failure scenario: 如果未来 CI 构建产物的文件名包含空格,sha256sum -- * 会在 SHA256SUMS.txt 中写入 hash Qwen Code Desktop.exe。后续的 while read -r _ asset 验证循环会错误地将 asset 截断为 "Qwen",导致 curl 请求 404。此时版本化资源已上传到 OSS,但验证失败,导致 OSS 处于不一致状态。

建议修复:将 sha256sum -- * > SHA256SUMS.txt 替换为 find . -maxdepth 1 -type f -exec sha256sum {} + > SHA256SUMS.txt,可以正确处理任意文件名。

— deepseek-v4-flash via Qwen Code /review (v0.21.10)


- name: 'Install ossutil'
env:
OSSUTIL_URL: "${{ vars.OSSUTIL_URL || 'https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip' }}"
Comment thread
yiliang114 marked this conversation as resolved.
OSSUTIL_SHA256: "${{ vars.OSSUTIL_SHA256 || 'dcc512e4a893e16bbee63bc769339d8e56b21744fd83c8212a9d8baf28767343' }}"
run: |
set -euo pipefail
tmp_dir="$(mktemp -d)"
curl -fsSL --connect-timeout 15 --max-time 300 "$OSSUTIL_URL" -o "$tmp_dir/ossutil.zip"
echo "$OSSUTIL_SHA256 $tmp_dir/ossutil.zip" | sha256sum -c -
unzip -q "$tmp_dir/ossutil.zip" -d "$tmp_dir"
ossutil_path="$(find "$tmp_dir" -type f \( -name 'ossutil' -o -name 'ossutil64' \) -print -quit)"
if [[ -z "$ossutil_path" ]]; then echo '::error::ossutil binary not found'; exit 1; fi
chmod +x "$ossutil_path"
mkdir -p "$HOME/.local/bin"
install -m 0755 "$ossutil_path" "$HOME/.local/bin/ossutil"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
rm -rf "$tmp_dir"
"$HOME/.local/bin/ossutil" >/dev/null

- name: 'Configure Aliyun OSS credentials'
env:
ALIYUN_OSS_ACCESS_KEY_ID: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}'
ALIYUN_OSS_ACCESS_KEY_SECRET: '${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}'
ALIYUN_OSS_ENDPOINT: "${{ vars.ALIYUN_OSS_ENDPOINT || 'https://oss-cn-hangzhou.aliyuncs.com' }}"
run: |
set -euo pipefail
if [[ -z "$ALIYUN_OSS_ACCESS_KEY_ID" || -z "$ALIYUN_OSS_ACCESS_KEY_SECRET" ]]; then
echo '::error::Missing Aliyun OSS credentials in the production-release environment.'
exit 1
fi
ossutil config -e "$ALIYUN_OSS_ENDPOINT" -i "$ALIYUN_OSS_ACCESS_KEY_ID" -k "$ALIYUN_OSS_ACCESS_KEY_SECRET" -L EN -c "$RUNNER_TEMP/.ossutilconfig"

- name: 'Upload versioned assets to Aliyun OSS'
env:
ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}"
VERSION: '${{ steps.release.outputs.version }}'
run: |
set -euo pipefail
mapfile -d '' assets < <(find dist/desktop -maxdepth 1 -type f -print0)
node scripts/upload-aliyun-oss-assets.js \
--bucket "$ALIYUN_OSS_BUCKET" \
--config "$RUNNER_TEMP/.ossutilconfig" \
--prefix "desktop/v${VERSION}" \
"${assets[@]}"

- name: 'Verify versioned assets on Aliyun OSS'
env:
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
VERSION: '${{ steps.release.outputs.version }}'
run: |
set -euo pipefail
base="${ALIYUN_OSS_PUBLIC_BASE_URL}/desktop/v${VERSION}"
directory="$(mktemp -d)"
trap 'rm -rf "$directory"' EXIT
cp dist/desktop/SHA256SUMS.txt "$directory/SHA256SUMS.txt"
while read -r _ asset; do
curl -fsSL --connect-timeout 15 --max-time 3600 "$base/$asset" -o "$directory/$asset"
done < "$directory/SHA256SUMS.txt"
(cd "$directory" && sha256sum -c SHA256SUMS.txt)

- name: 'Check whether release matches GitHub stable feed'
id: 'latest'
env:
GH_TOKEN: '${{ github.token }}'
run: |
set -euo pipefail
directory="$(mktemp -d)"
trap 'rm -rf "$directory"' EXIT
gh release download 'desktop-latest' --dir "$directory" --pattern 'desktop-latest.json'
expected="$(jq -r '.version' dist/desktop/desktop-latest.json)"
Comment on lines +189 to +190

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] desktop-latest release check fails on first-ever run or after release deletion

Failure scenario: When triggered via workflow_dispatch directly on sync-desktop-to-oss.yml, the desktop-latest release may not exist (first-ever run) or may have been deleted. gh release download 'desktop-latest' exits non-zero, and set -euo pipefail causes the entire step to fail. The versioned assets have already been uploaded to OSS by this point, so the job fails on a non-essential check.

Suggested change
gh release download 'desktop-latest' --dir "$directory" --pattern 'desktop-latest.json'
expected="$(jq -r '.version' dist/desktop/desktop-latest.json)"
- name: Check whether release matches GitHub stable feed
id: latest
run: |
set -euo pipefail
directory="$(mktemp -d)"
gh release view 'desktop-latest' >/dev/null 2>&1 || { echo 'matches=false' >> "$GITHUB_OUTPUT"; exit 0; }
gh release download 'desktop-latest' --dir "$directory" --pattern 'desktop-latest.json'
expected="$(jq -r '.version' dist/desktop/desktop-latest.json)"
actual="$(jq -r '.version' "$directory/desktop-latest.json")"
if [ "$expected" = "$actual" ]; then
echo 'matches=true' >> "$GITHUB_OUTPUT"
else
echo 'matches=false' >> "$GITHUB_OUTPUT"
fi

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

中文说明

Failure scenario: 当通过 workflow_dispatch 直接触发 sync-desktop-to-oss.yml 时,desktop-latest 发布可能不存在(首次运行)或已被删除。gh release download 'desktop-latest' 会非零退出,set -euo pipefail 导致整个步骤失败。此时版本化资源已上传到 OSS,但任务因非必需的检查而失败。

建议修复:在下载前先检查 desktop-latest 发布是否存在,如果不存在则设置 matches=false 并正常退出。

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

actual="$(jq -r '.version' "$directory/desktop-latest.json")"
if [ "$actual" = "$expected" ]; then
echo 'matches=true' >> "$GITHUB_OUTPUT"
else
echo 'matches=false' >> "$GITHUB_OUTPUT"
echo "::notice::GitHub stable feed is $actual; mirrored version $expected will not replace the OSS latest feed."
fi

- name: 'Publish latest manifest to Aliyun OSS'
if: "${{ steps.latest.outputs.matches == 'true' }}"
env:
ALIYUN_OSS_BUCKET: "${{ vars.ALIYUN_OSS_BUCKET || 'qwen-code-assets' }}"
run: |
node scripts/upload-aliyun-oss-assets.js \
--bucket "$ALIYUN_OSS_BUCKET" \
--config "$RUNNER_TEMP/.ossutilconfig" \
--prefix 'desktop/latest' \
dist/desktop/desktop-latest.json

- name: 'Verify latest manifest on Aliyun OSS'
if: "${{ steps.latest.outputs.matches == 'true' }}"
env:
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
run: |
set -euo pipefail
curl -fsSL --connect-timeout 15 --max-time 300 "$ALIYUN_OSS_PUBLIC_BASE_URL/desktop/latest/desktop-latest.json" -o "$RUNNER_TEMP/desktop-latest.json"
cmp dist/desktop/desktop-latest.json "$RUNNER_TEMP/desktop-latest.json"

- name: 'Publish mirror summary'
env:
ALIYUN_OSS_PUBLIC_BASE_URL: "${{ vars.ALIYUN_OSS_PUBLIC_BASE_URL || 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com' }}"
IS_LATEST: '${{ steps.latest.outputs.matches }}'
VERSION: '${{ steps.release.outputs.version }}'
run: |
{
echo '## Desktop OSS mirror'
echo
echo "Version: $VERSION"
echo "Assets: ${ALIYUN_OSS_PUBLIC_BASE_URL}/desktop/v${VERSION}/"
if [ "$IS_LATEST" != 'true' ]; then
echo 'Latest feed: unchanged (the mirrored version is not the current GitHub stable release)'
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: 'Cleanup Aliyun OSS credentials'
if: '${{ always() }}'
run: 'rm -f "$RUNNER_TEMP/.ossutilconfig"'
8 changes: 4 additions & 4 deletions docs/design/2026-07-31-desktop-web-shell-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ flowchart LR
C -->|authenticated loopback URL| D[Existing Web Shell]
A -->|retry / choose workspace / logs| B
B -->|exit event| A
E[GitHub latest.json + installers] -->|signed updater| B
E[OSS / GitHub update feeds + installers] -->|signed updater| B
```

### 组件职责
Expand Down Expand Up @@ -122,13 +122,13 @@ flowchart LR

## 更新模型

Tauri updater 使用签名更新产物和固定公开 key。应用启动后后台检查一次更新:
Tauri updater 使用签名更新产物和固定公开 key。稳定发布的安装包和 updater 产物同时保存在 GitHub Releases 与 Aliyun OSS;应用优先检查 OSS 的小型更新清单,并在请求失败或超时时回退 GitHub。两个清单分别指向同一版本在各自源中的签名产物。应用启动后后台检查一次更新:

- 无更新:不打扰用户。
- 检查失败:写日志,不阻塞启动。
- 有更新:bootstrap/Web Shell 上方显示原生确认对话框;用户确认后下载并安装,然后重启。

发布 CI 使用 `TAURI_SIGNING_PRIVATE_KEY` 与 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 生成 updater signatures。`latest.json` 指向同一 GitHub Release 的平台更新包。只有非 draft、非 prerelease 发布会更新固定的 `desktop-latest` feed release
发布 CI 使用 `TAURI_SIGNING_PRIVATE_KEY` 与 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 生成 updater signatures。只有非 draft、非 prerelease 发布会更新 GitHub 的 `desktop-latest` feed,并在校验版本化 OSS 产物后更新 OSS feed。GitHub 始终保留为权威发布源和回退源

## 平台发布矩阵

Expand All @@ -149,7 +149,7 @@ Windows WebView2 使用 download bootstrapper;系统离线且缺失 WebView2
5. 构建安装包和 updater artifacts。
6. 平台 runner 安装并启动 packaged app,等待 daemon/Web Shell ready 证据。
7. 上传产物;发布 job 生成 `latest.json` 和 `SHA256SUMS.txt`。
8. 非 draft stable release 更新 `desktop-latest` feed。
8. 非 draft stable release 更新 GitHub `desktop-latest` feed,将同一批产物同步并校验到 OSS,再更新 OSS feed。

缺失签名密钥时只允许 `dry_run=true`,公开发布必须 fail closed。

Expand Down
Loading
Loading