| author | |
| committer | |
| log | f9580ff08895380147181e1a771e6bb328dac439 |
| tree | 4f7305d88b3447aea4d98882d0ac3370d054fd15 |
| parent | 5e1dd56d2c534d90ba3363f97b1e1e23412507c0 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 1 | import type { DesktopAppBranding, DesktopAppStageLabel } from "@t3tools/contracts"; |
| 2 | 2 | |
| 3 | import { isNightlyDesktopVersion } from "./updateChannels"; | |
| 4 | ||
| 3 | 5 | const APP_BASE_NAME = "T3 Code"; |
| 4 | const NIGHTLY_VERSION_PATTERN = /-nightly\.\d{8}\.\d+$/; | |
| 5 | 6 | |
| 6 | 7 | export function resolveDesktopAppStageLabel(input: { |
| 7 | 8 | readonly isDevelopment: boolean; |
| ... | ... | @@ -11,7 +12,7 @@ export function resolveDesktopAppStageLabel(input: { |
| 11 | 12 | return "Dev"; |
| 12 | 13 | } |
| 13 | 14 | |
| 14 | return NIGHTLY_VERSION_PATTERN.test(input.appVersion) ? "Nightly" : "Alpha"; | |
| 15 | return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha"; | |
| 15 | 16 | } |
| 16 | 17 | |
| 17 | 18 | export function resolveDesktopAppBranding(input: { |
apps/desktop/src/desktopSettings.test.ts+66-3| ... | ... | @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; |
| 7 | 7 | import { |
| 8 | 8 | DEFAULT_DESKTOP_SETTINGS, |
| 9 | 9 | readDesktopSettings, |
| 10 | resolveDefaultDesktopSettings, | |
| 10 | 11 | setDesktopServerExposurePreference, |
| 11 | 12 | setDesktopUpdateChannelPreference, |
| 12 | 13 | writeDesktopSettings, |
| ... | ... | @@ -28,7 +29,15 @@ function makeSettingsPath() { |
| 28 | 29 | |
| 29 | 30 | describe("desktopSettings", () => { |
| 30 | 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 | }); |
| 33 | 42 | |
| 34 | 43 | it("persists and reloads the configured server exposure mode", () => { |
| ... | ... | @@ -37,11 +46,13 @@ describe("desktopSettings", () => { |
| 37 | 46 | writeDesktopSettings(settingsPath, { |
| 38 | 47 | serverExposureMode: "network-accessible", |
| 39 | 48 | updateChannel: "latest", |
| 49 | updateChannelConfiguredByUser: true, | |
| 40 | 50 | }); |
| 41 | 51 | |
| 42 | expect(readDesktopSettings(settingsPath)).toEqual({ | |
| 52 | expect(readDesktopSettings(settingsPath, "0.0.17")).toEqual({ | |
| 43 | 53 | serverExposureMode: "network-accessible", |
| 44 | 54 | updateChannel: "latest", |
| 55 | updateChannelConfiguredByUser: true, | |
| 45 | 56 | }); |
| 46 | 57 | }); |
| 47 | 58 | |
| ... | ... | @@ -51,12 +62,14 @@ describe("desktopSettings", () => { |
| 51 | 62 | { |
| 52 | 63 | serverExposureMode: "local-only", |
| 53 | 64 | updateChannel: "latest", |
| 65 | updateChannelConfiguredByUser: false, | |
| 54 | 66 | }, |
| 55 | 67 | "network-accessible", |
| 56 | 68 | ), |
| 57 | 69 | ).toEqual({ |
| 58 | 70 | serverExposureMode: "network-accessible", |
| 59 | 71 | updateChannel: "latest", |
| 72 | updateChannelConfiguredByUser: false, | |
| 60 | 73 | }); |
| 61 | 74 | }); |
| 62 | 75 | |
| ... | ... | @@ -66,12 +79,14 @@ describe("desktopSettings", () => { |
| 66 | 79 | { |
| 67 | 80 | serverExposureMode: "local-only", |
| 68 | 81 | updateChannel: "latest", |
| 82 | updateChannelConfiguredByUser: false, | |
| 69 | 83 | }, |
| 70 | 84 | "nightly", |
| 71 | 85 | ), |
| 72 | 86 | ).toEqual({ |
| 73 | 87 | serverExposureMode: "local-only", |
| 74 | 88 | updateChannel: "nightly", |
| 89 | updateChannelConfiguredByUser: true, | |
| 75 | 90 | }); |
| 76 | 91 | }); |
| 77 | 92 | |
| ... | ... | @@ -79,6 +94,54 @@ describe("desktopSettings", () => { |
| 79 | 94 | const settingsPath = makeSettingsPath(); |
| 80 | 95 | fs.writeFileSync(settingsPath, "{not-json", "utf8"); |
| 81 | 96 | |
| 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 | 2 | import * as Path from "node:path"; |
| 3 | 3 | import type { DesktopServerExposureMode, DesktopUpdateChannel } from "@t3tools/contracts"; |
| 4 | 4 | |
| 5 | import { resolveDefaultDesktopUpdateChannel } from "./updateChannels"; | |
| 6 | ||
| 5 | 7 | export interface DesktopSettings { |
| 6 | 8 | readonly serverExposureMode: DesktopServerExposureMode; |
| 7 | 9 | readonly updateChannel: DesktopUpdateChannel; |
| 10 | readonly updateChannelConfiguredByUser: boolean; | |
| 8 | 11 | } |
| 9 | 12 | |
| 10 | 13 | export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { |
| 11 | 14 | serverExposureMode: "local-only", |
| 12 | 15 | updateChannel: "latest", |
| 16 | updateChannelConfiguredByUser: false, | |
| 13 | 17 | }; |
| 14 | 18 | |
| 19 | export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings { | |
| 20 | return { | |
| 21 | ...DEFAULT_DESKTOP_SETTINGS, | |
| 22 | updateChannel: resolveDefaultDesktopUpdateChannel(appVersion), | |
| 23 | }; | |
| 24 | } | |
| 25 | ||
| 15 | 26 | export function setDesktopServerExposurePreference( |
| 16 | 27 | settings: DesktopSettings, |
| 17 | 28 | requestedMode: DesktopServerExposureMode, |
| ... | ... | @@ -28,33 +39,47 @@ export function setDesktopUpdateChannelPreference( |
| 28 | 39 | settings: DesktopSettings, |
| 29 | 40 | requestedChannel: DesktopUpdateChannel, |
| 30 | 41 | ): 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 | }; | |
| 37 | 47 | } |
| 38 | 48 | |
| 39 | export function readDesktopSettings(settingsPath: string): DesktopSettings { | |
| 49 | export function readDesktopSettings(settingsPath: string, appVersion: string): DesktopSettings { | |
| 50 | const defaultSettings = resolveDefaultDesktopSettings(appVersion); | |
| 51 | ||
| 40 | 52 | try { |
| 41 | 53 | if (!FS.existsSync(settingsPath)) { |
| 42 | return DEFAULT_DESKTOP_SETTINGS; | |
| 54 | return defaultSettings; | |
| 43 | 55 | } |
| 44 | 56 | |
| 45 | 57 | const raw = FS.readFileSync(settingsPath, "utf8"); |
| 46 | 58 | const parsed = JSON.parse(raw) as { |
| 47 | 59 | readonly serverExposureMode?: unknown; |
| 48 | 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"); | |
| 50 | 71 | |
| 51 | 72 | return { |
| 52 | 73 | serverExposureMode: |
| 53 | 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 | 81 | } catch { |
| 57 | return DEFAULT_DESKTOP_SETTINGS; | |
| 82 | return defaultSettings; | |
| 58 | 83 | } |
| 59 | 84 | } |
| 60 | 85 |
apps/desktop/src/main.ts+16-5| ... | ... | @@ -58,6 +58,7 @@ import { showDesktopConfirmDialog } from "./confirmDialog"; |
| 58 | 58 | import { resolveDesktopServerExposure } from "./serverExposure"; |
| 59 | 59 | import { syncShellEnvironment } from "./syncShellEnvironment"; |
| 60 | 60 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; |
| 61 | import { doesVersionMatchDesktopUpdateChannel } from "./updateChannels"; | |
| 61 | 62 | import { ServerListeningDetector } from "./serverListeningDetector"; |
| 62 | 63 | import { |
| 63 | 64 | createInitialDesktopUpdateState, |
| ... | ... | @@ -190,7 +191,7 @@ let desktopLogSink: RotatingFileSink | null = null; |
| 190 | 191 | let backendLogSink: RotatingFileSink | null = null; |
| 191 | 192 | let restoreStdIoCapture: (() => void) | null = null; |
| 192 | 193 | let backendObservabilitySettings = readPersistedBackendObservabilitySettings(); |
| 193 | let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH); | |
| 194 | let desktopSettings = readDesktopSettings(DESKTOP_SETTINGS_PATH, app.getVersion()); | |
| 194 | 195 | let desktopServerExposureMode: DesktopServerExposureMode = desktopSettings.serverExposureMode; |
| 195 | 196 | |
| 196 | 197 | let destructiveMenuIconCache: Electron.NativeImage | null | undefined; |
| ... | ... | @@ -1140,7 +1141,7 @@ function applyAutoUpdaterChannel(channel: DesktopUpdateChannel): void { |
| 1140 | 1141 | autoUpdater.allowPrerelease = channel === "nightly"; |
| 1141 | 1142 | autoUpdater.allowDowngrade = channel === "nightly"; |
| 1142 | 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 | } |
| 1146 | 1147 | |
| ... | ... | @@ -1285,6 +1286,15 @@ function configureAutoUpdater(): void { |
| 1285 | 1286 | console.info("[desktop-updater] Looking for updates..."); |
| 1286 | 1287 | }); |
| 1287 | 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 | 1298 | setUpdateState( |
| 1289 | 1299 | reduceDesktopUpdateStateOnUpdateAvailable( |
| 1290 | 1300 | updateState, |
| ... | ... | @@ -1792,13 +1802,14 @@ function registerIpcHandlers(): void { |
| 1792 | 1802 | } |
| 1793 | 1803 | |
| 1794 | 1804 | const nextChannel = rawChannel as DesktopUpdateChannel; |
| 1795 | if (nextChannel === desktopSettings.updateChannel) { | |
| 1796 | return updateState; | |
| 1797 | } | |
| 1798 | 1805 | |
| 1799 | 1806 | desktopSettings = setDesktopUpdateChannelPreference(desktopSettings, nextChannel); |
| 1800 | 1807 | writeDesktopSettings(DESKTOP_SETTINGS_PATH, desktopSettings); |
| 1801 | 1808 | |
| 1809 | if (nextChannel === updateState.channel) { | |
| 1810 | return updateState; | |
| 1811 | } | |
| 1812 | ||
| 1802 | 1813 | const enabled = shouldEnableAutoUpdates(); |
| 1803 | 1814 | setUpdateState(createBaseUpdateState(nextChannel, enabled)); |
| 1804 | 1815 |
apps/desktop/src/updateChannels.test.ts created+41| ... | ... | @@ -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 created+18| ... | ... | @@ -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+1-1| ... | ... | @@ -37,7 +37,7 @@ This document covers the unified release workflow for stable and nightly desktop |
| 37 | 37 | - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.<run_number>` |
| 38 | 38 | - release name includes the short commit SHA |
| 39 | 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 | 41 | - Publishes Electron auto-update metadata to the dedicated `nightly` updater channel, so desktop users can opt into that track independently from stable. |
| 42 | 42 | - Publishes the CLI package (`apps/server`, npm package `t3`) to the `nightly` npm dist-tag using the same nightly version. |
| 43 | 43 | - Does not commit version bumps back to `main`. |
scripts/release-smoke.ts+3-3| ... | ... | @@ -127,17 +127,17 @@ try { |
| 127 | 127 | ); |
| 128 | 128 | assertContains( |
| 129 | 129 | nightlyReleaseMetadata, |
| 130 | "version=9.9.9-nightly.20260413.321", | |
| 130 | "version=9.9.10-nightly.20260413.321", | |
| 131 | 131 | "Expected nightly metadata to contain the derived nightly version.", |
| 132 | 132 | ); |
| 133 | 133 | assertContains( |
| 134 | 134 | nightlyReleaseMetadata, |
| 135 | "tag=nightly-v9.9.9-nightly.20260413.321", | |
| 135 | "tag=nightly-v9.9.10-nightly.20260413.321", | |
| 136 | 136 | "Expected nightly metadata to contain the derived nightly tag.", |
| 137 | 137 | ); |
| 138 | 138 | assertContains( |
| 139 | 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 | 141 | "Expected nightly metadata to include the short commit SHA in the release name.", |
| 142 | 142 | ); |
| 143 | 143 |
scripts/resolve-nightly-release.test.ts+12-5| ... | ... | @@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest"; |
| 3 | 3 | import { |
| 4 | 4 | resolveNightlyBaseVersion, |
| 5 | 5 | resolveNightlyReleaseMetadata, |
| 6 | resolveNightlyTargetVersion, | |
| 6 | 7 | } from "./resolve-nightly-release.ts"; |
| 7 | 8 | |
| 8 | 9 | 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 | 12 | assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); |
| 12 | 13 | }); |
| 13 | 14 | |
| 15 | it("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 | ||
| 14 | 21 | it("derives nightly metadata including the short commit sha in the release name", () => { |
| 15 | 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", | |
| 19 | version: "9.9.9-nightly.20260413.321", | |
| 20 | tag: "nightly-v9.9.9-nightly.20260413.321", | |
| 21 | name: "T3 Code Nightly 9.9.9-nightly.20260413.321 (abcdef123456)", | |
| 25 | baseVersion: "9.9.10", | |
| 26 | version: "9.9.10-nightly.20260413.321", | |
| 27 | tag: "nightly-v9.9.10-nightly.20260413.321", | |
| 28 | name: "T3 Code Nightly 9.9.10-nightly.20260413.321 (abcdef123456)", | |
| 22 | 29 | shortSha: "abcdef123456", |
| 23 | 30 | }, |
| 24 | 31 | ); |
scripts/resolve-nightly-release.ts+12-1| ... | ... | @@ -32,6 +32,17 @@ const decodeDesktopPackageJson = Schema.decodeUnknownEffect( |
| 32 | 32 | |
| 33 | 33 | export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, ""); |
| 34 | 34 | |
| 35 | export 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 | ||
| 35 | 46 | export const resolveNightlyReleaseMetadata = ( |
| 36 | 47 | baseVersion: string, |
| 37 | 48 | date: string, |
| ... | ... | @@ -59,7 +70,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( |
| 59 | 70 | const packageJson = yield* fs |
| 60 | 71 | .readFileString(packageJsonPath) |
| 61 | 72 | .pipe(Effect.flatMap(decodeDesktopPackageJson)); |
| 62 | return resolveNightlyBaseVersion(packageJson.version); | |
| 73 | return resolveNightlyTargetVersion(packageJson.version); | |
| 63 | 74 | }); |
| 64 | 75 | |
| 65 | 76 | const writeOutput = Effect.fn("writeOutput")(function* ( |