From 94fc117561bbdea1054a020aab33fbe0227ffd58 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 4 Jun 2026 21:47:12 -0700 Subject: [PATCH] publish deploy status on relay deploy workflow --- .github/workflows/deploy-relay.yml | 26 +++++++++++- infra/relay/scripts/deploy.test.ts | 14 ++++++- infra/relay/scripts/deploy.ts | 65 ++++++++++++++++++++++++++---- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml index d813135c568cfd248d0bae26d58885afe2acc07f..1f777ed2911c9fce4e596d4d5b00df09270ea9ff 100644 --- a/.github/workflows/deploy-relay.yml +++ b/.github/workflows/deploy-relay.yml @@ -8,6 +8,7 @@ on: permissions: contents: read id-token: none + statuses: write concurrency: group: relay-production @@ -46,7 +47,8 @@ jobs: run-install: true - name: Deploy production relay stage - run: vp run --filter t3code-relay deploy --stage prod --yes + id: deploy + run: vp run --filter t3code-relay deploy --stage prod --yes --github-output env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }} @@ -54,3 +56,25 @@ jobs: AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }} + + - name: Publish relay deploy commit status + uses: actions/github-script@v8 + with: + script: | + const result = "${{ steps.deploy.outputs.result }}"; + const changed = "${{ steps.deploy.outputs.changed }}" === "true"; + const description = changed + ? "Relay production deploy applied infrastructure changes." + : result === "noop" + ? "Relay production deploy was a no-op." + : `Relay production deploy completed with result: ${result}.`; + + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: "success", + context: "Relay deploy / production", + description, + target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + }); diff --git a/infra/relay/scripts/deploy.test.ts b/infra/relay/scripts/deploy.test.ts index b8cb9b583ff2f3e75391a4d8d57df2a2ca53d7f4..db4d3df3122f471f08fc3d898e2f8d06ac0af0c8 100644 --- a/infra/relay/scripts/deploy.test.ts +++ b/infra/relay/scripts/deploy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { hasDeployChanges, reconcileRootEnvRelayUrl } from "./deploy.ts"; +import { hasDeployChanges, reconcileRootEnvRelayUrl, serializeGithubOutput } from "./deploy.ts"; describe("hasDeployChanges", () => { it("detects resource, binding, and deletion changes", () => { @@ -50,3 +50,15 @@ describe("reconcileRootEnvRelayUrl", () => { ); }); }); + +describe("serializeGithubOutput", () => { + it("serializes relay deploy metadata for GitHub Actions outputs", () => { + expect( + serializeGithubOutput({ + changed: false, + result: "noop", + relay_url: "https://relay.example.test", + }), + ).toBe("changed=false\nresult=noop\nrelay_url=https://relay.example.test\n"); + }); +}); diff --git a/infra/relay/scripts/deploy.ts b/infra/relay/scripts/deploy.ts index b2d1ecf0e87e0c454371e7f93aac5f694f8451c0..eb90a2ead334d79d16f196cdcd9b29b9b712ea2e 100644 --- a/infra/relay/scripts/deploy.ts +++ b/infra/relay/scripts/deploy.ts @@ -39,6 +39,7 @@ export interface RelayDeployOptions { readonly stage: Option.Option; readonly yes: boolean; readonly adopt: boolean; + readonly githubOutput: boolean; } export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): string { @@ -62,6 +63,20 @@ export function hasDeployChanges(plan: Plan.Plan): boolean { ); } +export type RelayDeployResult = "applied" | "noop" | "dry-run" | "cancelled"; + +export interface RelayDeployOutcome { + readonly result: RelayDeployResult; + readonly changed: boolean; + readonly relayUrl: Option.Option; +} + +export function serializeGithubOutput(entries: Readonly>): string { + return Object.entries(entries) + .map(([key, value]) => `${key}=${value}\n`) + .join(""); +} + const relayRoot = Effect.service(Path.Path).pipe( Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), ); @@ -102,6 +117,22 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* (r yield* Console.log(`Updated ${rootEnvPath} with T3CODE_RELAY_URL=${relayUrl}`); }); +const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* ( + outcome: RelayDeployOutcome, +) { + const fs = yield* FileSystem.FileSystem; + const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT"); + yield* fs.writeFileString( + githubOutputPath, + serializeGithubOutput({ + changed: outcome.changed, + result: outcome.result, + ...(Option.isSome(outcome.relayUrl) ? { relay_url: outcome.relayUrl.value } : {}), + }), + { flag: "a" }, + ); +}); + const deployServices = Layer.mergeAll( Layer.provideMerge(AlchemyContextLive, PlatformServices), Layer.provide(ProfileLive, PlatformServices), @@ -123,11 +154,16 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( const plan = yield* Plan.make(stack, { force: options.force }).pipe( Effect.provide(stack.services), ); + const changed = hasDeployChanges(plan); if (options.dryRun) { yield* cli.displayPlan(plan); - return Option.none(); + return { + result: "dry-run", + changed, + relayUrl: Option.none(), + } satisfies RelayDeployOutcome; } - if (!options.yes && hasDeployChanges(plan)) { + if (!options.yes && changed) { yield* cli.displayPlan(plan); const approved = yield* Prompt.run( Prompt.confirm({ @@ -136,7 +172,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( ); if (!approved) { yield* Console.log("Deployment cancelled."); - return Option.none(); + return { + result: "cancelled", + changed, + relayUrl: Option.none(), + } satisfies RelayDeployOutcome; } } const output = yield* Apply.apply(plan).pipe(Effect.provide(stack.services)); @@ -145,7 +185,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( message: "Alchemy relay deploy output did not include a URL", }); } - return Option.some(output.url); + return { + result: changed ? "applied" : "noop", + changed, + relayUrl: Option.some(output.url), + } satisfies RelayDeployOutcome; }, (effect, options, configProvider, stage) => effect.pipe( @@ -171,9 +215,12 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO Effect.provide(ConfigProvider.layer(configProvider)), ); const stage = Option.getOrElse(options.stage, () => configuredStage); - const relayUrl = yield* runRelayDeploy(options, configProvider, stage); - if (Option.isSome(relayUrl)) { - yield* reconcileRootEnv(relayUrl.value); + const outcome = yield* runRelayDeploy(options, configProvider, stage); + if (Option.isSome(outcome.relayUrl)) { + yield* reconcileRootEnv(outcome.relayUrl.value); + } + if (options.githubOutput) { + yield* writeGithubOutput(outcome); } }); @@ -206,6 +253,10 @@ export const relayDeployCommand = Command.make( Flag.withDescription("Adopt pre-existing cloud resources that conflict with this stack."), Flag.withDefault(false), ), + githubOutput: Flag.boolean("github-output").pipe( + Flag.withDescription("Append relay deployment metadata to GITHUB_OUTPUT."), + Flag.withDefault(false), + ), }, deploy, ).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy.")); -- 2.54.0