Skip to content

Commit f9580ff

Browse files
juliusmarmingecodexcursoragent
authored
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>
1 parent 5e1dd56 commit f9580ff

10 files changed

Lines changed: 207 additions & 30 deletions

apps/desktop/src/appBranding.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts";
22

3+
import { isNightlyDesktopVersion } from "./updateChannels";
4+
35
const APP_BASE_NAME = "T3 Code";
4-
const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/;
56

67
export function resolveDesktopAppStageLabel(input: {
78
readonly isDevelopment: boolean;
@@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: {
1112
return "Dev";
1213
}
1314

14-
return NIGHTLY_VERSION_PATTERN.test(input.appVersion) ? "Nightly" : "Alpha";
15+
return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha";
1516
}
1617

1718
export function resolveDesktopAppBranding(input: {

apps/desktop/src/desktopSettings.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";
77
import {
88
DEFAULT_DESKTOP_SETTINGS,
99
readDesktopSettings,
10+
resolveDefaultDesktopSettings,
1011
setDesktopServerExposurePreference,
1112
setDesktopUpdateChannelPreference,
1213
writeDesktopSettings,
@@ -28,7 +29,15 @@ function makeSettingsPath() {
2829

2930
describe("desktopSettings", () => {
3031
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+
});
3241
});
3342

3443
it("persists and reloads the configured server exposure mode", () => {
@@ -37,11 +46,13 @@ describe("desktopSettings", () => {
3746
writeDesktopSettings(settingsPath, {
3847
serverExposureMode: "network-accessible",
3948
updateChannel: "latest",
49+
updateChannelConfiguredByUser: true,
4050
});
4151

42-
expect(readDesktopSettings(settingsPath)).toEqual({
52+
expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual({
4353
serverExposureMode: "network-accessible",
4454
updateChannel: "latest",
55+
updateChannelConfiguredByUser: true,
4556
});
4657
});
4758

@@ -51,12 +62,14 @@ describe("desktopSettings", () => {
5162
{
5263
serverExposureMode: "local-only",
5364
updateChannel: "latest",
65+
updateChannelConfiguredByUser: false,
5466
},
5567
"network-accessible",
5668
),
5769
).toEqual({
5870
serverExposureMode: "network-accessible",
5971
updateChannel: "latest",
72+
updateChannelConfiguredByUser: false,
6073
});
6174
});
6275

@@ -66,19 +79,69 @@ describe("desktopSettings", () => {
6679
{
6780
serverExposureMode: "local-only",
6881
updateChannel: "latest",
82+
updateChannelConfiguredByUser: false,
6983
},
7084
"nightly",
7185
),
7286
).toEqual({
7387
serverExposureMode: "local-only",
7488
updateChannel: "nightly",
89+
updateChannelConfiguredByUser: true,
7590
});
7691
});
7792

7893
it("falls back to defaults when the settings file is malformed", () => {
7994
const settingsPath = makeSettingsPath();
8095
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+
});
83146
});
84147
});

apps/desktop/src/desktopSettings.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@ import * as FS from "node:fs";
22
import * as Path from "node:path";
33
import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts";
44

5+
import { resolveDefaultDesktopUpdateChannel } from "./updateChannels";
6+
57
export interface DesktopSettings {
68
readonly serverExposureMode: DesktopServerExposureMode;
79
readonly updateChannel: DesktopUpdateChannel;
10+
readonly updateChannelConfiguredByUser: boolean;
811
}
912

1013
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
1114
serverExposureMode: "local-only",
1215
updateChannel: "latest",
16+
updateChannelConfiguredByUser: false,
1317
};
1418

19+
export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings {
20+
return {
21+
...DEFAULT_DESKTOP_SETTINGS,
22+
updateChannel: resolveDefaultDesktopUpdateChannel(appVersion),
23+
};
24+
}
25+
1526
export function setDesktopServerExposurePreference(
1627
settings: DesktopSettings,
1728
requestedMode: DesktopServerExposureMode,
@@ -28,33 +39,47 @@ export function setDesktopUpdateChannelPreference(
2839
settings: DesktopSettings,
2940
requestedChannel: DesktopUpdateChannel,
3041
): DesktopSettings {
31-
return settings.updateChannel === requestedChannel
32-
? settings
33-
: {
34-
...settings,
35-
updateChannel: requestedChannel,
36-
};
42+
return {
43+
...settings,
44+
updateChannel: requestedChannel,
45+
updateChannelConfiguredByUser: true,
46+
};
3747
}
3848

39-
export function readDesktopSettings(settingsPath: string): DesktopSettings {
49+
export function readDesktopSettings(settingsPath: string, appVersion: string): DesktopSettings {
50+
const defaultSettings = resolveDefaultDesktopSettings(appVersion);
51+
4052
try {
4153
if (!FS.existsSync(settingsPath)) {
42-
return DEFAULT_DESKTOP_SETTINGS;
54+
return defaultSettings;
4355
}
4456

4557
const raw = FS.readFileSync(settingsPath, "utf8");
4658
const parsed = JSON.parse(raw) as {
4759
readonly serverExposureMode?: unknown;
4860
readonly updateChannel?: unknown;
61+
readonly updateChannelConfiguredByUser?: unknown;
4962
};
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

5172
return {
5273
serverExposureMode:
5374
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,
5580
};
5681
} catch {
57-
return DEFAULT_DESKTOP_SETTINGS;
82+
return defaultSettings;
5883
}
5984
}
6085

apps/desktop/src/main.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog";
5858
import { resolveDesktopServerExposure } from "./serverExposure";
5959
import { syncShellEnvironment } from "./syncShellEnvironment";
6060
import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
61+
import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels";
6162
import { ServerListeningDetector } from "./serverListeningDetector";
6263
import {
6364
createInitialDesktopUpdateState,
@@ -190,7 +191,7 @@ let desktopLogSink: RotatingFileSink | null = null;
190191
let backendLogSink: RotatingFileSink | null = null;
191192
let restoreStdIoCapture: (() => void) | null = null;
192193
let backendObservabilitySettings = readPersistedBackendObservabilitySettings();
193-
let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH);
194+
let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH, app.getVersion());
194195
let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode;
195196

196197
let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
@@ -1140,7 +1141,7 @@ function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void {
11401141
autoUpdater.allowPrerelease = channel === "nightly";
11411142
autoUpdater.allowDowngrade = channel === "nightly";
11421143
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"}).`,
11441145
);
11451146
}
11461147

@@ -1285,6 +1286,15 @@ function configureAutoUpdater(): void {
12851286
console.info("[desktop-updater] Looking for updates...");
12861287
});
12871288
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+
12881298
setUpdateState(
12891299
reduceDesktopUpdateStateOnUpdateAvailable(
12901300
updateState,
@@ -1792,13 +1802,14 @@ function registerIpcHandlers(): void {
17921802
}
17931803

17941804
const nextChannel = rawChannel as DesktopUpdateChannel;
1795-
if (nextChannel === desktopSettings.updateChannel) {
1796-
return updateState;
1797-
}
17981805

17991806
desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel);
18001807
writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings);
18011808

1809+
if (nextChannel === updateState.channel) {
1810+
return updateState;
1811+
}
1812+
18021813
const enabled = shouldEnableAutoUpdates();
18031814
setUpdateState(createBaseUpdateState(nextChannel, enabled));
18041815

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
doesVersionMatchDesktopUpdateChannel,
5+
isNightlyDesktopVersion,
6+
resolveDefaultDesktopUpdateChannel,
7+
} from "./updateChannels";
8+
9+
describe("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+
19+
describe("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+
29+
describe("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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { DesktopUpdateChannel } from "@t3tools/contracts";
2+
3+
const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/;
4+
5+
export function isNightlyDesktopVersion(version: string): boolean {
6+
return NIGHTLY_VERSION_PATTERN.test(version);
7+
}
8+
9+
export function resolveDefaultDesktopUpdateChannel(appVersion: string): DesktopUpdateChannel {
10+
return isNightlyDesktopVersion(appVersion) ? "nightly" : "latest";
11+
}
12+
13+
export function doesVersionMatchDesktopUpdateChannel(
14+
version: string,
15+
channel: DesktopUpdateChannel,
16+
): boolean {
17+
return resolveDefaultDesktopUpdateChannel(version) === channel;
18+
}

docs/release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ This document covers the unified release workflow for stable and nightly desktop
3737
- tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>`
3838
- release name includes the short commit SHA
3939
- `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.*`.
4141
- Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable.
4242
- Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version.
4343
- Does not commit version bumps back to `main`.

scripts/release-smoke.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,17 @@ try {
127127
);
128128
assertContains(
129129
nightlyReleaseMetadata,
130-
"version=9.9.9-nightly.20260413.321",
130+
"version=9.9.10-nightly.20260413.321",
131131
"Expected nightly metadata to contain the derived nightly version.",
132132
);
133133
assertContains(
134134
nightlyReleaseMetadata,
135-
"tag=nightly-v9.9.9-nightly.20260413.321",
135+
"tag=nightly-v9.9.10-nightly.20260413.321",
136136
"Expected nightly metadata to contain the derived nightly tag.",
137137
);
138138
assertContains(
139139
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)",
141141
"Expected nightly metadata to include the short commit SHA in the release name.",
142142
);
143143

0 commit comments

Comments
 (0)