1#!/usr/bin/env node
2
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Config from "effect/Config";
6import * as Console from "effect/Console";
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 { Argument, Command, Flag } from "effect/cli";
13import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
14
15export class ReleasePackageManifestError extends Schema.TaggedError<ReleasePackageManifestError>()(
16 "ReleasePackageManifestError",
17 {
18 operation: Schema.Literals(["read", "decode", "encode", "write"]),
19 filePath: Schema.String,
20 cause: Schema.Defect(),
21 },
22) {
23 override get message(): string {
24 return `Failed to ${this.operation} release package manifest '${this.filePath}'.`;
25 }
26}
27
28export class ReleaseGitHubOutputConfigurationError extends Schema.TaggedError<ReleaseGitHubOutputConfigurationError>()(
29 "ReleaseGitHubOutputConfigurationError",
30 { cause: Schema.Defect() },
31) {
32 override get message(): string {
33 return "Failed to resolve GITHUB_OUTPUT for release package version output.";
34 }
35}
36
37export class ReleaseGitHubOutputWriteError extends Schema.TaggedError<ReleaseGitHubOutputWriteError>()(
38 "ReleaseGitHubOutputWriteError",
39 {
40 filePath: Schema.String,
41 cause: Schema.Defect(),
42 },
43) {
44 override get message(): string {
45 return `Failed to append release package version output to '${this.filePath}'.`;
46 }
47}
48
49export const releasePackageFiles = [
50 "apps/server/package.json",
51 "apps/desktop/package.json",
52 "apps/web/package.json",
53 "packages/contracts/package.json",
54] as const;
55
56interface UpdateReleasePackageVersionsOptions {
57 readonly rootDir?: string | undefined;
58}
59
60const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
61const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
62const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
63const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
64
65export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* (
66 version: string,
67 options: UpdateReleasePackageVersionsOptions = {},
68) {
69 const fs = yield* FileSystem.FileSystem;
70 const path = yield* Path.Path;
71 const rootDir = path.resolve(options.rootDir ?? process.cwd());
72 let changed = false;
73
74 for (const relativePath of releasePackageFiles) {
75 const filePath = path.join(rootDir, relativePath);
76 const packageJsonText = yield* fs.readFileString(filePath).pipe(
77 Effect.mapError(
78 (cause) =>
79 new ReleasePackageManifestError({
80 operation: "read",
81 filePath,
82 cause,
83 }),
84 ),
85 );
86 const packageJson = yield* decodePackageJson(packageJsonText).pipe(
87 Effect.mapError(
88 (cause) =>
89 new ReleasePackageManifestError({
90 operation: "decode",
91 filePath,
92 cause,
93 }),
94 ),
95 );
96 if (packageJson.version === version) {
97 continue;
98 }
99
100 const packageJsonString = yield* encodePackageJson({ ...packageJson, version }).pipe(
101 Effect.mapError(
102 (cause) =>
103 new ReleasePackageManifestError({
104 operation: "encode",
105 filePath,
106 cause,
107 }),
108 ),
109 );
110 yield* fs.writeFileString(filePath, `${packageJsonString}\n`).pipe(
111 Effect.mapError(
112 (cause) =>
113 new ReleasePackageManifestError({
114 operation: "write",
115 filePath,
116 cause,
117 }),
118 ),
119 );
120 changed = true;
121 }
122
123 return { changed };
124});
125
126const writeGithubOutput = Effect.fn("writeGithubOutput")(function* (changed: boolean) {
127 const fs = yield* FileSystem.FileSystem;
128 const githubOutputPath = yield* Config.NonEmptyString("GITHUB_OUTPUT").pipe(
129 Effect.mapError(
130 (cause) =>
131 new ReleaseGitHubOutputConfigurationError({
132 cause,
133 }),
134 ),
135 );
136 yield* fs.writeFileString(githubOutputPath, `changed=${changed}\n`, { flag: "a" }).pipe(
137 Effect.mapError(
138 (cause) =>
139 new ReleaseGitHubOutputWriteError({
140 filePath: githubOutputPath,
141 cause,
142 }),
143 ),
144 );
145});
146
147export const updateReleasePackageVersionsCommand = Command.make(
148 "update-release-package-versions",
149 {
150 version: Argument.String("version").pipe(
151 Argument.withDescription("Release version to write into each releasable package.json."),
152 ),
153 root: Flag.String("root").pipe(
154 Flag.withDescription("Workspace root used to resolve the release package manifests."),
155 Flag.optional,
156 ),
157 githubOutput: Flag.Boolean("github-output").pipe(
158 Flag.withDescription("Append changed=<boolean> to GITHUB_OUTPUT."),
159 Flag.withDefault(false),
160 ),
161 },
162 ({ version, root, githubOutput }) =>
163 updateReleasePackageVersions(version, {
164 rootDir: Option.getOrUndefined(root),
165 }).pipe(
166 Effect.tap(({ changed }) =>
167 changed
168 ? Effect.void
169 : Console.log("All package.json versions already match release version."),
170 ),
171 Effect.tap(({ changed }) => (githubOutput ? writeGithubOutput(changed) : Effect.void)),
172 ),
173).pipe(Command.withDescription("Update release package versions across the workspace."));
174
175if (import.meta.main) {
176 Command.run(updateReleasePackageVersionsCommand, { version: "0.0.0" }).pipe(
177 Effect.provide(NodeServices.layer),
178 NodeRuntime.runMain,
179 );
180}