Tauri v2で作ったアプリに自動更新機能を追加する

Tauri v2で作ったデスクトップアプリに、GitHub Actions経由の自動更新機能とmacOS向けのコード署名・Notarization(公証)を導入した。

これやれば、AppStoreを使って配信していないアプリでもバージョンアップをした際に最新版があることをユーザーに通知し、更新を促すことができる。

リポジトリは公開していないので、配信先にはCloudflare R2を使っている。

設定箇所が多く、ハマりポイントもいくつかあったので、一連の流れをまとめて記録しておく。

この記事はmacOSへの配信のみを取り扱っています。

myappと書かれているところはアプリ名です。適宜置き換えてください。

Tauriアップデーターの概要

Tauri v2には @tauri-apps/plugin-updater というプラグインがあり、アプリ起動時にJSON(latest.json)を取得して新しいバージョンがあるかチェックし、あればダウンロード・インストール・再起動まで自動で行える。

latest.json の中身はこんな構造:

{
  "version": "1.4.5",
  "pub_date": "2026-03-28T03:16:52Z",
  "platforms": {
    "darwin-universal": {
      "url": "https://example.com/myapp/myapp.app.tar.gz",
      "signature": "dW50cnVzdGVk..."
    }
  }
}

signature はTauri独自の更新署名で、Apple署名とは別物。アプリ側に埋め込んだ公開鍵で検証し、改ざんされていないことを確認する仕組み。

コード署名をする理由

macOSではApple Developer IDで署名されていないアプリを起動しようとすると、Gatekeeperによってブロックされる。

ユーザーがシステム設定から手動で許可すれば起動できるが、体験として良くない。

さらに、Notarization(Apple公証)を通すことで「Appleが悪意のあるソフトウェアでないことを確認済み」という状態になり、初回起動時の警告もなくなる。

自動更新と組み合わせる場合、更新後のバイナリも署名・公証済みである必要があるため、CI/CDで自動化しておくのが現実的。

前提

  • Tauri v2(v2.10.3)
  • macOS向けビルド(Universal Binary: Intel + Apple Silicon)
  • GitHub Actionsでビルド・署名・公証・リリース
  • Cloudflare R2で latest.json とバイナリを配信(privateリポジトリ対応)
  • Apple Developer Programに登録済み

手順

1. Tauri更新署名キーの生成

アップデーターの改ざん検知に使うキーペアを生成する。Apple署名とは別物。

pnpm tauri signer generate -w ~/.tauri/myapp.key
  • 公開鍵 → tauri.conf.json に埋め込む
  • 秘密鍵(~/.tauri/myapp.key の中身)→ GitHub Secretに設定
  • パスワード → GitHub Secretに設定

2. Apple Developer ID 証明書の作成

macOSでApp Store外に配布するアプリの署名には「Developer ID Application」証明書が必要。Xcodeのインストールが前提。

CSR(証明書署名要求)の作成

キーチェーンアクセスを開く:

open -a "Keychain Access"

パスワードアプリを開くか、キーチェーンアクセスを開くか聞かれるので、「キーチェーンアクセス」を選択。

メニューから「キーチェーンアクセス」→「証明書アシスタント」→「認証局に証明書を要求」を選択。メールアドレスを入力し「ディスクに保存」でCSRファイルを保存する。

Apple Developerサイトで証明書を作成

  1. developer.apple.com にログイン
  2. 「Certificates, Identifiers & Profiles」→「Certificates」
  3. 「+」ボタンで新規作成
  4. 「Software」セクション内の 「Developer ID Application」 を選択
  5. 先ほど作成したCSRファイルをアップロード
  6. 生成された証明書をダウンロードし、ダブルクリックでキーチェーンに登録

注意: 「Apple Development」証明書は開発・テスト用であり、配布には使えない。必ず「Developer ID Application」を選ぶこと。

登録確認

security find-identity -v -p codesigning

以下のように表示されればOK。「Apple Development〜」と出ているものがあるときは、そっちは使わない。「Developer ID Application〜」とあるものが必要。

1) xxxxxxxx "Developer ID Application: Your Name (TEAMID)"

Developer ID Application が表示されない場合、CSR作成時の秘密鍵がキーチェーンにないか、証明書の登録に失敗している可能性がある。CSR作成からやり直すこと。

3. Notarization用のApp用パスワード作成

appleid.apple.com にログインし、「サインインとセキュリティ」→「アプリ用パスワード」から専用パスワードを生成する。これはNotarization APIへの認証に使う。Apple IDのパスワードとは別物。

4. .p12ファイルのエクスポート(CI用)

ローカルでしかビルドしない場合はこの手順は不要。GitHub ActionsなどCI環境で署名する場合、証明書を .p12 ファイルとしてエクスポートし、Base64エンコードしてSecretsに登録する。

# キーチェーンから証明書+秘密鍵を.p12形式でエクスポート
security export -k ~/Library/Keychains/login.keychain-db \
  -t identities -f pkcs12 -o cert.p12 -P "任意のパスワード"

-P で指定するパスワードは .p12 ファイル自体を保護するためのもので、自分で自由に決める。このパスワードはCI環境でのインポート時にも必要になるので、GitHub Secretの APPLE_CERTIFICATE_PASSWORD に設定する。

Base64エンコードしてクリップボードにコピー:

base64 -i cert.p12 | pbcopy

この値をGitHub Secretの APPLE_CERTIFICATE に設定する。

5. プラグインのインストール

Rust側:

# src-tauri/Cargo.toml
tauri-plugin-updater = "2"
tauri-plugin-process = "2"

フロントエンド側:

pnpm add @tauri-apps/plugin-updater @tauri-apps/plugin-process @tauri-apps/plugin-dialog

6. tauri.conf.jsonの設定

ポイントは3つ:

  • plugins.updater で配信先エンドポイントと公開鍵を設定
  • bundle.createUpdaterArtifactstrue にする(これがないと .sig ファイルが生成されない)
  • your-r2-domain.example.com/myappの部分は適宜置き換え
{
  "plugins": {
    "updater": {
      "endpoints": ["https://your-r2-domain.example.com/myapp/latest.json"],
      "pubkey": "公開鍵の文字列"
    }
  },
  "bundle": {
    "active": true,
    "createUpdaterArtifacts": true,
    "targets": "all",
    "macOS": {
      "signingIdentity": null
    }
  }
}

signingIdentitynull にしておき、CI側で環境変数 APPLE_SIGNING_IDENTITY として渡す。ローカルビルドでは署名をスキップできる。

7. Rustプラグイン登録

// src-tauri/src/lib.rs
tauri::Builder::default()
  .plugin(tauri_plugin_updater::Builder::new().build())
  .plugin(tauri_plugin_process::init())
  // ... 他のプラグイン

8. Capability追加

// src-tauri/capabilities/default.json
{
  "permissions": [
    "core:default",
    "updater:default",
    "process:default"
  ]
}

9. フロントエンド側の更新チェック

アプリ起動時にチェックし、更新があればダイアログで確認してからインストールする実装にした。

// src/lib/updater.ts
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { ask } from "@tauri-apps/plugin-dialog";

export async function checkForUpdates(): Promise<void> {
  try {
    const update = await check();
    if (!update) return;

    const yes = await ask(
      `新しいバージョン v${update.version} が利用可能です。更新しますか?`,
      { title: "アップデート", kind: "info", okLabel: "更新する", cancelLabel: "あとで" }
    );

    if (yes) {
      await update.downloadAndInstall();
      await relaunch();
    }
  } catch (error) {
    console.error("Update check failed:", error);
  }
}

Appコンポーネントの useEffect で呼び出す:

useEffect(() => {
  checkForUpdates();
}, []);

10. GitHub Secretsの設定

GitHub リポジトリの Settings → Secrets and variables → Actions に以下を設定:

Secret説明
APPLE_CERTIFICATEBase64エンコードした .p12 証明書
APPLE_CERTIFICATE_PASSWORD.p12 のパスワード
APPLE_SIGNING_IDENTITYDeveloper ID Application: 名前 (TEAMID)
APPLE_IDApple ID メールアドレス
APPLE_PASSWORDApp-specific password
APPLE_TEAM_ID10文字の Team ID
TAURI_SIGNING_PRIVATE_KEYTauri署名秘密鍵
TAURI_SIGNING_PRIVATE_KEY_PASSWORDTauri署名パスワード
R2_ACCESS_KEY_IDCloudflare R2のAccess Key ID
R2_SECRET_ACCESS_KEYCloudflare R2のSecret Access Key
R2_ENDPOINThttps://<ACCOUNT_ID>.r2.cloudflarestorage.com
R2_BUCKETR2バケット名
R2_PUBLIC_URLR2の公開URL

11. GitHub Actionsワークフロー

v* タグのpushをトリガーにビルド → 署名 → 公証 → GitHub Releaseドラフト作成 → R2アップロードを行う。

name: Release

on:
  push:
    tags:
      - "v*"

jobs:
  release:
    permissions:
      contents: write
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: pnpm/action-setup@v4
      - run: brew install nasm
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-darwin,x86_64-apple-darwin
      - uses: swatinem/rust-cache@v2
        with:
          workspaces: src-tauri
      - run: pnpm install

      - name: Import Apple certificate
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        run: |
          CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
          echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" \
            -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security set-key-partition-list -S apple-tool:,apple: \
            -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH

      - name: Build Tauri app
        uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        with:
          tagName: v__VERSION__
          releaseName: "myapp v__VERSION__"
          releaseDraft: true
          args: --target universal-apple-darwin

      - name: Upload to Cloudflare R2
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
          R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
          R2_BUCKET: ${{ secrets.R2_BUCKET }}
          R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
        run: |
          ARTIFACTS_DIR="src-tauri/target/universal-apple-darwin/release/bundle"
          # dmg, tar.gz, sig をR2にアップロード
          # latest.json のURLをR2に書き換えてアップロード

      - name: Cleanup keychain
        if: always()
        run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

12. Cloudflare R2の設定

privateリポジトリの場合、GitHub Releasesの latest.json にはアクセスできないため、Cloudflare R2に配置する。

カスタムドメインの場合はデフォルトがパブリックアクセスなので、ドメインを設定すればそのまま公開となる。

  1. Cloudflareダッシュボードで R2 バケットを作成
  2. Settings → Public access でR2.devサブドメインまたはカスタムドメインで公開
  3. R2 API Token(Object Read & Write)を作成し、Access Key IDとSecret Access Keyを取得

R2のS3互換エンドポイントは https://<ACCOUNT_ID>.r2.cloudflarestorage.com で、AWS CLIがそのまま使える。

ハマったポイント

pnpmのバージョン未指定

GitHub Actionsで pnpm/action-setup@v4 を使う際、pnpmバージョンが指定されていないとエラーになる。

Error: No pnpm version is specified.

package.jsonpackageManager フィールドを追加して解決:

{
  "packageManager": "pnpm@10.32.1"
}

nasmが見つからない(アプリ特有の問題)

これは自分が作っているアプリ特有の問題。

AVIF画像変換に使っている rav1e クレートがx86_64ビルド時に nasm(アセンブラ)を必要とする。

GitHub Actionsのmacosランナーにはデフォルトで入っていない。

NASM build failed. Make sure you have nasm installed

ワークフローに brew install nasm を追加して解決。

.sigファイルが生成されない

これが一番時間を使った。ビルドは成功するが .sig ファイルが一切生成されず、latest.json の signature が空になる問題。

TAURI_SIGNING_PRIVATE_KEY は正しく設定されているのに生成されない。原因は tauri.conf.jsoncreateUpdaterArtifacts: true が設定されていなかったこと。

{
  "bundle": {
    "createUpdaterArtifacts": true
  }
}

Tauri v2ではこの設定がないと、ビルド時に TAURI_SIGNING_PRIVATE_KEY を読み取って .sig を生成する処理自体がスキップされる。公式ドキュメントの Updater に記載があるが、見落としやすい。

tauri-action のソースコードを確認したところ、アクション側は .sig ファイルを「探す」だけで「生成する」わけではなく、tauri build 自体が生成する仕組みだった。

latest.jsonのGitHub Release取得

tauri-actionlatest.json をGitHub Releaseのアセットとしてアップロードする。ドラフトリリースの場合、gh release download のタイミングによっては no assets match the file pattern エラーになることがある。

対策として、ビルド成果物から直接 latest.json を生成してR2にアップロードする方式に切り替えた。

リリースの流れ

すべてセットアップが完了したら、リリースは以下の手順で行える:

git tag v1.5.0
git push origin v1.5.0

GitHub Actionsが自動で:

  1. Universal Binary(Intel + Apple Silicon)をビルド
  2. Developer ID Applicationで署名
  3. Apple Notarization(公証)を実行
  4. GitHub Releaseをドラフト作成
  5. Cloudflare R2にdmg、tar.gz、latest.jsonをアップロード

ドラフトリリースを確認してGitHubで公開すれば完了。既存ユーザーのアプリは次回起動時に自動で更新を検知する。

やり直す場合は既存タグを削除してから同じタグを打ち直せばよい:

git push origin :refs/tags/v1.5.0
git tag -d v1.5.0
git tag v1.5.0
git push origin v1.5.0

おわりに

手順が多く、めちゃ大変だった。一回やってしまえば、あとは慣れの問題な気もする。

個人的には、開発を開始した段階で、このフローを構築してしまうのが好み。

あとは、作成した証明書やパスワードは紛失しないように同じ場所にまとめて配置して管理しておきましょう。