- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
本文基于 next-tauri/doc/adr/008-updater-manifest.md 展开。MooTool 仓库同时维护 Java、Electron 与 Tauri 三条桌面产品线,如何让 Tauri 客户端拥有稳定的
latest.json静态更新源,又不破坏各产品线的独立发布边界,是本文要解决的核心问题。读完本文你将理解双层更新清单(根update-manifest.json+next-tauri-updater/latest.json)的职责划分、Rust Core 侧的校验逻辑、CI 发布与人工提升(promotion)流水线,以及防回退与幂等策略的工程细节。
背景:多产品线共存带来的更新源冲突
MooTool 仓库是一个多产品线仓库,同时维护:
- Java 桌面版(使用
v*tag 和version_summary.json更新清单); - MooTool Next Electron(使用
next-electron-v*tag 和独立的 Electron 更新清单); - MooTool Next Tauri(基于 Tauri 2、Rust、React 与 TypeScript 的独立产品线)。
ADR-008 明确指出问题的本质:
- Tauri Updater 需要一个平台级的静态
latest.json(版本号、发布日期、发行说明、各平台下载 URL 与 updater 签名),这是tauri-plugin-updater的常规消费格式; - 仓库又需要一个统一入口来表达不同产品的状态与历史;
- 如果直接让 Tauri 客户端读取 Electron 清单,或让多个产品共用同一个稳定更新 URL,就会破坏独立产品边界——一个产品的回滚、发布或失败会连带影响另一条产品线。
因此,这份 ADR(状态:已采纳,日期:2026-08-17)确定了两层清单的架构方案。
双层清单:根清单只做“产品发现”,latest.json才是“更新源”
第一层:根update-manifest.json只负责产品发现
仓库根目录的 update-manifest.json 是仓库级统一入口,使用schemaVersion: 1,下面挂载java、next-electron、next-tauri三个产品节点。以 Tauri 节点为例(当前仓库状态):
"next-tauri": { "displayName": "MooTool Next Tauri", "status": "active", "updaterManifestUrl": "https://github.com/rememberber/MooTool/releases/download/next-tauri-updater/latest.json", "releases": [ { "version": "0.1.0-rc.1", "title": "MooTool Next Tauri 0.1.0-rc.1", "prerelease": true, "releaseUrl": "https://github.com/rememberber/MooTool/releases/tag/next-tauri-v0.1.0-rc.1", "updaterManifestUrl": "https://github.com/rememberber/MooTool/releases/download/next-tauri-updater/latest.json", "assets": [ ] } ] }关键约定(继承自 ADR-008 决策点 1、3):
- Tauri 客户端固定读取
products.next-tauri,验证产品名、状态和专属更新 URL,不解析其他产品节点; - 首次正式提升前,
products.next-tauri.status保持planned;首次成功提升后才改为active(决策点 6 的结果)。当前仓库中该节点已是active,并记录了0.1.0-rc.1、0.1.0-rc.2两个候选版本; - 只有发布脚本允许修改该节点,Java/Electron 的版本、标签、资产与清单节点不在 Tauri 流程范围内。
第二层:next-tauri-updater/latest.json是 Tauri Updater 的稳定静态清单
Tauri 客户端实际消费的更新源,是next-tauri-updater这个独立 Release 中的latest.json。其结构在 next-tauri/scripts/prepare-release-assets.mjs 中定义:
{ "version": "0.1.0-rc.2", "notes": "发行说明正文", "pub_date": "2026-09-05T22:31:09Z", "release_url": "https://github.com/rememberber/MooTool/releases/tag/next-tauri-v0.1.0-rc.2", "platforms": { "darwin-x86_64": { "signature": "base64 minisign 签名", "url": "https://github.com/rememberber/MooTool/releases/download/next-tauri-v0.1.0-rc.2/MooTool-Next-Tauri-0.1.0-rc.2-mac-x64.app.tar.gz" }, "darwin-aarch64": { "signature": "...", "url": "...mac-arm64.app.tar.gz" }, "windows-x86_64": { "signature": "...", "url": "...win-x64-setup.exe" }, "linux-x86_64": { "signature": "...", "url": "...linux-x64.AppImage" } } }它覆盖 ADR-008 决策点 2 要求的所有字段:版本、发布日期、发行说明、Release URL,以及 macOS x64/arm64、Windows x64、Linux x64 的下载 URL 与 updater 签名。pub_date由发布工作流从 tag 提交时间解析为 RFC 3339 字符串(见 next-tauri-release.yml 中git show -s --format=%cI "${tag}^{commit}"),Rust Core 侧再格式化为前端可展示的日期。
Rust Core 侧:更新源固定且不可由渲染层注入
ADR-008 决策点 5(结果段)强调“更新源固定且由 Rust Core 校验,不能由渲染层注入任意端点”。这一约定在 next-tauri/src-tauri/src/commands/update.rs 中直接落地:
const PRODUCT_ID: &str = "next-tauri"; const PRODUCT_NAME: &str = "MooTool Next Tauri"; const UPDATE_REGISTRY_URL: &str = "https://raw.githubusercontent.com/rememberber/MooTool/master/update-manifest.json"; const UPDATE_MANIFEST_URL: &str = "https://github.com/rememberber/MooTool/releases/download/next-tauri-updater/latest.json"; const MAX_REGISTRY_BYTES: usize = 1024 * 1024; const MAX_RELEASE_NOTES_BYTES: usize = 64 * 1024;根清单的解析与校验链
fetch_product_feed()使用仅 HTTPS、最多 3 次重定向、15 秒超时的reqwest客户端拉取根清单,并限制响应不超过 1 MiB(read_limited)。随后product_feed_from_registry()(update.rs)执行五层校验:
- schema 校验:
schema_version != 1报update_manifest_invalid; - 产品存在性:根清单缺少
next-tauri节点报update_product_missing; - 产品身份:
displayName必须等于MooTool Next Tauri,否则报update_product_mismatch; - 状态机:
planned返回ProductFeed::Inactive(客户端直接判定“无更新”,不发起任何网络请求);不是active则报update_product_inactive; - 端点白名单:
updaterManifestUrl必须精确等于编译期常量UPDATE_MANIFEST_URL,否则报update_manifest_untrusted。
其中第 5 步是“端点不可注入”的关键实现——即使渲染层被攻破或根清单被篡改,只要updaterManifestUrl不是 Tauri 专属稳定通道,更新就会被拒绝。
单元测试对边界的验证
同一文件末尾的测试模块直接印证了 ADR 的约束:
planned_product_does_not_contact_an_update_endpoint:planned状态返回Inactive,不访问更新端点;active_product_accepts_only_the_tauri_channel_endpoint:只接受 Tauri 通道 URL,伪造的example.invalid/electron.json会被拒绝并返回update_manifest_untrusted;product_identity_and_schema_are_enforced:错误 schema 与错误产品名分别返回update_manifest_invalid、update_product_mismatch;release_notes_are_truncated_on_a_unicode_boundary:发行说明超 64 KiB 时在 Unicode 边界截断;publication_date_uses_javascript_compatible_rfc3339:发布日期以 RFC 3339 输出,前端new Date()可直接解析。
客户端完整更新流程:检查 → 下载 → 取消 → 安装 → 重启
Rust 命令层
update.rs暴露四个 Tauri Command:
check_for_product_update(L115-L172):读取当前版本 → 获取产品 feed → 用updater_builder().endpoints(vec![endpoint]).timeout(30s)构建 updater 并check()。仅当raw_json.release_url以https://github.com/rememberber/MooTool/releases/tag/next-tauri-v开头时才透传给前端;返回available / upToDate / inactive三种状态;install_product_update:通过update.download_and_install()流式回调进度事件(Started / Progress / Finished / Installed),并用tokio::select!支持取消(cancel_install唤醒Notify使下载中止,返回cancelled错误码);cancel_product_update/relaunch_after_product_update:分别取消进行中的下载、在重启前flush_window_state后调用app.restart()。
前端调用层
next-tauri/src/platform/api/updateApi.ts 将上述命令封装为ProductUpdateApi(check / install / cancel / relaunch),并在非 Tauri 环境(浏览器预览)降级为browserProductUpdateApi,明确提示“浏览器预览不能安装桌面更新”。事件契约定义在 next-tauri/src/platform/contracts/update.ts,ProductUpdateEvent涵盖started / progress / finished / cancelled / installed。对应的前端测试 updateApi.test.ts 断言只允许调用这 4 个产品专属命令。
“设置 → 关于与更新”界面
next-tauri/src/features/settings/UpdateCard.tsx 是用户可感知的完整闭环:点击“检查更新”调用check_for_product_update,展示最新版本、发布日期与发行说明;有可用更新时展示“安装并重启”按钮;下载期间实时渲染百分比进度条,并允许取消。这印证了 README 中的描述:设置中的更新入口“只读取根清单中的products.next-tauri,再进入 Tauri 专属静态更新通道,不会读取 Electron 更新节点”。
发布流水线:Draft Pre-release 先行,人工发布后才提升稳定通道
ADR-008 决策点 3 是发布流程的核心纪律:next-tauri-v{version}首先生成 Draft Pre-release;只有人工发布后,next-tauri-promote-update.yml才验证所有公开资产 URL、提升latest.json,随后只更新根清单的products.next-tauri。
第一步:.github/workflows/next-tauri-release.yml构建并生成 Draft
- 触发条件为
next-tauri-v*tag(支持workflow_dispatch指定已有 tag 重跑); preparejob 校验 tag 格式、package.json版本一致、release-notes/{version}.md存在,并执行 check-release-boundaries.mjs;buildjob 在四个平台(macOS Intel x64、macOS ARM64、Windows x64、Linux x64)构建,要求注入TAURI_SIGNING_PRIVATE_KEY/TAURI_SIGNING_PRIVATE_KEY_PASSWORD,并在上传前分别完成 macOS codesign 验证 + DMG 首次启动、Windows 静默安装/启动/卸载、Linux AppImage/deb 安装/启动/卸载的冒烟检查;draft-releasejob 调用prepare-release-assets.mjs prepare统一命名安装包、计算 SHA-512、读取.sig签名文件,生成latest.json与next-tauri-release.json,然后用gh release create "${RELEASE_TAG}" --draft --prerelease --latest=false创建Draft Pre-release。
关键点在于**--latest=false**(决策点 5):无论是版本 Release 还是更新通道,都显式不竞争仓库级Latest,避免 Tauri 的发布状态干扰 Java/Electron 的用户下载页。check-release-boundaries.mjs会硬性断言两个工作流都包含--latest=false,以及tauri.conf.json中bundle.targets为all、createUpdaterArtifacts为true、plugins.updater.endpoints为空数组(静态端点只能经products.next-tauri解析)、Windows 安装模式为passive等边界。
第二步:.github/workflows/next-tauri-promote-update.yml人工发布后的提升
该工作流由release.published事件触发,且仅当 tag 以next-tauri-v开头时运行。它完整实现决策点 3、4:
- 断言被发布的是一个非 Draft 的 Pre-release;
validate-promotion:校验latest.json与next-tauri-release.json版本一致、四个platformskey 齐全、每个签名格式合法、每个 URL 都落在next-tauri-v{version}独立版本 Release 下、release_url一致(对应 prepare-release-assets.mjs 的validatePromotion);- 用
curl --head逐一验证所有公开安装包与 updater 资产 URL 可达; - 创建或更新
next-tauri-updater通道 Release(同样--latest=false,标题固定为MooTool Next Tauri Update Channel),上传latest.json --clobber,并回读已发布的latest.json逐字节比对,确认稳定通道与本次提升版本完全一致; - 执行
update-registry,将products.next-tauri.status置为active、写入updaterManifestUrl与releases,提交并推送update-manifest.json。
防回退与幂等(决策点 4)
updateProductManifest(prepare-release-assets.mjs)在写入根清单前用完整的 SemVer 比较器(含 pre-release 段数值比较)检查:
if (latestExisting && compareVersions(release.version, latestExisting.version) < 0) { throw new Error(`Refusing to downgrade the Tauri update channel from ${latestExisting.version} to ${release.version}`) }即:拒绝把稳定通道指向低于现有最高版本的 Release;而相同版本写入会先过滤同版本旧条目再幂等重排,因此重复提升不会报错。release:rehearse脚本(见 package.json)可在本地演练整条提升路径。
独立发布、回滚与故障处理
ADR-008 的结果部分承诺的三项收益,在 next-tauri/doc/release-runbook.md 中有完整的操作化落地:
| 场景 | 处置方式 |
|---|---|
| Draft 未发布 | 修复源码后对同一 tag 重跑,工作流只允许覆盖仍为 Draft 的 Release |
| 已发布但尚未提升 | 修复公开资产或发布更高补丁版本,不要手工激活根清单 |
| 已提升 | 不覆盖既有安装包、不把稳定通道降级到旧版本;撤下有问题的 Release 并发布更高补丁版本修复 |
| updater 私钥丢失 | 停止发布,走经过评审的密钥轮换和人工安装方案,不得无提示更换公钥 |
本地发布门禁(在next-tauri目录):
npm ci npm run check npm audit --audit-level=high其中npm run check串联发布边界、TypeScript、前端与 Rust 测试、生产 Web 构建、Rustfmt 与 Clippy,并强制package.json、src-tauri/Cargo.toml、src-tauri/tauri.conf.json、release-notes/{version}.md四处版本一致。本地带签名构建示例:
TAURI_SIGNING_PRIVATE_KEY="$HOME/.tauri/mootool-next-tauri.key" \ TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(<"$HOME/.tauri/mootool-next-tauri.key.password")" \ APPLE_SIGNING_IDENTITY=- \ npx tauri build --bundles app,dmg仓库中的 src-tauri/tauri.conf.json 只保存 updater 公钥(含minisign public key标识),签名私钥通过 GitHub Secrets 注入且不得进入日志或 Artifact;公钥轮换必须走密钥轮换方案而非直接替换。相关签名决策另见 next-tauri/doc/adr/009-code-signing.md。
总结:一份 ADR 如何约束整条发布链路
ADR-008 的价值在于把“多产品线共存”的架构约束显式化,并通过仓库代码、脚本与 CI 逐层落地:
- 根 update-manifest.json是仓库统一入口,只承担产品发现;
products.next-tauri是 Tauri 客户端的唯一读取节点; next-tauri-updater/latest.json是平台级静态稳定清单,承载 Tauri Updater 所需的全部元数据与签名;- commands/update.rs用编译期常量 + 五层校验保证更新源固定、状态机清晰、端点不可注入;
- prepare-release-assets.mjs与两个 CI 工作流把 Draft → 人工发布 → 提升 → 根清单激活的全过程脚本化,并内置防回退与幂等;
- 结果:Java、Electron、Tauri 三条产品线可独立发布、回滚与演进;Draft 或资产不完整的 Release 不会被客户端发现;相同或更低版本不会被错误提示为更新。
如需完整阅读决策上下文与实操手册,建议依次查看 ADR-008、next-tauri/doc/release-runbook.md 以及上文索引的源码文件。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool Next Compose 更新通道 DIFF-027 深度解析:独立产品节点、只读清单与「校验后打开安装包」实践
MooTool Next Compose 更新通道 DIFF 027 深度解析:独立产品节点、只读清单与「校验后打开安装包」实践 导读 本文以 next com
桌面应用开发工具Release Please 实现设计解析:从发布分支到 Monorepo 清单的自动化发布架构
Release Please 实现设计解析:从发布分支到 Monorepo 清单的自动化发布架构 本指南基于 release please 仓库的 设计文档 h
开发工具CI/CDDevOpsMooTool Next Tauri:基于 Tauri 2 + Rust 构建独立桌面工具产品线的工程实践指南
MooTool Next Tauri:基于 Tauri 2 + Rust 构建独立桌面工具产品线的工程实践指南 MooTool Next Tauri 是 Moo
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考