| 1 | /** |
| 2 | * Railway CLI `up` equivalent: POST a gzipped Docker context to |
| 3 | * backboard `/project/:id/environment/:id/up`. Railway builds the |
| 4 | * generated Dockerfile; Alchemy never pushes an image. |
| 5 | */ |
| 6 | import * as Data from "effect/Data"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Redacted from "effect/Redacted"; |
| 9 | import * as HttpClient from "effect/http/HttpClient"; |
| 10 | import * as HttpClientRequest from "effect/http/HttpClientRequest"; |
| 11 | import { RailwayEnvironment } from "./Environment.ts"; |
| 12 | |
| 13 | export class DeployUploadFailed extends Data.TaggedError( |
| 14 | "Railway.DeployUploadFailed", |
| 15 | )<{ |
| 16 | status: number; |
| 17 | message: string; |
| 18 | }> {} |
| 19 | |
| 20 | export type UpResponse = { |
| 21 | readonly deploymentId: string; |
| 22 | readonly url: string; |
| 23 | readonly logsUrl: string; |
| 24 | readonly deploymentDomain: string; |
| 25 | }; |
| 26 | |
| 27 | const parseUpResponse = (value: unknown): UpResponse | undefined => { |
| 28 | if (value === null || typeof value !== "object") return undefined; |
| 29 | const rec = value as Record<string, unknown>; |
| 30 | if (typeof rec.deploymentId !== "string" || rec.deploymentId.length === 0) { |
| 31 | return undefined; |
| 32 | } |
| 33 | return { |
| 34 | deploymentId: rec.deploymentId, |
| 35 | url: typeof rec.url === "string" ? rec.url : "", |
| 36 | logsUrl: typeof rec.logsUrl === "string" ? rec.logsUrl : "", |
| 37 | deploymentDomain: |
| 38 | typeof rec.deploymentDomain === "string" ? rec.deploymentDomain : "", |
| 39 | }; |
| 40 | }; |
| 41 | |
| 42 | const uploadUrl = (input: { |
| 43 | apiBaseUrl: string; |
| 44 | projectId: string; |
| 45 | environmentId: string; |
| 46 | serviceId: string; |
| 47 | message?: string; |
| 48 | }): string => { |
| 49 | const base = input.apiBaseUrl.replace(/\/+$/, ""); |
| 50 | const url = new URL( |
| 51 | `${base}/project/${input.projectId}/environment/${input.environmentId}/up`, |
| 52 | ); |
| 53 | url.searchParams.set("serviceId", input.serviceId); |
| 54 | if (input.message !== undefined && input.message.length > 0) { |
| 55 | url.searchParams.set("message", input.message); |
| 56 | } |
| 57 | return url.toString(); |
| 58 | }; |
| 59 | |
| 60 | /** |
| 61 | * Upload a gzipped tar of a generated Docker context. Railway builds |
| 62 | * and deploys it. Same contract as `railway up`. |
| 63 | */ |
| 64 | export const uploadDeployTarball = Effect.fn(function* (input: { |
| 65 | projectId: string; |
| 66 | environmentId: string; |
| 67 | serviceId: string; |
| 68 | tarball: Uint8Array; |
| 69 | message?: string; |
| 70 | }) { |
| 71 | const env = yield* yield* RailwayEnvironment; |
| 72 | const http = yield* HttpClient.HttpClient; |
| 73 | const token = Redacted.value(env.token); |
| 74 | const url = uploadUrl({ |
| 75 | apiBaseUrl: env.apiBaseUrl, |
| 76 | projectId: input.projectId, |
| 77 | environmentId: input.environmentId, |
| 78 | serviceId: input.serviceId, |
| 79 | message: input.message, |
| 80 | }); |
| 81 | let request = HttpClientRequest.post(url).pipe( |
| 82 | HttpClientRequest.bodyUint8Array(input.tarball, "application/gzip"), |
| 83 | ); |
| 84 | request = |
| 85 | env.tokenKind === "project" |
| 86 | ? request.pipe(HttpClientRequest.setHeader("Project-Access-Token", token)) |
| 87 | : request.pipe(HttpClientRequest.bearerToken(token)); |
| 88 | const response = yield* http.execute(request); |
| 89 | if (response.status !== 200) { |
| 90 | const body = yield* response.text.pipe(Effect.orElseSucceed(() => "")); |
| 91 | return yield* new DeployUploadFailed({ |
| 92 | status: response.status, |
| 93 | message: |
| 94 | body.length > 0 |
| 95 | ? body |
| 96 | : `Failed to upload code with status ${String(response.status)}`, |
| 97 | }); |
| 98 | } |
| 99 | const json = yield* response.json; |
| 100 | const parsed = parseUpResponse(json); |
| 101 | if (parsed === undefined) { |
| 102 | return yield* new DeployUploadFailed({ |
| 103 | status: response.status, |
| 104 | message: "Railway /up did not return a deploymentId", |
| 105 | }); |
| 106 | } |
| 107 | return parsed; |
| 108 | }); |