From 4fbdb6ceac01659153d361841d495fd6f2ba716b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 15 Apr 2026 16:56:03 -0700 Subject: [PATCH 1/4] Default nightly desktop builds to nightly updates - Centralize desktop version/channel detection - Prefer nightly settings for packaged nightly builds - Ignore auto-update offers that do not match the selected channel --- apps/desktop/src/appBranding.ts | 5 +-- apps/desktop/src/desktopSettings.test.ts | 24 ++++++++++++-- apps/desktop/src/desktopSettings.ts | 22 ++++++++++--- apps/desktop/src/main.ts | 16 +++++++-- apps/desktop/src/updateChannels.test.ts | 41 ++++++++++++++++++++++++ apps/desktop/src/updateChannels.ts | 18 +++++++++++ 6 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/updateChannels.test.ts create mode 100644 apps/desktop/src/updateChannels.ts diff --git a/apps/desktop/src/appBranding.ts b/apps/desktop/src/appBranding.ts index fe1d13186bc..49cbcc6780f 100644 --- a/apps/desktop/src/appBranding.ts +++ b/apps/desktop/src/appBranding.ts @@ -1,7 +1,8 @@ import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts"; +import { isNightlyDesktopVersion } from "./updateChannels"; + const APP_BASE_NAME = "T3 Code"; -const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; export function resolveDesktopAppStageLabel(input: { readonly isDevelopment: boolean; @@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: { return "Dev"; } - return NIGHTLY_VERSION_PATTERN.test(input.appVersion) ? "Nightly" : "Alpha"; + return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha"; } export function resolveDesktopAppBranding(input: { diff --git a/apps/desktop/src/desktopSettings.test.ts b/apps/desktop/src/desktopSettings.test.ts index 7efdc88e73a..8d052fd41ce 100644 --- a/apps/desktop/src/desktopSettings.test.ts +++ b/apps/desktop/src/desktopSettings.test.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { DEFAULT_DESKTOP_SETTINGS, readDesktopSettings, + resolveDefaultDesktopSettings, setDesktopServerExposurePreference, setDesktopUpdateChannelPreference, writeDesktopSettings, @@ -28,7 +29,14 @@ function makeSettingsPath() { describe("desktopSettings", () => { it("returns defaults when no settings file exists", () => { - expect(readDesktopSettings(makeSettingsPath())).toEqual(DEFAULT_DESKTOP_SETTINGS); + expect(readDesktopSettings(makeSettingsPath(), "0.0.17")).toEqual(DEFAULT_DESKTOP_SETTINGS); + }); + + it("defaults packaged nightly builds to the nightly update channel", () => { + expect(resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1")).toEqual({ + serverExposureMode: "local-only", + updateChannel: "nightly", + }); }); it("persists and reloads the configured server exposure mode", () => { @@ -39,7 +47,7 @@ describe("desktopSettings", () => { updateChannel: "latest", }); - expect(readDesktopSettings(settingsPath)).toEqual({ + expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual({ serverExposureMode: "network-accessible", updateChannel: "latest", }); @@ -79,6 +87,16 @@ describe("desktopSettings", () => { const settingsPath = makeSettingsPath(); fs.writeFileSync(settingsPath, "{not-json", "utf8"); - expect(readDesktopSettings(settingsPath)).toEqual(DEFAULT_DESKTOP_SETTINGS); + expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual(DEFAULT_DESKTOP_SETTINGS); + }); + + it("falls back to the nightly channel for legacy nightly settings without an update track", () => { + const settingsPath = makeSettingsPath(); + fs.writeFileSync(settingsPath, JSON.stringify({ serverExposureMode: "local-only" }), "utf8"); + + expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({ + serverExposureMode: "local-only", + updateChannel: "nightly", + }); }); }); diff --git a/apps/desktop/src/desktopSettings.ts b/apps/desktop/src/desktopSettings.ts index 7fc0ddd7865..f0bbf8ed811 100644 --- a/apps/desktop/src/desktopSettings.ts +++ b/apps/desktop/src/desktopSettings.ts @@ -2,6 +2,8 @@ import * as FS from "node:fs"; import * as Path from "node:path"; import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts"; +import { resolveDefaultDesktopUpdateChannel } from "./updateChannels"; + export interface DesktopSettings { readonly serverExposureMode: DesktopServerExposureMode; readonly updateChannel: DesktopUpdateChannel; @@ -12,6 +14,13 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { updateChannel: "latest", }; +export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings { + return { + ...DEFAULT_DESKTOP_SETTINGS, + updateChannel: resolveDefaultDesktopUpdateChannel(appVersion), + }; +} + export function setDesktopServerExposurePreference( settings: DesktopSettings, requestedMode: DesktopServerExposureMode, @@ -36,10 +45,12 @@ export function setDesktopUpdateChannelPreference( }; } -export function readDesktopSettings(settingsPath: string): DesktopSettings { +export function readDesktopSettings(settingsPath: string, appVersion: string): DesktopSettings { + const defaultSettings = resolveDefaultDesktopSettings(appVersion); + try { if (!FS.existsSync(settingsPath)) { - return DEFAULT_DESKTOP_SETTINGS; + return defaultSettings; } const raw = FS.readFileSync(settingsPath, "utf8"); @@ -51,10 +62,13 @@ export function readDesktopSettings(settingsPath: string): DesktopSettings { return { serverExposureMode: parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only", - updateChannel: parsed.updateChannel === "nightly" ? "nightly" : "latest", + updateChannel: + parsed.updateChannel === "nightly" || parsed.updateChannel === "latest" + ? parsed.updateChannel + : defaultSettings.updateChannel, }; } catch { - return DEFAULT_DESKTOP_SETTINGS; + return defaultSettings; } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 987cad34089..c3773f2b9cf 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -58,6 +58,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog"; import { resolveDesktopServerExposure } from "./serverExposure"; import { syncShellEnvironment } from "./syncShellEnvironment"; import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; +import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels"; import { ServerListeningDetector } from "./serverListeningDetector"; import { createInitialDesktopUpdateState, @@ -190,7 +191,7 @@ let desktopLogSink: RotatingFileSink | null = null; let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); -let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH); +let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH, app.getVersion()); let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode; let destructiveMenuIconCache: Electron.NativeImage | null | undefined; @@ -1138,9 +1139,9 @@ function createBaseUpdateState( function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void { autoUpdater.channel = channel; autoUpdater.allowPrerelease = channel === "nightly"; - autoUpdater.allowDowngrade = channel === "nightly"; + autoUpdater.allowDowngrade = false; console.info( - `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}).`, + `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}, allowDowngrade=false).`, ); } @@ -1285,6 +1286,15 @@ function configureAutoUpdater(): void { console.info("[desktop-updater] Looking for updates..."); }); autoUpdater.on("update-available", (info) => { + if (!doesVersionMatchDesktopUpdateChannel(info.version, updateState.channel)) { + console.info( + `[desktop-updater] Ignoring ${info.version} because it does not match the selected '${updateState.channel}' channel.`, + ); + setUpdateState(reduceDesktopUpdateStateOnNoUpdate(updateState, new Date().toISOString())); + lastLoggedDownloadMilestone = -1; + return; + } + setUpdateState( reduceDesktopUpdateStateOnUpdateAvailable( updateState, diff --git a/apps/desktop/src/updateChannels.test.ts b/apps/desktop/src/updateChannels.test.ts new file mode 100644 index 00000000000..bd1dcc0c73c --- /dev/null +++ b/apps/desktop/src/updateChannels.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { + doesVersionMatchDesktopUpdateChannel, + isNightlyDesktopVersion, + resolveDefaultDesktopUpdateChannel, +} from "./updateChannels"; + +describe("isNightlyDesktopVersion", () => { + it("detects packaged nightly versions", () => { + expect(isNightlyDesktopVersion("0.0.17-nightly.20260415.1")).toBe(true); + }); + + it("does not flag stable versions as nightly", () => { + expect(isNightlyDesktopVersion("0.0.17")).toBe(false); + }); +}); + +describe("resolveDefaultDesktopUpdateChannel", () => { + it("defaults stable builds to latest", () => { + expect(resolveDefaultDesktopUpdateChannel("0.0.17")).toBe("latest"); + }); + + it("defaults nightly builds to nightly", () => { + expect(resolveDefaultDesktopUpdateChannel("0.0.17-nightly.20260415.1")).toBe("nightly"); + }); +}); + +describe("doesVersionMatchDesktopUpdateChannel", () => { + it("accepts nightly releases on the nightly channel", () => { + expect(doesVersionMatchDesktopUpdateChannel("0.0.17-nightly.20260416.1", "nightly")).toBe(true); + }); + + it("rejects stable releases on the nightly channel", () => { + expect(doesVersionMatchDesktopUpdateChannel("0.0.17", "nightly")).toBe(false); + }); + + it("rejects nightly releases on the stable channel", () => { + expect(doesVersionMatchDesktopUpdateChannel("0.0.17-nightly.20260416.1", "latest")).toBe(false); + }); +}); diff --git a/apps/desktop/src/updateChannels.ts b/apps/desktop/src/updateChannels.ts new file mode 100644 index 00000000000..615b8e6db66 --- /dev/null +++ b/apps/desktop/src/updateChannels.ts @@ -0,0 +1,18 @@ +import type { DesktopUpdateChannel } from "@t3tools/contracts"; + +const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; + +export function isNightlyDesktopVersion(version: string): boolean { + return NIGHTLY_VERSION_PATTERN.test(version); +} + +export function resolveDefaultDesktopUpdateChannel(appVersion: string): DesktopUpdateChannel { + return isNightlyDesktopVersion(appVersion) ? "nightly" : "latest"; +} + +export function doesVersionMatchDesktopUpdateChannel( + version: string, + channel: DesktopUpdateChannel, +): boolean { + return resolveDefaultDesktopUpdateChannel(version) === channel; +} From 76d6ea4f808e973b2e18432c37124537ca9d5703 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 15 Apr 2026 17:36:43 -0700 Subject: [PATCH 2/4] Preserve explicit update channel preferences - Track whether the update channel was user-configured - Keep nightly builds on nightly while allowing downgrade - Add migration coverage for legacy settings Co-authored-by: codex --- apps/desktop/src/desktopSettings.test.ts | 45 ++++++++++++++++++++++++ apps/desktop/src/desktopSettings.ts | 24 ++++++++----- apps/desktop/src/main.ts | 4 +-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/desktopSettings.test.ts b/apps/desktop/src/desktopSettings.test.ts index 8d052fd41ce..7c8be53f827 100644 --- a/apps/desktop/src/desktopSettings.test.ts +++ b/apps/desktop/src/desktopSettings.test.ts @@ -36,6 +36,7 @@ describe("desktopSettings", () => { expect(resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1")).toEqual({ serverExposureMode: "local-only", updateChannel: "nightly", + updateChannelConfiguredByUser: false, }); }); @@ -45,11 +46,13 @@ describe("desktopSettings", () => { writeDesktopSettings(settingsPath, { serverExposureMode: "network-accessible", updateChannel: "latest", + updateChannelConfiguredByUser: true, }); expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual({ serverExposureMode: "network-accessible", updateChannel: "latest", + updateChannelConfiguredByUser: true, }); }); @@ -59,12 +62,14 @@ describe("desktopSettings", () => { { serverExposureMode: "local-only", updateChannel: "latest", + updateChannelConfiguredByUser: false, }, "network-accessible", ), ).toEqual({ serverExposureMode: "network-accessible", updateChannel: "latest", + updateChannelConfiguredByUser: false, }); }); @@ -74,12 +79,14 @@ describe("desktopSettings", () => { { serverExposureMode: "local-only", updateChannel: "latest", + updateChannelConfiguredByUser: false, }, "nightly", ), ).toEqual({ serverExposureMode: "local-only", updateChannel: "nightly", + updateChannelConfiguredByUser: true, }); }); @@ -97,6 +104,44 @@ describe("desktopSettings", () => { expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({ serverExposureMode: "local-only", updateChannel: "nightly", + updateChannelConfiguredByUser: false, + }); + }); + + it("migrates legacy implicit stable settings to nightly when running a nightly build", () => { + const settingsPath = makeSettingsPath(); + fs.writeFileSync( + settingsPath, + JSON.stringify({ + serverExposureMode: "local-only", + updateChannel: "latest", + }), + "utf8", + ); + + expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({ + serverExposureMode: "local-only", + updateChannel: "nightly", + updateChannelConfiguredByUser: false, + }); + }); + + it("preserves an explicit stable choice on nightly builds", () => { + const settingsPath = makeSettingsPath(); + fs.writeFileSync( + settingsPath, + JSON.stringify({ + serverExposureMode: "local-only", + updateChannel: "latest", + updateChannelConfiguredByUser: true, + }), + "utf8", + ); + + expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({ + serverExposureMode: "local-only", + updateChannel: "latest", + updateChannelConfiguredByUser: true, }); }); }); diff --git a/apps/desktop/src/desktopSettings.ts b/apps/desktop/src/desktopSettings.ts index f0bbf8ed811..e3811129fa9 100644 --- a/apps/desktop/src/desktopSettings.ts +++ b/apps/desktop/src/desktopSettings.ts @@ -7,11 +7,13 @@ import { resolveDefaultDesktopUpdateChannel } from "./updateChannels"; export interface DesktopSettings { readonly serverExposureMode: DesktopServerExposureMode; readonly updateChannel: DesktopUpdateChannel; + readonly updateChannelConfiguredByUser: boolean; } export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { serverExposureMode: "local-only", updateChannel: "latest", + updateChannelConfiguredByUser: false, }; export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings { @@ -37,12 +39,11 @@ export function setDesktopUpdateChannelPreference( settings: DesktopSettings, requestedChannel: DesktopUpdateChannel, ): DesktopSettings { - return settings.updateChannel === requestedChannel - ? settings - : { - ...settings, - updateChannel: requestedChannel, - }; + return { + ...settings, + updateChannel: requestedChannel, + updateChannelConfiguredByUser: true, + }; } export function readDesktopSettings(settingsPath: string, appVersion: string): DesktopSettings { @@ -57,15 +58,22 @@ export function readDesktopSettings(settingsPath: string, appVersion: string): D const parsed = JSON.parse(raw) as { readonly serverExposureMode?: unknown; readonly updateChannel?: unknown; + readonly updateChannelConfiguredByUser?: unknown; }; + const parsedUpdateChannel = + parsed.updateChannel === "nightly" || parsed.updateChannel === "latest" + ? parsed.updateChannel + : null; + const updateChannelConfiguredByUser = parsed.updateChannelConfiguredByUser === true; return { serverExposureMode: parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only", updateChannel: - parsed.updateChannel === "nightly" || parsed.updateChannel === "latest" - ? parsed.updateChannel + updateChannelConfiguredByUser && parsedUpdateChannel !== null + ? parsedUpdateChannel : defaultSettings.updateChannel, + updateChannelConfiguredByUser, }; } catch { return defaultSettings; diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c3773f2b9cf..9bca15d6e13 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1139,9 +1139,9 @@ function createBaseUpdateState( function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void { autoUpdater.channel = channel; autoUpdater.allowPrerelease = channel === "nightly"; - autoUpdater.allowDowngrade = false; + autoUpdater.allowDowngrade = channel === "nightly"; console.info( - `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}, allowDowngrade=false).`, + `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}, allowDowngrade=${channel === "nightly"}).`, ); } From 651401dd274287a42927a7116dba5b58e7a55e31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Apr 2026 01:51:48 +0000 Subject: [PATCH 3/4] Fix migration losing explicit nightly preference and early return skipping user preference flag - In readDesktopSettings, infer updateChannelConfiguredByUser=true for legacy settings files that have updateChannel='nightly', since the old default was always 'latest' and seeing 'nightly' implies an explicit user choice. - In the UPDATE_SET_CHANNEL_CHANNEL IPC handler, persist the settings (including updateChannelConfiguredByUser) before the same-channel early return, so that confirming the current channel still records the explicit user preference. Applied via @cursor push command --- apps/desktop/src/desktopSettings.ts | 5 ++++- apps/desktop/src/main.ts | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/desktopSettings.ts b/apps/desktop/src/desktopSettings.ts index e3811129fa9..cb0829a8b65 100644 --- a/apps/desktop/src/desktopSettings.ts +++ b/apps/desktop/src/desktopSettings.ts @@ -64,7 +64,10 @@ export function readDesktopSettings(settingsPath: string, appVersion: string): D parsed.updateChannel === "nightly" || parsed.updateChannel === "latest" ? parsed.updateChannel : null; - const updateChannelConfiguredByUser = parsed.updateChannelConfiguredByUser === true; + const isLegacySettings = parsed.updateChannelConfiguredByUser === undefined; + const updateChannelConfiguredByUser = + parsed.updateChannelConfiguredByUser === true || + (isLegacySettings && parsedUpdateChannel === "nightly"); return { serverExposureMode: diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9bca15d6e13..4386c340f9c 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1802,13 +1802,14 @@ function registerIpcHandlers(): void { } const nextChannel = rawChannel as DesktopUpdateChannel; - if (nextChannel === desktopSettings.updateChannel) { - return updateState; - } desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel); writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); + if (nextChannel === updateState.channel) { + return updateState; + } + const enabled = shouldEnableAutoUpdates(); setUpdateState(createBaseUpdateState(nextChannel, enabled)); From 93ce4e82cf1f7de5bf3ba019ce1f79aedc3eacd4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 15 Apr 2026 19:26:55 -0700 Subject: [PATCH 4/4] Use next patch version for nightly releases - Bump nightly metadata generation and smoke tests - Document the new nightly base version rule --- docs/release.md | 2 +- scripts/release-smoke.ts | 6 +++--- scripts/resolve-nightly-release.test.ts | 17 ++++++++++++----- scripts/resolve-nightly-release.ts | 13 ++++++++++++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/release.md b/docs/release.md index 7486dc69607..fc17d3e39db 100644 --- a/docs/release.md +++ b/docs/release.md @@ -37,7 +37,7 @@ This document covers the unified release workflow for stable and nightly desktop - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.` - release name includes the short commit SHA - `make_latest` is always `false` -- Uses the current `apps/desktop/package.json` semver core (`X.Y.Z`) as the nightly base, then appends a nightly prerelease suffix. +- Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`. - Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable. - Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version. - Does not commit version bumps back to `main`. diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index 3ca283c11a9..960730a3547 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -127,17 +127,17 @@ try { ); assertContains( nightlyReleaseMetadata, - "version=9.9.9-nightly.20260413.321", + "version=9.9.10-nightly.20260413.321", "Expected nightly metadata to contain the derived nightly version.", ); assertContains( nightlyReleaseMetadata, - "tag=nightly-v9.9.9-nightly.20260413.321", + "tag=nightly-v9.9.10-nightly.20260413.321", "Expected nightly metadata to contain the derived nightly tag.", ); assertContains( nightlyReleaseMetadata, - "name=T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)", + "name=T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)", "Expected nightly metadata to include the short commit SHA in the release name.", ); diff --git a/scripts/resolve-nightly-release.test.ts b/scripts/resolve-nightly-release.test.ts index 8a381434456..56358d6c142 100644 --- a/scripts/resolve-nightly-release.test.ts +++ b/scripts/resolve-nightly-release.test.ts @@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest"; import { resolveNightlyBaseVersion, resolveNightlyReleaseMetadata, + resolveNightlyTargetVersion, } from "./resolve-nightly-release.ts"; it("strips prerelease and build metadata when deriving the nightly base version", () => { @@ -11,14 +12,20 @@ it("strips prerelease and build metadata when deriving the nightly base version" assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); }); +it("bumps the patch version before deriving nightly prerelease versions", () => { + assert.equal(resolveNightlyTargetVersion("0.0.17"), "0.0.18"); + assert.equal(resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10"); + assert.equal(resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4"); +}); + it("derives nightly metadata including the short commit sha in the release name", () => { assert.deepStrictEqual( - resolveNightlyReleaseMetadata("9.9.9", "20260413", 321, "abcdef1234567890"), + resolveNightlyReleaseMetadata("9.9.10", "20260413", 321, "abcdef1234567890"), { - baseVersion: "9.9.9", - version: "9.9.9-nightly.20260413.321", - tag: "nightly-v9.9.9-nightly.20260413.321", - name: "T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)", + baseVersion: "9.9.10", + version: "9.9.10-nightly.20260413.321", + tag: "nightly-v9.9.10-nightly.20260413.321", + name: "T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)", shortSha: "abcdef123456", }, ); diff --git a/scripts/resolve-nightly-release.ts b/scripts/resolve-nightly-release.ts index 571baec8f41..4a92ef63aed 100644 --- a/scripts/resolve-nightly-release.ts +++ b/scripts/resolve-nightly-release.ts @@ -32,6 +32,17 @@ const decodeDesktopPackageJson = Schema.decodeUnknownEffect( export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, ""); +export const resolveNightlyTargetVersion = (version: string) => { + const stableCore = resolveNightlyBaseVersion(version); + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore); + if (!match) { + throw new Error(`Invalid desktop package version '${version}'.`); + } + + const [, major, minor, patch] = match; + return `${major}.${minor}.${Number(patch) + 1}`; +}; + export const resolveNightlyReleaseMetadata = ( baseVersion: string, date: string, @@ -59,7 +70,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( const packageJson = yield* fs .readFileString(packageJsonPath) .pipe(Effect.flatMap(decodeDesktopPackageJson)); - return resolveNightlyBaseVersion(packageJson.version); + return resolveNightlyTargetVersion(packageJson.version); }); const writeOutput = Effect.fn("writeOutput")(function* (