import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import * as Config from "effect/Config";import * as ConfigProvider from "effect/ConfigProvider";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema";import { Command, CliError } from "effect/unstable/cli";import * as TestConsole from "effect/testing/TestConsole";import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; import { ReleaseGitHubOutputConfigurationError, ReleaseGitHubOutputWriteError, ReleasePackageManifestError, releasePackageFiles, updateReleasePackageVersions, updateReleasePackageVersionsCommand,} from "./update-release-package-versions.ts"; const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);const decodePackageJson = Schema.decodeEffect(PackageJsonPrettyJson);const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson); const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* ( rootDir: string, version: string,) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; for (const relativePath of releasePackageFiles) { const filePath = path.join(rootDir, relativePath); yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }); yield* fs.writeFileString( filePath, `${yield* encodePackageJson({ name: relativePath, version, private: true, })}\n`, ); }}); const readReleaseVersions = Effect.fn("readReleaseVersions")(function* (rootDir: string) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const versions = new Map<string, string>(); for (const relativePath of releasePackageFiles) { const filePath = path.join(rootDir, relativePath); const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson)); versions.set(relativePath, String(packageJson.version)); } return versions;}); const captureLogs = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.gen(function* () { const result = yield* effect; const logs = (yield* TestConsole.logLines).filter( (line): line is string => typeof line === "string", ); return { result, logs }; }); it.layer(ScriptTestLayer)("update-release-package-versions", (it) => { it.effect("updates all release package versions under the provided root", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-", }); yield* writePackageJsonFixtures(baseDir, "0.0.1"); const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir }); const versions = yield* readReleaseVersions(baseDir); assert.deepStrictEqual(result, { changed: true }); assert.deepStrictEqual( Array.from(versions.entries()), releasePackageFiles.map((relativePath) => [relativePath, "1.2.3"]), ); }), ); it.effect("returns changed=false when all versions already match", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-unchanged-", }); yield* writePackageJsonFixtures(baseDir, "1.2.3"); const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir }); assert.deepStrictEqual(result, { changed: false }); }), ); it.effect("preserves manifest read context and the filesystem cause", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-read-error-", }); const filePath = path.join(baseDir, releasePackageFiles[0]); const error = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir, }).pipe(Effect.flip); assert.instanceOf(error, ReleasePackageManifestError); assert.equal(error.operation, "read"); assert.equal(error.filePath, filePath); assert.instanceOf(error.cause, PlatformError.PlatformError); assert.equal(error.message, `Failed to read release package manifest '${filePath}'.`); }), ); it.effect("preserves manifest decode context and the schema cause", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-decode-error-", }); const filePath = path.join(baseDir, releasePackageFiles[0]); yield* writePackageJsonFixtures(baseDir, "0.0.1"); yield* fs.writeFileString(filePath, "not json"); const error = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir, }).pipe(Effect.flip); assert.equal(error.operation, "decode"); assert.equal(error.filePath, filePath); assert.isTrue(Schema.isSchemaError(error.cause)); assert.equal(error.message, `Failed to decode release package manifest '${filePath}'.`); }), ); it.effect("preserves manifest write context and the filesystem cause", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-write-error-", }); const filePath = path.join(baseDir, releasePackageFiles[0]); yield* writePackageJsonFixtures(baseDir, "0.0.1"); yield* fs.chmod(filePath, 0o400); const error = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir, }).pipe(Effect.flip, Effect.ensuring(fs.chmod(filePath, 0o600).pipe(Effect.orDie))); assert.equal(error.operation, "write"); assert.equal(error.filePath, filePath); assert.instanceOf(error.cause, PlatformError.PlatformError); assert.equal(error.message, `Failed to write release package manifest '${filePath}'.`); }), ); it.effect("accepts flags before the version positional and appends changed output", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-cli-", }); const githubOutputPath = path.join(baseDir, "github-output.txt"); yield* writePackageJsonFixtures(baseDir, "0.0.1"); yield* runCli(["--github-output", "--root", baseDir, "2.0.0"]).pipe( Effect.provide( ConfigProvider.layer( ConfigProvider.fromEnv({ env: { GITHUB_OUTPUT: githubOutputPath, }, }), ), ), ); const githubOutput = yield* fs.readFileString(githubOutputPath); assert.equal(githubOutput, "changed=true\n"); }), ); it.effect("logs when nothing changed", () => captureLogs( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-cli-log-", }); yield* writePackageJsonFixtures(baseDir, "3.0.0"); yield* runCli(["3.0.0", "--root", baseDir]); }), ).pipe( Effect.tap(({ logs }) => { assert.deepStrictEqual(logs, ["All package.json versions already match release version."]); return Effect.void; }), ), ); it.effect("requires GITHUB_OUTPUT when --github-output is set", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-cli-missing-output-", }); yield* writePackageJsonFixtures(baseDir, "0.0.1"); const error = yield* runCli(["4.0.0", "--root", baseDir, "--github-output"]).pipe( Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))), Effect.flip, ); assert.instanceOf(error, ReleaseGitHubOutputConfigurationError); assert.instanceOf(error.cause, Config.ConfigError); assert.equal( error.message, "Failed to resolve GITHUB_OUTPUT for release package version output.", ); }), ); it.effect("preserves GITHUB_OUTPUT write context and the filesystem cause", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "update-release-package-versions-cli-output-error-", }); yield* writePackageJsonFixtures(baseDir, "0.0.1"); const error = yield* runCli(["4.0.0", "--root", baseDir, "--github-output"]).pipe( Effect.provide( ConfigProvider.layer( ConfigProvider.fromEnv({ env: { GITHUB_OUTPUT: baseDir, }, }), ), ), Effect.flip, ); assert.instanceOf(error, ReleaseGitHubOutputWriteError); assert.equal(error.filePath, baseDir); assert.instanceOf(error.cause, PlatformError.PlatformError); assert.equal( error.message, `Failed to append release package version output to '${baseDir}'.`, ); }), ); it.effect("rejects unknown flags during cli parsing", () => Effect.gen(function* () { const error = yield* runCli(["1.2.3", "--unknown"]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } const optionError = error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error; if (!optionError || optionError._tag !== "UnrecognizedOption") { assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`); } assert.equal(optionError.option, "--unknown"); }), ); it.effect("rejects a missing version positional during cli parsing", () => Effect.gen(function* () { const error = yield* runCli(["--github-output"]).pipe(Effect.flip); if (!CliError.isCliError(error)) { assert.fail(`Expected CliError, got ${String(error)}`); } const versionError = error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error; if (!versionError || versionError._tag !== "MissingArgument") { assert.fail(`Expected MissingArgument, got ${String(versionError?._tag)}`); } assert.equal(versionError.argument, "version"); }), );});
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Config from "effect/Config";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";
import { Command, CliError } from "effect/unstable/cli";
import * as TestConsole from "effect/testing/TestConsole";
import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
import {
ReleaseGitHubOutputConfigurationError,
ReleaseGitHubOutputWriteError,
ReleasePackageManifestError,
releasePackageFiles,
updateReleasePackageVersions,
updateReleasePackageVersionsCommand,
} from "./update-release-package-versions.ts";
const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);
const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });
const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
const decodePackageJson = Schema.decodeEffect(PackageJsonPrettyJson);
const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (
rootDir: string,
version: string,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
for (const relativePath of releasePackageFiles) {
const filePath = path.join(rootDir, relativePath);
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
yield* fs.writeFileString(
filePath,
`${yield* encodePackageJson({
name: relativePath,
version,
private: true,
})}\n`,
);
}
});
const readReleaseVersions = Effect.fn("readReleaseVersions")(function* (rootDir: string) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const versions = new Map<string, string>();
for (const relativePath of releasePackageFiles) {
const filePath = path.join(rootDir, relativePath);
const packageJson = yield* fs.readFileString(filePath).pipe(Effect.flatMap(decodePackageJson));
versions.set(relativePath, String(packageJson.version));
}
return versions;
});
const captureLogs = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
const result = yield* effect;
const logs = (yield* TestConsole.logLines).filter(
(line): line is string => typeof line === "string",
);
return { result, logs };
});
it.layer(ScriptTestLayer)("update-release-package-versions", (it) => {
it.effect("updates all release package versions under the provided root", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-",
});
yield* writePackageJsonFixtures(baseDir, "0.0.1");
const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir });
const versions = yield* readReleaseVersions(baseDir);
assert.deepStrictEqual(result, { changed: true });
assert.deepStrictEqual(
Array.from(versions.entries()),
releasePackageFiles.map((relativePath) => [relativePath, "1.2.3"]),
);
}),
);
it.effect("returns changed=false when all versions already match", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-unchanged-",
});
yield* writePackageJsonFixtures(baseDir, "1.2.3");
const result = yield* updateReleasePackageVersions("1.2.3", { rootDir: baseDir });
assert.deepStrictEqual(result, { changed: false });
}),
);
it.effect("preserves manifest read context and the filesystem cause", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-read-error-",
});
const filePath = path.join(baseDir, releasePackageFiles[0]);
const error = yield* updateReleasePackageVersions("1.2.3", {
rootDir: baseDir,
}).pipe(Effect.flip);
assert.instanceOf(error, ReleasePackageManifestError);
assert.equal(error.operation, "read");
assert.equal(error.filePath, filePath);
assert.instanceOf(error.cause, PlatformError.PlatformError);
assert.equal(error.message, `Failed to read release package manifest '${filePath}'.`);
}),
);
it.effect("preserves manifest decode context and the schema cause", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-decode-error-",
});
const filePath = path.join(baseDir, releasePackageFiles[0]);
yield* writePackageJsonFixtures(baseDir, "0.0.1");
yield* fs.writeFileString(filePath, "not json");
const error = yield* updateReleasePackageVersions("1.2.3", {
rootDir: baseDir,
}).pipe(Effect.flip);
assert.equal(error.operation, "decode");
assert.equal(error.filePath, filePath);
assert.isTrue(Schema.isSchemaError(error.cause));
assert.equal(error.message, `Failed to decode release package manifest '${filePath}'.`);
}),
);
it.effect("preserves manifest write context and the filesystem cause", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-write-error-",
});
const filePath = path.join(baseDir, releasePackageFiles[0]);
yield* writePackageJsonFixtures(baseDir, "0.0.1");
yield* fs.chmod(filePath, 0o400);
const error = yield* updateReleasePackageVersions("1.2.3", {
rootDir: baseDir,
}).pipe(Effect.flip, Effect.ensuring(fs.chmod(filePath, 0o600).pipe(Effect.orDie)));
assert.equal(error.operation, "write");
assert.equal(error.filePath, filePath);
assert.instanceOf(error.cause, PlatformError.PlatformError);
assert.equal(error.message, `Failed to write release package manifest '${filePath}'.`);
}),
);
it.effect("accepts flags before the version positional and appends changed output", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-cli-",
});
const githubOutputPath = path.join(baseDir, "github-output.txt");
yield* writePackageJsonFixtures(baseDir, "0.0.1");
yield* runCli(["--github-output", "--root", baseDir, "2.0.0"]).pipe(
Effect.provide(
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
GITHUB_OUTPUT: githubOutputPath,
},
}),
),
),
);
const githubOutput = yield* fs.readFileString(githubOutputPath);
assert.equal(githubOutput, "changed=true\n");
}),
);
it.effect("logs when nothing changed", () =>
captureLogs(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-cli-log-",
});
yield* writePackageJsonFixtures(baseDir, "3.0.0");
yield* runCli(["3.0.0", "--root", baseDir]);
}),
).pipe(
Effect.tap(({ logs }) => {
assert.deepStrictEqual(logs, ["All package.json versions already match release version."]);
return Effect.void;
}),
),
);
it.effect("requires GITHUB_OUTPUT when --github-output is set", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-cli-missing-output-",
});
yield* writePackageJsonFixtures(baseDir, "0.0.1");
const error = yield* runCli(["4.0.0", "--root", baseDir, "--github-output"]).pipe(
Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))),
Effect.flip,
);
assert.instanceOf(error, ReleaseGitHubOutputConfigurationError);
assert.instanceOf(error.cause, Config.ConfigError);
assert.equal(
error.message,
"Failed to resolve GITHUB_OUTPUT for release package version output.",
);
}),
);
it.effect("preserves GITHUB_OUTPUT write context and the filesystem cause", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const baseDir = yield* fs.makeTempDirectoryScoped({
prefix: "update-release-package-versions-cli-output-error-",
});
yield* writePackageJsonFixtures(baseDir, "0.0.1");
const error = yield* runCli(["4.0.0", "--root", baseDir, "--github-output"]).pipe(
Effect.provide(
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
GITHUB_OUTPUT: baseDir,
},
}),
),
),
Effect.flip,
);
assert.instanceOf(error, ReleaseGitHubOutputWriteError);
assert.equal(error.filePath, baseDir);
assert.instanceOf(error.cause, PlatformError.PlatformError);
assert.equal(
error.message,
`Failed to append release package version output to '${baseDir}'.`,
);
}),
);
it.effect("rejects unknown flags during cli parsing", () =>
Effect.gen(function* () {
const error = yield* runCli(["1.2.3", "--unknown"]).pipe(Effect.flip);
if (!CliError.isCliError(error)) {
assert.fail(`Expected CliError, got ${String(error)}`);
}
const optionError =
error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error;
if (!optionError || optionError._tag !== "UnrecognizedOption") {
assert.fail(`Expected UnrecognizedOption, got ${String(optionError?._tag)}`);
}
assert.equal(optionError.option, "--unknown");
}),
);
it.effect("rejects a missing version positional during cli parsing", () =>
Effect.gen(function* () {
const error = yield* runCli(["--github-output"]).pipe(Effect.flip);
if (!CliError.isCliError(error)) {
assert.fail(`Expected CliError, got ${String(error)}`);
}
const versionError =
error._tag === "ShowHelp" ? (error.errors[0] as CliError.CliError | undefined) : error;
if (!versionError || versionError._tag !== "MissingArgument") {
assert.fail(`Expected MissingArgument, got ${String(versionError?._tag)}`);
}
assert.equal(versionError.argument, "version");
}),
);
});