dev-manager-tauri/docs/tauri-cicd-guide.md
2026-04-02 14:19:41 +09:00

408 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tauri v2 Windows 应用 CI/CD 部署指南
> **方案版本**: v1.02026-04-02
> **适用范围**: Tauri v2 + GitHub Actions + 阿里云 OSS + Windows 平台
> **验证状态**: 已在 dev-manager-tauri 项目实际跑通v0.1.9 构建成功)
>
> 本文档是一套完整可复用的部署方案,涵盖从密钥生成到客户端自动更新的全链路。
> 新 Tauri 项目可直接参照本方案配置 CI/CD避免重复踩坑。
---
## 整体架构
```
开发者 push tag (v*)
GitHub Actions (windows-latest)
├── 1. 构建 Tauri 应用NSIS 安装包 + 签名)
├── 2. 创建 GitHub Release 并上传安装包
└── 3. 上传到阿里云 OSS + 生成 latest.json
客户端 tauri-plugin-updater
└── 定期检查 OSS 上的 latest.json → 发现新版本 → 下载 .exe → 验证签名 → 安装更新
```
**为什么用阿里云 OSS** GitHub Releases 在国内访问慢且不稳定OSS 提供国内 CDN 加速。
---
## 一、前置准备
### 1.1 生成签名密钥
```bash
pnpm tauri signer generate -w "$HOME\.tauri\your-app.key"
```
输出示例:
```
Your keypair was generated successfully
Private: C:\Users\xxx\.tauri\your-app.key
Public: C:\Users\xxx\.tauri\your-app.key.pub
TAURI_SIGNING_PRIVATE_KEY=dW50cnVzdGVkIGNvbW1lbnQ6IH...(一长串 base64
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=
```
> **Windows 注意事项**
> - 不要用 `~` 作为路径前缀Windows 的 PowerShell 会把 `~` 当作字面量目录名创建
> - 使用 `$HOME` 或完整路径代替
### 1.2 获取公钥 base64用于 tauri.conf.json
```powershell
$content = Get-Content "$HOME\.tauri\your-app.key.pub" -Raw
$content = $content -replace "`r`n", "`n"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)
[Convert]::ToBase64String($bytes)
```
将输出的 base64 字符串填入 `tauri.conf.json``plugins.updater.pubkey`
### 1.3 创建阿里云 OSS Bucket
- 创建 Bucket`appforwin`),区域按需选择(如 `oss-cn-hangzhou`
- 设置 Bucket 为**公共读**(否则客户端无法下载更新)
- 创建一个 RAM 用户,授予 OSS 读写权限,记录 AccessKey ID 和 Secret
---
## 二、配置 tauri.conf.json
关键配置项(**每一项都不能少**
```jsonc
{
"bundle": {
"active": true,
"targets": "all",
"createUpdaterArtifacts": true, // ⚠️ 必须Tauri v2 需要显式开启,否则不生成签名文件
"windows": {
"nsis": {}
}
},
"plugins": {
"updater": {
"pubkey": "<公钥 base64见 1.2 步骤>",
"endpoints": [
"https://<bucket>.<endpoint>/latest.json"
],
"dialog": false // 推荐 false用代码控制更新 UI
}
}
}
```
### 踩坑记录
| 问题 | 现象 | 原因 |
|------|------|------|
| **缺少 `createUpdaterArtifacts: true`** | 构建成功但只有 `.exe`,没有 `.exe.sig` | Tauri v2 默认不生成更新包,必须显式开启 |
| **pubkey 填成了私钥** | 构建报错 `failed to decode pubkey: Base64 conversion failed` | pubkey 是公钥的 base64不是私钥 |
| **pubkey 与私钥不匹配** | 构建成功但客户端更新时签名验证失败 | 重新生成密钥后要同步更新两处 |
---
## 三、配置 GitHub Secrets
在仓库 Settings → Actions → Secrets and variables → Actions 中添加:
| Secret 名称 | 值 | 说明 |
|---|---|---|
| `TAURI_SIGNING_PRIVATE_KEY` | `pnpm tauri signer generate` 输出的那一长串 base64 | **不是** .key 文件内容,是终端输出的 base64 值 |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | 密钥密码(无密码则留空) | 密码错误会导致签名静默失败 |
| `OSS_KEY_ID` | 阿里云 AccessKey ID | |
| `OSS_KEY_SECRET` | 阿里云 AccessKey Secret | |
| `OSS_BUCKET` | Bucket 名称(如 `appforwin` | |
| `OSS_ENDPOINT` | Endpoint 域名(如 `oss-cn-hangzhou.aliyuncs.com` | 不带 `https://` |
### 踩坑记录
| 问题 | 现象 | 原因 |
|------|------|------|
| **私钥格式错误** | 有 `.exe` 但没有 `.exe.sig`,无报错 | 存了 .key 文件原始内容2行应该存 base64 编码值1行 |
| **密钥密码不匹配** | 同上,签名静默失败 | 密码为空时 Secret 也要创建但值留空 |
---
## 四、配置 GitHub Actions Workflow 权限
**必须操作**(否则无法创建 Release
1. 仓库 → Settings → **Actions → General**(不是 Secrets 页面)
2. 滚到页面最底部 → **Workflow permissions**
3. 选择 **Read and write permissions**
4. 点 Save
### 踩坑记录
| 问题 | 现象 | 原因 |
|------|------|------|
| **未改 Workflow permissions** | `Error: Resource not accessible by integration` | GITHUB_TOKEN 默认只读,无法创建 Release |
| **改了权限但 Re-run 仍失败** | 同上错误 | Re-run 复用旧 token需要 push 新 tag 触发全新 run |
| **tauri-action 创建 Release 仍失败** | 即使设了 Read and write 仍报错 | 用 `softprops/action-gh-release@v2` 先创建 Release 作为 workaround |
---
## 五、Workflow 文件模板
`.github/workflows/release.yml`
```yaml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- run: pnpm install --frozen-lockfile
# 验证签名密钥(提前暴露配置问题)
- name: Verify signing key
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
if ([string]::IsNullOrEmpty($env:TAURI_SIGNING_PRIVATE_KEY)) {
Write-Error "TAURI_SIGNING_PRIVATE_KEY is EMPTY!"
exit 1
}
Write-Host "TAURI_SIGNING_PRIVATE_KEY is set"
# 先创建 Releaseworkaroundtauri-action 创建 Release 可能遇到权限问题)
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: 'App Name ${{ github.ref_name }}'
body: |
## 更新内容
请查看提交记录了解详情。
draft: false
prerelease: false
# 构建 + 上传到已有 Release
- name: Build & Upload
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: 'App Name ${{ github.ref_name }}'
releaseDraft: false
prerelease: false
# 上传到阿里云 OSS + 生成 latest.json国内用户更新源
- name: Upload to OSS & generate latest.json
shell: pwsh
env:
OSS_KEY_ID: ${{ secrets.OSS_KEY_ID }}
OSS_KEY_SECRET: ${{ secrets.OSS_KEY_SECRET }}
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
run: |
# 下载 ossutil
$oss = "$env:GITHUB_WORKSPACE\ossutil64.exe"
Invoke-WebRequest -Uri "https://gosspublic.alicdn.com/ossutil/1.7.17/ossutil-v1.7.17-windows-amd64.zip" -OutFile oss.zip
Expand-Archive oss.zip -DestinationPath oss_tmp
$exeFound = Get-ChildItem oss_tmp -Filter "ossutil*.exe" -Recurse | Select-Object -First 1
Copy-Item $exeFound.FullName $oss
$tag = "${{ github.ref_name }}"
$version = $tag.TrimStart('v')
$nsisDir = "$env:GITHUB_WORKSPACE\src-tauri\target\release\bundle\nsis"
$all = Get-ChildItem $nsisDir
# Tauri v2 更新格式:.exe + .exe.sig不是 v1 的 .nsis.zip
$installer = $all | Where-Object { $_.Name.EndsWith('-setup.exe') } | Select-Object -First 1
$sigFile = $all | Where-Object { $_.Name.EndsWith('-setup.exe.sig') } | Select-Object -First 1
if (-not $installer) { Write-Error "installer not found!"; exit 1 }
$bucket = "oss://$env:OSS_BUCKET"
$endpoint = "https://$env:OSS_ENDPOINT"
& $oss config -e $endpoint -i $env:OSS_KEY_ID -k $env:OSS_KEY_SECRET -L CH
& $oss cp $installer.FullName "$bucket/releases/$tag/$($installer.Name)" -f
if ($sigFile) {
& $oss cp $sigFile.FullName "$bucket/releases/$tag/$($sigFile.Name)" -f
$date = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')
$baseUrl = "https://$env:OSS_BUCKET.$env:OSS_ENDPOINT/releases/$tag"
$sig = (Get-Content $sigFile.FullName -Raw).Trim()
$json = [ordered]@{
version = $version
pub_date = $date
notes = "版本 $tag 已发布"
platforms = [ordered]@{
'windows-x86_64' = [ordered]@{
signature = $sig
url = "$baseUrl/$($installer.Name)"
}
}
} | ConvertTo-Json -Depth 5
$json | Out-File -FilePath latest.json -Encoding utf8NoBOM
& $oss cp latest.json "$bucket/latest.json" -f
Write-Host "latest.json uploaded"
} else {
Write-Warning "sigFile not found - skipping updater upload"
}
```
---
## 六、Tauri v2 更新包格式变化vs v1
这是最容易踩坑的地方:
| | Tauri v1 | Tauri v2 |
|---|---|---|
| **更新包格式** | `.nsis.zip` + `.nsis.zip.sig` | `.exe` + `.exe.sig` |
| **生成条件** | 设置 `TAURI_PRIVATE_KEY` | 设置 `TAURI_SIGNING_PRIVATE_KEY` **+** `createUpdaterArtifacts: true` |
| **latest.json url** | 指向 `.nsis.zip` | 指向 `-setup.exe` |
| **签名静默失败** | 会报错 | **不报错**,只是不生成 `.sig` 文件 |
> **关键**Tauri v2 签名失败时**完全静默**——构建照常成功,只是少了 `.sig` 文件。
> 排查方法:检查 `bundle/nsis/` 目录下是否有 `.exe.sig` 文件。
---
## 七、发版流程(日常操作)
```bash
# 1. 更新版本号
# 编辑 src-tauri/tauri.conf.json 中的 "version" 字段
# 编辑 src-tauri/Cargo.toml 中的 version保持一致
# 2. 提交代码
git add -A
git commit -m "release: v0.2.0"
# 3. 打 tag 并推送
git tag v0.2.0
git push
git push --tags
# 4. 等待 GitHub Actions 自动完成:
# - 构建 → 签名 → 创建 Release → 上传 OSS → 生成 latest.json
# 约 10 分钟完成
```
---
## 八、客户端更新检测(前端代码)
```typescript
import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
async function checkForUpdate() {
const update = await check()
if (update) {
console.log(`发现新版本: ${update.version}`)
// 下载并安装
await update.downloadAndInstall()
// 重启应用
await relaunch()
}
}
```
需要在 `src-tauri/src/lib.rs` 中注册插件:
```rust
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
```
Cargo.toml 依赖:
```toml
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
```
---
## 九、完整踩坑清单速查
| # | 问题 | 解决方案 |
|---|------|---------|
| 1 | `createUpdaterArtifacts` 未设置 | `tauri.conf.json``bundle.createUpdaterArtifacts: true` |
| 2 | 私钥格式错误(存了原始内容而非 base64 | GitHub Secret 存 `tauri signer generate` 输出的 base64 值 |
| 3 | pubkey 填了私钥的 base64 | pubkey 要用公钥文件的 base64 编码 |
| 4 | pubkey 与私钥不配对 | 重新生成后两处都要更新 |
| 5 | `Resource not accessible by integration` | Settings → Actions → General → Workflow permissions → Read and write |
| 6 | 改了权限 Re-run 仍失败 | push 新 tag 触发全新 workflow run |
| 7 | tauri-action 创建 Release 权限问题 | 用 `softprops/action-gh-release@v2` 预创建 |
| 8 | ossutil 路径找不到 | 下载 zip → 解压 → 用绝对路径调用 |
| 9 | PowerShell `-Filter *.nsis.zip` 不匹配多后缀 | 用 `Where-Object { $_.Name.EndsWith(...) }` |
| 10 | Tauri v2 找 `.nsis.zip` 找不到 | v2 格式是 `.exe` + `.exe.sig`,不是 `.nsis.zip` |
| 11 | Windows 上 `~` 路径问题 | 用 `$HOME` 代替 `~` |
| 12 | PowerShell `&&` 语法错误 | PowerShell 5 不支持 `&&`,用 `;` 或分开执行 |
---
## 十、OSS 目录结构
```
<bucket>/
├── latest.json ← 客户端检查更新的入口
└── releases/
├── v0.1.0/
│ ├── app_0.1.0_x64-setup.exe
│ └── app_0.1.0_x64-setup.exe.sig
├── v0.2.0/
│ ├── app_0.2.0_x64-setup.exe
│ └── app_0.2.0_x64-setup.exe.sig
└── ...
```
`latest.json` 示例:
```json
{
"version": "0.2.0",
"pub_date": "2026-04-02T04:28:01Z",
"notes": "版本 v0.2.0 已发布",
"platforms": {
"windows-x86_64": {
"signature": "<.exe.sig 文件内容>",
"url": "https://<bucket>.<endpoint>/releases/v0.2.0/app_0.2.0_x64-setup.exe"
}
}
}
```