Qwen Code Desktop 发布加固实战:原子运行时替换、官方校验和验证与发布物白名单
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
Qwen Code Desktop 是围绕 Web Shell 构建的 Tauri 2 桌面外壳,其发布流程最脆弱的地方在于:捆绑 Node.js 运行时、打安装包、签名、上架更新源、镜像到 OSS 的每一步都可能因为中断、篡改或误传而产出不可用、不安全甚至版本回退的制品。本文基于仓库中的设计文档 desktop-release-hardening.md,逐条讲解其发布加固契约——从「旧运行时保留到新运行时完全组装完成」的原子替换,到「以官方校验和复核 Node.js 归档」「仅发布安装器/更新器产物」「预发布必须带 SemVer 后缀」「OSS 镜像与 GitHub 稳定源联动校验」等落地实现,并结合 prepare-runtime.js、test-release.js 与 desktop-release.yml 给出源码级依据。读完你将掌握一套可直接复用的桌面应用发布加固模式。
一、加固目标:五个必须守住的发布契约
设计文档将 Desktop 发布的加固要求归纳为五条核心契约:
- 保留最后的完整捆绑运行时,直到替代品完全组装完成;
- 交换被中断时,下一次运行要能恢复旧运行时;
- Node.js 归档必须用官方新鲜校验和(fresh official checksum)复核后复用;
- 只发布安装器 / 更新器产物;
- 发布的预发布版本必须带 SemVer 预发布后缀,保证后续稳定版在更新器视角下严格更新。
后两条之外,稳定版发布还需:先将版本化资产镜像到阿里云 OSS,再推进 OSS latest 清单;正常发布运行时若 GitHub 稳定源与刚发布的版本不一致则直接失败;手动回填旧版本不得改动 latest 源。
下面逐一展开这五条契约在仓库中的具体实现。
二、运行时原子替换与中断恢复
Desktop 的运行时会整体组装到packages/desktop-shell/runtime/qwen-code/,包含当前平台的 Node.js 运行时、捆绑的qwenCLI,以及构建好的 Web Shell(lib/web-shell/)。由于该目录会被 Tauri 应用直接消费,替换过程必须保证任意时刻都存在一个完整可用的运行时。
2.1 先组装、后替换的两阶段提交
prepare-runtime.js 的流程本质上是「两阶段提交」:
- 在
runtime/下用fs.mkdtempSync创建临时目录.prepare-*,在其中完成全部组装:拷贝dist产物到lib/、安装 Node 运行时、写启动脚本、写入 [LICENSE / NOTICE / manifest.json]、最后调用writeChecksums()生成 checksums.json; - 组装与校验全部通过后,才调用
replaceRuntime()做目录切换:- 若
runtime/qwen-code已存在,先rename为.prepare-*/previous; - 再把新组装好的
packageRootrename为runtime/qwen-code; - 一旦切换失败,回滚:把
previous再rename回来,然后重新抛出错误。
- 若
关键顺序被测试显式钉死:testDesktopReleaseHardening断言replaceRuntime();必须出现在writeChecksums();之后(见 test-release.js),即「先完成组装与校验,再替换」。
2.2 中断恢复:recoverInterruptedRuntime
如果替换发生在两次rename之间(例如进程被杀),旧运行时可能滞留在.prepare-*/previous中。prepare-runtime.js在每次运行开头调用recoverInterruptedRuntime():
- 扫描
runtime/下所有.prepare-*目录; - 若
runtime/qwen-code不存在且该目录内存在previous,则把previous还原为runtime/qwen-code; - 无论是否还原,都清理掉残留的
.prepare-*目录。
对应测试场景在testRuntimePreparation中:构造「complete-marker 文件 +.prepare-stranded/previous」的滞留现场并让下一次准备失败,断言 marker 内容仍被保留、残留的.prepare-*目录被清空(test-release.js)。这保证了任何一次失败的准备都不会把用户机器上的 Desktop 运行时弄丢。
三、Node.js 归档:官方校验和验证 + 可复用缓存
捆绑 Node.js 的下载、校验与缓存逻辑位于installNodeRuntime()(prepare-runtime.js)。
3.1 版本一致性门禁
脚本读取仓库根目录的.nvmrc,要求当前 Node 版本的主版本号与之匹配,否则直接报错(Node ${nodeVersion} does not match .nvmrc major version)。CI 中对应NODE_VERSION: '22.20.0'(desktop-release.yml)。这避免在不同 Node 主版本下产出运行时导致行为漂移。
3.2 每次都对官方 SHASUMS256.txt 做校验
与「信任本地缓存」不同,加固后的逻辑是:
- 每次下载
https://nodejs.org/dist/v${version}/SHASUMS256.txt(120 秒超时); - 先删除缓存目录里的旧
SHASUMS256.txt(fs.rmSync(path.join(cacheDir, 'SHASUMS256.txt'), { force: true })),保证永远用官方新鲜校验和比对; - 若本地缓存归档存在,先复制出来用本次校验和验证,通过则直接复用(
copyValidCachedArchive);验证失败则删除缓存并重新下载; - 重新下载的归档必须通过
verifyChecksum()(SHA-256 比对,见 prepare-runtime.js),通过后才写入缓存目录(先写.tmp再rename,避免半截文件污染缓存)。
缓存目录可通过环境变量QWEN_DESKTOP_NODE_CACHE_DIR指定,默认在系统临时目录的qwen-desktop-node-cache/。CI 中使用actions/cache以键desktop-node-v2-${rust_target}-${NODE_VERSION}-${dry_run}持久化该目录,并保证 restore 与 save 共用同一路径(desktop-release.yml、test-release.js)。
testRuntimePreparation用 mock fetch 完整验证了三态:首次下载并落缓存、二次运行命中缓存、缓存被篡改后自动回退重新下载(断言 fetch 日志中归档下载次数为 2、SHASUMS256.txt 为 3 次,且篡改后缓存目录里不再残留 SHASUMS256.txt),见 test-release.js。
3.3 平台目标与归档命名
目标平台由QWEN_DESKTOP_TARGET或process.platform-arch推导,支持darwin-arm64、darwin-x64、linux-arm64、linux-x64、win32-x64五种(含 rust target 别名映射)。归档名对应为node-v${version}-${target}.tar.gz|tar.xz|zip(prepare-runtime.js)。
四、发布物白名单:只发布安装器与更新器产物
「只发布安装器/更新器产物」的约束落在两处:版本门禁与资产收集过滤。
4.1 资产收集阶段的严格过滤
build作业收集 bundle 产物时(desktop-release.yml)按平台执行白名单case:
- macOS:只收集
.dmg、Qwen-Code-Desktop-*.zip、.app.tar.gz(含对应.sig),并做重命名(Qwen-Code-Desktop-$LEGACY_ARCH.dmg); - Windows:只收集
*-setup.exe与*-setup.exe.sig,其余一律continue; - Linux:只收集
*.AppImage、*.AppImage.sig、*.deb、*.deb.sig。
测试显式断言:Windows 收集分支不得包含通用*.exe(防止把嵌入的可执行文件误传),且必须保留*-setup.exe.sig签名文件(test-release.js)。收集结束后若无任何产物则::error::No desktop artifacts were produced.直接失败。
4.2 更新清单生成与签名强制
发布阶段用 create-desktop-update-manifest.mjs 生成desktop-latest.json(含各平台 URL 与签名),并用sha256sum -- * > SHA256SUMS.txt汇总校验。testUpdateManifest验证清单覆盖darwin-aarch64 / darwin-x86_64 / linux-x86_64 / windows-x86_64四个平台,且任一平台缺少.sig时脚本必须以Missing updater signature失败(test-release.js)。Tauri 更新器端点配置为 OSS 优先、GitHub 兜底(tauri.conf.json),Rust 侧更新检查超时固定为 3 秒(UPDATE_CHECK_TIMEOUT,见 main.rs 与 test-release.js)。
五、版本门禁:预发布必须带 SemVer 后缀
版本解析在prepare作业的Resolve version步骤(desktop-release.yml):
- 版本必须匹配
^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$; - 若
prerelease=true,版本必须带-开头的预发布后缀(如0.2.1-rc.1),否则报错退出——这是防止「预发布复用了稳定版版本号、导致后续稳定版在更新器眼里不更新」的关键; - 非 dry-run、非 draft、非 prerelease 的已发布稳定版则必须恰好是
X.Y.Z纯三段式。
对应断言见 test-release.js。桌面版本的三处来源——packages/desktop-shell/package.json、src-tauri/tauri.conf.json、src-tauri/Cargo.toml——由 version.js 同步修改,testVersionSynchronization验证三者一致。
另外electron_bridge场景(Electron 0.0.5 迁移到 Tauri 的一次性桥)要求版本严格大于0.0.5,防止桥接版本回退。
六、稳定发布与 OSS 镜像的联动校验
设计文档的后半部分聚焦稳定版发布与镜像的一致性。
6.1 GitHub 稳定源只进不退
Update stable updater feed步骤(desktop-release.yml)逻辑为:
- 下载
desktop-latestrelease 上的desktop-latest.json,校验其中版本必须是合法稳定版; - 用
sort -V比较「刚发布的版本」与「当前源版本」:若当前源已更新,正常流程输出 notice 并跳过(不降级),electron_bridge流程则直接报错退出; - 只有刚发布版本是较新(或相同)时才
--clobber上传新清单。
6.2 先传 OSS 版本化目录,再推 OSS latest
OSS 镜像由独立工作流 sync-desktop-to-oss.yml 负责,且只接受X.Y.Z稳定版与已发布的非 draft、非 prerelease 版本:
- 上传版本化资产:
scripts/upload-aliyun-oss-assets.js以desktop/v${VERSION}为前缀上传到 bucket(默认qwen-code-assets); - 验证版本化资产:下载远端文件逐项
sha256sum -c比对; - 再推 latest:比对刚上传的
desktop-latest.json与 GitHub 稳定源版本——两者一致才推进 OSS latest;GitHub 源已更新则 notice 跳过;若源反而落后于刚发布版本(且 source 为 artifact 场景)则::error::GitHub stable feed is ...失败。
这一「先版本化、后 latest」的顺序保证客户端在任何时刻指向的desktop-latest.json都指向已完整上传并验证过的资产,杜绝「清单先到、文件未齐」的窗口。
七、验证矩阵:工作流契约、helper 与双重冒烟
设计文档最后一条要求验证覆盖四类对象,仓库均有对应实现:
| 验证对象 | 位置 | 覆盖内容 |
|---|---|---|
| 发布工作流契约 | test-release.js(testDesktopReleaseHardening、testElectronBridgeWorkflow、testDesktopReleaseSigningWorkflow等) | 预发布版本门禁、资产白名单、Node 缓存键、签名顺序、电子桥清单生成与歧义检测 |
| Desktop 发布 helper | testRuntimePreparation、testUpdateManifest、testElectronBridgeManifest、testVersionSynchronization、testResolveLogRoot/testSliceNewLog | 运行时两阶段提交与中断恢复、更新清单、版本同步、日志增量读取 |
| 运行时冒烟 | smoke-runtime.js | 对runtime/qwen-code做完整性校验(manifest 字段 + checksums.json 全量 SHA-256),随后以随机 token 启动qwen serve,轮询/health与深健康/health?deep=true,再断言 Web Shell HTML 含<div id="root"></div> |
| 打包产物冒烟 | smoke-packaged.js | 启动真实安装包可执行文件,解析desktop-runtime.log等待就绪;验证未认证导航边界:Web Shell HTML 无需 token 可访问、API 路由保持 bearer 门禁且不得签发 cookie;macOS 上还校验打包运行时manifest.json的qwenCodeCommit与源码 HEAD 一致 |
此外dry_run输入可让任何 fork 在不发布的前提下完整走一遍「构建运行时 → 打无签名安装包」的打包路径,是文档中「dry-run installer build」的落地形式(desktop-release.yml)。
八、小结:可迁移的发布加固清单
从这份设计文档与对应实现中可以提炼出对任何「捆绑运行时 + 更新器」类桌面应用的通用加固清单:
- 两阶段提交替换运行时:临时目录组装 → 生成校验和 → 原子 rename,
previous兜底回滚,启动时清理滞留目录; - 外部二进制一律以官方新鲜校验和复核,缓存可复用但永不跳过校验,缓存文件用「tmp + rename」写入;
- 发布物按平台白名单收集,安装器与签名文件(
.sig)必须成对出现,更新清单缺签名即失败; - 预发布强制 SemVer 后缀,稳定发布只进不退(
sort -V守卫); - 镜像先版本化后 latest,并用
sha256sum回读远端文件做最终一致性验证; - 契约级测试 + 双重冒烟 + dry-run:把工作流行为、helper 逻辑、运行时与打包产物分别纳入自动化验证。
仓库中可直接对照阅读的入口:设计文档 desktop-release-hardening.md、运行时准备 prepare-runtime.js、发布测试 test-release.js、CI 工作流 desktop-release.yml 与 sync-desktop-to-oss.yml。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考