Skip to content
Source
Sign inSign up

resolve-nightly-release.ts

237 lines7.4 KB

1#!/usr/bin/env node
2​
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Console from "effect/Console";
6import * as Config from "effect/Config";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Option from "effect/Option";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
12import { Command, Flag } from "effect/cli";
13​
14export interface NightlyReleaseMetadata {
15 readonly baseVersion: string;
16 readonly version: string;
17 readonly tag: string;
18 readonly name: string;
19 readonly shortSha: string;
21​
22const DateSchema = Schema.String.check(Schema.isPattern(/^\d{8}$/));
23const RunNumberSchema = Schema.FiniteFromString.check(
24 Schema.isInt(),
25 Schema.isGreaterThanOrEqualTo(1),
26);
27const ShaSchema = Schema.String.check(Schema.isPattern(/^[0-9a-f]{7,40}$/i));
28const DesktopPackageJsonSchema = Schema.Struct({
29 version: Schema.NonEmptyString,
30});
31​
32export class InvalidDesktopPackageVersionError extends Schema.TaggedError<InvalidDesktopPackageVersionError>()(
33 "InvalidDesktopPackageVersionError",
34 {
35 version: Schema.String,
36 },
37) {
38 override get message(): string {
39 return `Invalid desktop package version '${this.version}'.`;
40 }
42​
43export class NightlyReleaseDesktopPackageError extends Schema.TaggedError<NightlyReleaseDesktopPackageError>()(
44 "NightlyReleaseDesktopPackageError",
45 {
46 operation: Schema.Literals(["read", "decode"]),
47 packageJsonPath: Schema.String,
48 cause: Schema.Defect(),
49 },
50) {
51 override get message(): string {
52 return `Failed to ${this.operation} desktop package metadata at ${this.packageJsonPath}.`;
53 }
55​
56export class NightlyReleaseGitHubOutputConfigError extends Schema.TaggedError<NightlyReleaseGitHubOutputConfigError>()(
57 "NightlyReleaseGitHubOutputConfigError",
58 {
59 cause: Schema.Defect(),
60 },
61) {
62 override get message(): string {
63 return "Failed to resolve the GITHUB_OUTPUT path for nightly release metadata.";
64 }
66​
67export class NightlyReleaseGitHubOutputAppendError extends Schema.TaggedError<NightlyReleaseGitHubOutputAppendError>()(
68 "NightlyReleaseGitHubOutputAppendError",
69 {
70 outputPath: Schema.String,
71 cause: Schema.Defect(),
72 },
73) {
74 override get message(): string {
75 return `Failed to append nightly release metadata to ${this.outputPath}.`;
76 }
78​
79const RepoRoot = Effect.service(Path.Path).pipe(
80 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
81);
82const decodeDesktopPackageJson = Schema.decodeUnknownEffect(
83 Schema.fromJsonString(DesktopPackageJsonSchema),
84);
85​
86export const resolveNightlyBaseVersion = (version: string) => version.replace(/[-+].*$/, "");
87​
88export const resolveNightlyTargetVersion = (version: string) => {
89 const stableCore = resolveNightlyBaseVersion(version);
90 const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(stableCore);
91 if (!match) {
92 return Effect.fail(new InvalidDesktopPackageVersionError({ version }));
93 }
94​
95 const [, major, minor, patch] = match;
96 return Effect.succeed(`${major}.${minor}.${Number(patch) + 1}`);
97};
98​
99export const PrereleaseChannel = Schema.Literals(["nightly", "preview"]);
100export type PrereleaseChannel = typeof PrereleaseChannel.Type;
101​
102const CHANNEL_RELEASE_LABELS: Record<PrereleaseChannel, string> = {
103 nightly: "Nightly",
104 preview: "Preview (maintainer test build, do not install)",
105};
106​
107export const resolveNightlyReleaseMetadata = (
108 baseVersion: string,
109 date: string,
110 runNumber: number,
111 sha: string,
112 channel: PrereleaseChannel = "nightly",
113) => {
114 const shortSha = sha.slice(0, 12);
115 const version = `${baseVersion}-${channel}.${date}.${runNumber}`;
116 return {
117 baseVersion,
118 version,
119 tag: `v${version}`,
120 name: `T3 Code ${CHANNEL_RELEASE_LABELS[channel]} ${version} (${shortSha})`,
121 shortSha,
122 };
123};
124​
125export const readDesktopBaseVersion = Effect.fn("readDesktopBaseVersion")(function* (
126 rootDir: string | undefined,
127) {
128 const fs = yield* FileSystem.FileSystem;
129 const path = yield* Path.Path;
130 const workspaceRoot = rootDir ? path.resolve(rootDir) : yield* RepoRoot;
131 const packageJsonPath = path.join(workspaceRoot, "apps/desktop/package.json");
132 const packageJsonSource = yield* fs.readFileString(packageJsonPath).pipe(
133 Effect.mapError(
134 (cause) =>
135 new NightlyReleaseDesktopPackageError({
136 operation: "read",
137 packageJsonPath,
138 cause,
139 }),
140 ),
141 );
142 const packageJson = yield* decodeDesktopPackageJson(packageJsonSource).pipe(
143 Effect.mapError(
144 (cause) =>
145 new NightlyReleaseDesktopPackageError({
146 operation: "decode",
147 packageJsonPath,
148 cause,
149 }),
150 ),
151 );
152 return yield* resolveNightlyTargetVersion(packageJson.version);
153});
154​
155export const writeNightlyReleaseOutput = Effect.fn("writeNightlyReleaseOutput")(function* (
156 metadata: NightlyReleaseMetadata,
157 writeGithubOutput: boolean,
158) {
159 const fs = yield* FileSystem.FileSystem;
160​
161 const entries = [
162 ["base_version", metadata.baseVersion],
163 ["version", metadata.version],
164 ["tag", metadata.tag],
165 ["name", metadata.name],
166 ["short_sha", metadata.shortSha],
167 ] as const;
168​
169 if (writeGithubOutput) {
170 const githubOutputPath = yield* Config.NonEmptyString("GITHUB_OUTPUT").pipe(
171 Effect.mapError(
172 (cause) =>
173 new NightlyReleaseGitHubOutputConfigError({
174 cause,
175 }),
176 ),
177 );
178 const serialized = entries.map(([key, value]) => `${key}=${value}\n`).join("");
179 yield* fs.writeFileString(githubOutputPath, serialized, { flag: "a" }).pipe(
180 Effect.mapError(
181 (cause) =>
182 new NightlyReleaseGitHubOutputAppendError({
183 outputPath: githubOutputPath,
184 cause,
185 }),
186 ),
187 );
188 } else {
189 for (const [key, value] of entries) {
190 yield* Console.log(`${key}=${value}`);
191 }
192 }
193});
194​
195const command = Command.make(
196 "resolve-nightly-release",
197 {
198 date: Flag.String("date").pipe(
199 Flag.withSchema(DateSchema),
200 Flag.withDescription("Nightly build date in YYYYMMDD."),
201 ),
202 runNumber: Flag.String("run-number").pipe(
203 Flag.withSchema(RunNumberSchema),
204 Flag.withDescription("GitHub Actions run number."),
205 ),
206 sha: Flag.String("sha").pipe(
207 Flag.withSchema(ShaSchema),
208 Flag.withDescription("Commit sha for the nightly build."),
209 ),
210 channel: Flag.Literals("channel", PrereleaseChannel.literals).pipe(
211 Flag.withDescription("Prerelease channel whose identifier the version carries."),
212 Flag.withDefault("nightly" as const),
213 ),
214 githubOutput: Flag.Boolean("github-output").pipe(
215 Flag.withDescription("Write values to GITHUB_OUTPUT instead of stdout."),
216 Flag.withDefault(false),
217 ),
218 root: Flag.String("root").pipe(
219 Flag.withDescription("Workspace root used to resolve apps/desktop/package.json."),
220 Flag.optional,
221 ),
222 },
223 ({ date, runNumber, sha, channel, githubOutput, root }) =>
224 readDesktopBaseVersion(Option.getOrUndefined(root)).pipe(
225 Effect.map((baseVersion) =>
226 resolveNightlyReleaseMetadata(baseVersion, date, runNumber, sha, channel),
227 ),
228 Effect.flatMap((metadata) => writeNightlyReleaseOutput(metadata, githubOutput)),
229 ),
230).pipe(Command.withDescription("Resolve nightly release version metadata."));
231​
232if (import.meta.main) {
233 Command.run(command, { version: "0.0.0" }).pipe(
234 Effect.provide(NodeServices.layer),
235 NodeRuntime.runMain,
236 );