Chapter 10

自动更新与 CI/CD

plugin-updater 实现安全可靠的自动更新,GitHub Actions 完成持续发布

自动更新架构

应用启动 / 用户触发检查 │ ▼ 检查更新服务器 GET /latest.json │ ┌────┴────┐ │ 有新版 │ 没有新版本 │ 本可用 │ ─────────────→ 结束 └────┬────┘ │ 下载更新包 (显示进度条) │ ▼ 验证数字签名 (防止中间人篡改) │ ▼ 安装并重启应用

plugin-updater 安装与配置

npm install @tauri-apps/plugin-updater
# Cargo.toml
tauri-plugin-updater = "2"
// src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .run(tauri::generate_context!())
        .unwrap();
}
// tauri.conf.json
{
  "plugins": {
    "updater": {
      "pubkey": "YOUR_UPDATER_PUBLIC_KEY",
      "endpoints": [
        "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
      ],
      "dialog": false
    }
  }
}
{{target}}
目标平台:linuxwindowsdarwin(macOS)
{{arch}}
CPU 架构:x86_64aarch64
{{current_version}}
当前版本号(如 1.2.3),服务器可据此返回是否有更高版本
pubkey
更新签名验证公钥。Tauri 要求每个更新包必须有对应私钥的签名,客户端用公钥验证,防止供应链攻击。

生成签名密钥对

# 生成 updater 密钥对
npm run tauri signer generate -- -w ~/.tauri/myapp.key

# 输出:
# Private key: ~/.tauri/myapp.key  (保密!用于 CI/CD 签名)
# Public key: dW50cnVzdGVkIGNvbW1lbnQ6...  (填入 tauri.conf.json pubkey)

# 查看公钥
cat ~/.tauri/myapp.key.pub

更新服务器响应格式

// 有更新时返回 200 + 以下 JSON
{
  "version": "1.3.0",
  "notes": "## 更新内容\n- 修复若干 Bug\n- 新增深色主题",
  "pub_date": "2024-11-15T08:00:00Z",
  "platforms": {
    "darwin-x86_64": {
      "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
      "url": "https://cdn.myapp.com/v1.3.0/MyApp_x64.dmg"
    },
    "darwin-aarch64": {
      "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
      "url": "https://cdn.myapp.com/v1.3.0/MyApp_aarch64.dmg"
    },
    "windows-x86_64": {
      "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
      "url": "https://cdn.myapp.com/v1.3.0/MyApp_x64-setup.nsis.zip"
    },
    "linux-x86_64": {
      "signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
      "url": "https://cdn.myapp.com/v1.3.0/MyApp_amd64.AppImage.tar.gz"
    }
  }
}
// 没有更新时返回 204 No Content

前端更新 UI 实现

import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import { useState } from 'react';

function UpdateChecker() {
  const [status, setStatus] = useState('idle');
  const [update, setUpdate] = useState<Update | null>(null);
  const [progress, setProgress] = useState(0);

  async function checkForUpdates() {
    setStatus('checking');
    const result = await check();

    if (!result?.available) {
      setStatus('up-to-date');
      return;
    }

    setUpdate(result);
    setStatus('available');
  }

  async function installUpdate() {
    if (!update) return;
    setStatus('downloading');

    let downloaded = 0;
    let contentLength = 0;

    await update.downloadAndInstall((event) => {
      switch (event.event) {
        case 'Started':
          contentLength = event.data.contentLength ?? 0;
          break;
        case 'Progress':
          downloaded += event.data.chunkLength;
          if (contentLength > 0) {
            setProgress(Math.round((downloaded / contentLength) * 100));
          }
          break;
        case 'Finished':
          setStatus('installing');
          break;
      }
    });

    // 安装完成,重启应用
    await relaunch();
  }

  return (
    <div>
      {status === 'idle' && (
        <button onClick={checkForUpdates}>检查更新</button>
      )}
      {status === 'checking' && <p>正在检查更新...</p>}
      {status === 'up-to-date' && <p>已是最新版本</p>}
      {status === 'available' && (
        <div>
          <p>发现新版本: {update?.version}</p>
          <p>{update?.body}</p>
          <button onClick={installUpdate}>立即更新</button>
        </div>
      )}
      {status === 'downloading' && (
        <div>
          <p>下载中... {progress}%</p>
          <progress value={progress} max="100" />
        </div>
      )}
      {status === 'installing' && <p>正在安装,即将重启...</p>}
    </div>
  );
}

版本号管理

# 版本号在两个地方定义,需要保持同步:
# 1. src-tauri/tauri.conf.json: "version": "1.2.3"
# 2. src-tauri/Cargo.toml: version = "1.2.3"

# 使用 tauri-version 工具同步更新
npm install -g @tauri-apps/cli
tauri version 1.3.0  # 同时更新 tauri.conf.json 和 Cargo.toml

# 或手动更新后创建 git tag 触发 CI
git add src-tauri/tauri.conf.json src-tauri/Cargo.toml
git commit -m "chore: bump version to v1.3.0"
git tag v1.3.0
git push origin main --tags

GitHub Actions 自动发布与更新 JSON

# .github/workflows/release.yml
name: Release

on:
  push:
    tags: ['v[0-9]+.[0-9]+.[0-9]+']

jobs:
  create-release:
    runs-on: ubuntu-latest
    outputs:
      release-id: ${{ steps.create-release.outputs.result }}
    steps:
      - id: create-release
        uses: actions/github-script@v7
        with:
          script: |
            const { data } = await github.rest.repos.createRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              tag_name: context.ref.replace('refs/tags/', ''),
              name: context.ref.replace('refs/tags/', ''),
              draft: true,
              prerelease: false,
            });
            return data.id;

  build-tauri:
    needs: create-release
    strategy:
      matrix:
        include:
          - platform: macos-latest
            args: '--target aarch64-apple-darwin'
          - platform: macos-latest
            args: '--target x86_64-apple-darwin'
          - platform: ubuntu-22.04
            args: ''
          - platform: windows-latest
            args: ''
    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
      - uses: dtolnay/rust-toolchain@stable
      - run: npm install
      - 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:
          releaseId: ${{ needs.create-release.outputs.release-id }}
          args: ${{ matrix.args }}

  publish-release:
    runs-on: ubuntu-latest
    needs: [create-release, build-tauri]
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            await github.rest.repos.updateRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              release_id: ${{ needs.create-release.outputs.release-id }},
              draft: false,
            });

使用 GitHub Releases 作为更新服务器

无需自建服务器,可以直接使用 GitHub Releases 作为更新源。tauri-action 会自动生成 latest.json 并上传到 Release:

// tauri.conf.json — 使用 GitHub Releases
{
  "plugins": {
    "updater": {
      "pubkey": "YOUR_PUBLIC_KEY",
      "endpoints": [
        "https://github.com/username/repo/releases/latest/download/latest.json"
      ]
    }
  }
}
使用 Tauri Update Server 简化部署

开源项目 tauri-update-server 提供了一个简单的 Cloudflare Worker,可部署为代理层:根据请求的 target/arch/version 从 GitHub Releases 找到合适的更新包并返回标准格式的 JSON。这样既利用了 GitHub 的免费托管,又满足了 Tauri updater 的 API 格式要求,是小型项目的理想选择。

应用启动时自动检查更新

最常见的更新模式是应用启动后在后台静默检查,有更新时通知用户而不强制更新:

use tauri::{AppHandle, Manager};
use tauri_plugin_updater::UpdaterExt;

// 在 setup 中异步检查更新(不阻塞应用启动)
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .setup(|app| {
            let handle = app.handle().clone();

            // 使用 spawn 在后台检查,不阻塞主线程
            tauri::async_runtime::spawn(async move {
                check_for_update(handle).await;
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .unwrap();
}

async fn check_for_update(app: AppHandle) {
    // 等待 5 秒,确保应用完全启动后再检查
    tokio::time::sleep(std::time::Duration::from_secs(5)).await;

    match app.updater_builder().build() {
        Ok(updater) => {
            match updater.check().await {
                Ok(Some(_update)) => {
                    // 有更新:通知前端
                    app.emit("update-available", ()).unwrap();
                }
                Ok(None) => {
                    // 已是最新版本
                }
                Err(e) => {
                    // 检查失败(网络问题等),静默处理
                    eprintln!("Update check failed: {}", e);
                }
            }
        }
        Err(e) => eprintln!("Updater build failed: {}", e),
    }
}
import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';

// 在 App 根组件中监听更新通知
function App() {
  const [showUpdateBanner, setShowUpdateBanner] = useState(false);

  useEffect(() => {
    const unlisten = listen('update-available', () => {
      setShowUpdateBanner(true);
    });
    return () => { unlisten.then(f => f()); };
  }, []);

  return (
    <div>
      {showUpdateBanner && (
        <div className="update-banner">
          有新版本可用!
          <button onClick={() => navigate('/settings/update')}>
            查看更新
          </button>
          <button onClick={() => setShowUpdateBanner(false)}>
            稍后提醒
          </button>
        </div>
      )}
      {/* 其他应用内容 */}
    </div>
  );
}

更新前数据迁移

当应用版本更新涉及数据格式变化(数据库 schema 升级、配置格式改变)时,需要在应用启动时执行迁移:

use semver::Version;

#[tauri::command]
pub async fn run_migrations(app: AppHandle) -> Result<(), String> {
    let current_version = Version::parse(env!("CARGO_PKG_VERSION"))
        .map_err(|e| e.to_string())?;

    // 读取上次运行的版本号
    let data_dir = app.path().app_data_dir().unwrap();
    let version_file = data_dir.join("version.txt");
    let last_version = std::fs::read_to_string(&version_file)
        .ok()
        .and_then(|s| Version::parse(s.trim()).ok());

    if let Some(last) = last_version {
        // 从旧版本迁移到新版本
        if last < Version::parse("1.3.0").unwrap() {
            migrate_to_1_3_0(&app).await?;
        }
    }

    // 更新版本文件
    std::fs::write(&version_file, current_version.to_string())
        .map_err(|e| e.to_string())?;

    Ok(())
}

async fn migrate_to_1_3_0(app: &AppHandle) -> Result<(), String> {
    // 执行数据库 schema 升级
    println!("Migrating to v1.3.0...");
    // ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0;
    Ok(())
}

本章小结

本章核心要点(也是整个教程的收尾)
完结:你已掌握 Tauri 2.0 全栈开发

恭喜完成 Tauri 教程!你现在能够:构建 WebView + Rust 双进程桌面应用、实现前后端双向通信(Commands + Events)、使用系统 API(文件、托盘、通知、快捷键)、集成 SQLite 数据库、完成跨平台打包签名,以及实现安全的自动更新。Tauri 2.0 更进一步支持 iOS 和 Android,让一套代码覆盖所有主流平台成为可能。