- Derive desktop updater channel from the running app version - Keep stable builds on latest and prereleases on their own track - Add tests and release docs for channel matching
Unified · Split
| @@ -54,6 +54,11 @@ import { isBackendReadinessAborted, waitForHttpReady } from "./backendReadiness" · 53 unmodified lines | ||
| 54 | 54 | import { showDesktopConfirmDialog } from "./confirmDialog"; |
| 55 | 55 | import { resolveDesktopServerExposure } from "./serverExposure"; |
| 56 | 56 | import { syncShellEnvironment } from "./syncShellEnvironment"; |
| 57 | +import { | |
| 58 | + resolveDesktopUpdateChannel, | |
| 59 | + shouldAcceptDesktopUpdateForCurrentVersion, | |
| 60 | + shouldAllowDesktopPrereleaseUpdates, | |
| 61 | +} from "./updateChannel"; | |
| 57 | 62 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; |
| 58 | 63 | import { ServerListeningDetector } from "./serverListeningDetector"; |
| 59 | 64 | import { |
| @@ -116,8 +121,6 @@ const APP_RUN_ID = Crypto.randomBytes(6).toString("hex"); · 56 unmodified lines | ||
| 116 | 121 | const SERVER_SETTINGS_PATH = Path.join(STATE_DIR, "settings.json"); |
| 117 | 122 | const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000; |
| 118 | 123 | const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000; |
| 119 | −const DESKTOP_UPDATE_CHANNEL = "latest"; | |
| 120 | −const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; | |
| 121 | 124 | const DESKTOP_LOOPBACK_HOST = "127.0.0.1"; |
| 122 | 125 | const DESKTOP_REQUIRED_PORT_PROBE_HOSTS = ["0.0.0.0", "::"] as const; |
| 123 | 126 | const TITLEBAR_HEIGHT = 40; |
| @@ -1168,6 +1171,9 @@ async function installDownloadedUpdate(): Promise<{ accepted: boolean; completed · 1044 unmodified lines | ||
| 1168 | 1171 | } |
| 1169 | 1172 | |
| 1170 | 1173 | function configureAutoUpdater(): void { |
| 1174 | + const currentVersion = app.getVersion(); | |
| 1175 | + const updateChannel = resolveDesktopUpdateChannel(currentVersion); | |
| 1176 | + const allowPrerelease = shouldAllowDesktopPrereleaseUpdates(currentVersion); | |
| 1171 | 1177 | const githubToken = |
| 1172 | 1178 | process.env.T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || ""; |
| 1173 | 1179 | if (githubToken) { |
| @@ -1205,12 +1211,14 @@ function configureAutoUpdater(): void { · 31 unmodified lines | ||
| 1205 | 1211 | |
| 1206 | 1212 | autoUpdater.autoDownload = false; |
| 1207 | 1213 | autoUpdater.autoInstallOnAppQuit = false; |
| 1208 | − // Keep alpha branding, but force all installs onto the stable update track. | |
| 1209 | − autoUpdater.channel = DESKTOP_UPDATE_CHANNEL; | |
| 1210 | − autoUpdater.allowPrerelease = DESKTOP_UPDATE_ALLOW_PRERELEASE; | |
| 1214 | + autoUpdater.channel = updateChannel; | |
| 1215 | + autoUpdater.allowPrerelease = allowPrerelease; | |
| 1211 | 1216 | autoUpdater.allowDowngrade = false; |
| 1212 | 1217 | autoUpdater.disableDifferentialDownload = isArm64HostRunningIntelBuild(desktopRuntimeInfo); |
| 1213 | 1218 | let lastLoggedDownloadMilestone = -1; |
| 1219 | + console.info( | |
| 1220 | + `[desktop-updater] Configured update channel=${updateChannel} currentVersion=${currentVersion} allowPrerelease=${allowPrerelease}.`, | |
| 1221 | + ); | |
| 1214 | 1222 | |
| 1215 | 1223 | if (isArm64HostRunningIntelBuild(desktopRuntimeInfo)) { |
| 1216 | 1224 | console.info( |
| @@ -1222,6 +1230,15 @@ function configureAutoUpdater(): void { · 5 unmodified lines | ||
| 1222 | 1230 | console.info("[desktop-updater] Looking for updates..."); |
| 1223 | 1231 | }); |
| 1224 | 1232 | autoUpdater.on("update-available", (info) => { |
| 1233 | + if (!shouldAcceptDesktopUpdateForCurrentVersion(currentVersion, info.version)) { | |
| 1234 | + setUpdateState(reduceDesktopUpdateStateOnNoUpdate(updateState, new Date().toISOString())); | |
| 1235 | + lastLoggedDownloadMilestone = -1; | |
| 1236 | + console.info( | |
| 1237 | + `[desktop-updater] Ignoring update ${info.version} on channel=${resolveDesktopUpdateChannel(info.version)} while currentVersion=${currentVersion} is on channel=${updateChannel}.`, | |
| 1238 | + ); | |
| 1239 | + return; | |
| 1240 | + } | |
| 1241 | + | |
| 1225 | 1242 | setUpdateState( |
| 1226 | 1243 | reduceDesktopUpdateStateOnUpdateAvailable( |
| 1227 | 1244 | updateState, |
| @@ -0,0 +1,40 @@ | ||
| 1 | +import { describe, expect, it } from "vitest"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + resolveDesktopUpdateChannel, | |
| 5 | + shouldAcceptDesktopUpdateForCurrentVersion, | |
| 6 | + shouldAllowDesktopPrereleaseUpdates, | |
| 7 | +} from "./updateChannel"; | |
| 8 | + | |
| 9 | +describe("updateChannel", () => { | |
| 10 | + it("uses the stable latest channel for plain versions", () => { | |
| 11 | + expect(resolveDesktopUpdateChannel("0.0.17")).toBe("latest"); | |
| 12 | + expect(shouldAllowDesktopPrereleaseUpdates("0.0.17")).toBe(false); | |
| 13 | + }); | |
| 14 | + | |
| 15 | + it("derives the prerelease channel from the installed version", () => { | |
| 16 | + expect(resolveDesktopUpdateChannel("0.0.18-nightly.20260415")).toBe("nightly"); | |
| 17 | + expect(shouldAllowDesktopPrereleaseUpdates("0.0.18-nightly.20260415")).toBe(true); | |
| 18 | + }); | |
| 19 | + | |
| 20 | + it("accepts updates that stay on the same prerelease track", () => { | |
| 21 | + expect( | |
| 22 | + shouldAcceptDesktopUpdateForCurrentVersion( | |
| 23 | + "0.0.18-nightly.20260415", | |
| 24 | + "0.0.18-nightly.20260416", | |
| 25 | + ), | |
| 26 | + ).toBe(true); | |
| 27 | + }); | |
| 28 | + | |
| 29 | + it("rejects stable releases while running a nightly build", () => { | |
| 30 | + expect(shouldAcceptDesktopUpdateForCurrentVersion("0.0.18-nightly.20260415", "0.0.17")).toBe( | |
| 31 | + false, | |
| 32 | + ); | |
| 33 | + }); | |
| 34 | + | |
| 35 | + it("rejects updates from a different prerelease track", () => { | |
| 36 | + expect( | |
| 37 | + shouldAcceptDesktopUpdateForCurrentVersion("0.0.18-nightly.20260415", "0.0.18-beta.1"), | |
| 38 | + ).toBe(false); | |
| 39 | + }); | |
| 40 | +}); | |
| @@ -0,0 +1,26 @@ | ||
| 1 | +const VERSION_PRERELEASE_CHANNEL_PATTERN = | |
| 2 | + /^\d+\.\d+\.\d+(?:-([0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z.-]+)?$/; | |
| 3 | + | |
| 4 | +function resolvePrereleaseChannel(version: string): string | null { | |
| 5 | + const normalizedVersion = version.trim(); | |
| 6 | + const match = VERSION_PRERELEASE_CHANNEL_PATTERN.exec(normalizedVersion); | |
| 7 | + const channel = match?.[1]?.trim().toLowerCase(); | |
| 8 | + return channel && channel.length > 0 ? channel : null; | |
| 9 | +} | |
| 10 | + | |
| 11 | +export function resolveDesktopUpdateChannel(version: string): string { | |
| 12 | + return resolvePrereleaseChannel(version) ?? "latest"; | |
| 13 | +} | |
| 14 | + | |
| 15 | +export function shouldAllowDesktopPrereleaseUpdates(version: string): boolean { | |
| 16 | + return resolvePrereleaseChannel(version) !== null; | |
| 17 | +} | |
| 18 | + | |
| 19 | +export function shouldAcceptDesktopUpdateForCurrentVersion( | |
| 20 | + currentVersion: string, | |
| 21 | + candidateVersion: string, | |
| 22 | +): boolean { | |
| 23 | + return ( | |
| 24 | + resolveDesktopUpdateChannel(currentVersion) === resolveDesktopUpdateChannel(candidateVersion) | |
| 25 | + ); | |
| 26 | +} | |
| @@ -25,6 +25,9 @@ This document covers how to run desktop releases from one tag, first without sig · 24 unmodified lines | ||
| 25 | 25 | - Background checks run on startup delay + interval. |
| 26 | 26 | - No automatic download or install. |
| 27 | 27 | - The desktop UI shows a rocket update button when an update is available; click once to download, click again after download to restart/install. |
| 28 | +- Update channel selection: | |
| 29 | + - Stable builds (`X.Y.Z`) only accept stable updates. | |
| 30 | + - Prerelease builds (for example `X.Y.Z-nightly.N`) stay on that prerelease track and ignore stable or differently-tagged prerelease releases. | |
| 28 | 31 | - Provider: GitHub Releases (`provider: github`) configured at build time. |
| 29 | 32 | - Repository slug source: |
| 30 | 33 | - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. |