From d9475507ca88cb4e8de40c017ad8f3bb981122ff Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 05:58:42 +0200 Subject: [PATCH 1/2] Resolve desktop artifact version from HEAD semver tags --- scripts/build-desktop-artifact.test.ts | 23 ++++++++++++ scripts/build-desktop-artifact.ts | 51 +++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 86452693d09..6ec24fc4028 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest"; import { ConfigProvider, Effect, Option } from "effect"; import { + resolveDesktopArtifactVersion, resolveBuildOptions, resolveDesktopBuildIconAssets, resolveDesktopProductName, @@ -23,6 +24,28 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)"); }); + it("resolves explicit desktop artifact versions before HEAD tags", () => { + assert.equal(resolveDesktopArtifactVersion("9.9.9", ["v1.2.3"], "0.0.22"), "9.9.9"); + }); + + it("falls back to the server package version when HEAD has no exact semver tags", () => { + assert.equal( + resolveDesktopArtifactVersion(undefined, ["release-1.2.3", "1.2", "1.2.3-dev+"], "0.0.22"), + "0.0.22", + ); + }); + + it("strips a leading v from exact semver tags on HEAD", () => { + assert.equal(resolveDesktopArtifactVersion(undefined, ["v1.2.3"], "0.0.22"), "1.2.3"); + }); + + it("resolves multiple HEAD semver tags deterministically", () => { + assert.equal( + resolveDesktopArtifactVersion(undefined, ["v1.2.4", "1.2.3", "not-a-version"], "0.0.22"), + "1.2.4", + ); + }); + it("switches desktop packaging icons to the nightly artwork for nightly versions", () => { assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17"), { macIconPng: BRAND_ASSET_PATHS.productionMacIconPng, diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 74e8bed0cb8..ec2e41a658c 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -150,6 +150,31 @@ const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRo return hash.toLowerCase(); }); +const resolveGitHeadTags = Effect.fn("resolveGitHeadTags")(function* (repoRoot: string) { + const result = yield* spawnAndCollectOutput( + ChildProcess.make("git", ["tag", "--points-at", "HEAD"], { + cwd: repoRoot, + }), + ).pipe( + Effect.catch(() => + Effect.succeed({ + stdout: "", + stderr: "", + exitCode: 1, + }), + ), + ); + + if (result.exitCode !== 0) { + return []; + } + + return result.stdout + .split(/\r?\n/) + .map((tag) => tag.trim()) + .filter((tag) => tag.length > 0); +}); + const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -532,6 +557,26 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest"; } +const ExactSemverTagPattern = + /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + +export function resolveDesktopArtifactVersion( + explicitVersion: string | undefined, + headTags: ReadonlyArray, + fallbackVersion: string, +): string { + if (explicitVersion) { + return explicitVersion; + } + + const headSemverVersions = headTags + .filter((tag) => ExactSemverTagPattern.test(tag)) + .map((tag) => tag.replace(/^v/, "")) + .toSorted((a, b) => a.localeCompare(b, "en")); + + return headSemverVersions.at(-1) ?? fallbackVersion; +} + export function resolveDesktopBuildIconAssets(version: string): DesktopBuildIconAssets { if (resolveDesktopUpdateChannel(version) === "nightly") { return { @@ -710,7 +755,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( }), }); - const appVersion = options.version ?? serverPackageJson.version; + const appVersion = resolveDesktopArtifactVersion( + options.version, + yield* resolveGitHeadTags(repoRoot), + serverPackageJson.version, + ); const iconAssets = resolveDesktopBuildIconAssets(appVersion); const commitHash = yield* resolveGitCommitHash(repoRoot); const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped; From 314437c5268e3b49bd848331155ac9456d53b319 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 06:07:07 +0200 Subject: [PATCH 2/2] Resolve desktop artifact version from HEAD semver tags --- scripts/build-desktop-artifact.test.ts | 7 +++ scripts/build-desktop-artifact.ts | 60 +++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 6ec24fc4028..3efd48081e3 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -46,6 +46,13 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { ); }); + it("orders HEAD semver tags by semver precedence instead of lexicographic order", () => { + assert.equal( + resolveDesktopArtifactVersion(undefined, ["v1.2.0", "v1.10.0"], "0.0.22"), + "1.10.0", + ); + }); + it("switches desktop packaging icons to the nightly artwork for nightly versions", () => { assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17"), { macIconPng: BRAND_ASSET_PATHS.productionMacIconPng, diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index ec2e41a658c..ad7fe4704c6 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -558,7 +558,63 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl } const ExactSemverTagPattern = - /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + +interface ParsedSemver { + readonly major: number; + readonly minor: number; + readonly patch: number; + readonly prerelease: ReadonlyArray; +} + +function parseExactSemver(version: string): ParsedSemver { + const [, major = "0", minor = "0", patch = "0", prerelease = ""] = + ExactSemverTagPattern.exec(version) ?? []; + + return { + major: Number(major), + minor: Number(minor), + patch: Number(patch), + prerelease: prerelease ? prerelease.split(".") : [], + }; +} + +function comparePrereleaseIdentifier(a: string, b: string): number { + const aNumeric = /^\d+$/.test(a); + const bNumeric = /^\d+$/.test(b); + + if (aNumeric && bNumeric) { + return Number(a) - Number(b); + } + if (aNumeric) return -1; + if (bNumeric) return 1; + + return a.localeCompare(b, "en"); +} + +function compareSemverPrecedence(a: string, b: string): number { + const left = parseExactSemver(a); + const right = parseExactSemver(b); + + const releaseDifference = + left.major - right.major || left.minor - right.minor || left.patch - right.patch; + if (releaseDifference !== 0) return releaseDifference; + + if (left.prerelease.length === 0 && right.prerelease.length > 0) return 1; + if (left.prerelease.length > 0 && right.prerelease.length === 0) return -1; + + for (const [index, leftIdentifier] of left.prerelease.entries()) { + const rightIdentifier = right.prerelease[index]; + if (rightIdentifier === undefined) return 1; + + const difference = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier); + if (difference !== 0) return difference; + } + + if (right.prerelease.length > left.prerelease.length) return -1; + + return a.localeCompare(b, "en"); +} export function resolveDesktopArtifactVersion( explicitVersion: string | undefined, @@ -572,7 +628,7 @@ export function resolveDesktopArtifactVersion( const headSemverVersions = headTags .filter((tag) => ExactSemverTagPattern.test(tag)) .map((tag) => tag.replace(/^v/, "")) - .toSorted((a, b) => a.localeCompare(b, "en")); + .toSorted(compareSemverPrecedence); return headSemverVersions.at(-1) ?? fallbackVersion; }