Files
openhuman/docs/AUTO_UPDATE.md
T

8.7 KiB

Auto-update

The desktop shell (app/src-tauri) auto-updates itself via Tauri's 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 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.jsonpnpm 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):

    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.

    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.

    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 → AboutCheck 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 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 devpnpm 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.