{"version":3,"file":"image-deploy.cjs","names":["run","readFileSync","join","colors","icons"],"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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"}