From 59b00a3e8931174fd382472fb1ad8d6b764ec743 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 01:43:40 +0000 Subject: [PATCH 1/7] feat(release): announce releases on Discord Co-authored-by: Julius Marminge --- .github/workflows/release.yml | 24 ++++ scripts/notify-discord-release.test.ts | 86 +++++++++++++ scripts/notify-discord-release.ts | 166 +++++++++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 scripts/notify-discord-release.test.ts create mode 100644 scripts/notify-discord-release.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 366a6b4de3c..dabfb451fc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -508,6 +508,30 @@ jobs: fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} + - name: Announce prerelease on Discord + if: needs.preflight.outputs.is_prerelease == 'true' + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} + run: | + node scripts/notify-discord-release.ts prerelease \ + --mention "@t3-code-nightly-announcements" \ + --release-name "${{ needs.preflight.outputs.release_name }}" \ + --version "${{ needs.preflight.outputs.version }}" \ + --tag "${{ needs.preflight.outputs.tag }}" \ + --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" + + - name: Announce latest release on Discord + if: needs.preflight.outputs.make_latest == 'true' + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} + run: | + node scripts/notify-discord-release.ts latest \ + --mention "@t3-code-announcements" \ + --release-name "${{ needs.preflight.outputs.release_name }}" \ + --version "${{ needs.preflight.outputs.version }}" \ + --tag "${{ needs.preflight.outputs.tag }}" \ + --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" + finalize: name: Finalize release if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} diff --git a/scripts/notify-discord-release.test.ts b/scripts/notify-discord-release.test.ts new file mode 100644 index 00000000000..022669e9c79 --- /dev/null +++ b/scripts/notify-discord-release.test.ts @@ -0,0 +1,86 @@ +import { assert, it } from "@effect/vitest"; + +import { buildDiscordReleaseAnnouncement } from "./notify-discord-release.ts"; + +it("builds a prerelease Discord announcement for nightly subscribers", () => { + assert.deepStrictEqual( + buildDiscordReleaseAnnouncement({ + target: "prerelease", + mention: "@t3-code-nightly-announcements", + 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", + timestamp: "2026-05-01T01:41:00.000Z", + }), + { + content: + "@t3-code-nightly-announcements Prerelease published: T3 Code Nightly 1.2.4-nightly.20260501.17 (abcdef123456)", + allowed_mentions: { + parse: ["roles"], + }, + embeds: [ + { + title: "T3 Code Nightly 1.2.4-nightly.20260501.17 (abcdef123456)", + url: "https://github.com/t3dotgg/t3-code/releases/tag/v1.2.4-nightly.20260501.17", + description: "A new T3 Code prerelease is available for nightly testers.", + color: 0x5865f2, + fields: [ + { + name: "Version", + value: "1.2.4-nightly.20260501.17", + inline: true, + }, + { + name: "Tag", + value: "v1.2.4-nightly.20260501.17", + inline: true, + }, + ], + timestamp: "2026-05-01T01:41:00.000Z", + }, + ], + }, + ); +}); + +it("builds a latest Discord announcement for stable subscribers", () => { + assert.deepStrictEqual( + buildDiscordReleaseAnnouncement({ + target: "latest", + mention: "@t3-code-announcements", + 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", + timestamp: "2026-05-01T01:41:00.000Z", + }), + { + content: "@t3-code-announcements Latest published: T3 Code v1.2.3", + allowed_mentions: { + parse: ["roles"], + }, + embeds: [ + { + title: "T3 Code v1.2.3", + url: "https://github.com/t3dotgg/t3-code/releases/tag/v1.2.3", + description: "A new T3 Code latest release is available.", + color: 0x2ecc71, + fields: [ + { + name: "Version", + value: "1.2.3", + inline: true, + }, + { + name: "Tag", + value: "v1.2.3", + inline: true, + }, + ], + timestamp: "2026-05-01T01:41:00.000Z", + }, + ], + }, + ); +}); diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts new file mode 100644 index 00000000000..ee6a5511655 --- /dev/null +++ b/scripts/notify-discord-release.ts @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { Config, Effect, Schema } from "effect"; +import { Argument, Command, Flag } from "effect/unstable/cli"; + +export type DiscordReleaseTarget = "prerelease" | "latest"; + +interface DiscordReleaseAnnouncementOptions { + readonly target: DiscordReleaseTarget; + readonly mention: string; + readonly releaseName: string; + readonly version: string; + readonly tag: string; + readonly releaseUrl: string; + readonly timestamp: string; +} + +interface DiscordWebhookPayload { + readonly content: string; + readonly allowed_mentions: { + readonly parse: ReadonlyArray<"roles">; + }; + 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 DiscordReleaseTargetSchema = Schema.Literal("prerelease", "latest"); +const WebUrlSchema = Schema.String.check(Schema.isPattern(/^https?:\/\/\S+$/)); +const DiscordWebhookUrl = Config.nonEmptyString("DISCORD_WEBHOOK_URL"); + +const targetLabels = { + prerelease: "Prerelease", + latest: "Latest", +} as const satisfies Record; + +const targetColors = { + prerelease: 0x5865f2, + latest: 0x2ecc71, +} as const satisfies Record; + +export const buildDiscordReleaseAnnouncement = ( + options: DiscordReleaseAnnouncementOptions, +): DiscordWebhookPayload => ({ + content: `${options.mention} ${targetLabels[options.target]} published: ${options.releaseName}`, + allowed_mentions: { + parse: ["roles"], + }, + embeds: [ + { + title: options.releaseName, + url: options.releaseUrl, + 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, + }, + ], +}); + +const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( + webhookUrl: string, + payload: DiscordWebhookPayload, +) { + const response = yield* Effect.tryPromise({ + try: () => + fetch(webhookUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(payload), + }), + catch: (cause) => new Error(`Failed to post Discord release announcement: ${String(cause)}`), + }); + + if (!response.ok) { + const body = yield* Effect.promise(() => response.text().catch(() => "")); + return yield* Effect.fail( + new Error( + `Discord release announcement failed with HTTP ${response.status}${ + body ? `: ${body}` : "" + }`, + ), + ); + } +}); + +export const notifyDiscordReleaseCommand = Command.make( + "notify-discord-release", + { + target: Argument.string("target").pipe( + Argument.withSchema(DiscordReleaseTargetSchema), + Argument.withDescription("Discord announcement target: prerelease or latest."), + ), + mention: Flag.string("mention").pipe( + Flag.withSchema(Schema.NonEmptyString), + Flag.withDescription("Discord mention text included at the start of the message."), + ), + releaseName: Flag.string("release-name").pipe( + Flag.withSchema(Schema.NonEmptyString), + Flag.withDescription("Human-readable release name."), + ), + version: Flag.string("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(WebUrlSchema), + Flag.withDescription("Public GitHub release URL."), + ), + }, + ({ target, mention, releaseName, version, tag, releaseUrl }) => + DiscordWebhookUrl.pipe( + Effect.flatMap((webhookUrl) => + postDiscordWebhook( + webhookUrl, + buildDiscordReleaseAnnouncement({ + target, + mention, + releaseName, + version, + tag, + releaseUrl, + timestamp: new Date().toISOString(), + }), + ), + ), + ), +).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(NodeServices.layer), + NodeRuntime.runMain, + ); +} From 96c222eb2795e5b8d50fda9427b40de8cfc37177 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 01:47:43 +0000 Subject: [PATCH 2/7] fix(release): typecheck Discord announcement script Co-authored-by: Julius Marminge --- 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 ee6a5511655..ad4476dbeec 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -2,7 +2,7 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { Config, Effect, Schema } from "effect"; +import { Config, Data, Effect, Schema } from "effect"; import { Argument, Command, Flag } from "effect/unstable/cli"; export type DiscordReleaseTarget = "prerelease" | "latest"; @@ -36,10 +36,15 @@ interface DiscordWebhookPayload { }>; } -const DiscordReleaseTargetSchema = Schema.Literal("prerelease", "latest"); +const DISCORD_RELEASE_TARGETS = ["prerelease", "latest"] as const; const WebUrlSchema = Schema.String.check(Schema.isPattern(/^https?:\/\/\S+$/)); const DiscordWebhookUrl = Config.nonEmptyString("DISCORD_WEBHOOK_URL"); +class DiscordReleaseAnnouncementError extends Data.TaggedError("DiscordReleaseAnnouncementError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + const targetLabels = { prerelease: "Prerelease", latest: "Latest", @@ -96,26 +101,27 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( }, body: JSON.stringify(payload), }), - catch: (cause) => new Error(`Failed to post Discord release announcement: ${String(cause)}`), + catch: (cause) => + new DiscordReleaseAnnouncementError({ + message: "Failed to post Discord release announcement.", + cause, + }), }); if (!response.ok) { const body = yield* Effect.promise(() => response.text().catch(() => "")); - return yield* Effect.fail( - new Error( - `Discord release announcement failed with HTTP ${response.status}${ - body ? `: ${body}` : "" - }`, - ), - ); + return yield* new DiscordReleaseAnnouncementError({ + message: `Discord release announcement failed with HTTP ${response.status}${ + body ? `: ${body}` : "" + }`, + }); } }); export const notifyDiscordReleaseCommand = Command.make( "notify-discord-release", { - target: Argument.string("target").pipe( - Argument.withSchema(DiscordReleaseTargetSchema), + target: Argument.choice("target", DISCORD_RELEASE_TARGETS).pipe( Argument.withDescription("Discord announcement target: prerelease or latest."), ), mention: Flag.string("mention").pipe( @@ -140,22 +146,21 @@ export const notifyDiscordReleaseCommand = Command.make( ), }, ({ target, mention, releaseName, version, tag, releaseUrl }) => - DiscordWebhookUrl.pipe( - Effect.flatMap((webhookUrl) => - postDiscordWebhook( - webhookUrl, - buildDiscordReleaseAnnouncement({ - target, - mention, - releaseName, - version, - tag, - releaseUrl, - timestamp: new Date().toISOString(), - }), - ), - ), - ), + Effect.gen(function* () { + const webhookUrl = yield* DiscordWebhookUrl; + yield* postDiscordWebhook( + webhookUrl, + buildDiscordReleaseAnnouncement({ + target, + mention, + releaseName, + version, + tag, + releaseUrl, + timestamp: new Date().toISOString(), + }), + ); + }), ).pipe(Command.withDescription("Post a T3 Code release announcement to Discord.")); if (import.meta.main) { From 648f45c48291795d4a60b65637bce33329d138b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 01:55:36 +0000 Subject: [PATCH 3/7] fix(release): mention Discord roles by ID Co-authored-by: Julius Marminge --- .github/workflows/release.yml | 6 ++++-- scripts/notify-discord-release.test.ts | 12 ++++++------ scripts/notify-discord-release.ts | 19 ++++++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dabfb451fc9..38a44b917b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -511,10 +511,11 @@ jobs: - name: Announce prerelease on Discord if: needs.preflight.outputs.is_prerelease == 'true' env: + DISCORD_RELEASE_NIGHTLY_ROLE_ID: ${{ vars.DISCORD_RELEASE_NIGHTLY_ROLE_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} run: | node scripts/notify-discord-release.ts prerelease \ - --mention "@t3-code-nightly-announcements" \ + --role-id "$DISCORD_RELEASE_NIGHTLY_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --version "${{ needs.preflight.outputs.version }}" \ --tag "${{ needs.preflight.outputs.tag }}" \ @@ -523,10 +524,11 @@ jobs: - name: Announce latest release on Discord if: needs.preflight.outputs.make_latest == 'true' env: + DISCORD_RELEASE_LATEST_ROLE_ID: ${{ vars.DISCORD_RELEASE_LATEST_ROLE_ID }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK_URL }} run: | node scripts/notify-discord-release.ts latest \ - --mention "@t3-code-announcements" \ + --role-id "$DISCORD_RELEASE_LATEST_ROLE_ID" \ --release-name "${{ needs.preflight.outputs.release_name }}" \ --version "${{ needs.preflight.outputs.version }}" \ --tag "${{ needs.preflight.outputs.tag }}" \ diff --git a/scripts/notify-discord-release.test.ts b/scripts/notify-discord-release.test.ts index 022669e9c79..e9de4afe4d0 100644 --- a/scripts/notify-discord-release.test.ts +++ b/scripts/notify-discord-release.test.ts @@ -6,7 +6,7 @@ it("builds a prerelease Discord announcement for nightly subscribers", () => { assert.deepStrictEqual( buildDiscordReleaseAnnouncement({ target: "prerelease", - mention: "@t3-code-nightly-announcements", + roleId: "111111111111111111", 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", @@ -15,9 +15,9 @@ it("builds a prerelease Discord announcement for nightly subscribers", () => { }), { content: - "@t3-code-nightly-announcements Prerelease published: T3 Code Nightly 1.2.4-nightly.20260501.17 (abcdef123456)", + "<@&111111111111111111> Prerelease published: T3 Code Nightly 1.2.4-nightly.20260501.17 (abcdef123456)", allowed_mentions: { - parse: ["roles"], + roles: ["111111111111111111"], }, embeds: [ { @@ -48,7 +48,7 @@ it("builds a latest Discord announcement for stable subscribers", () => { assert.deepStrictEqual( buildDiscordReleaseAnnouncement({ target: "latest", - mention: "@t3-code-announcements", + roleId: "222222222222222222", releaseName: "T3 Code v1.2.3", version: "1.2.3", tag: "v1.2.3", @@ -56,9 +56,9 @@ it("builds a latest Discord announcement for stable subscribers", () => { timestamp: "2026-05-01T01:41:00.000Z", }), { - content: "@t3-code-announcements Latest published: T3 Code v1.2.3", + content: "<@&222222222222222222> Latest published: T3 Code v1.2.3", allowed_mentions: { - parse: ["roles"], + roles: ["222222222222222222"], }, embeds: [ { diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index ad4476dbeec..b580b195f72 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -9,7 +9,7 @@ export type DiscordReleaseTarget = "prerelease" | "latest"; interface DiscordReleaseAnnouncementOptions { readonly target: DiscordReleaseTarget; - readonly mention: string; + readonly roleId: string; readonly releaseName: string; readonly version: string; readonly tag: string; @@ -20,7 +20,7 @@ interface DiscordReleaseAnnouncementOptions { interface DiscordWebhookPayload { readonly content: string; readonly allowed_mentions: { - readonly parse: ReadonlyArray<"roles">; + readonly roles: ReadonlyArray; }; readonly embeds: ReadonlyArray<{ readonly title: string; @@ -37,6 +37,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"); @@ -58,9 +59,9 @@ const targetColors = { export const buildDiscordReleaseAnnouncement = ( options: DiscordReleaseAnnouncementOptions, ): DiscordWebhookPayload => ({ - content: `${options.mention} ${targetLabels[options.target]} published: ${options.releaseName}`, + content: `<@&${options.roleId}> ${targetLabels[options.target]} published: ${options.releaseName}`, allowed_mentions: { - parse: ["roles"], + roles: [options.roleId], }, embeds: [ { @@ -124,9 +125,9 @@ export const notifyDiscordReleaseCommand = Command.make( target: Argument.choice("target", DISCORD_RELEASE_TARGETS).pipe( Argument.withDescription("Discord announcement target: prerelease or latest."), ), - mention: Flag.string("mention").pipe( - Flag.withSchema(Schema.NonEmptyString), - Flag.withDescription("Discord mention text included at the start of the message."), + 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), @@ -145,14 +146,14 @@ export const notifyDiscordReleaseCommand = Command.make( Flag.withDescription("Public GitHub release URL."), ), }, - ({ target, mention, releaseName, version, tag, releaseUrl }) => + ({ target, roleId, releaseName, version, tag, releaseUrl }) => Effect.gen(function* () { const webhookUrl = yield* DiscordWebhookUrl; yield* postDiscordWebhook( webhookUrl, buildDiscordReleaseAnnouncement({ target, - mention, + roleId, releaseName, version, tag, From a51c5918f8b1eacd7e9fa3e3f0e85d31e662525a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 01:58:15 +0000 Subject: [PATCH 4/7] fix(release): use Effect HttpClient for Discord webhook Co-authored-by: Julius Marminge --- scripts/notify-discord-release.ts | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index b580b195f72..036bb7a7709 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -2,8 +2,9 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { Config, Data, Effect, Schema } from "effect"; +import { Config, Data, Effect, Layer, Schema } from "effect"; import { Argument, Command, Flag } from "effect/unstable/cli"; +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; export type DiscordReleaseTarget = "prerelease" | "latest"; @@ -93,24 +94,21 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( webhookUrl: string, payload: DiscordWebhookPayload, ) { - const response = yield* Effect.tryPromise({ - try: () => - fetch(webhookUrl, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify(payload), - }), - catch: (cause) => - new DiscordReleaseAnnouncementError({ - message: "Failed to post Discord release announcement.", - cause, - }), - }); + const httpClient = yield* HttpClient.HttpClient; + const response = yield* HttpClientRequest.post(webhookUrl).pipe( + HttpClientRequest.bodyJson(payload), + Effect.flatMap(httpClient.execute), + Effect.mapError( + (cause) => + new DiscordReleaseAnnouncementError({ + message: "Failed to post Discord release announcement.", + cause, + }), + ), + ); - if (!response.ok) { - const body = yield* Effect.promise(() => response.text().catch(() => "")); + if (response.status < 200 || response.status >= 300) { + const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed(""))); return yield* new DiscordReleaseAnnouncementError({ message: `Discord release announcement failed with HTTP ${response.status}${ body ? `: ${body}` : "" @@ -119,6 +117,8 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( } }); +const runtimeLayer = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer); + export const notifyDiscordReleaseCommand = Command.make( "notify-discord-release", { @@ -166,7 +166,7 @@ export const notifyDiscordReleaseCommand = Command.make( if (import.meta.main) { Command.run(notifyDiscordReleaseCommand, { version: "0.0.0" }).pipe( - Effect.provide(NodeServices.layer), + Effect.provide(runtimeLayer), NodeRuntime.runMain, ); } From 0d943b1fe12c0085349db2ad8d9cab36c956dba8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 02:00:58 +0000 Subject: [PATCH 5/7] fix(release): retry transient Discord webhook failures Co-authored-by: Julius Marminge --- scripts/notify-discord-release.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index 036bb7a7709..1d448c98267 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -4,7 +4,12 @@ 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 { 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"; @@ -94,10 +99,17 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( webhookUrl: string, payload: DiscordWebhookPayload, ) { - const httpClient = yield* HttpClient.HttpClient; - const response = yield* HttpClientRequest.post(webhookUrl).pipe( + const httpClient = (yield* HttpClient.HttpClient).pipe( + HttpClient.retryTransient({ + retryOn: "errors-and-responses", + times: 3, + }), + ); + + yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), + Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError( (cause) => new DiscordReleaseAnnouncementError({ @@ -106,15 +118,6 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( }), ), ); - - if (response.status < 200 || response.status >= 300) { - const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed(""))); - return yield* new DiscordReleaseAnnouncementError({ - message: `Discord release announcement failed with HTTP ${response.status}${ - body ? `: ${body}` : "" - }`, - }); - } }); const runtimeLayer = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer); From ee0a82c5f3584fa5f1b8f3909fe7ffb96b65822c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 02:03:12 +0000 Subject: [PATCH 6/7] fix(release): announce Discord after finalization Co-authored-by: Julius Marminge --- .github/workflows/release.yml | 83 ++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38a44b917b1..6c3ca830fb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -508,32 +508,6 @@ jobs: fail_on_unmatched_files: true token: ${{ steps.app_token.outputs.token }} - - name: Announce prerelease on Discord - if: needs.preflight.outputs.is_prerelease == 'true' - env: - DISCORD_RELEASE_NIGHTLY_ROLE_ID: ${{ vars.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" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" - - - name: Announce latest release on Discord - if: needs.preflight.outputs.make_latest == 'true' - env: - DISCORD_RELEASE_LATEST_ROLE_ID: ${{ vars.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" \ - --release-name "${{ needs.preflight.outputs.release_name }}" \ - --version "${{ needs.preflight.outputs.version }}" \ - --tag "${{ needs.preflight.outputs.tag }}" \ - --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" - finalize: name: Finalize release if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }} @@ -611,3 +585,60 @@ jobs: git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json bun.lock git commit -m "chore(release): prepare $RELEASE_TAG" git push origin HEAD:main + + announce_discord: + name: Announce release on Discord + if: | + always() && !cancelled() && + needs.preflight.result == 'success' && + needs.release.result == 'success' && + (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') + needs: [preflight, release, finalize] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ needs.preflight.outputs.ref }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version-file: package.json + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: package.json + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Announce prerelease on Discord + if: needs.preflight.outputs.is_prerelease == 'true' + continue-on-error: true + env: + DISCORD_RELEASE_NIGHTLY_ROLE_ID: ${{ vars.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" \ + --release-name "${{ needs.preflight.outputs.release_name }}" \ + --version "${{ needs.preflight.outputs.version }}" \ + --tag "${{ needs.preflight.outputs.tag }}" \ + --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" + + - name: Announce latest release on Discord + if: needs.preflight.outputs.make_latest == 'true' + continue-on-error: true + env: + DISCORD_RELEASE_LATEST_ROLE_ID: ${{ vars.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" \ + --release-name "${{ needs.preflight.outputs.release_name }}" \ + --version "${{ needs.preflight.outputs.version }}" \ + --tag "${{ needs.preflight.outputs.tag }}" \ + --release-url "https://github.com/${{ github.repository }}/releases/tag/${{ needs.preflight.outputs.tag }}" From 5b6672898f7ea90b661e02c78a640390a93054eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 1 May 2026 02:07:40 +0000 Subject: [PATCH 7/7] fix(release): use HttpClient status filter Co-authored-by: Julius Marminge --- scripts/notify-discord-release.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/scripts/notify-discord-release.ts b/scripts/notify-discord-release.ts index 1d448c98267..db51efd066a 100644 --- a/scripts/notify-discord-release.ts +++ b/scripts/notify-discord-release.ts @@ -4,12 +4,7 @@ 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 { Argument, Command, Flag } from "effect/unstable/cli"; -import { - FetchHttpClient, - HttpClient, - HttpClientRequest, - HttpClientResponse, -} from "effect/unstable/http"; +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; export type DiscordReleaseTarget = "prerelease" | "latest"; @@ -104,12 +99,12 @@ const postDiscordWebhook = Effect.fn("postDiscordWebhook")(function* ( retryOn: "errors-and-responses", times: 3, }), + HttpClient.filterStatusOk, ); yield* HttpClientRequest.post(webhookUrl).pipe( HttpClientRequest.bodyJson(payload), Effect.flatMap(httpClient.execute), - Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError( (cause) => new DiscordReleaseAnnouncementError({