From 1ed0bc88642a1393663b070e3d3db3d981a6c644 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:28:05 -0700 Subject: [PATCH 1/7] Add Discord webhook logging for release notifications - Log webhook request and response metadata - Log announcement payload summary and command lifecycle --- scripts/notify-discord-release.ts | 101 +++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index db51efd066a..63c4774e1ef 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -2,9 +2,14 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { Config, Data, Effect, Layer, Schema } from "effect"; +import { Config, Data, Effect, Layer, Logger, Schema } from "effect"; import { Argument, Command, Flag } from "effect/unstable/cli"; -import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from "effect/unstable/http"; export type DiscordReleaseTarget = "prerelease" | "latest"; @@ -57,6 +62,32 @@ const targetColors = { latest: 0x2ecc71, } as const satisfies Record; +function describeWebhookUrl(webhookUrl: string) { + try { + const url = new URL(webhookUrl); + return { + configured: true, + origin: url.origin, + pathnameSegmentCount: url.pathname.split("/").filter(Boolean).length, + } as const; + } catch { + return { + configured: true, + origin: "invalid", + pathnameSegmentCount: 0, + } 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 => ({ @@ -99,10 +130,14 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( retryOn: "errors-and-responses", times: 3, }), - HttpClient.filterStatusOk, ); - yield* HttpClientRequest.post(webhookUrl).pipe( + yield* Effect.logInfo("discord webhook request dispatching", { + ...describeWebhookUrl(webhookUrl), + ...summarizePayload(payload), + }); + + const response = yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), Effect.mapError( @@ -113,9 +148,28 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( }), ), ); + + yield* Effect.logInfo("discord webhook response received", { + status: response.status, + ok: response.status >= 200 && response.status < 300, + }); + + yield* HttpClientResponse.filterStatusOk(response).pipe( + Effect.mapError( + (cause) => + new DiscordReleaseAnnouncementError({ + message: `Discord webhook returned status ${response.status}.`, + cause, + }), + ), + ); }); -const runtimeLayer = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer); +const runtimeLayer = Layer.mergeAll( + Logger.layer([Logger.consolePretty()]), + NodeServices.layer, + FetchHttpClient.layer, +); export const notifyDiscordReleaseCommand = Command.make( "notify-discord-release", @@ -146,19 +200,34 @@ export const notifyDiscordReleaseCommand = Command.make( }, ({ target, roleId, releaseName, version, tag, releaseUrl }) => Effect.gen(function* () { + yield* Effect.logInfo("discord release announcement starting", { + target, + roleIdProvided: roleId.length > 0, + roleIdLength: roleId.length, + releaseName, + version, + tag, + releaseUrl, + discordWebhookConfigured: Boolean(process.env.DISCORD_WEBHOOK_URL?.trim()), + }); + const webhookUrl = yield* DiscordWebhookUrl; - yield* postDiscordWebhook( - webhookUrl, - buildDiscordReleaseAnnouncement({ - target, - roleId, - releaseName, - version, - tag, - releaseUrl, - timestamp: new Date().toISOString(), - }), + const payload = buildDiscordReleaseAnnouncement({ + target, + roleId, + releaseName, + version, + tag, + releaseUrl, + timestamp: new Date().toISOString(), + }); + + yield* Effect.logInfo( + "discord release announcement payload built", + summarizePayload(payload), ); + yield* postDiscordWebhook(webhookUrl, payload); + yield* Effect.logInfo("discord release announcement completed"); }), ).pipe(Command.withDescription("Post a T3 Code release announcement to Discord.")); From fd1ce5d2d8fd1f45994fc67787dc8c1b5309903d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:30:32 -0700 Subject: [PATCH 2/7] Validate Discord webhook URL as URL - Parse `DISCORD_WEBHOOK_URL` with `Config.url` - Pass a typed `URL` through announcement logging and delivery - Use `href` when posting the webhook --- scripts/notify-discord-release.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index 63c4774e1ef..9cea923f79a 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -45,7 +45,7 @@ interface DiscordWebhookPayload { const DISCORD_RELEASE_TARGETS = ["prerelease", "latest"] as const; const DiscordRoleIdSchema = Schema.String.check(Schema.isPattern(/^\d+$/)); const WebUrlSchema = Schema.String.check(Schema.isPattern(/^https?:\/\/\S+$/)); -const DiscordWebhookUrl = Config.nonEmptyString("DISCORD_WEBHOOK_URL"); +const DiscordWebhookUrl = Config.url("DISCORD_WEBHOOK_URL"); class DiscordReleaseAnnouncementError extends Data.TaggedError("DiscordReleaseAnnouncementError")<{ readonly message: string; @@ -62,21 +62,12 @@ const targetColors = { latest: 0x2ecc71, } as const satisfies Record; -function describeWebhookUrl(webhookUrl: string) { - try { - const url = new URL(webhookUrl); - return { - configured: true, - origin: url.origin, - pathnameSegmentCount: url.pathname.split("/").filter(Boolean).length, - } as const; - } catch { - return { - configured: true, - origin: "invalid", - pathnameSegmentCount: 0, - } as const; - } +function describeWebhookUrl(webhookUrl: URL) { + return { + configured: true, + origin: webhookUrl.origin, + pathnameSegmentCount: webhookUrl.pathname.split("/").filter(Boolean).length, + } as const; } function summarizePayload(payload: DiscordWebhookPayload) { @@ -122,7 +113,7 @@ export const buildDiscordReleaseAnnouncement = ( }); const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( - webhookUrl: string, + webhookUrl: URL, payload: DiscordWebhookPayload, ) { const httpClient = (yield* HttpClient.HttpClient).pipe( @@ -137,7 +128,7 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( ...summarizePayload(payload), }); - const response = yield* HttpClientRequest.post(webhookUrl).pipe( + const response = yield* HttpClientRequest.post(webhookUrl.href).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), Effect.mapError( From 342c585e4a9a0d3ce988c8233d39513e76e5a9b1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:37:11 -0700 Subject: [PATCH 3/7] Use secrets for Discord release mention roles - Read Discord mention role IDs from secrets in release workflow - Unify prerelease and latest notifications on `DISCORD_MENTION_ROLE_ID` --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c3ca830fb1..4d46e78c54a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -619,11 +619,11 @@ jobs: if: needs.preflight.outputs.is_prerelease == 'true' continue-on-error: true env: - DISCORD_RELEASE_NIGHTLY_ROLE_ID: ${{ vars.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} + DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} run: | node scripts/notify-discord-release.ts prerelease \ - --role-id "$DISCORD_RELEASE_NIGHTLY_ROLE_ID" \ + --role-id "$DISCORD_MENTION_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --version "${{ needs.preflight.outputs.version }}" \ --tag "${{ needs.preflight.outputs.tag }}" \ @@ -633,11 +633,11 @@ jobs: if: needs.preflight.outputs.make_latest == 'true' continue-on-error: true env: - DISCORD_RELEASE_LATEST_ROLE_ID: ${{ vars.DISCORD_RELEASE_LATEST_ROLE_ID }} + DISCORD_MENTION_ROLE_ID: ${{ secrets.DISCORD_RELEASE_LATEST_ROLE_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} run: | node scripts/notify-discord-release.ts latest \ - --role-id "$DISCORD_RELEASE_LATEST_ROLE_ID" \ + --role-id "$DISCORD_MENTION_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --version "${{ needs.preflight.outputs.version }}" \ --tag "${{ needs.preflight.outputs.tag }}" \ From 8d1df96a872b6d26b49eaad1c4d2573e5a59a3c2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:41:32 -0700 Subject: [PATCH 4/7] Use URL types for Discord release webhook - Parse release URLs as `URL` values at the command boundary - Pass typed URLs through announcement building and webhook posting - Remove the ad hoc web URL string schema --- scripts/notify-discord-release.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index 9cea923f79a..a5a09a0237b 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -19,7 +19,7 @@ interface DiscordReleaseAnnouncementOptions { readonly releaseName: string; readonly version: string; readonly tag: string; - readonly releaseUrl: string; + readonly releaseUrl: URL; readonly timestamp: string; } @@ -44,7 +44,6 @@ interface DiscordWebhookPayload { const DISCORD_RELEASE_TARGETS = ["prerelease", "latest"] as const; const DiscordRoleIdSchema = Schema.String.check(Schema.isPattern(/^\d+$/)); -const WebUrlSchema = Schema.String.check(Schema.isPattern(/^https?:\/\/\S+$/)); const DiscordWebhookUrl = Config.url("DISCORD_WEBHOOK_URL"); class DiscordReleaseAnnouncementError extends Data.TaggedError("DiscordReleaseAnnouncementError")<{ @@ -89,7 +88,7 @@ export const buildDiscordReleaseAnnouncement = ( embeds: [ { title: options.releaseName, - url: options.releaseUrl, + url: options.releaseUrl.href, description: options.target === "prerelease" ? "A new T3 Code prerelease is available for nightly testers." @@ -128,7 +127,7 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( ...summarizePayload(payload), }); - const response = yield* HttpClientRequest.post(webhookUrl.href).pipe( + const response = yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), Effect.mapError( @@ -185,7 +184,7 @@ export const notifyDiscordReleaseCommand = Command.make( Flag.withDescription("Git tag for the release."), ), releaseUrl: Flag.string("release-url").pipe( - Flag.withSchema(WebUrlSchema), + Flag.withSchema(Schema.URLFromString), Flag.withDescription("Public GitHub release URL."), ), }, From ffaea3cde774e371516ad7ca55e46e953a073ff5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:45:38 -0700 Subject: [PATCH 5/7] Annotate Discord webhook logs - Switch Discord release logging to log annotations - Keep webhook payload and response metadata attached to structured logs --- scripts/notify-discord-release.ts | 61 +++++++++++++++++-------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index a5a09a0237b..c093a0e6cfd 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -122,10 +122,12 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( }), ); - yield* Effect.logInfo("discord webhook request dispatching", { - ...describeWebhookUrl(webhookUrl), - ...summarizePayload(payload), - }); + yield* Effect.logInfo("discord webhook request dispatching").pipe( + Effect.annotateLogs({ + ...describeWebhookUrl(webhookUrl), + ...summarizePayload(payload), + }), + ); const response = yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), @@ -139,10 +141,12 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( ), ); - yield* Effect.logInfo("discord webhook response received", { - status: response.status, - ok: response.status >= 200 && response.status < 300, - }); + 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( @@ -155,12 +159,6 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( ); }); -const runtimeLayer = Layer.mergeAll( - Logger.layer([Logger.consolePretty()]), - NodeServices.layer, - FetchHttpClient.layer, -); - export const notifyDiscordReleaseCommand = Command.make( "notify-discord-release", { @@ -190,16 +188,18 @@ export const notifyDiscordReleaseCommand = Command.make( }, ({ target, roleId, releaseName, version, tag, releaseUrl }) => Effect.gen(function* () { - yield* Effect.logInfo("discord release announcement starting", { - target, - roleIdProvided: roleId.length > 0, - roleIdLength: roleId.length, - releaseName, - version, - tag, - releaseUrl, - discordWebhookConfigured: Boolean(process.env.DISCORD_WEBHOOK_URL?.trim()), - }); + yield* Effect.logInfo("discord release announcement starting").pipe( + Effect.annotateLogs({ + target, + roleIdProvided: roleId.length > 0, + roleIdLength: roleId.length, + releaseName, + version, + tag, + releaseUrl, + discordWebhookConfigured: Boolean(process.env.DISCORD_WEBHOOK_URL?.trim()), + }), + ); const webhookUrl = yield* DiscordWebhookUrl; const payload = buildDiscordReleaseAnnouncement({ @@ -212,9 +212,8 @@ export const notifyDiscordReleaseCommand = Command.make( timestamp: new Date().toISOString(), }); - yield* Effect.logInfo( - "discord release announcement payload built", - summarizePayload(payload), + yield* Effect.logInfo("discord release announcement payload built").pipe( + Effect.annotateLogs(summarizePayload(payload)), ); yield* postDiscordWebhook(webhookUrl, payload); yield* Effect.logInfo("discord release announcement completed"); @@ -223,7 +222,13 @@ export const notifyDiscordReleaseCommand = Command.make( if (import.meta.main) { Command.run(notifyDiscordReleaseCommand, { version: "0.0.0" }).pipe( - Effect.provide(runtimeLayer), + Effect.provide( + Layer.mergeAll( + Logger.layer([Logger.consolePretty()]), + NodeServices.layer, + FetchHttpClient.layer, + ), + ), NodeRuntime.runMain, ); } From 9744d2760186cd103ad04d9afc45f0cfe435fe00 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:47:17 -0700 Subject: [PATCH 6/7] Accept URL release links in Discord notification tests - Update release notification tests to pass `URL` objects for `releaseUrl` - Covers both nightly prerelease and stable release announcements --- scripts/notify-discord-release.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/notify-discord-release.test.ts b/scripts/notify-discord-release.test.ts index e9de4afe4d0..23f73fa0d76 100644 --- a/scripts/notify-discord-release.test.ts +++ b/scripts/notify-discord-release.test.ts @@ -10,7 +10,9 @@ it("builds a prerelease Discord announcement for nightly subscribers", () => { releaseName: "T3 Code Nightly 1.2.4-nightly.20260501.17 (abcdef123456)", version: "1.2.4-nightly.20260501.17", tag: "v1.2.4-nightly.20260501.17", - releaseUrl: "https://github.com/t3dotgg/t3-code/releases/tag/v1.2.4-nightly.20260501.17", + releaseUrl: new URL( + "https://github.com/t3dotgg/t3-code/releases/tag/v1.2.4-nightly.20260501.17", + ), timestamp: "2026-05-01T01:41:00.000Z", }), { @@ -52,7 +54,7 @@ it("builds a latest Discord announcement for stable subscribers", () => { releaseName: "T3 Code v1.2.3", version: "1.2.3", tag: "v1.2.3", - releaseUrl: "https://github.com/t3dotgg/t3-code/releases/tag/v1.2.3", + releaseUrl: new URL("https://github.com/t3dotgg/t3-code/releases/tag/v1.2.3"), timestamp: "2026-05-01T01:41:00.000Z", }), { From 9a15254dace84d5fcca5693c5cabd7f200c4a009 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Apr 2026 20:49:29 -0700 Subject: [PATCH 7/7] Remove discord webhook config from release log --- scripts/notify-discord-release.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index c093a0e6cfd..1789a166f2c 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -197,7 +197,6 @@ export const notifyDiscordReleaseCommand = Command.make( version, tag, releaseUrl, - discordWebhookConfigured: Boolean(process.env.DISCORD_WEBHOOK_URL?.trim()), }), );