authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:31:09-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:31:09-07:00
log7eb7b4f4a36aea3b858494e5983ee9bc9a288933
tree7fdb256de8a6cabf020f9227874f52cbccec38fb
parente01b1903de403c720792a896d8c50ebcd8163b44
signature Signed by PGP key B5690EEEBB952194

[codex] Structure release metadata failures (#3296)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 111 insertions(+), 33 deletions(-)

scripts/resolve-nightly-release.test.ts+18-5
......@@ -1,4 +1,5 @@
11import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
23
34import {
45 resolveNightlyBaseVersion,
......@@ -12,11 +13,23 @@ it("strips prerelease and build metadata when deriving the nightly base version"
1213 assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3");
1314});
1415
15it("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});
16it.effect("bumps the patch version before deriving nightly prerelease versions", () =>
17 Effect.gen(function* () {
18 assert.equal(yield* resolveNightlyTargetVersion("0.0.17"), "0.0.18");
19 assert.equal(yield* resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10");
20 assert.equal(yield* resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4");
21 }),
22);
23
24it.effect("reports the invalid desktop package version", () =>
25 Effect.gen(function* () {
26 const error = yield* resolveNightlyTargetVersion("nightly").pipe(Effect.flip);
27
28 assert.equal(error._tag, "InvalidDesktopPackageVersionError");
29 assert.equal(error.version, "nightly");
30 assert.equal(error.message, "Invalid desktop package version 'nightly'.");
31 }),
32);
2033
2134it("derives nightly metadata including the short commit sha in the release name", () => {
2235 assert.deepStrictEqual(
scripts/resolve-nightly-release.ts+14-3
......@@ -29,6 +29,17 @@ const DesktopPackageJsonSchema = Schema.Struct({
2929 version: Schema.NonEmptyString,
3030});
3131
32export class InvalidDesktopPackageVersionError extends Schema.TaggedErrorClass<InvalidDesktopPackageVersionError>()(
33 "InvalidDesktopPackageVersionError",
34 {
35 version: Schema.String,
36 },
37) {
38 override get message(): string {
39 return `Invalid desktop package version '${this.version}'.`;
40 }
41}
42
3243const RepoRoot = Effect.service(Path.Path).pipe(
3344 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
3445);
......@@ -42,11 +53,11 @@ export const resolveNightlyTargetVersion = (version: string) => {
4253 const stableCore = resolveNightlyBaseVersion(version);
4354 const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore);
4455 if (!match) {
45 throw new Error(`Invalid desktop package version '${version}'.`);
56 return Effect.fail(new InvalidDesktopPackageVersionError({ version }));
4657 }
4758
4859 const [, major, minor, patch] = match;
49 return `${major}.${minor}.${Number(patch) + 1}`;
60 return Effect.succeed(`${major}.${minor}.${Number(patch) + 1}`);
5061};
5162
5263export const resolveNightlyReleaseMetadata = (
......@@ -76,7 +87,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* (
7687 const packageJson = yield* fs
7788 .readFileString(packageJsonPath)
7889 .pipe(Effect.flatMap(decodeDesktopPackageJson));
79 return resolveNightlyTargetVersion(packageJson.version);
90 return yield* resolveNightlyTargetVersion(packageJson.version);
8091});
8192
8293const writeOutput = Effect.fn("writeOutput")(function* (
scripts/resolve-previous-release-tag.test.ts created+39
......@@ -0,0 +1,39 @@
1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3
4import { resolvePreviousReleaseTag } from "./resolve-previous-release-tag.ts";
5
6it.effect("selects the latest earlier stable tag and ignores nightlies", () =>
7 Effect.gen(function* () {
8 const previous = yield* resolvePreviousReleaseTag("stable", "v1.2.0", [
9 "v1.1.0",
10 "v1.1.1-nightly.20260619.1",
11 "v1.1.2",
12 "v1.2.0",
13 ]);
14
15 assert.equal(previous, "v1.1.2");
16 }),
17);
18
19it.effect("accepts legacy nightly tags when selecting the previous nightly", () =>
20 Effect.gen(function* () {
21 const previous = yield* resolvePreviousReleaseTag("nightly", "v1.2.0-nightly.20260620.2", [
22 "nightly-v1.2.0-nightly.20260620.1",
23 "v1.1.0-nightly.20260619.9",
24 ]);
25
26 assert.equal(previous, "nightly-v1.2.0-nightly.20260620.1");
27 }),
28);
29
30it.effect("reports the invalid tag with its release channel", () =>
31 Effect.gen(function* () {
32 const error = yield* resolvePreviousReleaseTag("nightly", "v1.2.0", []).pipe(Effect.flip);
33
34 assert.equal(error._tag, "InvalidReleaseTagError");
35 assert.equal(error.channel, "nightly");
36 assert.equal(error.currentTag, "v1.2.0");
37 assert.equal(error.message, "Invalid nightly release tag 'v1.2.0'.");
38 }),
39);
scripts/resolve-previous-release-tag.ts+40-25
......@@ -15,6 +15,18 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1515const ReleaseChannel = Schema.Literals(["stable", "nightly"]);
1616type ReleaseChannel = typeof ReleaseChannel.Type;
1717
18export class InvalidReleaseTagError extends Schema.TaggedErrorClass<InvalidReleaseTagError>()(
19 "InvalidReleaseTagError",
20 {
21 channel: ReleaseChannel,
22 currentTag: Schema.String,
23 },
24) {
25 override get message(): string {
26 return `Invalid ${this.channel} release tag '${this.currentTag}'.`;
27 }
28}
29
1830interface StableVersion {
1931 readonly major: number;
2032 readonly minor: number;
......@@ -121,41 +133,44 @@ const parseNightlyTag = (tag: string): NightlyVersion | undefined => {
121133 };
122134};
123135
124const resolvePreviousReleaseTag = (
136export const resolvePreviousReleaseTag = (
125137 channel: ReleaseChannel,
126138 currentTag: string,
127139 tags: ReadonlyArray<string>,
128): string | undefined => {
129 if (channel === "stable") {
130 const current = parseStableTag(currentTag);
140) =>
141 Effect.gen(function* () {
142 if (channel === "stable") {
143 const current = parseStableTag(currentTag);
144 if (!current) {
145 return yield* new InvalidReleaseTagError({ channel, currentTag });
146 }
147
148 const candidates = tags
149 .map((tag) => ({ tag, parsed: parseStableTag(tag) }))
150 .filter(
151 (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined,
152 )
153 .filter((entry) => compareStableVersions(entry.parsed, current) < 0)
154 .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed));
155
156 return candidates[0]?.tag;
157 }
158
159 const current = parseNightlyTag(currentTag);
131160 if (!current) {
132 throw new Error(`Invalid stable release tag '${currentTag}'.`);
161 return yield* new InvalidReleaseTagError({ channel, currentTag });
133162 }
134163
135164 const candidates = tags
136 .map((tag) => ({ tag, parsed: parseStableTag(tag) }))
165 .map((tag) => ({ tag, parsed: parseNightlyTag(tag) }))
137166 .filter(
138 (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined,
167 (entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined,
139168 )
140 .filter((entry) => compareStableVersions(entry.parsed, current) < 0)
141 .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed));
169 .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0)
170 .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed));
142171
143172 return candidates[0]?.tag;
144 }
145
146 const current = parseNightlyTag(currentTag);
147 if (!current) {
148 throw new Error(`Invalid nightly release tag '${currentTag}'.`);
149 }
150
151 const candidates = tags
152 .map((tag) => ({ tag, parsed: parseNightlyTag(tag) }))
153 .filter((entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined)
154 .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0)
155 .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed));
156
157 return candidates[0]?.tag;
158};
173 });
159174
160175const listGitTags = Effect.fn("listGitTags")(function* () {
161176 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
......@@ -205,7 +220,7 @@ const command = Command.make(
205220 },
206221 ({ channel, currentTag, githubOutput }) =>
207222 listGitTags().pipe(
208 Effect.map((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)),
223 Effect.flatMap((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)),
209224 Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)),
210225 ),
211226).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series."));