Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest";
import { ConfigProvider, Effect, Option } from "effect";

import {
resolveDesktopArtifactVersion,
resolveBuildOptions,
resolveDesktopBuildIconAssets,
resolveDesktopProductName,
Expand All @@ -23,6 +24,35 @@ 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("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,
Expand Down
107 changes: 106 additions & 1 deletion scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -532,6 +557,82 @@ 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-]+)*)?$/;

interface ParsedSemver {
readonly major: number;
readonly minor: number;
readonly patch: number;
readonly prerelease: ReadonlyArray<string>;
}

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,
headTags: ReadonlyArray<string>,
fallbackVersion: string,
): string {
if (explicitVersion) {
return explicitVersion;
}

const headSemverVersions = headTags
.filter((tag) => ExactSemverTagPattern.test(tag))
.map((tag) => tag.replace(/^v/, ""))
.toSorted(compareSemverPrecedence);

return headSemverVersions.at(-1) ?? fallbackVersion;
}

export function resolveDesktopBuildIconAssets(version: string): DesktopBuildIconAssets {
if (resolveDesktopUpdateChannel(version) === "nightly") {
return {
Expand Down Expand Up @@ -710,7 +811,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;
Expand Down
Loading