From 3d8a6f07b4073af40be6f330e1049d6c60e38a3e Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:12:22 +0800 Subject: [PATCH] feat(app): Enable signed auto-updates from GitHub releases Configure the updater public key and the releases/latest latest.json endpoint, and build updater artifacts so tauri-action uploads signatures and latest.json. Check for updates once per app start (release builds only) and offer Install and Restart when one is available. --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/window.rs | 20 ++++++++++++++ src-tauri/tauri.conf.json | 5 ++-- src/bindings.ts | 11 ++++++++ src/features/app/appActions.ts | 45 ++++++++++++++++++++------------ src/features/app/index.ts | 6 +++-- 6 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index b697c7675..fd3d8e27b 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -239,6 +239,7 @@ pub fn builder() -> tauri_specta::Builder { window::app_log_location, window::app_build_info, window::app_check_update, + window::app_startup_update_check, window::app_install_update, debug::debug_exception_filters, debug::debug_set_exception_breakpoints, diff --git a/src-tauri/src/commands/window.rs b/src-tauri/src/commands/window.rs index 0718fe643..894ae18bb 100644 --- a/src-tauri/src/commands/window.rs +++ b/src-tauri/src/commands/window.rs @@ -268,6 +268,26 @@ pub async fn app_check_update(app: AppHandle) -> IdeResult { }) } +/// The background check made once per process when the first window starts; +/// later calls (other windows) and debug builds get `None`. Failures are logged, +/// not reported: the user didn't ask for this check. +#[tauri::command] +#[specta::specta] +pub async fn app_startup_update_check(app: AppHandle) -> Option { + use std::sync::atomic::{AtomicBool, Ordering}; + static DONE: AtomicBool = AtomicBool::new(false); + if cfg!(debug_assertions) || DONE.swap(true, Ordering::SeqCst) { + return None; + } + match app_check_update(app).await { + Ok(check) => check.update, + Err(e) => { + tracing::warn!("startup update check failed: {e}"); + None + } + } +} + /// Downloads and installs the available update, then quits through every /// window's unsaved-changes check and relaunches. If a window cancels the quit, /// the update applies on the next start. diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5df9ef1a9..bd4803de7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -36,6 +36,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -56,8 +57,8 @@ "mainBinaryName": "openidea", "plugins": { "updater": { - "pubkey": "", - "endpoints": [] + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFCOUU0MUFFOUY4NzZCNkQKUldSdGE0ZWZya0dlRzNnRFJUR2loUVdNZ1BmamxkNzFoaWNPUTBsRmI0cjhxZFRqQURybkMwakEK", + "endpoints": ["https://github.com/l0ng-ai/openidea/releases/latest/download/latest.json"] } } } diff --git a/src/bindings.ts b/src/bindings.ts index e0bed49df..8fb2473dd 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -459,6 +459,17 @@ export const commands = { appLogLocation: () => typedError(__TAURI_INVOKE("app_log_location")), appBuildInfo: () => __TAURI_INVOKE("app_build_info"), appCheckUpdate: () => typedError(__TAURI_INVOKE("app_check_update")), + /** + * The background check made once per process when the first window starts; + * later calls (other windows) and debug builds get `None`. Failures are logged, + * not reported: the user didn't ask for this check. + */ + appStartupUpdateCheck: () => __TAURI_INVOKE<{ + version: string, + currentVersion: string, + notes: string | null, + date: string | null, +} | null>("app_startup_update_check"), /** * Downloads and installs the available update, then quits through every * window's unsaved-changes check and relaunches. If a window cancels the quit, diff --git a/src/features/app/appActions.ts b/src/features/app/appActions.ts index 7d52c4a88..a78b961e2 100644 --- a/src/features/app/appActions.ts +++ b/src/features/app/appActions.ts @@ -1,6 +1,6 @@ import type { Action } from "@/platform/actions"; import { t } from "@/platform/i18n"; -import { call, commands, isTauri } from "@/platform/ipc"; +import { call, commands, isTauri, type UpdateInfo } from "@/platform/ipc"; import { notifications } from "@/workbench/contributions"; import { showAbout } from "./AboutDialog"; import { quitApp } from "./closeGuard"; @@ -42,28 +42,39 @@ export async function checkForUpdates(): Promise { key: "app.update", }); } else { - notifications().notify({ - level: "info", - title: t("app.update.available", { version: r.update.version }), - body: r.update.notes ?? undefined, - sticky: true, - key: "app.update", - actions: [ - { - label: t("app.update.install"), - run: () => - void call(commands.appInstallUpdate()).catch((e) => - notifications().notify({ level: "error", title: t("app.update.installFailed"), body: errorText(e) }), - ), - }, - ], - }); + notifyUpdate(r.update); } } catch (e) { notifications().notify({ level: "error", title: t("app.update.failed"), body: errorText(e), key: "app.update" }); } } +/** Silent check once per app start; only an available update is shown. */ +export async function checkForUpdatesOnStartup(): Promise { + if (!isTauri()) return; + const update = await commands.appStartupUpdateCheck().catch(() => null); + if (update) notifyUpdate(update); +} + +function notifyUpdate(update: UpdateInfo) { + notifications().notify({ + level: "info", + title: t("app.update.available", { version: update.version }), + body: update.notes ?? undefined, + sticky: true, + key: "app.update", + actions: [ + { + label: t("app.update.install"), + run: () => + void call(commands.appInstallUpdate()).catch((e) => + notifications().notify({ level: "error", title: t("app.update.installFailed"), body: errorText(e) }), + ), + }, + ], + }); +} + /** * Edit menu items on Windows/Linux (macOS uses native roles). They act on the * focused element like the native commands would; CodeMirror handles the diff --git a/src/features/app/index.ts b/src/features/app/index.ts index 063350b38..999d9f8e1 100644 --- a/src/features/app/index.ts +++ b/src/features/app/index.ts @@ -4,7 +4,7 @@ */ import { registerOverlay } from "@/workbench/contributions"; import { AboutOverlay } from "./AboutDialog"; -import { appActions, updateActions } from "./appActions"; +import { appActions, checkForUpdatesOnStartup, updateActions } from "./appActions"; import { installCloseGuard } from "./closeGuard"; import { InvalidateCachesOverlay, registerInvalidateCachesActions } from "./InvalidateCaches"; import { buildInfo } from "./host"; @@ -32,7 +32,9 @@ export function activate(): () => void { ]; let disposed = false; void buildInfo().then((b) => { - if (b?.updates && !disposed) disposers.push(registerLocalizedActions(updateActions)); + if (!b?.updates || disposed) return; + disposers.push(registerLocalizedActions(updateActions)); + void checkForUpdatesOnStartup(); }); return () => { disposed = true;