authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-04 21:47:12-07:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-04 21:47:12-07:00
log94fc117561bbdea1054a020aab33fbe0227ffd58
tree84ad450a12aa5f7fadf25f03e63362a173f3f086
parent113b9d84c786a77b916d5944fdf5e3efdef6bf9f

publish deploy status on relay deploy workflow


3 files changed, 96 insertions(+), 9 deletions(-)

.github/workflows/deploy-relay.yml+25-1
......@@ -8,6 +8,7 @@ on:
88permissions:
99 contents: read
1010 id-token: none
11 statuses: write
1112
1213concurrency:
1314 group: relay-production
......@@ -46,7 +47,8 @@ jobs:
4647 run-install: true
4748
4849 - name: Deploy production relay stage
49 run: vp run --filter t3code-relay deploy --stage prod --yes
50 id: deploy
51 run: vp run --filter t3code-relay deploy --stage prod --yes --github-output
5052 env:
5153 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
5254 PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
......@@ -54,3 +56,25 @@ jobs:
5456 AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
5557 CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
5658 APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }}
59
60 - name: Publish relay deploy commit status
61 uses: actions/github-script@v8
62 with:
63 script: |
64 const result = "${{ steps.deploy.outputs.result }}";
65 const changed = "${{ steps.deploy.outputs.changed }}" === "true";
66 const description = changed
67 ? "Relay production deploy applied infrastructure changes."
68 : result === "noop"
69 ? "Relay production deploy was a no-op."
70 : `Relay production deploy completed with result: ${result}.`;
71
72 await github.rest.repos.createCommitStatus({
73 owner: context.repo.owner,
74 repo: context.repo.repo,
75 sha: context.sha,
76 state: "success",
77 context: "Relay deploy / production",
78 description,
79 target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
80 });
infra/relay/scripts/deploy.test.ts+13-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it } from "vite-plus/test";
22
3import { hasDeployChanges, reconcileRootEnvRelayUrl } from "./deploy.ts";
3import { hasDeployChanges, reconcileRootEnvRelayUrl, serializeGithubOutput } from "./deploy.ts";
44
55describe("hasDeployChanges", () => {
66 it("detects resource, binding, and deletion changes", () => {
......@@ -50,3 +50,15 @@ describe("reconcileRootEnvRelayUrl", () => {
5050 );
5151 });
5252});
53
54describe("serializeGithubOutput", () => {
55 it("serializes relay deploy metadata for GitHub Actions outputs", () => {
56 expect(
57 serializeGithubOutput({
58 changed: false,
59 result: "noop",
60 relay_url: "https://relay.example.test",
61 }),
62 ).toBe("changed=false\nresult=noop\nrelay_url=https://relay.example.test\n");
63 });
64});
infra/relay/scripts/deploy.ts+58-7
......@@ -39,6 +39,7 @@ export interface RelayDeployOptions {
3939 readonly stage: Option.Option<string>;
4040 readonly yes: boolean;
4141 readonly adopt: boolean;
42 readonly githubOutput: boolean;
4243}
4344
4445export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): string {
......@@ -62,6 +63,20 @@ export function hasDeployChanges(plan: Plan.Plan): boolean {
6263 );
6364}
6465
66export type RelayDeployResult = "applied" | "noop" | "dry-run" | "cancelled";
67
68export interface RelayDeployOutcome {
69 readonly result: RelayDeployResult;
70 readonly changed: boolean;
71 readonly relayUrl: Option.Option<string>;
72}
73
74export function serializeGithubOutput(entries: Readonly<Record<string, string | boolean>>): string {
75 return Object.entries(entries)
76 .map(([key, value]) => `${key}=${value}\n`)
77 .join("");
78}
79
6580const relayRoot = Effect.service(Path.Path).pipe(
6681 Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))),
6782);
......@@ -102,6 +117,22 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* (r
102117 yield* Console.log(`Updated ${rootEnvPath} with T3CODE_RELAY_URL=${relayUrl}`);
103118});
104119
120const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* (
121 outcome: RelayDeployOutcome,
122) {
123 const fs = yield* FileSystem.FileSystem;
124 const githubOutputPath = yield* Config.nonEmptyString("GITHUB_OUTPUT");
125 yield* fs.writeFileString(
126 githubOutputPath,
127 serializeGithubOutput({
128 changed: outcome.changed,
129 result: outcome.result,
130 ...(Option.isSome(outcome.relayUrl) ? { relay_url: outcome.relayUrl.value } : {}),
131 }),
132 { flag: "a" },
133 );
134});
135
105136const deployServices = Layer.mergeAll(
106137 Layer.provideMerge(AlchemyContextLive, PlatformServices),
107138 Layer.provide(ProfileLive, PlatformServices),
......@@ -123,11 +154,16 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")(
123154 const plan = yield* Plan.make(stack, { force: options.force }).pipe(
124155 Effect.provide(stack.services),
125156 );
157 const changed = hasDeployChanges(plan);
126158 if (options.dryRun) {
127159 yield* cli.displayPlan(plan);
128 return Option.none<string>();
160 return {
161 result: "dry-run",
162 changed,
163 relayUrl: Option.none<string>(),
164 } satisfies RelayDeployOutcome;
129165 }
130 if (!options.yes && hasDeployChanges(plan)) {
166 if (!options.yes && changed) {
131167 yield* cli.displayPlan(plan);
132168 const approved = yield* Prompt.run(
133169 Prompt.confirm({
......@@ -136,7 +172,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")(
136172 );
137173 if (!approved) {
138174 yield* Console.log("Deployment cancelled.");
139 return Option.none<string>();
175 return {
176 result: "cancelled",
177 changed,
178 relayUrl: Option.none<string>(),
179 } satisfies RelayDeployOutcome;
140180 }
141181 }
142182 const output = yield* Apply.apply(plan).pipe(Effect.provide(stack.services));
......@@ -145,7 +185,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")(
145185 message: "Alchemy relay deploy output did not include a URL",
146186 });
147187 }
148 return Option.some(output.url);
188 return {
189 result: changed ? "applied" : "noop",
190 changed,
191 relayUrl: Option.some(output.url),
192 } satisfies RelayDeployOutcome;
149193 },
150194 (effect, options, configProvider, stage) =>
151195 effect.pipe(
......@@ -171,9 +215,12 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO
171215 Effect.provide(ConfigProvider.layer(configProvider)),
172216 );
173217 const stage = Option.getOrElse(options.stage, () => configuredStage);
174 const relayUrl = yield* runRelayDeploy(options, configProvider, stage);
175 if (Option.isSome(relayUrl)) {
176 yield* reconcileRootEnv(relayUrl.value);
218 const outcome = yield* runRelayDeploy(options, configProvider, stage);
219 if (Option.isSome(outcome.relayUrl)) {
220 yield* reconcileRootEnv(outcome.relayUrl.value);
221 }
222 if (options.githubOutput) {
223 yield* writeGithubOutput(outcome);
177224 }
178225});
179226
......@@ -206,6 +253,10 @@ export const relayDeployCommand = Command.make(
206253 Flag.withDescription("Adopt pre-existing cloud resources that conflict with this stack."),
207254 Flag.withDefault(false),
208255 ),
256 githubOutput: Flag.boolean("github-output").pipe(
257 Flag.withDescription("Append relay deployment metadata to GITHUB_OUTPUT."),
258 Flag.withDefault(false),
259 ),
209260 },
210261 deploy,
211262).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy."));