Skip to content

Commit d79643c

Browse files
authored
Merge pull request #5 from torturado/clawsweeper/torturado-t3code-issue-3-tagged-artifact-version
Resolve desktop artifact version from HEAD semver tags
2 parents 35721d9 + 314437c commit d79643c

2 files changed

Lines changed: 136 additions & 1 deletion

File tree

scripts/build-desktop-artifact.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { assert, it } from "@effect/vitest";
33
import { ConfigProvider, Effect, Option } from "effect";
44

55
import {
6+
resolveDesktopArtifactVersion,
67
resolveBuildOptions,
78
resolveDesktopBuildIconAssets,
89
resolveDesktopProductName,
@@ -23,6 +24,35 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
2324
assert.equal(resolveDesktopProductName("0.0.17-nightly.20260413.42"), "T3 Code (Nightly)");
2425
});
2526

27+
it("resolves explicit desktop artifact versions before HEAD tags", () => {
28+
assert.equal(resolveDesktopArtifactVersion("9.9.9", ["v1.2.3"], "0.0.22"), "9.9.9");
29+
});
30+
31+
it("falls back to the server package version when HEAD has no exact semver tags", () => {
32+
assert.equal(
33+
resolveDesktopArtifactVersion(undefined, ["release-1.2.3", "1.2", "1.2.3-dev+"], "0.0.22"),
34+
"0.0.22",
35+
);
36+
});
37+
38+
it("strips a leading v from exact semver tags on HEAD", () => {
39+
assert.equal(resolveDesktopArtifactVersion(undefined, ["v1.2.3"], "0.0.22"), "1.2.3");
40+
});
41+
42+
it("resolves multiple HEAD semver tags deterministically", () => {
43+
assert.equal(
44+
resolveDesktopArtifactVersion(undefined, ["v1.2.4", "1.2.3", "not-a-version"], "0.0.22"),
45+
"1.2.4",
46+
);
47+
});
48+
49+
it("orders HEAD semver tags by semver precedence instead of lexicographic order", () => {
50+
assert.equal(
51+
resolveDesktopArtifactVersion(undefined, ["v1.2.0", "v1.10.0"], "0.0.22"),
52+
"1.10.0",
53+
);
54+
});
55+
2656
it("switches desktop packaging icons to the nightly artwork for nightly versions", () => {
2757
assert.deepStrictEqual(resolveDesktopBuildIconAssets("0.0.17"), {
2858
macIconPng: BRAND_ASSET_PATHS.productionMacIconPng,

scripts/build-desktop-artifact.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ const resolveGitCommitHash = Effect.fn("resolveGitCommitHash")(function* (repoRo
150150
return hash.toLowerCase();
151151
});
152152

153+
const resolveGitHeadTags = Effect.fn("resolveGitHeadTags")(function* (repoRoot: string) {
154+
const result = yield* spawnAndCollectOutput(
155+
ChildProcess.make("git", ["tag", "--points-at", "HEAD"], {
156+
cwd: repoRoot,
157+
}),
158+
).pipe(
159+
Effect.catch(() =>
160+
Effect.succeed({
161+
stdout: "",
162+
stderr: "",
163+
exitCode: 1,
164+
}),
165+
),
166+
);
167+
168+
if (result.exitCode !== 0) {
169+
return [];
170+
}
171+
172+
return result.stdout
173+
.split(/\r?\n/)
174+
.map((tag) => tag.trim())
175+
.filter((tag) => tag.length > 0);
176+
});
177+
153178
const resolvePythonForNodeGyp = Effect.fn("resolvePythonForNodeGyp")(function* () {
154179
const fs = yield* FileSystem.FileSystem;
155180
const path = yield* Path.Path;
@@ -532,6 +557,82 @@ export function resolveDesktopUpdateChannel(version: string): "latest" | "nightl
532557
return /-nightly\.\d{8}\.\d+$/.test(version) ? "nightly" : "latest";
533558
}
534559

560+
const ExactSemverTagPattern =
561+
/^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-]+)*)?$/;
562+
563+
interface ParsedSemver {
564+
readonly major: number;
565+
readonly minor: number;
566+
readonly patch: number;
567+
readonly prerelease: ReadonlyArray<string>;
568+
}
569+
570+
function parseExactSemver(version: string): ParsedSemver {
571+
const [, major = "0", minor = "0", patch = "0", prerelease = ""] =
572+
ExactSemverTagPattern.exec(version) ?? [];
573+
574+
return {
575+
major: Number(major),
576+
minor: Number(minor),
577+
patch: Number(patch),
578+
prerelease: prerelease ? prerelease.split(".") : [],
579+
};
580+
}
581+
582+
function comparePrereleaseIdentifier(a: string, b: string): number {
583+
const aNumeric = /^\d+$/.test(a);
584+
const bNumeric = /^\d+$/.test(b);
585+
586+
if (aNumeric && bNumeric) {
587+
return Number(a) - Number(b);
588+
}
589+
if (aNumeric) return -1;
590+
if (bNumeric) return 1;
591+
592+
return a.localeCompare(b, "en");
593+
}
594+
595+
function compareSemverPrecedence(a: string, b: string): number {
596+
const left = parseExactSemver(a);
597+
const right = parseExactSemver(b);
598+
599+
const releaseDifference =
600+
left.major - right.major || left.minor - right.minor || left.patch - right.patch;
601+
if (releaseDifference !== 0) return releaseDifference;
602+
603+
if (left.prerelease.length === 0 && right.prerelease.length > 0) return 1;
604+
if (left.prerelease.length > 0 && right.prerelease.length === 0) return -1;
605+
606+
for (const [index, leftIdentifier] of left.prerelease.entries()) {
607+
const rightIdentifier = right.prerelease[index];
608+
if (rightIdentifier === undefined) return 1;
609+
610+
const difference = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
611+
if (difference !== 0) return difference;
612+
}
613+
614+
if (right.prerelease.length > left.prerelease.length) return -1;
615+
616+
return a.localeCompare(b, "en");
617+
}
618+
619+
export function resolveDesktopArtifactVersion(
620+
explicitVersion: string | undefined,
621+
headTags: ReadonlyArray<string>,
622+
fallbackVersion: string,
623+
): string {
624+
if (explicitVersion) {
625+
return explicitVersion;
626+
}
627+
628+
const headSemverVersions = headTags
629+
.filter((tag) => ExactSemverTagPattern.test(tag))
630+
.map((tag) => tag.replace(/^v/, ""))
631+
.toSorted(compareSemverPrecedence);
632+
633+
return headSemverVersions.at(-1) ?? fallbackVersion;
634+
}
635+
535636
export function resolveDesktopBuildIconAssets(version: string): DesktopBuildIconAssets {
536637
if (resolveDesktopUpdateChannel(version) === "nightly") {
537638
return {
@@ -710,7 +811,11 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
710811
}),
711812
});
712813

713-
const appVersion = options.version ?? serverPackageJson.version;
814+
const appVersion = resolveDesktopArtifactVersion(
815+
options.version,
816+
yield* resolveGitHeadTags(repoRoot),
817+
serverPackageJson.version,
818+
);
714819
const iconAssets = resolveDesktopBuildIconAssets(appVersion);
715820
const commitHash = yield* resolveGitCommitHash(repoRoot);
716821
const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;

0 commit comments

Comments
 (0)