| ... | @@ -39,6 +39,7 @@ export interface RelayDeployOptions { | ... | @@ -39,6 +39,7 @@ export interface RelayDeployOptions { |
| 39 | readonly stage: Option.Option<string>; | 39 | readonly stage: Option.Option<string>; |
| 40 | readonly yes: boolean; | 40 | readonly yes: boolean; |
| 41 | readonly adopt: boolean; | 41 | readonly adopt: boolean; |
| | 42 | readonly githubOutput: boolean; |
| 42 | } | 43 | } |
| 43 | | 44 | |
| 44 | export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): string { | 45 | export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): string { |
| ... | @@ -62,6 +63,20 @@ export function hasDeployChanges(plan: Plan.Plan): boolean { | ... | @@ -62,6 +63,20 @@ export function hasDeployChanges(plan: Plan.Plan): boolean { |
| 62 | ); | 63 | ); |
| 63 | } | 64 | } |
| 64 | | 65 | |
| | 66 | export type RelayDeployResult = "applied" | "noop" | "dry-run" | "cancelled"; |
| | 67 | |
| | 68 | export interface RelayDeployOutcome { |
| | 69 | readonly result: RelayDeployResult; |
| | 70 | readonly changed: boolean; |
| | 71 | readonly relayUrl: Option.Option<string>; |
| | 72 | } |
| | 73 | |
| | 74 | export 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 | |
| 65 | const relayRoot = Effect.service(Path.Path).pipe( | 80 | const relayRoot = Effect.service(Path.Path).pipe( |
| 66 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), | 81 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 67 | ); | 82 | ); |
| ... | @@ -102,6 +117,22 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* (r | ... | @@ -102,6 +117,22 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* (r |
| 102 | yield* Console.log(`Updated ${rootEnvPath} with T3CODE_RELAY_URL=${relayUrl}`); | 117 | yield* Console.log(`Updated ${rootEnvPath} with T3CODE_RELAY_URL=${relayUrl}`); |
| 103 | }); | 118 | }); |
| 104 | | 119 | |
| | 120 | const 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 | |
| 105 | const deployServices = Layer.mergeAll( | 136 | const deployServices = Layer.mergeAll( |
| 106 | Layer.provideMerge(AlchemyContextLive, PlatformServices), | 137 | Layer.provideMerge(AlchemyContextLive, PlatformServices), |
| 107 | Layer.provide(ProfileLive, PlatformServices), | 138 | Layer.provide(ProfileLive, PlatformServices), |
| ... | @@ -123,11 +154,16 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -123,11 +154,16 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 123 | const plan = yield* Plan.make(stack, { force: options.force }).pipe( | 154 | const plan = yield* Plan.make(stack, { force: options.force }).pipe( |
| 124 | Effect.provide(stack.services), | 155 | Effect.provide(stack.services), |
| 125 | ); | 156 | ); |
| | 157 | const changed = hasDeployChanges(plan); |
| 126 | if (options.dryRun) { | 158 | if (options.dryRun) { |
| 127 | yield* cli.displayPlan(plan); | 159 | 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; |
| 129 | } | 165 | } |
| 130 | if (!options.yes && hasDeployChanges(plan)) { | 166 | if (!options.yes && changed) { |
| 131 | yield* cli.displayPlan(plan); | 167 | yield* cli.displayPlan(plan); |
| 132 | const approved = yield* Prompt.run( | 168 | const approved = yield* Prompt.run( |
| 133 | Prompt.confirm({ | 169 | Prompt.confirm({ |
| ... | @@ -136,7 +172,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -136,7 +172,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 136 | ); | 172 | ); |
| 137 | if (!approved) { | 173 | if (!approved) { |
| 138 | yield* Console.log("Deployment cancelled."); | 174 | 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; |
| 140 | } | 180 | } |
| 141 | } | 181 | } |
| 142 | const output = yield* Apply.apply(plan).pipe(Effect.provide(stack.services)); | 182 | const output = yield* Apply.apply(plan).pipe(Effect.provide(stack.services)); |
| ... | @@ -145,7 +185,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -145,7 +185,11 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 145 | message: "Alchemy relay deploy output did not include a URL", | 185 | message: "Alchemy relay deploy output did not include a URL", |
| 146 | }); | 186 | }); |
| 147 | } | 187 | } |
| 148 | return Option.some(output.url); | 188 | return { |
| | 189 | result: changed ? "applied" : "noop", |
| | 190 | changed, |
| | 191 | relayUrl: Option.some(output.url), |
| | 192 | } satisfies RelayDeployOutcome; |
| 149 | }, | 193 | }, |
| 150 | (effect, options, configProvider, stage) => | 194 | (effect, options, configProvider, stage) => |
| 151 | effect.pipe( | 195 | effect.pipe( |
| ... | @@ -171,9 +215,12 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO | ... | @@ -171,9 +215,12 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO |
| 171 | Effect.provide(ConfigProvider.layer(configProvider)), | 215 | Effect.provide(ConfigProvider.layer(configProvider)), |
| 172 | ); | 216 | ); |
| 173 | const stage = Option.getOrElse(options.stage, () => configuredStage); | 217 | const stage = Option.getOrElse(options.stage, () => configuredStage); |
| 174 | const relayUrl = yield* runRelayDeploy(options, configProvider, stage); | 218 | const outcome = yield* runRelayDeploy(options, configProvider, stage); |
| 175 | if (Option.isSome(relayUrl)) { | 219 | if (Option.isSome(outcome.relayUrl)) { |
| 176 | yield* reconcileRootEnv(relayUrl.value); | 220 | yield* reconcileRootEnv(outcome.relayUrl.value); |
| | 221 | } |
| | 222 | if (options.githubOutput) { |
| | 223 | yield* writeGithubOutput(outcome); |
| 177 | } | 224 | } |
| 178 | }); | 225 | }); |
| 179 | | 226 | |
| ... | @@ -206,6 +253,10 @@ export const relayDeployCommand = Command.make( | ... | @@ -206,6 +253,10 @@ export const relayDeployCommand = Command.make( |
| 206 | Flag.withDescription("Adopt pre-existing cloud resources that conflict with this stack."), | 253 | Flag.withDescription("Adopt pre-existing cloud resources that conflict with this stack."), |
| 207 | Flag.withDefault(false), | 254 | Flag.withDefault(false), |
| 208 | ), | 255 | ), |
| | 256 | githubOutput: Flag.boolean("github-output").pipe( |
| | 257 | Flag.withDescription("Append relay deployment metadata to GITHUB_OUTPUT."), |
| | 258 | Flag.withDefault(false), |
| | 259 | ), |
| 209 | }, | 260 | }, |
| 210 | deploy, | 261 | deploy, |
| 211 | ).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy.")); | 262 | ).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy.")); |