外观
CI 三平台打包与发布设计方案
description: GitHub Actions 用 tauri-action 产出 macOS/Windows/Linux 安装包、softprops/action-gh-release 发布 Release 草稿的流水线设计
created: 2026-10-08 09:28:16
updated: 2026-10-15 16:50:00
author: sjzhao
1. 设计目标与硬约束
scope: 本文覆盖 GitHub Actions 构建流水线——三平台安装包产物、tag 触发的 Release 发布、手动触发产物下载。 边界:不覆盖代码签名与公证(macOS Developer ID / Windows 证书)、自动更新(tauri-updater)、e2e 测试入 CI、PR 触发的快速检查 job(后续可选)。 关联代码:
.github/workflows/release.yml(新建)、src-tauri/tauri.conf.json、src-tauri/Cargo.toml、frontend/package.json、.github/workflows/docs.yml(写法参考)
| 目标 | 量化口径 |
|---|---|
| 三平台产物 | 每次构建产出:macOS .app/.dmg、Windows .msi/.exe(NSIS)、Linux .deb/.rpm/.AppImage |
| 发布操作 | 打 tag v* 即自动创建 Release 草稿,产物已挂载、body 已拼接版本段,人工核对后发布 |
| 构建可复现 | Cargo.lock 与 frontend/package-lock.json 均已入库,CI 用 npm ci / cargo 锁定解析 |
| 缓存生效 | Rust 编译缓存(Swatinem/rust-cache)+ npm 缓存(setup-node),二次构建显著提速 |
| 最小权限 | workflow 仅申请 contents: write(创建 Release 所需),无额外 secrets 依赖 |
必须遵守的既有项目约束:
- 前端包管理器为 npm(
frontend/package-lock.json为准;根目录pnpm-workspace.yaml仅是allowBuilds残留,忽略); tauri.conf.json的bundle.targets: "all"、version: "1.0.0";根package.json是 docs 站点专用,与构建无关;- 窗口由
lib.rs代码创建(WebviewWindowBuilder),CI 无头环境可正常构建; - 当前为 ad-hoc 签名(无签名 identity),CI 产物同样未签名——见 §7 与非目标。
2. 必须遵守的既有项目约束
- 仓库布局为 Tauri 标准双目录:
frontend/(Node 侧,含@tauri-apps/cli)与src-tauri/(Rust 侧);tauri.conf.json的beforeBuildCommand以cwd: ../frontend执行npm run build(含vue-tsc -b类型检查),因此 CI 必须先在frontend/装依赖; - Node 版本基线 22(docs.yml 已用 22,保持一致);
- workflow 文件内注释风格与 docs.yml 一致(中文注释说明意图)。
3. 现状盘点与差距
| 位置 | 现状 | 差距 |
|---|---|---|
.github/workflows/ | 仅 docs.yml(VitePress 部署 Pages) | 无任何构建/打包流水线 |
README.md | 声称「.github/workflows/ CI 自动打包脚本」 | 与事实不符,流水线落地后成立 |
| 版本号 | tauri.conf.json / Cargo.toml / frontend/package.json 均为 0.1.0 | 可直接打首个 tag |
| 产物分发 | 无 | 无法向用户提供安装包 |
结论:本设计补齐「三平台打包、tag 发布、产物分发」三块,版本号无需改动。
4. 目标与非目标
目标
- 新建
.github/workflows/release.yml:三平台 matrix 构建,产物上传; push tag v*触发:自动创建 Release 草稿(draft),挂载全部平台产物,body 自动拼接版本段,维护者核对后直接发布;workflow_dispatch手动触发:仅构建并上传 workflow artifact,用于发版前验证,不产生 Release。
非目标
- 不做 macOS 签名/公证(无 Apple 开发者账号时产物下载后需手动放行 Gatekeeper,见 §7);
- 不做 Windows Authenticode 签名;
- 不接入 tauri-updater 自动更新(updater 需要
includeUpdaterJson与签名密钥,届时另立设计); - 不在 CI 跑 e2e(需 docker sshd 服务,另行评估);不新增 PR 快速检查 job(可后续追加到本文件);
- 不做 macOS x86_64 目标(matrix 仅
macos-latest= arm64;Intel Mac 支持列入未决问题)。
5. 总体架构
text
push tag v* ─┐
├─▶ release.yml ─┐
dispatch ────┘ │
├─ build job(matrix 三跑,timeout 60min,concurrency: release-<ref>)
│ ├─ checkout
│ ├─ rust stable + rust-cache(src-tauri)
│ ├─ node 22 + npm cache(frontend lock)
│ ├─ [linux] apt 装 webkit/gtk/appindicator/rsvg/patchelf
│ ├─ npm ci(frontend/)
│ ├─ tauri-action:仅构建(tagName='' 跳过发布,beforeBuildCommand 先跑前端构建)
│ └─ upload-artifact:bundle 目录 → bundles-<os>
│
└─ release job(仅 tag 触发,needs build 全绿,timeout 10min)
├─ download-artifact(pattern: bundles-*)
└─ softprops/action-gh-release:创建草稿 + 挂载分发产物(不含 .app)6. 详细设计
6.1 workflow 全文(.github/workflows/release.yml)
设计取舍:build 与 release 拆成两段。build matrix 三平台并行构建并上传 workflow artifact;release job 等 build 全绿后才创建 Release 草稿并挂载全部产物——避免单平台失败时草稿只挂部分产物被误判为"全部就绪"。tauri-action 仅用于构建(
tagName: ''跳过其内置发布步骤),发布交给softprops/action-gh-release@v2统一控制。
yaml
# 三平台打包与发布:tag v* → Release 草稿;手动触发 → 仅产物(无 Release)
# 产物未签名(ad-hoc):macOS 首次打开需右键 → 打开,Windows/Linux 有 SmartScreen/未知来源提示
# Release body 由 release-body-prefix.md(安装须知)+ CHANGELOG.zh-CN.md 当前版本段拼接(ci-release-design §10)
name: Build & Release
on:
push:
tags: ['v*']
workflow_dispatch:
# 创建 Release 草稿所需的最小权限
permissions:
contents: write
# 同一 tag/dispatch 不并发;dispatch 排队不取消,避免产物半成品
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-22.04, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60 # 首次无缓存构建可能 30-60min,卡死时及时止损
steps:
- name: Checkout
# v5 是最小 node24 major:v6 把 credentials 移出 .git/config,v7 默认阻止 fork-PR checkout,不越级
uses: actions/checkout@v5
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Setup Node
# v5 默认启用 package-manager cache,已用 cache: npm 显式指定不受影响
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
# Tauri v2 Linux 构建的系统依赖(webkit2gtk-4.1 为 Tauri 2 基线)
- name: Install Linux system deps
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf
# beforeBuildCommand 会在 frontend/ 里跑 npm run build(含 vue-tsc 类型检查)
- name: Install frontend deps
working-directory: frontend
run: npm ci
# 仅构建:tagName 传空串跳过 tauri-action 内置发布步骤;发布交由下游 release job
- name: Build
uses: tauri-apps/tauri-action@v0
with:
projectPath: src-tauri
tagName: ''
includeUpdaterJson: false
# build 产物上传 workflow artifact;tag 触发时由 release job 下载后统一挂载到 Release
# upload-artifact 必须用 v6:v5 仍声明 node20,会被强制跑 Node 24 并报警告
- name: Upload bundle artifacts
uses: actions/upload-artifact@v6
with:
name: bundles-${{ matrix.os }}
path: |
src-tauri/target/release/bundle/dmg/*.dmg
src-tauri/target/release/bundle/macos/*.app
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/rpm/*.rpm
src-tauri/target/release/bundle/appimage/*.AppImage
if-no-files-found: warn
# 仅 tag 触发执行:等三平台 build 全绿后下载全部 artifact,
# 用 CHANGELOG.zh-CN.md 当前版本段 + 安装须知前缀拼接 Release body,创建草稿
release:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
# 为读取 frontend/package.json 版本号提供 Node 运行时
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
# 下载所有平台 artifact,按 artifact 名解压到 <name>/ 子目录
# download-artifact 必须用 v7:与 upload-artifact v6 配对,v5 改变了单 artifact 下载路径
- name: Download all bundle artifacts
uses: actions/download-artifact@v7
with:
path: bundles
pattern: bundles-*
# 拼接 Release body:安装须知前缀 + 分隔线 + CHANGELOG.zh-CN.md 对应版本段
# 版本号取自 frontend/package.json,与三处版本对齐约定一致
- name: Build release body
run: |
VERSION=$(node -p "require('./frontend/package.json').version")
awk -v ver="$VERSION" '
$0 ~ ("^## \\[" ver "\\]") {flag=1; next}
/^## \[/ && flag {exit}
flag {print}
' CHANGELOG.zh-CN.md > changelog-latest.md
# 防呆:CHANGELOG.zh-CN.md 未回写当前版本段时直接失败,避免发出空 body
if [ ! -s changelog-latest.md ]; then
echo "::error::CHANGELOG.zh-CN.md 未包含 [${VERSION}] 段落,请先回写后再打 tag"
exit 1
fi
cat .github/release-body-prefix.md > release-body.md
printf '\n---\n\n' >> release-body.md
cat changelog-latest.md >> release-body.md
echo '--- release-body.md ---'
cat release-body.md
# 统一创建 Release 草稿,glob 挂载分发产物(.app 目录不挂载,用户应下 dmg)
# softprops v2 优先 body_path
- name: Create release draft
uses: softprops/action-gh-release@v2
with:
name: Rhost ${{ github.ref_name }}
draft: true
prerelease: false
body_path: release-body.md
files: |
bundles/**/*.dmg
bundles/**/*.msi
bundles/**/*.exe
bundles/**/*.deb
bundles/**/*.rpm
bundles/**/*.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}6.2 关键取值与理由
| 取值 | 理由 |
|---|---|
ubuntu-22.04 | Tauri 2 文档基线,libwebkit2gtk-4.1 包名稳定;ubuntu-latest(24.04)亦可用但不作为锚点 |
fail-fast: false | 单平台失败不吞掉其余平台的产物,便于定位平台相关问题 |
includeUpdaterJson: false | 未接入 tauri-updater,不生成 latest.json |
projectPath: src-tauri | action 以该目录定位 tauri.conf.json;beforeBuildCommand 的 cwd: ../frontend 相对解析不变 |
| build → release 两段式 | build 仅构建+上传 artifact;release needs: build 全绿后才创建草稿,避免单平台失败导致草稿只挂部分产物被误判为"全部就绪" |
build tagName: '' | 空串让 tauri-action 跳过内置发布步骤,发布权交给 release job 的 softprops/action-gh-release@v2,单一职责 |
softprops/action-gh-release@v2 | 比 tauri-action 内置发布更可控;files 用 glob 只挂分发产物(dmg/msi/exe/deb/rpm/AppImage),排除 .app 目录;body_path 指向拼接好的 release-body.md |
body_path + CHANGELOG 版本段 | Release body 由 .github/release-body-prefix.md(安装须知)+ CHANGELOG.zh-CN.md 当前版本段(awk 提取)拼接,单一事实来源;段落缺失时流水线直接失败(防呆),避免发出空 body(详见 §10) |
| action 版本(checkout@v5 / setup-node@v5 / upload@v6 / download@v7) | v5 为最小 node24 major;upload v6/download v7 配对,v5 改变了单 artifact 下载路径 |
concurrency: release-${{ github.ref }} | 同一 tag/dispatch 不并发;dispatch 排队不取消,避免产物半成品;与 docs.yml 的 concurrency 风格一致 |
build timeout-minutes: 60 | 首次无缓存构建可能 30-60min,卡死时及时止损,避免长时间占用 runner |
release timeout-minutes: 10 | 仅下载+创建草稿,10min 足够 |
if-no-files-found: warn | targets 为 all,个别平台某格式可能未产出,警告即可不阻断 |
6.3 产物与版本
- 版本号来源于
tauri.conf.json的version(当前1.0.0),文件名形如rhost_1.0.0_aarch64.dmg、rhost_1.0.0_x64-setup.exe、rhost_1.0.0_amd64.deb; - 发版流程:三处 version 对齐 → 提交 →
git tag v1.0.0 && git push origin v1.0.0→ Actions 生成草稿 → 人工核对发布; - tag 与
tauri.conf.jsonversion 不一致时以 conf 为准(产物名按 conf 版本),发布前人工核对。
6.4 产物命名规范
Tauri 2 默认产物名由 productName + version + arch 派生,可定制空间有限。本节约束项目层面的取舍,使产物名稳定、可预期、可被脚本解析。
命名来源与硬约束:
| 字段 | 来源 | 取值 | 备注 |
|---|---|---|---|
| 产品名段 | tauri.conf.json → productName | rhost(全小写) | 已固化,禁止改为 Rhost/RHOST,否则所有产物名连带变更 |
| 版本段 | tauri.conf.json → version | 1.0.0(语义化版本) | 禁止从 tag 字符串解析版本号;tag 必须与 conf 对齐(见 §6.3) |
| 架构段 | Tauri 按 runner 自动注入 | aarch64 / x64 / amd64 / x86_64 | 各平台格式不同,见下表 |
| OS 段 | 不注入 | — | dmg/msi/exe/deb/rpm/AppImage 后缀已天然区分 OS,无需再追加 OS 段 |
各平台产物命名(Tauri 默认,不可重命名):
| 平台 | 格式 | 示例(v1.0.0, arm64/x64) | Release 是否挂载 |
|---|---|---|---|
| macOS | DMG | rhost_1.0.0_aarch64.dmg / rhost_1.0.0_x64.dmg | ✅ |
| macOS | App bundle | rhost.app(目录,无版本/架构) | ❌(仅本地构建产物,release job 显式排除) |
| Windows | NSIS | rhost_1.0.0_x64-setup.exe | ✅ |
| Windows | MSI | rhost_1.0.0_x64_en-US.msi | ✅ |
| Linux | Deb | rhost_1.0.0_amd64.deb | ✅ |
| Linux | Rpm | rhost_1.0.0.x86_64.rpm | ✅ |
| Linux | AppImage | rhost_1.0.0_x64.AppImage | ✅ |
注:rpm 用
.分隔版本与架构、deb 用amd64表示 x64、MSI 含语言段en-US——均为 Tauri/bundler 默认行为,不得手动重命名。
Release 资产展示规则:
- Release 资产名保持 Tauri 默认输出,禁止在 workflow 中重命名;
- Release 标题:
Rhost v1.0.0(与 §6.1 release job 的name字段一致); - Release body:由
.github/release-body-prefix.md(安装须知前缀)+CHANGELOG.zh-CN.md当前版本段拼接为release-body.md,经body_path注入;维护者核对后直接发布,无需手写说明; - 预发布(pre-release)用
v1.0.0-rc.1形式,confversion同步为1.0.0-rc.1,产物文件名随之变化。
命名规范的非目标:
- 不为产物加 OS 段(
macos/windows/linux),后缀已足够区分; - 不引入构建时间戳、commit hash、构建号——版本号已唯一标识一次发版;
- 不做 universal2 / x86_64 macOS 产物(见 §9 未决问题)。
7. 安全与降级
- 权限最小化:workflow 级
contents: write是创建 Release 的必要权限;未申请packages/id-token等; - 无自定义 secrets:仅用内置
GITHUB_TOKEN,无泄漏面;签名体系接入前不引入任何密钥; - 产物未签名的用户侧影响(写入 Release 说明模板):macOS 从浏览器下载的
.dmg装好后若提示「"rhost" 已损坏,无法打开」,运行xattr -dr com.apple.quarantine /Applications/rhost.app移除隔离属性后再打开(ad-hoc 签名 + quarantine 触发 Gatekeeper 标记"已损坏",右键 → 打开对此错误无效);Windows SmartScreen 选「仍要运行」;签名/公证为后续独立事项; - 构建失败:
fail-fast: false保证其余平台 build 产物仍上传 artifact;release jobneeds: build要求三平台全绿才执行,单平台失败时不会创建 Release 草稿,无半成品草稿风险;单平台失败先查该 runner 的系统依赖与 Rust 缓存,必要时rust-cache加cache-all-crates或清理重跑; - 超时降级:build
timeout-minutes: 60、releasetimeout-minutes: 10,卡死时自动取消不堆积 runner。
8. 测试与验证
- 语法与触发验证:workflow YAML 本地可用
actionlint(若有)或推送后看 Actions 解析结果; - 手动触发一轮:
workflow_dispatch→ 三平台 build job 全绿 → 下载bundles-*artifact,核对三平台产物齐全(dispatch 不触发 release job,无 Release 草稿); - tag 验证:打
v1.0.0→ build 全绿 → release job 创建草稿、挂载 6-7 个分发产物(不含.app)、body 含安装须知 +CHANGELOG.zh-CN.md当前版本段 → 维护者核对后直接发布; - 冒烟:下载产物在本机安装启动(macOS arm64 可真机验证;Windows/Linux 产物至少确认文件完整可解包)。
9. 未决问题
- macOS x86_64:是否为 Intel Mac 增加
macos-13matrix 条目或出 universal2 包(构建时长翻倍),待有 Intel 用户诉求后决策; - macOS 签名/公证、Windows Authenticode:需要开发者账号与证书 secrets,发布对外正式版前评估;
- e2e 测试入 CI(ubuntu runner 起 docker/debian-sshd 容器跑
cargo test --test ssh_e2e)与 PR 快速检查 job(cargo check+vue-tsc)——待构建流水线稳定后追加; - tauri-updater 自动更新通道(
autoUpdate/updateChannel设置项已预留但后端未实现)——另立设计;
10. CHANGELOG 引入与提取方案
status: 已落地。中英双版入库(
CHANGELOG.md英文 /CHANGELOG.zh-CN.md中文);release.ymlrelease job 用 awk 从中文版提取当前版本段,与.github/release-body-prefix.md(安装须知)拼接为body_path;段落缺失时流水线失败(防呆)。
10.1 目标与非目标
目标:
- 提供入库的、可 diff、可离线检索的版本变更记录,不依赖 GitHub 平台;
- Release body 从
CHANGELOG.zh-CN.md当前版本段提取,单一事实来源,仓库与 Release 页内容一致。
非目标:
- 不自动 commit
CHANGELOG.md/CHANGELOG.zh-CN.md回 main(避免 bot token 与分支保护冲突,由开发者发版前手工提交); - 不引入 release-please / changesets(与 Tauri 双目录 + npm/cargo 混合构建不匹配,过重);
- 不引入 conventional-changelog 等自动生成方案:版本段需面向用户做主题化总结(亮点分组、过滤内部实现细节、双语维护),机械罗列 commit 达不到该质量。
10.2 格式与位置
- 格式:Keep a Changelog 1.1.0 + SemVer 2.0.0;
- 位置:仓库根
CHANGELOG.zh-CN.md(中文,Release body 提取源)与CHANGELOG.md(英文,GitHub 默认展示),顶部互加语言切换链接(社区惯例,用户克隆即可见); - 段落结构:
## [VERSION] - YYYY-MM-DD下分### Added/### Changed/### Deprecated/### Removed/### Fixed/### Security(中文版对应「新增 / 变更 / 废弃 / 移除 / 修复 / 安全」;两版段落标题格式严格一致,保证同一提取逻辑对两版均可用)。
10.3 配套:conventional commits
版本段由人工总结撰写,不依赖自动生成;但规范化的提交历史仍便于归纳与追溯:
- 根
package.json已配置commitlint+husky钩子(commit-msg 检查); - README 已补 commit 规范说明;
- 历史 commit 不回溯改写,仅约束新 commit。
10.4 提取与维护
- 提取:
release.yml的Build release body步骤用 awk 取CHANGELOG.zh-CN.md中## [<version>]至下一个## [之间的内容,版本号取自frontend/package.json(完整 yaml 见 §6.1); - 防呆:段落缺失(忘回写)时
::error::报错并exit 1,避免发出空 body; - 维护:发版前人工总结提交历史,向中文版与英文版各 prepend 一个版本段,与三处版本号改动一并提交;
- 不引入任何 changelog 生成工具,根
package.json只承载 docs 站点脚本。
10.5 语言策略与 body 一致性
- 英文版
CHANGELOG.md对齐英文README.md,作为 GitHub 仓库默认展示与社区阅读入口;中文版CHANGELOG.zh-CN.md对齐中文README.zh-CN.md; - Release body 取中文版:
release-body-prefix.md(安装须知)为中文,与中文版本段同语种,避免页内中英混排; - body 拼接步骤完整 yaml 见 §6.1,不在此重复;
- 文档站(VitePress)仅中文,不镜像
CHANGELOG.zh-CN.md(变更记录以仓库文件为单一事实来源)。
10.6 落地检查清单
- [x] 根
package.json加commitlint+husky; - [x] README 补 commit 规范说明;
- [x]
CHANGELOG.md(英文)入库,人工总结历史档案(2.0.0 / 1.0.0); - [x]
CHANGELOG.zh-CN.md(中文)入库,并作为 Release body 提取源; - [x] 新增
.github/release-body-prefix.md; - [x]
release.ymlrelease job 用 awk 提取中文版版本段,改用body_path;删除generate_release_notes与原body字段; - [x] 验证:awk 提取在当前版本段(
## [2.0.0])上实测通过。
10.7 风险与回退
- 未回写当前版本段(忘改
CHANGELOG.zh-CN.md)→ 流水线::error::防呆失败,不产出草稿;回写后重新打 tag 即可; - 中英两版不同步 → 以中文版为准(Release body 提取源),英文版仅在下次发版前对齐;
body_path与body不可同时生效(softprops v2 优先body_path),切换时务必删旧body。