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.
Цей коміт міститься в:
l0ng-ai committed 2026-10-24 14:13:12 +08:00
1 джерело 99b4f8bdf6
коміт 3d8a6f07b4
6 files changed
+67 -21

No files matched your search

+1
Переглянути файл
@@ -239,6 +239,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
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,
+20
Переглянути файл
@@ -268,6 +268,26 @@ pub async fn app_check_update(app: AppHandle) -> IdeResult<UpdateCheck> {
})
}
/// 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<UpdateInfo> {
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.
+3 -2
Переглянути файл
@@ -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"]
}
}
}
+11
Переглянути файл
@@ -459,6 +459,17 @@ export const commands = {
appLogLocation: () => typedError<LogLocation, IdeError>(__TAURI_INVOKE("app_log_location")),
appBuildInfo: () => __TAURI_INVOKE<BuildInfo>("app_build_info"),
appCheckUpdate: () => typedError<UpdateCheck, IdeError>(__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,
+28 -17
Переглянути файл
@@ -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<void> {
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<void> {
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
+4 -2
Переглянути файл
@@ -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;