# Tauri v2 Windows 应用 CI/CD 部署指南

> **方案版本**: v1.0（2026-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
    }
  }
}
```

### 2.2 配置 Capabilities 权限

`src-tauri/capabilities/default.json` 中必须添加以下权限，否则前端调用 updater / process API 会被拦截：

```jsonc
{
  "permissions": [
    "core:default",
    // ... 其他权限 ...
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://*.aliyuncs.com/**" }  // ⚠️ 允许访问 OSS 域名
      ]
    },
    "updater:default",   // ⚠️ 必须！否则报 updater.check not allowed
    "process:default"    // ⚠️ 必须！否则 relaunch() 无法调用
  ]
}
```

### 踩坑记录

| 问题 | 现象 | 原因 |
|------|------|------|
| **缺少 `createUpdaterArtifacts: true`** | 构建成功但只有 `.exe`，没有 `.exe.sig` | Tauri v2 默认不生成更新包，必须显式开启 |
| **pubkey 填成了私钥** | 构建报错 `failed to decode pubkey: Base64 conversion failed` | pubkey 是公钥的 base64，不是私钥 |
| **pubkey 与私钥不匹配** | 构建成功但客户端更新时签名验证失败 | 重新生成密钥后要同步更新两处 |
| **缺少 `updater:default` 权限** | 点击检查更新报 `updater.check not allowed` | Tauri v2 capabilities 需显式声明 updater 权限 |
| **缺少 `process:default` 权限** | 更新后 `relaunch()` 无响应 | 同上，需声明 process 权限 |
| **http 权限未放行 OSS 域名** | 报 `Could not fetch a valid release JSON` | capabilities 的 http allow 列表需包含 OSS 域名 |
| **OSS Bucket 为私有** | 同上错误，curl 返回 `AccessDenied` | Bucket ACL 必须设为**公共读** |

---

## 三、配置 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"

      # 先创建 Release（workaround：tauri-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": "0.2.0"
#    - src-tauri/Cargo.toml       → version = "0.2.0"
#    - package.json               → "version": "0.2.0"

# 2. 提交代码
git add -A
git commit -m "release: v0.2.0"

# 3. 打 tag 并推送（PowerShell 不支持 &&，分开执行）
git tag v0.2.0
git push
git push --tags

# 4. 等待 GitHub Actions 自动完成：
#    - 构建 → 签名 → 创建 Release → 上传 OSS → 生成 latest.json
#    约 10 分钟完成
```

> **注意**：版本号必须三处同步，否则 `latest.json` 中的 version 与客户端内部版本不一致，可能导致重复提示更新。

---

## 八、客户端自动更新

客户端侧的完整实现（插件安装、权限配置、前端 UI 状态机、踩坑记录）已独立成文档：

**→ [tauri-updater-guide.md](./tauri-updater-guide.md)**

---

## 九、完整踩坑清单速查

| # | 问题 | 解决方案 |
|---|------|---------|
| 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 不支持 `&&`，用 `;` 或分开执行 |
| 13 | `updater.check not allowed` | `capabilities/default.json` 添加 `updater:default` 权限 |
| 14 | `relaunch()` 无效 | `capabilities/default.json` 添加 `process:default` 权限 |
| 15 | `Could not fetch a valid release JSON` | 检查：1) OSS Bucket 是否公共读 2) capabilities http allow 是否包含 OSS 域名 |
| 16 | 版本号不一致导致重复更新提示 | 三处版本号必须同步：`tauri.conf.json`、`Cargo.toml`、`package.json` |

---

## 十、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"
    }
  }
}
```
