Files
openhuman/docs/AUTO_UPDATE.md
T

174 lines
8.7 KiB
Markdown

# Auto-update
The desktop shell (`app/src-tauri`) auto-updates itself via Tauri's
[`plugin-updater`](https://tauri.app/plugin/updater/) against a manifest
published on GitHub Releases. The OpenHuman core sidecar (`openhuman` binary)
ships inside the `.app` bundle, so a shell update upgrades both.
## Architecture
| Piece | Role |
| --------------------------------------------------- | ------------------------------------------------------------- |
| `app/src-tauri/Cargo.toml` | declares `tauri-plugin-updater` |
| `app/src-tauri/tauri.conf.json` (`plugins.updater`) | endpoint + minisign pubkey |
| `app/src-tauri/permissions/allow-app-update.toml` | ACL allow-list for the four updater commands |
| `app/src-tauri/src/lib.rs::check_app_update` | probe-only; returns version info |
| `app/src-tauri/src/lib.rs::download_app_update` | downloads bundle bytes, stages them, does NOT install |
| `app/src-tauri/src/lib.rs::install_app_update` | installs previously-staged bytes + relaunches |
| `app/src-tauri/src/lib.rs::apply_app_update` | legacy combined download+install+restart (kept for compat) |
| `app/src/hooks/useAppUpdate.ts` | React state machine, auto-check, auto-download |
| `app/src/components/AppUpdatePrompt.tsx` | global banner (mounted in `App.tsx`) — silent during download |
| `app/src/components/settings/panels/AboutPanel.tsx` | manual "Check for updates" |
| `.github/workflows/release.yml` | builds + signs + publishes `latest.json` |
The shell emits two Tauri events while updating:
- `app-update:status` — string payload, one of `checking`, `downloading`,
`ready_to_install`, `installing`, `restarting`, `up_to_date`, `error`
- `app-update:progress``{ chunk: number, total: number | null }`
`useAppUpdate` listens on both and exposes a state machine
(`idle | checking | available | downloading | ready_to_install | installing |
restarting | up_to_date | error`).
## User flow (Option 2: auto-download, prompt to restart)
1. ~30 seconds after launch, the hook runs a silent `check_app_update`. It
re-checks every 4 hours.
2. If the manifest reports a newer version, the hook **automatically calls
`download_app_update`** in the background — the user sees nothing.
3. Once the bytes are staged, the Rust side emits `ready_to_install` and the
bottom-right banner appears with the header **"Update ready to install"**
and a body line of **"Version <version> is ready to install."** (falling
back to **"A new version is ready to install."** when the manifest didn't
supply a version), followed by **Restart now** / **Later** buttons.
4. Clicking **Restart now** invokes `install_app_update`, which acquires the
core restart lock, shuts down the in-process core, calls
`Update::install(staged_bytes)` (no re-download), and then `app.restart()`.
5. **Later** dismisses the banner without canceling the staged bytes — the
user can also click "Check for updates" in Settings → About to surface the
prompt again on demand.
Why this flow vs. silent install: a chat / AI app often has in-flight
conversations and background agent work. Yanking the process away mid-task
costs more user trust than a one-click "Restart now" prompt earns in
convenience. We download invisibly so the _only_ action the user takes is
choosing the restart moment.
## Validating end-to-end (issue #677 acceptance criteria)
The auto-update path must be validated against a real signed bundle and a
real `latest.json``pnpm tauri dev` does not produce updater-compatible
artifacts. Use this recipe.
### Prerequisites
- A published GitHub release at a higher version than what you'll build
locally (e.g. the latest `v0.53.x`). The release must include the signed
bundle for your platform plus `latest.json`.
- **No signing key needed for verification.** Minisign signature
verification on the downloaded bundle uses the public key already baked
into `tauri.conf.json::plugins.updater.pubkey`, which the local build
picks up automatically. `TAURI_SIGNING_PRIVATE_KEY` (+ its password) is
required only by CI to _sign_ new releases — never to verify existing
ones. Treat the private key as a secret and keep it out of dev machines.
### Recipe
1. **Pick a target older than the published release.** Edit all four version
sources to a known-older value (e.g. `0.53.0` if `0.53.4` is published):
```text
app/package.json::version
app/src-tauri/Cargo.toml::package.version
app/src-tauri/tauri.conf.json::version
Cargo.toml::workspace.package.version
```
`scripts/release/verify-version-sync.js` exists exactly to keep these
four in lockstep — run it after editing.
2. **Build a packaged bundle locally.**
```bash
pnpm --filter openhuman-app tauri:ensure # vendored CEF-aware tauri-cli
pnpm --filter openhuman-app tauri:build:ui # exports CEF_PATH + builds the .app
```
`tauri:build:ui` exports `CEF_PATH=~/Library/Caches/tauri-cef` before
running `cargo tauri build` — the bundler needs this to copy
`Chromium Embedded Framework.framework` into `Contents/Frameworks/`.
A bare `pnpm tauri build` skips that step and the resulting binary
panics in `cef::library_loader::LibraryLoader::new`.
On macOS the artifact lands in
`app/src-tauri/target/release/bundle/macos/OpenHuman.app`.
3. **Run the packaged build.**
```bash
open app/src-tauri/target/release/bundle/macos/OpenHuman.app
# or, with Rust + frontend logs in the terminal:
./app/src-tauri/target/release/bundle/macos/OpenHuman.app/Contents/MacOS/OpenHuman
```
You should see `[app-update]` lines start to flow ~30 seconds after
launch (auto-check), or immediately after clicking
**Settings → About → Check for updates**.
4. **Trigger the check** — either wait ~30s for the auto-check, or open
**Settings → About** → **Check for updates**. The check is silent;
the prompt appears only once the download has staged.
5. **Watch the auto-download flow** (fires automatically — no click needed
to start the download). Expected log sequence:
- `[app-update] check requested (current: <old-version>)`
- `[app-update] update available: <old> -> <new>`
- `[app-update] download_app_update invoked from frontend`
- `[app-update] downloading <new-version> (background)`
- `[app-update] download complete — staging for install`
- `[app-update] staged <new-version> — awaiting user-initiated install`
At this point the bottom-right banner appears with the header
**"Update ready to install"** and a body line of
**"Version <new-version> is ready to install."** (or
**"A new version is ready to install."** as the fallback when the
manifest didn't supply a version string), followed by **Restart now** /
**Later** buttons.
6. **Click "Restart now"**. Expected log sequence:
- `[app-update] install_app_update invoked from frontend`
- `[app-update] installing staged version <new-version>`
- `[app-update] install complete — relaunching`
The app relaunches itself; the new bundle's version (in
**Settings → About**) should match the published release.
7. **Confirm the core sidecar came back up.** `[core]` log lines should
appear after relaunch and `core_rpc` calls from the UI must succeed.
### Troubleshooting
- **"signature did not verify"** — the local bundle was built with a
different signing key than the one whose pubkey is in `tauri.conf.json`.
Rebuild against the same `TAURI_SIGNING_PRIVATE_KEY` used by the
release workflow, or temporarily swap the pubkey while testing.
- **"endpoint did not return a valid JSON manifest"** — the redirect from
`releases/latest/download/latest.json` resolved to a release that lacks
the asset. Confirm the latest non-draft release on GitHub has
`latest.json` attached (job `publish-updater-manifest`).
- **Updater doesn't fire in dev** — `pnpm tauri dev` sets
`bundle.createUpdaterArtifacts: false` (see `scripts/prepareTauriConfig.js`),
so the dev profile never produces a bundle the updater can swap in. Use
`pnpm tauri build`.
- **The banner never shows on first launch** — that's expected; the
initial probe is delayed 30s. To force it, click "Check for updates" in
the About panel.
## Logs
- Rust side: `log::info!("[app-update] ...")` / `log::warn!` / `log::error!`
- Frontend: `console.debug('[app-update] ...')` and friends
Both prefixes are stable and grep-friendly per `CLAUDE.md`.