| 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 | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | |
| 2 | 3 | |
| 3 | 4 | import { |
| 4 | 5 | resolveNightlyBaseVersion, |
| ... | ... | @@ -12,11 +13,23 @@ it("strips prerelease and build metadata when deriving the nightly base version" |
| 12 | 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 | 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 | }); | |
| 16 | it.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 | ||
| 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 | 34 | it("derives nightly metadata including the short commit sha in the release name", () => { |
| 22 | 35 | assert.deepStrictEqual( |
scripts/resolve-nightly-release.ts+14-3| ... | ... | @@ -29,6 +29,17 @@ const DesktopPackageJsonSchema = Schema.Struct({ |
| 29 | 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 | 43 | const RepoRoot = Effect.service(Path.Path).pipe( |
| 33 | 44 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 34 | 45 | ); |
| ... | ... | @@ -42,11 +53,11 @@ export const resolveNightlyTargetVersion = (version: string) => { |
| 42 | 53 | const stableCore = resolveNightlyBaseVersion(version); |
| 43 | 54 | const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore); |
| 44 | 55 | if (!match) { |
| 45 | throw new Error(`Invalid desktop package version '${version}'.`); | |
| 56 | return Effect.fail(new InvalidDesktopPackageVersionError({ version })); | |
| 46 | 57 | } |
| 47 | 58 | |
| 48 | 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 | 63 | export const resolveNightlyReleaseMetadata = ( |
| ... | ... | @@ -76,7 +87,7 @@ const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* ( |
| 76 | 87 | const packageJson = yield* fs |
| 77 | 88 | .readFileString(packageJsonPath) |
| 78 | 89 | .pipe(Effect.flatMap(decodeDesktopPackageJson)); |
| 79 | return resolveNightlyTargetVersion(packageJson.version); | |
| 90 | return yield* resolveNightlyTargetVersion(packageJson.version); | |
| 80 | 91 | }); |
| 81 | 92 | |
| 82 | 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 | 15 | const ReleaseChannel = Schema.Literals(["stable", "nightly"]); |
| 16 | 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 | 30 | interface StableVersion { |
| 19 | 31 | readonly major: number; |
| 20 | 32 | readonly minor: number; |
| ... | ... | @@ -121,41 +133,44 @@ const parseNightlyTag = (tag: string): NightlyVersion | undefined => { |
| 121 | 133 | }; |
| 122 | 134 | }; |
| 123 | 135 | |
| 124 | const resolvePreviousReleaseTag = ( | |
| 136 | export const resolvePreviousReleaseTag = ( | |
| 125 | 137 | channel: ReleaseChannel, |
| 126 | 138 | currentTag: string, |
| 127 | 139 | 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); | |
| 131 | 160 | if (!current) { |
| 132 | throw new Error(`Invalid stable release tag '${currentTag}'.`); | |
| 161 | return yield* new InvalidReleaseTagError({ channel, currentTag }); | |
| 133 | 162 | } |
| 134 | 163 | |
| 135 | 164 | const candidates = tags |
| 136 | .map((tag) => ({ tag, parsed: parseStableTag(tag) })) | |
| 165 | .map((tag) => ({ tag, parsed: parseNightlyTag(tag) })) | |
| 137 | 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) | |
| 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)); | |
| 142 | 171 | |
| 143 | 172 | 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 | }); | |
| 159 | 174 | |
| 160 | 175 | const listGitTags = Effect.fn("listGitTags")(function* () { |
| 161 | 176 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| ... | ... | @@ -205,7 +220,7 @@ const command = Command.make( |
| 205 | 220 | }, |
| 206 | 221 | ({ channel, currentTag, githubOutput }) => |
| 207 | 222 | listGitTags().pipe( |
| 208 | Effect.map((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), | |
| 223 | Effect.flatMap((tags) => resolvePreviousReleaseTag(channel, currentTag, tags)), | |
| 209 | 224 | Effect.flatMap((previousTag) => writeOutput(previousTag, githubOutput)), |
| 210 | 225 | ), |
| 211 | 226 | ).pipe(Command.withDescription("Resolve the previous release tag for a stable or nightly series.")); |