authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-15 21:31:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-15 21:31:24-07:00
logf9580ff08895380147181e1a771e6bb328dac439
tree4f7305d88b3447aea4d98882d0ac3370d054fd15
parent5e1dd56d2c534d90ba3363f97b1e1e23412507c0
signature Signed by PGP key B5690EEEBB952194

Default nightly desktop builds to the nightly update channel (#2049)

Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>

10 files changed, 207 insertions(+), 30 deletions(-)

apps/desktop/src/appBranding.ts+3-2
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";1import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";
22
3import { isNightlyDesktopVersion } from "./updateChannels";
4
3const APP_BASE_NAME = "T3 Code";5const APP_BASE_NAME = "T3 Code";
4const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/;
56
6export function resolveDesktopAppStageLabel(input: {7export function resolveDesktopAppStageLabel(input: {
7 readonly isDevelopment: boolean;8 readonly isDevelopment: boolean;
...@@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: {...@@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: {
11 return "Dev";12 return "Dev";
12 }13 }
1314
14 return NIGHTLY_VERSION_PATTERN.test(input.appVersion) ? "Nightly" : "Alpha";15 return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";
15}16}
1617
17export function resolveDesktopAppBranding(input: {18export function resolveDesktopAppBranding(input: {
apps/desktop/src/desktopSettings.test.ts+66-3
...@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";...@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";
7import {7import {
8 DEFAULT_DESKTOP_SETTINGS,8 DEFAULT_DESKTOP_SETTINGS,
9 readDesktopSettings,9 readDesktopSettings,
10 resolveDefaultDesktopSettings,
10 setDesktopServerExposurePreference,11 setDesktopServerExposurePreference,
11 setDesktopUpdateChannelPreference,12 setDesktopUpdateChannelPreference,
12 writeDesktopSettings,13 writeDesktopSettings,
...@@ -28,7 +29,15 @@ function makeSettingsPath() {...@@ -28,7 +29,15 @@ function makeSettingsPath() {
2829
29describe("desktopSettings", () => {30describe("desktopSettings", () => {
30 it("returns defaults when no settings file exists", () => {31 it("returns defaults when no settings file exists", () => {
31 expect(readDesktopSettings(makeSettingsPath())).toEqual(DEFAULT_DESKTOP_SETTINGS);32 expect(readDesktopSettings(makeSettingsPath(), "0.0.17")).toEqual(DEFAULT_DESKTOP_SETTINGS);
33 });
34
35 it("defaults packaged nightly builds to the nightly update channel", () => {
36 expect(resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1")).toEqual({
37 serverExposureMode: "local-only",
38 updateChannel: "nightly",
39 updateChannelConfiguredByUser: false,
40 });
32 });41 });
3342
34 it("persists and reloads the configured server exposure mode", () => {43 it("persists and reloads the configured server exposure mode", () => {
...@@ -37,11 +46,13 @@ describe("desktopSettings", () => {...@@ -37,11 +46,13 @@ describe("desktopSettings", () => {
37 writeDesktopSettings(settingsPath, {46 writeDesktopSettings(settingsPath, {
38 serverExposureMode: "network-accessible",47 serverExposureMode: "network-accessible",
39 updateChannel: "latest",48 updateChannel: "latest",
49 updateChannelConfiguredByUser: true,
40 });50 });
4151
42 expect(readDesktopSettings(settingsPath)).toEqual({52 expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual({
43 serverExposureMode: "network-accessible",53 serverExposureMode: "network-accessible",
44 updateChannel: "latest",54 updateChannel: "latest",
55 updateChannelConfiguredByUser: true,
45 });56 });
46 });57 });
4758
...@@ -51,12 +62,14 @@ describe("desktopSettings", () => {...@@ -51,12 +62,14 @@ describe("desktopSettings", () => {
51 {62 {
52 serverExposureMode: "local-only",63 serverExposureMode: "local-only",
53 updateChannel: "latest",64 updateChannel: "latest",
65 updateChannelConfiguredByUser: false,
54 },66 },
55 "network-accessible",67 "network-accessible",
56 ),68 ),
57 ).toEqual({69 ).toEqual({
58 serverExposureMode: "network-accessible",70 serverExposureMode: "network-accessible",
59 updateChannel: "latest",71 updateChannel: "latest",
72 updateChannelConfiguredByUser: false,
60 });73 });
61 });74 });
6275
...@@ -66,12 +79,14 @@ describe("desktopSettings", () => {...@@ -66,12 +79,14 @@ describe("desktopSettings", () => {
66 {79 {
67 serverExposureMode: "local-only",80 serverExposureMode: "local-only",
68 updateChannel: "latest",81 updateChannel: "latest",
82 updateChannelConfiguredByUser: false,
69 },83 },
70 "nightly",84 "nightly",
71 ),85 ),
72 ).toEqual({86 ).toEqual({
73 serverExposureMode: "local-only",87 serverExposureMode: "local-only",
74 updateChannel: "nightly",88 updateChannel: "nightly",
89 updateChannelConfiguredByUser: true,
75 });90 });
76 });91 });
7792
...@@ -79,6 +94,54 @@ describe("desktopSettings", () => {...@@ -79,6 +94,54 @@ describe("desktopSettings", () => {
79 const settingsPath = makeSettingsPath();94 const settingsPath = makeSettingsPath();
80 fs.writeFileSync(settingsPath, "{not-json", "utf8");95 fs.writeFileSync(settingsPath, "{not-json", "utf8");
8196
82 expect(readDesktopSettings(settingsPath)).toEqual(DEFAULT_DESKTOP_SETTINGS);97 expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual(DEFAULT_DESKTOP_SETTINGS);
98 });
99
100 it("falls back to the nightly channel for legacy nightly settings without an update track", () => {
101 const settingsPath = makeSettingsPath();
102 fs.writeFileSync(settingsPath, JSON.stringify({ serverExposureMode: "local-only" }), "utf8");
103
104 expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({
105 serverExposureMode: "local-only",
106 updateChannel: "nightly",
107 updateChannelConfiguredByUser: false,
108 });
109 });
110
111 it("migrates legacy implicit stable settings to nightly when running a nightly build", () => {
112 const settingsPath = makeSettingsPath();
113 fs.writeFileSync(
114 settingsPath,
115 JSON.stringify({
116 serverExposureMode: "local-only",
117 updateChannel: "latest",
118 }),
119 "utf8",
120 );
121
122 expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({
123 serverExposureMode: "local-only",
124 updateChannel: "nightly",
125 updateChannelConfiguredByUser: false,
126 });
127 });
128
129 it("preserves an explicit stable choice on nightly builds", () => {
130 const settingsPath = makeSettingsPath();
131 fs.writeFileSync(
132 settingsPath,
133 JSON.stringify({
134 serverExposureMode: "local-only",
135 updateChannel: "latest",
136 updateChannelConfiguredByUser: true,
137 }),
138 "utf8",
139 );
140
141 expect(readDesktopSettings(settingsPath, "0.0.17-nightly.20260415.1")).toEqual({
142 serverExposureMode: "local-only",
143 updateChannel: "latest",
144 updateChannelConfiguredByUser: true,
145 });
83 });146 });
84});147});
apps/desktop/src/desktopSettings.ts+35-10
...@@ -2,16 +2,27 @@ import * as FS from "node:fs";...@@ -2,16 +2,27 @@ import * as FS from "node:fs";
2import * as Path from "node:path";2import * as Path from "node:path";
3import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";3import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";
44
5import { resolveDefaultDesktopUpdateChannel } from "./updateChannels";
6
5export interface DesktopSettings {7export interface DesktopSettings {
6 readonly serverExposureMode: DesktopServerExposureMode;8 readonly serverExposureMode: DesktopServerExposureMode;
7 readonly updateChannel: DesktopUpdateChannel;9 readonly updateChannel: DesktopUpdateChannel;
10 readonly updateChannelConfiguredByUser: boolean;
8}11}
912
10export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {13export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
11 serverExposureMode: "local-only",14 serverExposureMode: "local-only",
12 updateChannel: "latest",15 updateChannel: "latest",
16 updateChannelConfiguredByUser: false,
13};17};
1418
19export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings {
20 return {
21 ...DEFAULT_DESKTOP_SETTINGS,
22 updateChannel: resolveDefaultDesktopUpdateChannel(appVersion),
23 };
24}
25
15export function setDesktopServerExposurePreference(26export function setDesktopServerExposurePreference(
16 settings: DesktopSettings,27 settings: DesktopSettings,
17 requestedMode: DesktopServerExposureMode,28 requestedMode: DesktopServerExposureMode,
...@@ -28,33 +39,47 @@ export function setDesktopUpdateChannelPreference(...@@ -28,33 +39,47 @@ export function setDesktopUpdateChannelPreference(
28 settings: DesktopSettings,39 settings: DesktopSettings,
29 requestedChannel: DesktopUpdateChannel,40 requestedChannel: DesktopUpdateChannel,
30): DesktopSettings {41): DesktopSettings {
31 return settings.updateChannel === requestedChannel42 return {
32 ? settings43 ...settings,
33 : {44 updateChannel: requestedChannel,
34 ...settings,45 updateChannelConfiguredByUser: true,
35 updateChannel: requestedChannel,46 };
36 };
37}47}
3848
39export function readDesktopSettings(settingsPath: string): DesktopSettings {49export function readDesktopSettings(settingsPath: string, appVersion: string): DesktopSettings {
50 const defaultSettings = resolveDefaultDesktopSettings(appVersion);
51
40 try {52 try {
41 if (!FS.existsSync(settingsPath)) {53 if (!FS.existsSync(settingsPath)) {
42 return DEFAULT_DESKTOP_SETTINGS;54 return defaultSettings;
43 }55 }
4456
45 const raw = FS.readFileSync(settingsPath, "utf8");57 const raw = FS.readFileSync(settingsPath, "utf8");
46 const parsed = JSON.parse(raw) as {58 const parsed = JSON.parse(raw) as {
47 readonly serverExposureMode?: unknown;59 readonly serverExposureMode?: unknown;
48 readonly updateChannel?: unknown;60 readonly updateChannel?: unknown;
61 readonly updateChannelConfiguredByUser?: unknown;
49 };62 };
63 const parsedUpdateChannel =
64 parsed.updateChannel === "nightly" || parsed.updateChannel === "latest"
65 ? parsed.updateChannel
66 : null;
67 const isLegacySettings = parsed.updateChannelConfiguredByUser === undefined;
68 const updateChannelConfiguredByUser =
69 parsed.updateChannelConfiguredByUser === true ||
70 (isLegacySettings && parsedUpdateChannel === "nightly");
5071
51 return {72 return {
52 serverExposureMode:73 serverExposureMode:
53 parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",74 parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",
54 updateChannel: parsed.updateChannel === "nightly" ? "nightly" : "latest",75 updateChannel:
76 updateChannelConfiguredByUser && parsedUpdateChannel !== null
77 ? parsedUpdateChannel
78 : defaultSettings.updateChannel,
79 updateChannelConfiguredByUser,
55 };80 };
56 } catch {81 } catch {
57 return DEFAULT_DESKTOP_SETTINGS;82 return defaultSettings;
58 }83 }
59}84}
6085
apps/desktop/src/main.ts+16-5
...@@ -58,6 +58,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog";...@@ -58,6 +58,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog";
58import { resolveDesktopServerExposure } from "./serverExposure";58import { resolveDesktopServerExposure } from "./serverExposure";
59import { syncShellEnvironment } from "./syncShellEnvironment";59import { syncShellEnvironment } from "./syncShellEnvironment";
60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";60import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
61import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels";
61import { ServerListeningDetector } from "./serverListeningDetector";62import { ServerListeningDetector } from "./serverListeningDetector";
62import {63import {
63 createInitialDesktopUpdateState,64 createInitialDesktopUpdateState,
...@@ -190,7 +191,7 @@ let desktopLogSink: RotatingFileSink | null = null;...@@ -190,7 +191,7 @@ let desktopLogSink: RotatingFileSink | null = null;
190let backendLogSink: RotatingFileSink | null = null;191let backendLogSink: RotatingFileSink | null = null;
191let restoreStdIoCapture: (() => void) | null = null;192let restoreStdIoCapture: (() => void) | null = null;
192let backendObservabilitySettings = readPersistedBackendObservabilitySettings();193let backendObservabilitySettings = readPersistedBackendObservabilitySettings();
193let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH);194let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH, app.getVersion());
194let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode;195let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode;
195196
196let destructiveMenuIconCache: Electron.NativeImage | null | undefined;197let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
...@@ -1140,7 +1141,7 @@ function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void {...@@ -1140,7 +1141,7 @@ function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void {
1140 autoUpdater.allowPrerelease = channel === "nightly";1141 autoUpdater.allowPrerelease = channel === "nightly";
1141 autoUpdater.allowDowngrade = channel === "nightly";1142 autoUpdater.allowDowngrade = channel === "nightly";
1142 console.info(1143 console.info(
1143 `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}).`,1144 `[desktop-updater] Using update channel '${channel}' (allowPrerelease=${channel === "nightly"}, allowDowngrade=${channel === "nightly"}).`,
1144 );1145 );
1145}1146}
11461147
...@@ -1285,6 +1286,15 @@ function configureAutoUpdater(): void {...@@ -1285,6 +1286,15 @@ function configureAutoUpdater(): void {
1285 console.info("[desktop-updater] Looking for updates...");1286 console.info("[desktop-updater] Looking for updates...");
1286 });1287 });
1287 autoUpdater.on("update-available", (info) => {1288 autoUpdater.on("update-available", (info) => {
1289 if (!doesVersionMatchDesktopUpdateChannel(info.version, updateState.channel)) {
1290 console.info(
1291 `[desktop-updater] Ignoring ${info.version} because it does not match the selected '${updateState.channel}' channel.`,
1292 );
1293 setUpdateState(reduceDesktopUpdateStateOnNoUpdate(updateState, new Date().toISOString()));
1294 lastLoggedDownloadMilestone = -1;
1295 return;
1296 }
1297
1288 setUpdateState(1298 setUpdateState(
1289 reduceDesktopUpdateStateOnUpdateAvailable(1299 reduceDesktopUpdateStateOnUpdateAvailable(
1290 updateState,1300 updateState,
...@@ -1792,13 +1802,14 @@ function registerIpcHandlers(): void {...@@ -1792,13 +1802,14 @@ function registerIpcHandlers(): void {
1792 }1802 }
17931803
1794 const nextChannel = rawChannel as DesktopUpdateChannel;1804 const nextChannel = rawChannel as DesktopUpdateChannel;
1795 if (nextChannel === desktopSettings.updateChannel) {
1796 return updateState;
1797 }
17981805
1799 desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel);1806 desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel);
1800 writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);1807 writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);
18011808
1809 if (nextChannel === updateState.channel) {
1810 return updateState;
1811 }
1812
1802 const enabled = shouldEnableAutoUpdates();1813 const enabled = shouldEnableAutoUpdates();
1803 setUpdateState(createBaseUpdateState(nextChannel, enabled));1814 setUpdateState(createBaseUpdateState(nextChannel, enabled));
18041815
apps/desktop/src/updateChannels.test.ts created+41
...@@ -0,0 +1,41 @@
1import { describe, expect, it } from "vitest";
2
3import {
4 doesVersionMatchDesktopUpdateChannel,
5 isNightlyDesktopVersion,
6 resolveDefaultDesktopUpdateChannel,
7} from "./updateChannels";
8
9describe("isNightlyDesktopVersion", () => {
10 it("detects packaged nightly versions", () => {
11 expect(isNightlyDesktopVersion("0.0.17-nightly.20260415.1")).toBe(true);
12 });
13
14 it("does not flag stable versions as nightly", () => {
15 expect(isNightlyDesktopVersion("0.0.17")).toBe(false);
16 });
17});
18
19describe("resolveDefaultDesktopUpdateChannel", () => {
20 it("defaults stable builds to latest", () => {
21 expect(resolveDefaultDesktopUpdateChannel("0.0.17")).toBe("latest");
22 });
23
24 it("defaults nightly builds to nightly", () => {
25 expect(resolveDefaultDesktopUpdateChannel("0.0.17-nightly.20260415.1")).toBe("nightly");
26 });
27});
28
29describe("doesVersionMatchDesktopUpdateChannel", () => {
30 it("accepts nightly releases on the nightly channel", () => {
31 expect(doesVersionMatchDesktopUpdateChannel("0.0.17-nightly.20260416.1", "nightly")).toBe(true);
32 });
33
34 it("rejects stable releases on the nightly channel", () => {
35 expect(doesVersionMatchDesktopUpdateChannel("0.0.17", "nightly")).toBe(false);
36 });
37
38 it("rejects nightly releases on the stable channel", () => {
39 expect(doesVersionMatchDesktopUpdateChannel("0.0.17-nightly.20260416.1", "latest")).toBe(false);
40 });
41});
apps/desktop/src/updateChannels.ts created+18
...@@ -0,0 +1,18 @@
1import type { DesktopUpdateChannel } from "@t3tools/contracts";
2
3const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/;
4
5export function isNightlyDesktopVersion(version: string): boolean {
6 return NIGHTLY_VERSION_PATTERN.test(version);
7}
8
9export function resolveDefaultDesktopUpdateChannel(appVersion: string): DesktopUpdateChannel {
10 return isNightlyDesktopVersion(appVersion) ? "nightly" : "latest";
11}
12
13export function doesVersionMatchDesktopUpdateChannel(
14 version: string,
15 channel: DesktopUpdateChannel,
16): boolean {
17 return resolveDefaultDesktopUpdateChannel(version) === channel;
18}
docs/release.md+1-1
...@@ -37,7 +37,7 @@ This document covers the unified release workflow for stable and nightly desktop...@@ -37,7 +37,7 @@ This document covers the unified release workflow for stable and nightly desktop
37 - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>`37 - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>`
38 - release name includes the short commit SHA38 - release name includes the short commit SHA
39 - `make_latest` is always `false`39 - `make_latest` is always `false`
40- Uses the current `apps/desktop/package.json` semver core (`X.Y.Z`) as the nightly base, then appends a nightly prerelease suffix.40- Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`.
41- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.41- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.
42- Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version.42- Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version.
43- Does not commit version bumps back to `main`.43- Does not commit version bumps back to `main`.
scripts/release-smoke.ts+3-3
...@@ -127,17 +127,17 @@ try {...@@ -127,17 +127,17 @@ try {
127 );127 );
128 assertContains(128 assertContains(
129 nightlyReleaseMetadata,129 nightlyReleaseMetadata,
130 "version=9.9.9-nightly.20260413.321",130 "version=9.9.10-nightly.20260413.321",
131 "Expected nightly metadata to contain the derived nightly version.",131 "Expected nightly metadata to contain the derived nightly version.",
132 );132 );
133 assertContains(133 assertContains(
134 nightlyReleaseMetadata,134 nightlyReleaseMetadata,
135 "tag=nightly-v9.9.9-nightly.20260413.321",135 "tag=nightly-v9.9.10-nightly.20260413.321",
136 "Expected nightly metadata to contain the derived nightly tag.",136 "Expected nightly metadata to contain the derived nightly tag.",
137 );137 );
138 assertContains(138 assertContains(
139 nightlyReleaseMetadata,139 nightlyReleaseMetadata,
140 "name=T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)",140 "name=T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)",
141 "Expected nightly metadata to include the short commit SHA in the release name.",141 "Expected nightly metadata to include the short commit SHA in the release name.",
142 );142 );
143143
scripts/resolve-nightly-release.test.ts+12-5
...@@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest";...@@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest";
3import {3import {
4 resolveNightlyBaseVersion,4 resolveNightlyBaseVersion,
5 resolveNightlyReleaseMetadata,5 resolveNightlyReleaseMetadata,
6 resolveNightlyTargetVersion,
6} from "./resolve-nightly-release.ts";7} from "./resolve-nightly-release.ts";
78
8it("strips prerelease and build metadata when deriving the nightly base version", () => {9it("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"...@@ -11,14 +12,20 @@ it("strips prerelease and build metadata when deriving the nightly base version"
11 assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3");12 assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3");
12});13});
1314
15it("bumps the patch version before deriving nightly prerelease versions", () => {
16 assert.equal(resolveNightlyTargetVersion("0.0.17"), "0.0.18");
17 assert.equal(resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10");
18 assert.equal(resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4");
19});
20
14it("derives nightly metadata including the short commit sha in the release name", () => {21it("derives nightly metadata including the short commit sha in the release name", () => {
15 assert.deepStrictEqual(22 assert.deepStrictEqual(
16 resolveNightlyReleaseMetadata("9.9.9", "20260413", 321, "abcdef1234567890"),23 resolveNightlyReleaseMetadata("9.9.10", "20260413", 321, "abcdef1234567890"),
17 {24 {
18 baseVersion: "9.9.9",25 baseVersion: "9.9.10",
19 version: "9.9.9-nightly.20260413.321",26 version: "9.9.10-nightly.20260413.321",
20 tag: "nightly-v9.9.9-nightly.20260413.321",27 tag: "nightly-v9.9.10-nightly.20260413.321",
21 name: "T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)",28 name: "T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)",
22 shortSha: "abcdef123456",29 shortSha: "abcdef123456",
23 },30 },
24 );31 );
scripts/resolve-nightly-release.ts+12-1
...@@ -32,6 +32,17 @@ const decodeDesktopPackageJson = Schema.decodeUnknownEffect(...@@ -32,6 +32,17 @@ const decodeDesktopPackageJson = Schema.decodeUnknownEffect(
3232
33export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, "");33export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, "");
3434
35export const resolveNightlyTargetVersion = (version: string) => {
36 const stableCore = resolveNightlyBaseVersion(version);
37 const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore);
38 if (!match) {
39 throw new Error(`Invalid desktop package version '${version}'.`);
40 }
41
42 const [, major, minor, patch] = match;
43 return `${major}.${minor}.${Number(patch) + 1}`;
44};
45
35export const resolveNightlyReleaseMetadata = (46export const resolveNightlyReleaseMetadata = (
36 baseVersion: string,47 baseVersion: string,
37 date: string,48 date: string,
...@@ -59,7 +70,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* (...@@ -59,7 +70,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* (
59 const packageJson = yield* fs70 const packageJson = yield* fs
60 .readFileString(packageJsonPath)71 .readFileString(packageJsonPath)
61 .pipe(Effect.flatMap(decodeDesktopPackageJson));72 .pipe(Effect.flatMap(decodeDesktopPackageJson));
62 return resolveNightlyBaseVersion(packageJson.version);73 return resolveNightlyTargetVersion(packageJson.version);
63});74});
6475
65const writeOutput = Effect.fn("writeOutput")(function* (76const writeOutput = Effect.fn("writeOutput")(function* (