#!/usr/bin/env node import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Config from "effect/Config";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Schema from "effect/Schema";import { Argument, Command, Flag } from "effect/unstable/cli";import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse,} from "effect/unstable/http"; export type DiscordReleaseTarget = "prerelease" | "latest"; export interface DiscordReleaseAnnouncementOptions { readonly target: DiscordReleaseTarget; readonly roleId: string; readonly releaseName: string; readonly version: string; readonly tag: string; readonly releaseUrl: URL; readonly timestamp: string;} interface DiscordWebhookPayload { readonly content: string; readonly allowed_mentions: { readonly roles: ReadonlyArray<string>; }; readonly embeds: ReadonlyArray<{ readonly title: string; readonly url: string; readonly description: string; readonly color: number; readonly fields: ReadonlyArray<{ readonly name: string; readonly value: string; readonly inline: boolean; }>; readonly timestamp: string; }>;} const DISCORD_RELEASE_TARGETS = ["prerelease", "latest"] as const;const DiscordRoleIdSchema = Schema.String.check(Schema.isPattern(/^\d+$/));const DiscordWebhookUrl = Config.url("DISCORD_WEBHOOK_URL"); const discordReleaseErrorContext = { target: Schema.Literals(["prerelease", "latest"]), releaseName: Schema.String, version: Schema.String, tag: Schema.String, releaseUrl: Schema.String, webhookOrigin: Schema.String, webhookPathnameSegmentCount: Schema.Number, contentLength: Schema.Number, embedCount: Schema.Number, allowedRoleMentionCount: Schema.Number, hasRoleMentionSyntax: Schema.Boolean,}; export class DiscordReleaseWebhookRequestError extends Schema.TaggedErrorClass<DiscordReleaseWebhookRequestError>()( "DiscordReleaseWebhookRequestError", { ...discordReleaseErrorContext, cause: Schema.Defect(), },) { override get message(): string { return `Failed to post Discord ${this.target} release announcement for "${this.tag}" to ${this.webhookOrigin}.`; }} export class DiscordReleaseWebhookResponseError extends Schema.TaggedErrorClass<DiscordReleaseWebhookResponseError>()( "DiscordReleaseWebhookResponseError", { ...discordReleaseErrorContext, status: Schema.Number, cause: Schema.Defect(), },) { override get message(): string { return `Discord ${this.target} release webhook for "${this.tag}" returned status ${this.status}.`; }} export const DiscordReleaseAnnouncementError = Schema.Union([ DiscordReleaseWebhookRequestError, DiscordReleaseWebhookResponseError,]);export type DiscordReleaseAnnouncementError = typeof DiscordReleaseAnnouncementError.Type;export const isDiscordReleaseAnnouncementError = Schema.is(DiscordReleaseAnnouncementError); const targetLabels = { prerelease: "Prerelease", latest: "Latest",} as const satisfies Record<DiscordReleaseTarget, string>; const targetColors = { prerelease: 0x5865f2, latest: 0x2ecc71,} as const satisfies Record<DiscordReleaseTarget, number>; function describeWebhookUrl(webhookUrl: URL) { return { configured: true, origin: webhookUrl.origin, pathnameSegmentCount: webhookUrl.pathname.split("/").filter(Boolean).length, } as const;} function summarizePayload(payload: DiscordWebhookPayload) { return { contentLength: payload.content.length, embedCount: payload.embeds.length, allowedRoleMentionCount: payload.allowed_mentions.roles.length, hasRoleMentionSyntax: payload.content.includes("<@&"), } as const;} export const buildDiscordReleaseAnnouncement = ( options: DiscordReleaseAnnouncementOptions,): DiscordWebhookPayload => ({ content: `<@&${options.roleId}> ${targetLabels[options.target]} published: ${options.releaseName}`, allowed_mentions: { roles: [options.roleId], }, embeds: [ { title: options.releaseName, url: options.releaseUrl.href, description: options.target === "prerelease" ? "A new T3 Code prerelease is available for nightly testers." : "A new T3 Code latest release is available.", color: targetColors[options.target], fields: [ { name: "Version", value: options.version, inline: true, }, { name: "Tag", value: options.tag, inline: true, }, ], timestamp: options.timestamp, }, ],}); export const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( webhookUrl: URL, payload: DiscordWebhookPayload, announcement: DiscordReleaseAnnouncementOptions,) { const httpClient = (yield* HttpClient.HttpClient).pipe( HttpClient.retryTransient({ retryOn: "errors-and-responses", times: 3, }), ); yield* Effect.logInfo("discord webhook request dispatching").pipe( Effect.annotateLogs({ ...describeWebhookUrl(webhookUrl), ...summarizePayload(payload), }), ); const errorContext = { target: announcement.target, releaseName: announcement.releaseName, version: announcement.version, tag: announcement.tag, releaseUrl: announcement.releaseUrl.href, webhookOrigin: webhookUrl.origin, webhookPathnameSegmentCount: webhookUrl.pathname.split("/").filter(Boolean).length, ...summarizePayload(payload), } as const; const response = yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), Effect.mapError( (cause) => new DiscordReleaseWebhookRequestError({ ...errorContext, cause, }), ), ); yield* Effect.logInfo("discord webhook response received").pipe( Effect.annotateLogs({ status: response.status, ok: response.status >= 200 && response.status < 300, }), ); yield* HttpClientResponse.filterStatusOk(response).pipe( Effect.mapError( (cause) => new DiscordReleaseWebhookResponseError({ ...errorContext, status: response.status, cause, }), ), );}); export const notifyDiscordReleaseCommand = Command.make( "notify-discord-release", { target: Argument.choice("target", DISCORD_RELEASE_TARGETS).pipe( Argument.withDescription("Discord announcement target: prerelease or latest."), ), roleId: Flag.string("role-id").pipe( Flag.withSchema(DiscordRoleIdSchema), Flag.withDescription("Discord role ID to mention in the release announcement."), ), releaseName: Flag.string("release-name").pipe( Flag.withSchema(Schema.NonEmptyString), Flag.withDescription("Human-readable release name."), ), releaseVersion: Flag.string("release-version").pipe( Flag.withSchema(Schema.NonEmptyString), Flag.withDescription("Release version."), ), tag: Flag.string("tag").pipe( Flag.withSchema(Schema.NonEmptyString), Flag.withDescription("Git tag for the release."), ), releaseUrl: Flag.string("release-url").pipe( Flag.withSchema(Schema.URLFromString), Flag.withDescription("Public GitHub release URL."), ), }, ({ target, roleId, releaseName, releaseVersion, tag, releaseUrl }) => Effect.gen(function* () { yield* Effect.logInfo("discord release announcement starting").pipe( Effect.annotateLogs({ target, roleIdProvided: roleId.length > 0, roleIdLength: roleId.length, releaseName, version: releaseVersion, tag, releaseUrl, }), ); const webhookUrl = yield* DiscordWebhookUrl; const timestamp = DateTime.formatIso(yield* DateTime.now); const announcement = { target, roleId, releaseName, version: releaseVersion, tag, releaseUrl, timestamp, } satisfies DiscordReleaseAnnouncementOptions; const payload = buildDiscordReleaseAnnouncement(announcement); yield* Effect.logInfo("discord release announcement payload built").pipe( Effect.annotateLogs(summarizePayload(payload)), ); yield* postDiscordWebhook(webhookUrl, payload, announcement); yield* Effect.logInfo("discord release announcement completed"); }),).pipe(Command.withDescription("Post a T3 Code release announcement to Discord.")); if (import.meta.main) { Command.run(notifyDiscordReleaseCommand, { version: "0.0.0" }).pipe( Effect.provide( Layer.mergeAll( Logger.layer([Logger.consolePretty()]), NodeServices.layer, FetchHttpClient.layer, ), ), NodeRuntime.runMain, );}
#!/usr/bin/env node
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Config from "effect/Config";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Logger from "effect/Logger";
import * as Schema from "effect/Schema";
import { Argument, Command, Flag } from "effect/unstable/cli";
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from "effect/unstable/http";
export type DiscordReleaseTarget = "prerelease" | "latest";
export interface DiscordReleaseAnnouncementOptions {
readonly target: DiscordReleaseTarget;
readonly roleId: string;
readonly releaseName: string;
readonly version: string;
readonly tag: string;
readonly releaseUrl: URL;
readonly timestamp: string;
}
interface DiscordWebhookPayload {
readonly content: string;
readonly allowed_mentions: {
readonly roles: ReadonlyArray<string>;
};
readonly embeds: ReadonlyArray<{
readonly title: string;
readonly url: string;
readonly description: string;
readonly color: number;
readonly fields: ReadonlyArray<{
readonly name: string;
readonly value: string;
readonly inline: boolean;
}>;
readonly timestamp: string;
}>;
}
const DISCORD_RELEASE_TARGETS = ["prerelease", "latest"] as const;
const DiscordRoleIdSchema = Schema.String.check(Schema.isPattern(/^\d+$/));
const DiscordWebhookUrl = Config.url("DISCORD_WEBHOOK_URL");
const discordReleaseErrorContext = {
target: Schema.Literals(["prerelease", "latest"]),
releaseName: Schema.String,
version: Schema.String,
tag: Schema.String,
releaseUrl: Schema.String,
webhookOrigin: Schema.String,
webhookPathnameSegmentCount: Schema.Number,
contentLength: Schema.Number,
embedCount: Schema.Number,
allowedRoleMentionCount: Schema.Number,
hasRoleMentionSyntax: Schema.Boolean,
};
export class DiscordReleaseWebhookRequestError extends Schema.TaggedErrorClass<DiscordReleaseWebhookRequestError>()(
"DiscordReleaseWebhookRequestError",
{
...discordReleaseErrorContext,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to post Discord ${this.target} release announcement for "${this.tag}" to ${this.webhookOrigin}.`;
}
}
export class DiscordReleaseWebhookResponseError extends Schema.TaggedErrorClass<DiscordReleaseWebhookResponseError>()(
"DiscordReleaseWebhookResponseError",
{
...discordReleaseErrorContext,
status: Schema.Number,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Discord ${this.target} release webhook for "${this.tag}" returned status ${this.status}.`;
}
}
export const DiscordReleaseAnnouncementError = Schema.Union([
DiscordReleaseWebhookRequestError,
DiscordReleaseWebhookResponseError,
]);
export type DiscordReleaseAnnouncementError = typeof DiscordReleaseAnnouncementError.Type;
export const isDiscordReleaseAnnouncementError = Schema.is(DiscordReleaseAnnouncementError);
const targetLabels = {
prerelease: "Prerelease",
latest: "Latest",
} as const satisfies Record<DiscordReleaseTarget, string>;
const targetColors = {
prerelease: 0x5865f2,
latest: 0x2ecc71,
} as const satisfies Record<DiscordReleaseTarget, number>;
function describeWebhookUrl(webhookUrl: URL) {
return {
configured: true,
origin: webhookUrl.origin,
pathnameSegmentCount: webhookUrl.pathname.split("/").filter(Boolean).length,
} as const;
}
function summarizePayload(payload: DiscordWebhookPayload) {
return {
contentLength: payload.content.length,
embedCount: payload.embeds.length,
allowedRoleMentionCount: payload.allowed_mentions.roles.length,
hasRoleMentionSyntax: payload.content.includes("<@&"),
} as const;
}
export const buildDiscordReleaseAnnouncement = (
options: DiscordReleaseAnnouncementOptions,
): DiscordWebhookPayload => ({
content: `<@&${options.roleId}> ${targetLabels[options.target]} published: ${options.releaseName}`,
allowed_mentions: {
roles: [options.roleId],
},
embeds: [
{
title: options.releaseName,
url: options.releaseUrl.href,
description:
options.target === "prerelease"
? "A new T3 Code prerelease is available for nightly testers."
: "A new T3 Code latest release is available.",
color: targetColors[options.target],
fields: [
{
name: "Version",
value: options.version,
inline: true,
},
{
name: "Tag",
value: options.tag,
inline: true,
},
],
timestamp: options.timestamp,
},
],
});
export const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* (
webhookUrl: URL,
payload: DiscordWebhookPayload,
announcement: DiscordReleaseAnnouncementOptions,
) {
const httpClient = (yield* HttpClient.HttpClient).pipe(
HttpClient.retryTransient({
retryOn: "errors-and-responses",
times: 3,
}),
);
yield* Effect.logInfo("discord webhook request dispatching").pipe(
Effect.annotateLogs({
...describeWebhookUrl(webhookUrl),
...summarizePayload(payload),
}),
);
const errorContext = {
target: announcement.target,
releaseName: announcement.releaseName,
version: announcement.version,
tag: announcement.tag,
releaseUrl: announcement.releaseUrl.href,
webhookOrigin: webhookUrl.origin,
webhookPathnameSegmentCount: webhookUrl.pathname.split("/").filter(Boolean).length,
...summarizePayload(payload),
} as const;
const response = yield* HttpClientRequest.post(webhookUrl).pipe(
HttpClientRequest.bodyJson(payload),
Effect.flatMap(httpClient.execute),
Effect.mapError(
(cause) =>
new DiscordReleaseWebhookRequestError({
...errorContext,
cause,
}),
),
);
yield* Effect.logInfo("discord webhook response received").pipe(
Effect.annotateLogs({
status: response.status,
ok: response.status >= 200 && response.status < 300,
}),
);
yield* HttpClientResponse.filterStatusOk(response).pipe(
Effect.mapError(
(cause) =>
new DiscordReleaseWebhookResponseError({
...errorContext,
status: response.status,
cause,
}),
),
);
});
export const notifyDiscordReleaseCommand = Command.make(
"notify-discord-release",
{
target: Argument.choice("target", DISCORD_RELEASE_TARGETS).pipe(
Argument.withDescription("Discord announcement target: prerelease or latest."),
),
roleId: Flag.string("role-id").pipe(
Flag.withSchema(DiscordRoleIdSchema),
Flag.withDescription("Discord role ID to mention in the release announcement."),
),
releaseName: Flag.string("release-name").pipe(
Flag.withSchema(Schema.NonEmptyString),
Flag.withDescription("Human-readable release name."),
),
releaseVersion: Flag.string("release-version").pipe(
Flag.withSchema(Schema.NonEmptyString),
Flag.withDescription("Release version."),
),
tag: Flag.string("tag").pipe(
Flag.withSchema(Schema.NonEmptyString),
Flag.withDescription("Git tag for the release."),
),
releaseUrl: Flag.string("release-url").pipe(
Flag.withSchema(Schema.URLFromString),
Flag.withDescription("Public GitHub release URL."),
),
},
({ target, roleId, releaseName, releaseVersion, tag, releaseUrl }) =>
Effect.gen(function* () {
yield* Effect.logInfo("discord release announcement starting").pipe(
Effect.annotateLogs({
target,
roleIdProvided: roleId.length > 0,
roleIdLength: roleId.length,
releaseName,
version: releaseVersion,
tag,
releaseUrl,
}),
);
const webhookUrl = yield* DiscordWebhookUrl;
const timestamp = DateTime.formatIso(yield* DateTime.now);
const announcement = {
target,
roleId,
releaseName,
version: releaseVersion,
tag,
releaseUrl,
timestamp,
} satisfies DiscordReleaseAnnouncementOptions;
const payload = buildDiscordReleaseAnnouncement(announcement);
yield* Effect.logInfo("discord release announcement payload built").pipe(
Effect.annotateLogs(summarizePayload(payload)),
);
yield* postDiscordWebhook(webhookUrl, payload, announcement);
yield* Effect.logInfo("discord release announcement completed");
}),
).pipe(Command.withDescription("Post a T3 Code release announcement to Discord."));
if (import.meta.main) {
Command.run(notifyDiscordReleaseCommand, { version: "0.0.0" }).pipe(
Effect.provide(
Layer.mergeAll(
Logger.layer([Logger.consolePretty()]),
NodeServices.layer,
FetchHttpClient.layer,
),
),
NodeRuntime.runMain,
);
}