everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
368 lines (366 loc) • 12 kB
JavaScript
import { colors, icons } from "../../utils/theme.mjs";
import { buildAndPushImage, deployImageToRailway, hasDocker, imageRefWithDigest, repointServiceSource, resolveImageRef, resolveRegistryDigest } from "./image.mjs";
import { deriveImageRefFromPlatformManifest } from "./platform-manifest.mjs";
import { rollbackToPreviousSnapshot } from "./rollback-flow.mjs";
import { runSmoke } from "./smoke.mjs";
import { existsSync } from "node:fs";
import { join } from "node:path";
import process from "node:process";
//#region src/services/deploy/legs.ts
function resolveRuntimeTier(input) {
return input.imageEnv || input.hasDockerfile ? "root" : "child";
}
/**
* The child image pin: the platform release manifest resolves through the
* extends chain (authored `platform` pin, else nearest-wins) to `ref:tag`,
* and the registry resolves the tag's digest. Undefined when no manifest
* resolves — the additive fallback (ADR 0034) is today's float redeploy.
*/
async function resolveChildImagePin(input) {
const derived = await deriveImageRefFromPlatformManifest({
configDir: input.configDir,
localConfig: input.localConfig,
fetchManifest: input.fetchManifest
});
if (!derived) return void 0;
const digest = await (input.fetchDigest ?? resolveRegistryDigest)({ ref: `${derived.image}:${derived.tag}` });
return {
image: derived.image,
tag: derived.tag,
digest
};
}
function resolveDeployPlan(input) {
let image;
if (input.digestHandover && input.imageRef) image = {
action: "prebuilt",
image: input.imageRef.image,
digest: input.digestHandover
};
else if (input.tier === "child") image = input.childPin ? {
action: "pinned",
image: input.childPin.image,
tag: input.childPin.tag,
digest: input.childPin.digest
} : {
action: "skip",
reason: "child runtime inherits the root's universal image (ADR 0020) — no image leg in this train"
};
else if (!input.imageRef) image = {
action: "skip",
reason: "set BOS_IMAGE (or repository in bos.config.json) to build and push the runtime image"
};
else if (!input.hasDocker) image = {
action: "skip",
reason: "docker is not available"
};
else if (!input.hasDockerfile) image = {
action: "skip",
reason: "no Dockerfile at the config root"
};
else image = {
action: "build",
image: input.imageRef.image
};
let smoke;
if (input.smokeRequested) {
if (image.action === "pinned" || image.action === "prebuilt") smoke = {
action: "boot",
image: image.image,
ref: imageRefWithDigest(image.image, image.digest)
};
else if (image.action === "build") smoke = {
action: "boot",
image: image.image
};
else smoke = {
action: "skip",
reason: input.tier === "child" ? "child runtime inherits the root's universal image — no image digest to boot in this train" : "no runtime image digest — pass BOS_IMAGE_DIGEST for a pre-pushed image"
};
}
return {
tier: input.tier,
image,
...smoke ? { smoke } : {}
};
}
/**
* The railway step, decided after the image leg ran: a root runtime must
* have pinned bytes (`pinnedDigest` — handover, pin, or build capture);
* without them the "pulled bytes are what this train validated" guarantee
* (ADR 0021) is unmeetable, and that is a failure, not a skip. A child
* runtime never needs a digest: with a pin the service source repoints to
* the pinned ref before the redeploy pulls it; without one, `redeploy
* --from-source` is just a reload trigger for the freshly published config.
* The token is project-scoped and cannot touch another project's service.
*/
function resolveRailwayStep(input) {
if (!input.token) return {
action: "skip",
detail: "RAILWAY_TOKEN not set"
};
if (!input.service) return input.tier === "root" ? {
action: "fail",
detail: "ci.railway.service is not configured in bos.config.json"
} : {
action: "skip",
detail: "set ci.railway.service in bos.config.json to trigger a Railway redeploy"
};
if (input.tier === "root" && !input.pinnedDigest) return {
action: "fail",
detail: "Railway deploy requires a runtime image (pass BOS_IMAGE_DIGEST for a pre-pushed image)"
};
if (input.childPin) return {
action: "repoint-and-redeploy",
service: input.service,
image: `${input.childPin.image}:${input.childPin.tag}`
};
return {
action: "redeploy",
service: input.service
};
}
const realPostPublishLegsDeps = {
buildAndPushImage,
hasDocker,
runSmoke,
deployImageToRailway,
repointServiceSource,
resolveChildImagePin,
rollbackToPreviousSnapshot
};
async function runPostPublishLegs(input, deps = realPostPublishLegsDeps) {
const legs = {};
let image;
let imageDigest;
let service;
const hasDockerfile = existsSync(join(input.sessionRoot, "Dockerfile"));
const tier = resolveRuntimeTier({
imageEnv: process.env.BOS_IMAGE,
hasDockerfile
});
let childPin;
if (tier === "child") try {
childPin = await deps.resolveChildImagePin({
configDir: input.sessionRoot,
localConfig: input.localConfig ?? {}
});
} catch (error) {
const detail = `Image pin failed: ${error instanceof Error ? error.message : String(error)}`;
legs.image = {
status: "failed",
detail
};
console.log(colors.yellow(` ${detail}`));
return {
legs,
failure: `Config published but ${detail}`
};
}
const plan = resolveDeployPlan({
tier,
childPin,
imageRef: resolveImageRef({
repository: input.repository,
env: process.env
}),
digestHandover: input.imageDigestInput,
hasDocker: tier === "root" ? await deps.hasDocker() : false,
hasDockerfile,
smokeRequested: input.smoke
});
switch (plan.image.action) {
case "skip":
legs.image = {
status: "skipped",
detail: plan.image.reason
};
console.log();
console.log(colors.yellow(` Image skipped: ${plan.image.reason}`));
break;
case "pinned":
image = plan.image.image;
imageDigest = plan.image.digest;
legs.image = {
status: "ok",
detail: `pinned ${plan.image.image}:${plan.image.tag} @ ${plan.image.digest.slice(0, 19)}…`
};
console.log();
console.log(colors.green(` ${icons.ok} Pinned ${plan.image.image}:${plan.image.tag} @ ${plan.image.digest.slice(0, 19)}… (platform manifest)`));
break;
case "prebuilt":
image = plan.image.image;
imageDigest = plan.image.digest;
legs.image = {
status: "ok",
detail: `pre-pushed ${plan.image.image}@${plan.image.digest.slice(0, 19)}…`
};
console.log();
console.log(colors.green(` ${icons.ok} Using pre-pushed image ${plan.image.image}@${plan.image.digest.slice(0, 19)}…`));
break;
case "build":
console.log();
try {
const imageResult = await deps.buildAndPushImage({
image: plan.image.image,
configDir: input.sessionRoot,
verbose: input.verbose
});
image = imageResult.image;
imageDigest = imageResult.digest;
legs.image = {
status: "ok",
detail: `built and pushed ${imageResult.image}:${imageResult.tag}`
};
console.log(colors.green(` ${icons.ok} Image pushed ${imageResult.image}:${imageResult.tag}${imageResult.digest ? ` (${imageResult.digest.slice(0, 19)}…)` : ""}`));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const imageError = message.includes("not found") || message.includes("ENOENT") ? "Docker not found — install Docker to build the runtime image" : `Image build/push failed: ${message}`;
legs.image = {
status: "failed",
detail: imageError
};
console.log(colors.yellow(` ${imageError}`));
return {
legs,
failure: `Config published but ${imageError}`
};
}
}
if (plan.smoke) {
if (plan.smoke.action === "skip") {
legs.smoke = {
status: "skipped",
detail: plan.smoke.reason
};
console.log();
console.log(colors.yellow(` Post-publish smoke skipped: ${plan.smoke.reason}`));
} else {
const ref = plan.smoke.ref ?? (imageDigest ? imageRefWithDigest(plan.smoke.image, imageDigest) : void 0);
if (!ref || !imageDigest) {
const smokeSkip = "no runtime image digest — pass BOS_IMAGE_DIGEST for a pre-pushed image";
legs.smoke = {
status: "skipped",
detail: smokeSkip
};
console.log();
console.log(colors.yellow(` Post-publish smoke skipped: ${smokeSkip}`));
} else {
console.log();
console.log(colors.cyan(" ▶ Post-publish smoke (boot gate)"));
const smokeResult = await deps.runSmoke({
image: ref,
account: input.account,
gateway: input.domain
});
if (!smokeResult.ok) {
const cause = smokeResult.error ?? "unknown failure";
console.log();
console.log(colors.error(` ${icons.err} Post-publish smoke failed: ${cause}`));
if (smokeResult.diagnosis) console.log(colors.yellow(` ${smokeResult.diagnosis}`));
legs.smoke = {
status: "failed",
detail: cause
};
if (!input.account || !input.domain) {
console.log(colors.yellow(" Rollback skipped: no registry identity in the session"));
return {
legs,
failure: `Post-publish smoke failed (${cause}) — config NOT rolled back: no registry identity`
};
}
console.log(colors.yellow(" Rolling the published config back…"));
const rollback = await deps.rollbackToPreviousSnapshot({
account: input.account,
domain: input.domain,
env: input.env,
runtimeConfig: input.runtimeConfig,
configDir: input.sessionRoot,
registry: input.registry,
network: input.network,
privateKey: input.privateKey,
verbose: input.verbose
});
const rollbackNote = rollback.ok ? `config rolled back to block ${rollback.blockHeight}` : `config NOT rolled back: ${rollback.error ?? "unknown"}`;
console.log(colors.yellow(` ${rollbackNote}`));
return {
legs,
failure: `Post-publish smoke failed (${cause}) — ${rollbackNote}`
};
}
legs.smoke = {
status: "ok",
detail: smokeResult.summary
};
console.log(colors.green(` ${icons.ok} Post-publish smoke passed: ${smokeResult.summary ?? ""}`));
}
}
}
const railwayStep = resolveRailwayStep({
tier,
token: process.env.RAILWAY_TOKEN,
service: input.railwayService,
pinnedDigest: imageDigest,
childPin
});
if (railwayStep.action === "skip") {
legs.railway = {
status: "skipped",
detail: railwayStep.detail
};
console.log();
console.log(colors.yellow(` Railway deploy skipped: ${railwayStep.detail}`));
} else if (railwayStep.action === "fail") {
legs.railway = {
status: "failed",
detail: railwayStep.detail
};
console.log();
console.log(colors.yellow(` Railway deploy failed: ${railwayStep.detail}`));
return {
legs,
failure: `Config published but ${railwayStep.detail}`
};
} else {
service = railwayStep.service;
try {
if (railwayStep.action === "repoint-and-redeploy") await deps.repointServiceSource({
service: railwayStep.service,
image: railwayStep.image,
configDir: input.sessionRoot
});
await deps.deployImageToRailway({
service: railwayStep.service,
configDir: input.sessionRoot
});
legs.railway = {
status: "ok",
detail: railwayStep.action === "repoint-and-redeploy" ? `pinned ${railwayStep.image}, redeployed ${service}` : `redeployed ${service}`
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const railError = message.includes("not found") || message.includes("ENOENT") ? "Railway CLI not found. Install it: npm i -g @railway/cli" : `Railway deploy failed: ${message}`;
legs.railway = {
status: "failed",
detail: railError
};
console.log(colors.yellow(` ${railError}`));
return {
legs,
image,
imageDigest,
service,
failure: `Config published but ${railError}`
};
}
}
return {
image,
imageDigest,
service,
legs
};
}
//#endregion
export { realPostPublishLegsDeps, resolveChildImagePin, resolveDeployPlan, resolveRailwayStep, resolveRuntimeTier, runPostPublishLegs };
//# sourceMappingURL=legs.mjs.map