diff --git a/scripts/resolve-nightly-release.test.ts b/scripts/resolve-nightly-release.test.ts index 82b25737a58defb278ace8d2e933658e7b6b7d5a..ecc94c57f5994e96c02cf7f60db787443c49a3ca 100644 --- a/scripts/resolve-nightly-release.test.ts +++ b/scripts/resolve-nightly-release.test.ts @@ -1,4 +1,5 @@ import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; import { resolveNightlyBaseVersion, @@ -12,11 +13,23 @@ it("strips prerelease and build metadata when deriving the nightly base version" assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); }); -it("bumps the patch version before deriving nightly prerelease versions", () => { - assert.equal(resolveNightlyTargetVersion("0.0.17"), "0.0.18"); - assert.equal(resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10"); - assert.equal(resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4"); -}); +it.effect("bumps the patch version before deriving nightly prerelease versions", () => + Effect.gen(function* () { + assert.equal(yield* resolveNightlyTargetVersion("0.0.17"), "0.0.18"); + assert.equal(yield* resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10"); + assert.equal(yield* resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4"); + }), +); + +it.effect("reports the invalid desktop package version", () => + Effect.gen(function* () { + const error = yield* resolveNightlyTargetVersion("nightly").pipe(Effect.flip); + + assert.equal(error._tag, "InvalidDesktopPackageVersionError"); + assert.equal(error.version, "nightly"); + assert.equal(error.message, "Invalid desktop package version 'nightly'."); + }), +); it("derives nightly metadata including the short commit sha in the release name", () => { assert.deepStrictEqual( diff --git a/scripts/resolve-nightly-release.ts b/scripts/resolve-nightly-release.ts index e3f064305bf25fae975bfa1eeef1340e1f1de3f1..ae6bc323c67b5126777efc5e77083a7c66f8c693 100644 --- a/scripts/resolve-nightly-release.ts +++ b/scripts/resolve-nightly-release.ts @@ -29,6 +29,17 @@ const DesktopPackageJsonSchema = Schema.Struct({ version: Schema.NonEmptyString, }); +export class InvalidDesktopPackageVersionError extends Schema.TaggedErrorClass()( + "InvalidDesktopPackageVersionError", + { + version: Schema.String, + }, +) { + override get message(): string { + return `Invalid desktop package version '${this.version}'.`; + } +} + const RepoRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); @@ -42,11 +53,11 @@ export const resolveNightlyTargetVersion = (version: string) => { const stableCore = resolveNightlyBaseVersion(version); const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore); if (!match) { - throw new Error(`Invalid desktop package version '${version}'.`); + return Effect.fail(new InvalidDesktopPackageVersionError({ version })); } const [, major, minor, patch] = match; - return `${major}.${minor}.${Number(patch) + 1}`; + return Effect.succeed(`${major}.${minor}.${Number(patch) + 1}`); }; export const resolveNightlyReleaseMetadata = ( @@ -76,7 +87,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( const packageJson = yield* fs .readFileString(packageJsonPath) .pipe(Effect.flatMap(decodeDesktopPackageJson)); - return resolveNightlyTargetVersion(packageJson.version); + return yield* resolveNightlyTargetVersion(packageJson.version); }); const writeOutput = Effect.fn("writeOutput")(function* ( diff --git a/scripts/resolve-previous-release-tag.test.ts b/scripts/resolve-previous-release-tag.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecf564c005e0cbf64581115e4c022613ae3b5eeb --- /dev/null +++ b/scripts/resolve-previous-release-tag.test.ts @@ -0,0 +1,39 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { resolvePreviousReleaseTag } from "./resolve-previous-release-tag.ts"; + +it.effect("selects the latest earlier stable tag and ignores nightlies", () => + Effect.gen(function* () { + const previous = yield* resolvePreviousReleaseTag("stable", "v1.2.0", [ + "v1.1.0", + "v1.1.1-nightly.20260619.1", + "v1.1.2", + "v1.2.0", + ]); + + assert.equal(previous, "v1.1.2"); + }), +); + +it.effect("accepts legacy nightly tags when selecting the previous nightly", () => + Effect.gen(function* () { + const previous = yield* resolvePreviousReleaseTag("nightly", "v1.2.0-nightly.20260620.2", [ + "nightly-v1.2.0-nightly.20260620.1", + "v1.1.0-nightly.20260619.9", + ]); + + assert.equal(previous, "nightly-v1.2.0-nightly.20260620.1"); + }), +); + +it.effect("reports the invalid tag with its release channel", () => + Effect.gen(function* () { + const error = yield* resolvePreviousReleaseTag("nightly", "v1.2.0", []).pipe(Effect.flip); + + assert.equal(error._tag, "InvalidReleaseTagError"); + assert.equal(error.channel, "nightly"); + assert.equal(error.currentTag, "v1.2.0"); + assert.equal(error.message, "Invalid nightly release tag 'v1.2.0'."); + }), +); diff --git a/scripts/resolve-previous-release-tag.ts b/scripts/resolve-previous-release-tag.ts index f75c3a4f8a4b261781785cc90003cc4c159bdb9a..8b1f1fc96480c5f860ab4d8deddf6a0fa4ba59b9 100644 --- a/scripts/resolve-previous-release-tag.ts +++ b/scripts/resolve-previous-release-tag.ts @@ -15,6 +15,18 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const ReleaseChannel = Schema.Literals(["stable", "nightly"]); type ReleaseChannel = typeof ReleaseChannel.Type; +export class InvalidReleaseTagError extends Schema.TaggedErrorClass()( + "InvalidReleaseTagError", + { + channel: ReleaseChannel, + currentTag: Schema.String, + }, +) { + override get message(): string { + return `Invalid ${this.channel} release tag '${this.currentTag}'.`; + } +} + interface StableVersion { readonly major: number; readonly minor: number; @@ -121,41 +133,44 @@ const parseNightlyTag = (tag: string): NightlyVersion | undefined => { }; }; -const resolvePreviousReleaseTag = ( +export const resolvePreviousReleaseTag = ( channel: ReleaseChannel, currentTag: string, tags: ReadonlyArray, -): string | undefined => { - if (channel === "stable") { - const current = parseStableTag(currentTag); +) => + Effect.gen(function* () { + if (channel === "stable") { + const current = parseStableTag(currentTag); + if (!current) { + return yield* new InvalidReleaseTagError({ channel, currentTag }); + } + + const candidates = tags + .map((tag) => ({ tag, parsed: parseStableTag(tag) })) + .filter( + (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined, + ) + .filter((entry) => compareStableVersions(entry.parsed, current) < 0) + .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed)); + + return candidates[0]?.tag; + } + + const current = parseNightlyTag(currentTag); if (!current) { - throw new Error(`Invalid stable release tag '${currentTag}'.`); + return yield* new InvalidReleaseTagError({ channel, currentTag }); } const candidates = tags - .map((tag) => ({ tag, parsed: parseStableTag(tag) })) + .map((tag) => ({ tag, parsed: parseNightlyTag(tag) })) .filter( - (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined, + (entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined, ) - .filter((entry) => compareStableVersions(entry.parsed, current) < 0) - .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed)); + .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0) + .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed)); return candidates[0]?.tag; - } - - const current = parseNightlyTag(currentTag); - if (!current) { - throw new Error(`Invalid nightly release tag '${currentTag}'.`); - } - - const candidates = tags - .map((tag) => ({ tag, parsed: parseNightlyTag(tag) })) - .filter((entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined) - .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0) - .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed)); - - return candidates[0]?.tag; -}; + }); const listGitTags = Effect.fn("listGitTags")(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; @@ -205,7 +220,7 @@ const command = Command.make( }, ({ channel, currentTag, githubOutput }) => listGitTags().pipe( - Effect.map((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), + Effect.flatMap((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)), ), ).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series."));