Skip to content

Commit df5c78b

Browse files
koki-developclaude
andcommitted
feat(updater): add in-app updater via tauri-plugin-updater
- Sonner toast bridge (UpdaterToast) drives the available → downloading → installing → error UI in a single in-place toast, with Install and Restart as the action button and Release notes as a link to the GitHub release. - Auto-check runs once on main window mount; manual check via Cork > Check for Updates... works in every window. - minisign signs the .app.tar.gz; tauri.conf.json sets bundle.macOS.signingIdentity = '-' so the in-app updater never invalidates Apple Silicon's signature requirement (no Apple Developer ID needed). - CI uploads .app.tar.gz / .sig alongside the existing DMG and generates latest.json with the release body embedded as notes. - Homebrew cask gains auto_updates true so brew upgrade and the in-app updater don't fight over the same install. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5fc89f9 commit df5c78b

30 files changed

Lines changed: 1729 additions & 11 deletions

File tree

.github/workflows/release-please.yml

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,21 @@ jobs:
4646
persist-credentials: false
4747
- uses: ./.github/actions/setup
4848
- run: bun run tauri build
49+
env:
50+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
51+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
4952
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
5053
with:
5154
name: dmg
5255
path: src-tauri/target/release/bundle/dmg/*.dmg
5356
if-no-files-found: error
57+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
58+
with:
59+
name: updater-bundle
60+
path: |
61+
src-tauri/target/release/bundle/macos/*.app.tar.gz
62+
src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
63+
if-no-files-found: error
5464

5565
release:
5666
name: Release
@@ -61,11 +71,39 @@ jobs:
6171
permissions:
6272
contents: write # for upload release assets
6373
steps:
74+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
75+
with:
76+
persist-credentials: false
77+
- uses: ./.github/actions/setup
6478
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
6579
with:
6680
name: dmg
6781
path: artifacts
68-
- run: gh release upload "$TAG_NAME" artifacts/*.dmg --clobber --repo "$GITHUB_REPOSITORY"
82+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
83+
with:
84+
name: updater-bundle
85+
path: artifacts
86+
- name: Upload DMG and updater bundle
87+
run: |
88+
gh release upload "$TAG_NAME" \
89+
artifacts/*.dmg \
90+
artifacts/*.app.tar.gz \
91+
artifacts/*.app.tar.gz.sig \
92+
--clobber --repo "$GITHUB_REPOSITORY"
93+
env:
94+
GH_TOKEN: ${{ github.token }}
95+
TAG_NAME: ${{ needs.release-please.outputs.tag-name }}
96+
- name: Generate and upload latest.json
97+
run: |
98+
VERSION="${TAG_NAME#v}"
99+
RELEASE_BODY=$(gh release view "$TAG_NAME" --json body --jq .body --repo "$GITHUB_REPOSITORY")
100+
SIG_FILE=$(ls artifacts/*.app.tar.gz.sig | head -1)
101+
bun run ./scripts/build-update-manifest.ts \
102+
--version "$VERSION" \
103+
--signature "$SIG_FILE" \
104+
--notes "$RELEASE_BODY" \
105+
--out ./latest.json
106+
gh release upload "$TAG_NAME" ./latest.json --clobber --repo "$GITHUB_REPOSITORY"
69107
env:
70108
GH_TOKEN: ${{ github.token }}
71109
TAG_NAME: ${{ needs.release-please.outputs.tag-name }}

bun.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-06-24

openspec/changes/in-app-updater/design.md

Lines changed: 278 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
## Why
2+
3+
現状 Cork のバージョン更新はユーザー任せ — DMG を再ダウンロードして手で差し替えるか、Homebrew Cask 利用者なら `brew upgrade --cask cork` を明示実行するかの二択しか提供できていない。アプリ起動中に新バージョンの存在を知る手段が一切なく、リリース告知 → 更新までの導線が極端に長い。配布手段は二系統あるが Apple Developer Program に加入しない方針のため、Sparkle や Apple 公式 Updater のような Developer ID 前提の解決策は採れず、「ad-hoc 署名のままで成立する自走更新」を独自に設計する必要がある。
4+
5+
## What Changes
6+
7+
- **Tauri v2 公式 `tauri-plugin-updater` を採用**して、アプリ起動時の自動チェック・手動チェック・ダウンロード・インストール・再起動の一連の自走更新フローを提供する
8+
- **minisign による独立署名 + ビルド時 ad-hoc codesign の二段構え**を導入。Apple Developer ID を取得せずに、アップデート tar.gz の真正性検証(minisign)と macOS 起動要件(ad-hoc codesign)の双方を満たす
9+
- **GitHub Releases に静的 `latest.json` を配置**し、`tauri-plugin-updater` の endpoint として使う。`latest.json` の生成と署名は CI が担う
10+
- **`Cork > Check for Updates...` メニュー項目**を追加する
11+
- **更新通知は sonner toast**として右下に表示する(中央 modal ではなく、ユーザー作業を妨げないスタイル)。state machine を sonner の同 id update セマンティクスにブリッジし、`available → downloading → installing → (success or error)`**同じ toast カード内で in-place 遷移**させる。自動クローズ無し、× で手動クローズ
12+
- **`Update available` toast には `Install and Restart` action ボタン + `Release notes ↗` リンク + 閉じる X**。リリースノート本文は UI には表示せず、リンクから GitHub Release ページを開く
13+
- **Homebrew Cask に `auto_updates true` を追加**`scripts/build-cask.ts` を更新し、`brew upgrade --cask cork`(名前指定)は従来通り、bare `brew upgrade` は Brew 側の bundle-version 比較で二重更新を避ける挙動に揃える
14+
- **CI(`.github/workflows/release-please.yml`)の `build`/`release` ジョブを拡張**`TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` を Secret として読み込み、`Cork.app.tar.gz` + `Cork.app.tar.gz.sig` を upload、`scripts/build-update-manifest.ts`(新規)が `latest.json` を生成して release にアップロード
15+
- **`tauri.conf.json``bundle.macOS.signingIdentity: "-"``bundle.createUpdaterArtifacts: true``plugins.updater.{endpoints, pubkey}` を追加**
16+
- **`capabilities/default.json``updater:default``process:default` を追加**(updater の check/install と Tauri runtime restart のため)
17+
18+
### 設計の変更履歴(実装中に決定)
19+
20+
初稿では Settings 画面に「自動チェック ON/OFF トグル + 最終チェック時刻 + Check Now ボタン」の Update セクションを追加する案だったが、実装直前に **「常に自動チェック、ユーザー制御不要」「中央 modal ではなく通知 toast」** の方針へ転換した。これに伴い以下を本 change から除外:
21+
22+
- Settings 画面の Update セクション (`UpdaterSection.tsx`) → 設置しない
23+
- 永続化設定 `updater.{autoCheck, lastCheckedAt}` → 保存しない(`tauri-plugin-store` には触れない)
24+
- Rust 側 `updater.rs` モジュール → 不要、削除
25+
- `get_updater_settings` / `update_updater_settings` / `get_app_version` Tauri command → 不要、削除
26+
- `UpdaterSettings` 型 → `UpdaterState` (state machine) の型のみが `@/types/updater.ts` に残る
27+
28+
## Capabilities
29+
30+
### New Capabilities
31+
32+
- `updater`: アプリ自走更新の機能全体。自動/手動チェック・ダウンロード・インストール・再起動・通知 UI(メニュー・toast)の要件を含む
33+
34+
### Modified Capabilities
35+
36+
(なし — 既存の canonical spec は変更しない。`updater` は完全に新規の capability として導入する)
37+
38+
## Impact
39+
40+
**Code(新規)**
41+
42+
- `src/api/updater.ts``@tauri-apps/plugin-updater` (`checkForUpdate` / `downloadAndInstall`) と `@tauri-apps/plugin-process` (`relaunchApp`) の薄ラッパー。Rust コマンドのラッパーは含まない(Rust 側 updater モジュールが存在しないため)
43+
- `src/hooks/useUpdater.ts` — 状態遷移(idle / checking / available / downloading / installing / error)+ 起動時自動チェック(`main` Window 限定ゲート)+ menu イベント listen + dev-only delay 機構
44+
- `src/types/updater.ts``UpdaterState` 型(discriminated union)。`@/hooks` への直接 import を禁ずる oxlint ルールを回避するため `@/types` 側に置く
45+
- `src/components/organisms/shell/UpdaterToast.tsx``useUpdater` の state を sonner toast に橋渡しする organism。null を返す(UI は sonner の `<Toaster>` スタックで描画)。state 遷移ごとに同じ id で `toast(...)` / `toast.loading(...)` / `toast.error(...)` を呼んで in-place 更新
46+
- `scripts/build-update-manifest.ts``latest.json` 生成スクリプト(release body から CHANGELOG エントリを読み込んで `notes` に埋め込む)
47+
48+
**Code(変更)**
49+
50+
- `src-tauri/Cargo.toml``tauri-plugin-updater``tauri-plugin-process` 追加
51+
- `package.json``@tauri-apps/plugin-updater` + `@tauri-apps/plugin-process` 追加
52+
- `src-tauri/src/lib.rs` — プラグイン登録のみ追加(updater 専用の `#[tauri::command]` は無いので `invoke_handler` は無変更)
53+
- `src-tauri/src/menu.rs``app_menu``Check for Updates...``about()` 直後に挿入、メニューイベントで focused window に `menu:check-for-updates` を emit
54+
- `src-tauri/tauri.conf.json``bundle.createUpdaterArtifacts` / `bundle.macOS.signingIdentity` / `plugins.updater.*` を追加
55+
- `src-tauri/capabilities/default.json``updater:default` + `process:default`
56+
- `src/App.tsx``useUpdater()` 呼び出し + `<UpdaterToast>` レンダリング(module-scope の `openReleaseNotes` ヘルパで参照同一性を保つ)
57+
- `src/api/menu.ts` + `src/api/index.ts``onCheckForUpdates` listener 追加 + 公開
58+
- `scripts/build-cask.ts` — Cask 文字列に `auto_updates true` 追加
59+
- `.github/workflows/release-please.yml` — Secret 経由の env vars、updater 成果物 upload、`latest.json` 生成ステップ追加
60+
- `AGENTS.md` / `src-tauri/AGENTS.md` / `src/api/AGENTS.md` / `src/hooks/AGENTS.md` / `src/components/organisms/shell/AGENTS.md` / `src/types/AGENTS.md` — モジュール一覧と CI 説明を更新
61+
62+
**Dependencies**
63+
64+
- 追加: `tauri-plugin-updater` + `tauri-plugin-process` (Rust), `@tauri-apps/plugin-updater` + `@tauri-apps/plugin-process` (npm)
65+
66+
**Infrastructure**
67+
68+
- GitHub repo の Secrets に `TAURI_SIGNING_PRIVATE_KEY``TAURI_SIGNING_PRIVATE_KEY_PASSWORD` を登録(minisign 鍵ペア生成はメンテナがローカルで行う)
69+
- `koki-develop/homebrew-tap``Casks/cork.rb` が次リリースで `auto_updates true` を含む形に置き換わる(自動)
70+
71+
**Distribution**
72+
73+
- 既存配布ルート(DMG 直ダウンロード / Brew Cask)は維持。新たに「自走更新」が両方の経路で動作する
74+
- minisign 公開鍵は `tauri.conf.json` にハードコードされ、初回リリース以降の鍵ローテーションは大規模オペレーション(既存ユーザー全員に旧鍵→新鍵の橋渡し版を配信する必要がある)になる点を `design.md` で明示
75+
76+
**範囲外**
77+
78+
- Windows / Linux の updater 設定(プロジェクトが macOS only)
79+
- delta update / 差分更新
80+
- ロールバック機能
81+
- 自動チェックの ON/OFF 制御(常に有効、ユーザー側に opt-out を提供しない)
82+
- 永続化設定 (`updater.autoCheck`, `updater.lastCheckedAt`) — 不要のため `tauri-plugin-store` に触れない
83+
- 現状の Cask preflight 相当の `--identifier` / `designated requirement` カスタム再署名のビルド時統合(初回リリース後の TCC 挙動次第で別 change として検討)

0 commit comments

Comments
 (0)