From cc629eabde8c6d9766a83312c5032257fe29e6d6 Mon Sep 17 00:00:00 2001 From: oratis Date: Thu, 28 May 2026 18:27:23 +0800 Subject: [PATCH] feat(desktop): Tauri auto-updater wired to GitHub Releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When DeepCode.app launches: 1. Background poll of https://github.com/oratis/deepcode/releases/latest/download/latest.json 2. If a newer version exists → download in background, signature-verify with ed25519 pubkey 3. Fire `update-downloaded` event → renderer shows UpdateBanner 4. User clicks "Relaunch now" → tauri-plugin-process relaunch() → new version starts · apps/desktop/src/lib/updater.ts (NEW) — startUpdaterPolling / onUpdateDownloaded / relaunchNow. Silent on offline / 404 (early ship phase has no releases yet). · apps/desktop/src/App.tsx — calls startUpdaterPolling on mount, subscribes both shim + real updater. · apps/desktop/src/components/UpdateBanner.tsx — "Relaunch now" now actually calls relaunchNow(). Shows "Relaunching…" state. · apps/desktop/src-tauri/tauri.conf.json — updater.active=true, endpoint, pubkey (ed25519 generated via `tauri signer generate`). Private key at ~/.tauri/deepcode-updater.key (gitignored — must be backed up by maintainer). · apps/desktop/src-tauri/Cargo.toml — added tauri-plugin-process. · apps/desktop/src-tauri/src/lib.rs — registers plugin-process. · apps/desktop/src-tauri/capabilities/default.json — added process permissions. · apps/desktop/package.json — added @tauri-apps/plugin-process. When the maintainer cuts a release (git tag v0.X.Y), they need to: 1. Run sign-and-notarize.sh — produces signed .app + .dmg 2. Sign update artifacts with the private key via `tauri signer sign` 3. Generate latest.json pointing to the signed .tar.gz/.zip 4. Upload to GitHub Releases v0.X.Y The auto-update flow then kicks in for every installed client. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/package.json | 1 + apps/desktop/src-tauri/Cargo.lock | 11 +++ apps/desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 4 +- apps/desktop/src-tauri/src/lib.rs | 1 + apps/desktop/src-tauri/tauri.conf.json | 4 +- apps/desktop/src/App.tsx | 12 ++- apps/desktop/src/components/UpdateBanner.tsx | 33 +++++--- apps/desktop/src/lib/updater.ts | 84 +++++++++++++++++++ pnpm-lock.yaml | 10 +++ 10 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/lib/updater.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4ca902d..e200028 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -25,6 +25,7 @@ "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-fs": "^2.0.0", "@tauri-apps/plugin-opener": "^2.0.0", + "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "react": "^18.3.0", diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 586f252..ef83151 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -682,6 +682,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", + "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", "thiserror 1.0.69", @@ -3894,6 +3895,16 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.5" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index bd42e2a..0826990 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-plugin-fs = "2" tauri-plugin-opener = "2" tauri-plugin-shell = "2" tauri-plugin-updater = "2" +tauri-plugin-process = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "1" diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index ae1f743..b23265a 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -14,6 +14,8 @@ "fs:default", "opener:default", "shell:default", - "updater:default" + "updater:default", + "process:default", + "process:allow-restart" ] } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 107a627..c8f0e62 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -29,6 +29,7 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) .invoke_handler(tauri::generate_handler![ get_app_info, read_credentials, diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 08ff4b5..6f2fbc7 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -55,12 +55,12 @@ }, "plugins": { "updater": { - "active": false, + "active": true, "endpoints": [ "https://github.com/oratis/deepcode/releases/latest/download/latest.json" ], "dialog": false, - "pubkey": "" + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDk0MDMzRUQ0RkVFNUREODUKUldTRjNlWCsxRDREbEttVHEwUEplK1FKbnRCakpGb3dVWTYveFdxSmxwK2ZROWFZcW1kYzZMSGcK" } } } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 232ea1c..924d06c 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'; import { Nav, type ScreenName } from './components/Nav.js'; import { UpdateBanner } from './components/UpdateBanner.js'; +import { onUpdateDownloaded, startUpdaterPolling } from './lib/updater.js'; import { AboutScreen } from './screens/About.js'; import { ChatScreen } from './screens/Chat.js'; import { MCPManagerScreen } from './screens/MCPManager.js'; @@ -26,8 +27,15 @@ export function App(): JSX.Element { useEffect(() => { void window.deepcode.version().then(setVersion); void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey)); - const off = window.deepcode.onUpdateDownloaded((info) => setUpdate(info)); - return () => off(); + const offShim = window.deepcode.onUpdateDownloaded((info) => setUpdate(info)); + // Also subscribe to the real Tauri updater (so even if the shim + // surface drifts, the banner still fires). + const offReal = onUpdateDownloaded((info) => setUpdate(info)); + startUpdaterPolling(); + return () => { + offShim(); + offReal(); + }; }, []); if (hasKey === null) { diff --git a/apps/desktop/src/components/UpdateBanner.tsx b/apps/desktop/src/components/UpdateBanner.tsx index bf6612c..abc7668 100644 --- a/apps/desktop/src/components/UpdateBanner.tsx +++ b/apps/desktop/src/components/UpdateBanner.tsx @@ -1,9 +1,9 @@ -// "Relaunch to update vX.Y.Z" banner — fires when electron-updater has -// downloaded a new release. Click → app.relaunch() (host wires this in M6-rest). +// "Relaunch to update vX.Y.Z" banner — fires when tauri-plugin-updater has +// downloaded a new release. Clicking Relaunch calls tauri-plugin-process.relaunch(). // Spec: docs/VISUAL_DESIGN.html screen #11 -// Milestone: M6 skeleton import { useState } from 'react'; +import { relaunchNow } from '../lib/updater.js'; import type { UpdateInfo } from '../types/global.js'; interface BannerProps { @@ -12,22 +12,29 @@ interface BannerProps { export function UpdateBanner({ info }: BannerProps): JSX.Element | null { const [dismissed, setDismissed] = useState(false); + const [relaunching, setRelaunching] = useState(false); if (dismissed) return null; + + async function handleRelaunch(): Promise { + setRelaunching(true); + try { + await relaunchNow(); + } catch (err) { + console.error('relaunch failed:', err); + setRelaunching(false); + } + } + return (
- - DeepCode v{info.version} is ready to install. Relaunch to update. - + DeepCode v{info.version} is ready to install. Relaunch to update.