everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 14.8 kB
Source Map (JSON)
{"version":3,"file":"image-deploy.mjs","names":[],"sources":["../src/image-deploy.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { run } from \"./utils/run\";\nimport { colors, icons } from \"./utils/theme\";\n\nexport interface ResolvedImageRef {\n image: string;\n source: \"ci\" | \"env\" | \"repository\";\n}\n\n/**\n * Image reference resolution precedence (ADR 0021): `ci.image` in\n * bos.config.json → `BOS_IMAGE` env → derived from `repository`.\n */\nexport function resolveImageRef(input: {\n ciImage?: string;\n repository?: string;\n env?: Record<string, string | undefined>;\n}): ResolvedImageRef | undefined {\n const envImage = input.env?.BOS_IMAGE;\n if (input.ciImage) return { image: input.ciImage, source: \"ci\" };\n if (envImage) return { image: envImage, source: \"env\" };\n\n const repo = input.repository\n ?.replace(/^https?:\\/\\/(www\\.)?github\\.com\\//i, \"\")\n .replace(/\\.git$/i, \"\")\n .replace(/\\/+$/, \"\")\n .toLowerCase();\n if (!repo?.includes(\"/\") || repo.split(\"/\").length !== 2) return undefined;\n return { image: `ghcr.io/${repo}`, source: \"repository\" };\n}\n\nexport async function hasDocker(): Promise<boolean> {\n try {\n const result = await run(\"docker\", [\"info\", \"--format\", \"{{.ServerVersion}}\"], {\n capture: true,\n });\n return result?.exitCode === 0;\n } catch {\n return false;\n }\n}\n\nfunction parseDigest(pushOutput: string): string | undefined {\n const match = pushOutput.match(/digest:\\s*(sha256:[a-f0-9]+)/i);\n return match?.[1];\n}\n\nconst SEMVER_PATTERN = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?$/;\n\n/**\n * The runtime framework version baked into the image is the workspace source's\n * `everything-dev` package version — the image builds from `configDir`, so that\n * file is the truth. Missing (child repos without the workspace) → undefined.\n */\nfunction readFrameworkVersion(configDir: string): string | undefined {\n try {\n const raw = readFileSync(\n join(configDir, \"packages\", \"everything-dev\", \"package.json\"),\n \"utf-8\",\n );\n const pkg = JSON.parse(raw) as { version?: unknown };\n return typeof pkg.version === \"string\" && SEMVER_PATTERN.test(pkg.version)\n ? pkg.version\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface ImageTags {\n /** Exact refs to build and push, in push order (latest excluded). */\n tags: string[];\n /** Push `:latest` after the pinned tags. */\n pushLatest: boolean;\n /** Human-readable reason when `latest` is held. */\n latestHoldReason?: string;\n /** Push `:rc` after the pinned tags — the floating prerelease-train ref. */\n pushRc: boolean;\n}\n\n/**\n * Tag scheme (ADR 0021): `sha-<short>` always; the exact `v<version>` tag; a\n * floating major tag (`v2`) on stable releases only. `latest` is held during\n * prereleases (npm's `rc` dist-tag analog) so existing deployments pinned to\n * `:latest` keep serving the last stable image — and `:rc` is the floating\n * ref for the prerelease train (npm's `rc` dist-tag mirror): it moves on\n * every prerelease deploy and freezes at the last rc-train build once the\n * workspace releases a stable version.\n */\nexport function computeImageTags(input: { shortSha: string; version?: string }): ImageTags {\n const tags = [`sha-${input.shortSha}`];\n const match = input.version?.trim().match(SEMVER_PATTERN);\n if (!match) {\n return { tags, pushLatest: true, pushRc: false };\n }\n const [, major, minor, patch, prerelease] = match;\n const exact = prerelease\n ? `v${major}.${minor}.${patch}-${prerelease}`\n : `v${major}.${minor}.${patch}`;\n if (prerelease) {\n return {\n tags: [...tags, exact],\n pushLatest: false,\n latestHoldReason: `${exact} is a prerelease — :latest still serves the last stable image`,\n pushRc: true,\n };\n }\n return { tags: [...tags, exact, `v${major}`], pushLatest: true, pushRc: false };\n}\n\nexport interface ImageDeployResult {\n image: string;\n /** Primary pinned tag (the sha tag). */\n tag: string;\n /** All tags pushed, including the primary. */\n tags: string[];\n latestPushed: boolean;\n /** Whether the floating `:rc` prerelease-train ref was pushed. */\n rcPushed: boolean;\n digest?: string;\n}\n\nexport type DeployImagePlan =\n | { kind: \"prebuilt\"; image: string; digest: string }\n | { kind: \"build\"; image: string }\n | { kind: \"skip\"; reason: string };\n\nconst DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;\n\n/** A Railway pin is `FROM image@<digest>` — anything else cannot resolve. */\nexport function isValidImageDigest(digest: string): boolean {\n return DIGEST_PATTERN.test(digest);\n}\n\n/**\n * Decide the image leg's source: a digest handed over from a pre-pushed image\n * (the CI image job) wins; otherwise the local docker build runs when the\n * toolchain and Dockerfile are present (ADR 0021, as amended).\n */\nexport function resolveDeployImagePlan(input: {\n imageRef: ResolvedImageRef | undefined;\n imageDigest?: string;\n hasDocker: boolean;\n hasDockerfile: boolean;\n}): DeployImagePlan {\n if (!input.imageRef) {\n return {\n kind: \"skip\",\n reason: \"set ci.image in bos.config.json (or BOS_IMAGE) to build and push the runtime image\",\n };\n }\n if (input.imageDigest) {\n return { kind: \"prebuilt\", image: input.imageRef.image, digest: input.imageDigest };\n }\n if (!input.hasDocker) {\n return { kind: \"skip\", reason: \"docker is not available\" };\n }\n if (!input.hasDockerfile) {\n return { kind: \"skip\", reason: \"no Dockerfile at the config root\" };\n }\n return { kind: \"build\", image: input.imageRef.image };\n}\n\n/**\n * Image leg (ADR 0021): build the runtime stage, tag `sha-<short>` + version\n * tags, push them, capture the digest, then push the floating refs — `:latest`\n * unless the version is a prerelease, and `:rc` when it is. Callers skip the\n * leg with a notice when no image is configured (child repos get\n * build+upload+publish only).\n */\nexport async function buildAndPushImage(input: {\n image: string;\n configDir: string;\n verbose?: boolean;\n}): Promise<ImageDeployResult> {\n const shaResult = await run(\"git\", [\"rev-parse\", \"--short=7\", \"HEAD\"], {\n cwd: input.configDir,\n capture: true,\n });\n const shortSha = shaResult?.stdout.trim();\n if (!shaResult || shaResult.exitCode !== 0 || !shortSha) {\n const detail = shaResult?.stderr.trim();\n throw new Error(\n `Failed to resolve the current git SHA in ${input.configDir} (exit code ${shaResult?.exitCode ?? \"unknown\"}${detail ? `: ${detail}` : \"\"})`,\n );\n }\n\n const version = readFrameworkVersion(input.configDir);\n const { tags, pushLatest, latestHoldReason, pushRc } = computeImageTags({\n shortSha,\n version,\n });\n const refs = tags.map((tag) => `${input.image}:${tag}`);\n const latestRef = `${input.image}:latest`;\n const rcRef = `${input.image}:rc`;\n const buildRefs = pushLatest ? [...refs, latestRef] : [...refs];\n if (pushRc) buildRefs.push(rcRef);\n\n console.log(\n ` Building runtime image ${colors.cyan(buildRefs.join(\", \"))}${pushLatest ? \"\" : colors.yellow(\" (latest held)\")}`,\n );\n await run(\n \"docker\",\n [\"build\", \"--target\", \"runtime\", ...buildRefs.flatMap((ref) => [\"-t\", ref]), \".\"],\n { cwd: input.configDir },\n );\n\n console.log(` Pushing ${colors.cyan(refs.join(\", \"))}...`);\n let digest: string | undefined;\n for (const ref of refs) {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n }\n\n const pushFloating = async (ref: string): Promise<boolean> => {\n const push = await run(\"docker\", [\"push\", ref], {\n cwd: input.configDir,\n capture: true,\n });\n if (!push || push.exitCode !== 0) {\n throw new Error(`docker push ${ref} failed with exit code ${push?.exitCode ?? \"unknown\"}`);\n }\n digest ??= parseDigest(push.stdout);\n if (input.verbose && push.stdout.trim()) {\n console.log(colors.dim(push.stdout.trim()));\n }\n return true;\n };\n\n let latestPushed = false;\n if (pushLatest) {\n latestPushed = await pushFloating(latestRef);\n } else {\n console.log(colors.yellow(` latest held: ${latestHoldReason}`));\n }\n const rcPushed = pushRc ? await pushFloating(rcRef) : false;\n\n if (!digest) {\n console.log(colors.yellow(\" Could not capture the image digest from push output\"));\n }\n\n return { image: input.image, tag: tags[0]!, tags, latestPushed, rcPushed, digest };\n}\n\n/**\n * Railway leg (ADR 0021): pull-only deploy of the pushed image digest — a\n * thin `FROM image@digest` Dockerfile pins the digest so Railway never\n * rebuilds; `RAILWAY_DOCKERFILE_PATH` points at the generated file.\n */\nexport async function deployImageToRailway(input: {\n image: string;\n digest: string;\n service: string;\n configDir: string;\n}): Promise<void> {\n const deployDir = join(input.configDir, \".bos\", \"deploy\");\n mkdirSync(deployDir, { recursive: true });\n const dockerfilePath = join(deployDir, \"Dockerfile\");\n writeFileSync(dockerfilePath, `FROM ${input.image}@${input.digest}\\n`);\n\n console.log(` Deploying to Railway service ${colors.cyan(input.service)} (pull-only)...`);\n const result = await run(\"railway\", [\"up\", \"--service\", input.service, \"--ci\"], {\n cwd: input.configDir,\n capture: true,\n env: { RAILWAY_DOCKERFILE_PATH: \".bos/deploy/Dockerfile\" },\n });\n if (result?.stdout) {\n for (const line of result.stdout.split(\"\\n\")) {\n if (line.trim()) console.log(` ${colors.dim(line.trim())}`);\n }\n }\n if (!result || result.exitCode !== 0) {\n throw new Error(\n result?.stderr.trim()\n ? `railway up failed: ${result.stderr.trim().split(\"\\n\").slice(-3).join(\" \")}`\n : `railway up failed with exit code ${result?.exitCode ?? \"unknown\"}`,\n );\n }\n console.log(colors.green(` ${icons.ok} Railway deploy complete`));\n}\n"],"mappings":"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"}