| author | |
| committer | |
| log | 7eb7b4f4a36aea3b858494e5983ee9bc9a288933 |
| tree | 7fdb256de8a6cabf020f9227874f52cbccec38fb |
| parent | e01b1903de403c720792a896d8c50ebcd8163b44 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { assert, it } from "@effect/vitest"; | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | ||
| 2 | 3 | ||
| 3 | import { | 4 | import { |
| 4 | resolveNightlyBaseVersion, | 5 | resolveNightlyBaseVersion, |
| ... | @@ -12,11 +13,23 @@ it("strips prerelease and build metadata when deriving the nightly base version" | ... | @@ -12,11 +13,23 @@ it("strips prerelease and build metadata when deriving the nightly base version" |
| 12 | assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); | 13 | assert.equal(resolveNightlyBaseVersion("1.2.3-beta.4+build.9"), "1.2.3"); |
| 13 | }); | 14 | }); |
| 14 | 15 | ||
| 15 | it("bumps the patch version before deriving nightly prerelease versions", () => { | 16 | it.effect("bumps the patch version before deriving nightly prerelease versions", () => |
| 16 | assert.equal(resolveNightlyTargetVersion("0.0.17"), "0.0.18"); | 17 | Effect.gen(function* () { |
| 17 | assert.equal(resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10"); | 18 | assert.equal(yield* resolveNightlyTargetVersion("0.0.17"), "0.0.18"); |
| 18 | assert.equal(resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4"); | 19 | assert.equal(yield* resolveNightlyTargetVersion("9.9.9-smoke.0"), "9.9.10"); |
| 19 | }); | 20 | assert.equal(yield* resolveNightlyTargetVersion("1.2.3-beta.4+build.9"), "1.2.4"); |
| 21 | }), | ||
| 22 | ); | ||
| 23 | |||
| 24 | it.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 | ); | ||
| 20 | 33 | ||
| 21 | it("derives nightly metadata including the short commit sha in the release name", () => { | 34 | it("derives nightly metadata including the short commit sha in the release name", () => { |
| 22 | assert.deepStrictEqual( | 35 | assert.deepStrictEqual( |
scripts/resolve-nightly-release.ts+14-3| ... | @@ -29,6 +29,17 @@ const DesktopPackageJsonSchema = Schema.Struct({ | ... | @@ -29,6 +29,17 @@ const DesktopPackageJsonSchema = Schema.Struct({ |
| 29 | version: Schema.NonEmptyString, | 29 | version: Schema.NonEmptyString, |
| 30 | }); | 30 | }); |
| 31 | 31 | ||
| 32 | export 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 | |||
| 32 | const RepoRoot = Effect.service(Path.Path).pipe( | 43 | const RepoRoot = Effect.service(Path.Path).pipe( |
| 33 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), | 44 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 34 | ); | 45 | ); |
| ... | @@ -42,11 +53,11 @@ export const resolveNightlyTargetVersion = (version: string) => { | ... | @@ -42,11 +53,11 @@ export const resolveNightlyTargetVersion = (version: string) => { |
| 42 | const stableCore = resolveNightlyBaseVersion(version); | 53 | const stableCore = resolveNightlyBaseVersion(version); |
| 43 | const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore); | 54 | const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore); |
| 44 | if (!match) { | 55 | if (!match) { |
| 45 | throw new Error(`Invalid desktop package version '${version}'.`); | 56 | return Effect.fail(new InvalidDesktopPackageVersionError({ version })); |
| 46 | } | 57 | } |
| 47 | 58 | ||
| 48 | const [, major, minor, patch] = match; | 59 | const [, major, minor, patch] = match; |
| 49 | return `${major}.${minor}.${Number(patch) + 1}`; | 60 | return Effect.succeed(`${major}.${minor}.${Number(patch) + 1}`); |
| 50 | }; | 61 | }; |
| 51 | 62 | ||
| 52 | export const resolveNightlyReleaseMetadata = ( | 63 | export const resolveNightlyReleaseMetadata = ( |
| ... | @@ -76,7 +87,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( | ... | @@ -76,7 +87,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( |
| 76 | const packageJson = yield* fs | 87 | const packageJson = yield* fs |
| 77 | .readFileString(packageJsonPath) | 88 | .readFileString(packageJsonPath) |
| 78 | .pipe(Effect.flatMap(decodeDesktopPackageJson)); | 89 | .pipe(Effect.flatMap(decodeDesktopPackageJson)); |
| 79 | return resolveNightlyTargetVersion(packageJson.version); | 90 | return yield* resolveNightlyTargetVersion(packageJson.version); |
| 80 | }); | 91 | }); |
| 81 | 92 | ||
| 82 | const writeOutput = Effect.fn("writeOutput")(function* ( | 93 | const writeOutput = Effect.fn("writeOutput")(function* ( |
scripts/resolve-previous-release-tag.test.ts created+39| ... | @@ -0,0 +1,39 @@ | ||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | |||
| 4 | import { resolvePreviousReleaseTag } from "./resolve-previous-release-tag.ts"; | ||
| 5 | |||
| 6 | it.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 | |||
| 19 | it.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 | |||
| 30 | it.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"; | ... | @@ -15,6 +15,18 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 15 | const ReleaseChannel = Schema.Literals(["stable", "nightly"]); | 15 | const ReleaseChannel = Schema.Literals(["stable", "nightly"]); |
| 16 | type ReleaseChannel = typeof ReleaseChannel.Type; | 16 | type ReleaseChannel = typeof ReleaseChannel.Type; |
| 17 | 17 | ||
| 18 | export 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 | |||
| 18 | interface StableVersion { | 30 | interface StableVersion { |
| 19 | readonly major: number; | 31 | readonly major: number; |
| 20 | readonly minor: number; | 32 | readonly minor: number; |
| ... | @@ -121,41 +133,44 @@ const parseNightlyTag = (tag: string): NightlyVersion | undefined => { | ... | @@ -121,41 +133,44 @@ const parseNightlyTag = (tag: string): NightlyVersion | undefined => { |
| 121 | }; | 133 | }; |
| 122 | }; | 134 | }; |
| 123 | 135 | ||
| 124 | const resolvePreviousReleaseTag = ( | 136 | export const resolvePreviousReleaseTag = ( |
| 125 | channel: ReleaseChannel, | 137 | channel: ReleaseChannel, |
| 126 | currentTag: string, | 138 | currentTag: string, |
| 127 | tags: ReadonlyArray<string>, | 139 | tags: ReadonlyArray<string>, |
| 128 | ): string | undefined => { | 140 | ) => |
| 129 | if (channel === "stable") { | 141 | Effect.gen(function* () { |
| 130 | const current = parseStableTag(currentTag); | 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); | ||
| 131 | if (!current) { | 160 | if (!current) { |
| 132 | throw new Error(`Invalid stable release tag '${currentTag}'.`); | 161 | return yield* new InvalidReleaseTagError({ channel, currentTag }); |
| 133 | } | 162 | } |
| 134 | 163 | ||
| 135 | const candidates = tags | 164 | const candidates = tags |
| 136 | .map((tag) => ({ tag, parsed: parseStableTag(tag) })) | 165 | .map((tag) => ({ tag, parsed: parseNightlyTag(tag) })) |
| 137 | .filter( | 166 | .filter( |
| 138 | (entry): entry is { tag: string; parsed: StableVersion } => entry.parsed !== undefined, | 167 | (entry): entry is { tag: string; parsed: NightlyVersion } => entry.parsed !== undefined, |
| 139 | ) | 168 | ) |
| 140 | .filter((entry) => compareStableVersions(entry.parsed, current) < 0) | 169 | .filter((entry) => compareNightlyVersions(entry.parsed, current) < 0) |
| 141 | .toSorted((left, right) => compareStableVersions(right.parsed, left.parsed)); | 170 | .toSorted((left, right) => compareNightlyVersions(right.parsed, left.parsed)); |
| 142 | 171 | ||
| 143 | return candidates[0]?.tag; | 172 | return candidates[0]?.tag; |
| 144 | } | 173 | }); |
| 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 | }; | ||
| 159 | 174 | ||
| 160 | const listGitTags = Effect.fn("listGitTags")(function* () { | 175 | const listGitTags = Effect.fn("listGitTags")(function* () { |
| 161 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 176 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| ... | @@ -205,7 +220,7 @@ const command = Command.make( | ... | @@ -205,7 +220,7 @@ const command = Command.make( |
| 205 | }, | 220 | }, |
| 206 | ({ channel, currentTag, githubOutput }) => | 221 | ({ channel, currentTag, githubOutput }) => |
| 207 | listGitTags().pipe( | 222 | listGitTags().pipe( |
| 208 | Effect.map((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), | 223 | Effect.flatMap((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), |
| 209 | Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)), | 224 | Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)), |
| 210 | ), | 225 | ), |
| 211 | ).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series.")); | 226 | ).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series.")); |