everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
326 lines (324 loc) • 10.7 kB
JavaScript
import { fetchDeployManifests } from "../services/registry/fastkv.mjs";
import { MISSING_CONFIG_MESSAGE } from "../services/resolution/load.mjs";
import { isValidImageDigest } from "../services/deploy/image.mjs";
import { ResolutionSessionService, openResolution } from "../services/resolution/session.mjs";
import { summarizeSlotPins, verifyRollbackSnapshot } from "../services/deploy/rollback.mjs";
import { DeployTrainService } from "../services/deploy/train.mjs";
import { fetchLiveConfig, fetchRollbackHistory, isSnapshotLive, publishRollbackSnapshot } from "../services/deploy/rollback-flow.mjs";
import { runPostPublishLegs } from "../services/deploy/legs.mjs";
import { Effect } from "effect";
import process from "node:process";
//#region src/handlers/deploy.ts
const mapTrainError = (error) => Effect.succeed({
...error.toResult(),
status: "error"
});
function unknownAuthoredKeysError(keys) {
return `The authored config has unknown keys: ${keys.join(", ")} — remove them from bos.app.ts. ci.image in particular was retired (ADR 0034): children pin the runtime image via platform: "<version>" (the platform release manifest resolves the image ref and digest at deploy time); the root runtime derives its image from repository.`;
}
function registerDeploy(builder) {
return {
publish: builder.publish.effect(function* (_opts, input) {
const session = yield* ResolutionSessionService;
const train = yield* DeployTrainService;
if (!session?.config) return {
status: "error",
registryUrl: "",
error: MISSING_CONFIG_MESSAGE
};
const result = yield* train.publish({
bosConfig: session.config,
runtimeConfig: session.runtime,
configDir: session.root,
env: input.env,
build: false,
dryRun: input.dryRun,
verbose: input.verbose,
packages: "all",
network: input.network,
privateKey: input.privateKey,
wallet: input.wallet,
registry: input.registry
}).pipe(Effect.catchTag("PublishTrainError", mapTrainError));
return {
status: result.status,
registryUrl: result.registryUrl,
txHash: result.txHash,
...result.status === "error" && result.error ? { error: result.error } : {},
built: result.built,
skipped: result.skipped,
deployResults: result.deployResults
};
}),
rollback: builder.rollback.effect(function* (_opts, input) {
const session = yield* ResolutionSessionService;
if (!session) return {
status: "error",
registryUrl: "",
error: MISSING_CONFIG_MESSAGE
};
return yield* Effect.tryPromise({
try: () => rollbackBody(session, input),
catch: (error) => ({
status: "error",
registryUrl: "",
error: error instanceof Error ? error.message : String(error)
})
});
}),
deploy: builder.deploy.effect(function* (_opts, input) {
const session = yield* ResolutionSessionService;
const train = yield* DeployTrainService;
if (!session?.config) return {
status: "error",
registryUrl: "",
error: MISSING_CONFIG_MESSAGE
};
const retiredKeys = session.unknownAuthoredKeys;
if (retiredKeys.length > 0) return {
status: "error",
registryUrl: "",
error: unknownAuthoredKeysError(retiredKeys)
};
const deploySession = {
config: session.config,
runtime: session.runtime,
root: session.root,
rawConfig: session.rawConfig
};
return yield* Effect.tryPromise({
try: () => deployBody(train, deploySession, input),
catch: (error) => ({
status: "error",
registryUrl: "",
error: error instanceof Error ? error.message : String(error)
})
});
})
};
}
async function rollbackBody(session, input) {
const { account, domain } = session.config ?? {};
if (!account || !domain) return {
status: "error",
registryUrl: "",
error: "authored config must define account and domain to roll back"
};
let history;
try {
history = await fetchRollbackHistory({
account,
domain,
registry: input.registry,
limit: input.limit
});
} catch (error) {
return {
status: "error",
registryUrl: "",
error: `Failed to fetch publish history: ${error instanceof Error ? error.message : String(error)}`
};
}
if (history.length === 0) return {
status: "error",
registryUrl: "",
error: `No publish history for ${account}/${domain} — nothing to roll back to`
};
const historyEntries = history.map((entry) => ({
blockHeight: entry.blockHeight,
blockTimestamp: new Date(Number(entry.blockTimestampNs.slice(0, 13))).toISOString(),
...entry.txHash ? { txHash: entry.txHash } : {},
summary: summarizeSlotPins(entry.value)
}));
if (input.listOnly) return {
status: "list",
registryUrl: "",
history: historyEntries
};
const target = input.version !== void 0 ? history.find((entry) => {
const iso = new Date(Number(entry.blockTimestampNs.slice(0, 13))).toISOString();
return String(entry.blockHeight) === input.version || iso.startsWith(input.version);
}) : input.previous ? history[1] : void 0;
if (!target) return {
status: "error",
registryUrl: "",
history: historyEntries,
error: input.version ? `No history entry matches "${input.version}" — pick a block height from the listing` : "Specify --version <block-height> or --previous (interactive selection happens CLI-side)"
};
const liveValue = await fetchLiveConfig({
account,
domain,
registry: input.registry
});
if (liveValue !== void 0 && isSnapshotLive(target.value, liveValue)) return {
status: "error",
registryUrl: "",
error: "The selected snapshot is identical to the live config — nothing to do"
};
const verification = await verifyRollbackSnapshot(target.value);
const slotChecks = verification.slots.map((check) => ({
slot: check.slot,
ok: check.ok,
...check.reason ? { reason: check.reason } : {}
}));
if (!verification.ok && verification.verifiable) return {
status: "error",
registryUrl: "",
verification: slotChecks,
error: "Refusing to roll back — a pinned slot's bytes are gone or no longer match their SRI"
};
if (!verification.verifiable && !input.force) return {
status: "error",
registryUrl: "",
error: "Snapshot predates version manifests and cannot be verified — its bytes were overwritten in place. Re-run with --force to publish it anyway."
};
const rollback = await publishRollbackSnapshot(target.value, {
account,
domain,
env: input.env,
runtimeConfig: session.runtime,
configDir: session.root,
registry: input.registry,
network: input.network,
privateKey: input.privateKey,
wallet: input.wallet,
verbose: input.verbose,
dryRun: input.dryRun
}, { skipVerify: true });
if (!rollback.ok) return {
status: "error",
registryUrl: rollback.registryUrl,
history: historyEntries,
error: rollback.error
};
return {
status: rollback.dryRun ? "dry-run" : "published",
registryUrl: rollback.registryUrl,
txHash: rollback.txHash,
history: historyEntries,
...slotChecks.length > 0 ? { verification: slotChecks } : {}
};
}
async function deployBody(train, session, input) {
if (input.statusList) {
if (!session.config?.account || !session.config.domain) return {
status: "error",
registryUrl: "",
error: "authored config must define account and domain to list deploy manifests"
};
try {
return {
status: "list",
registryUrl: "",
history: (await fetchDeployManifests({
accountId: session.config.account,
gatewayId: session.config.domain,
registry: input.registry
})).map((entry) => ({
key: entry.key,
blockHeight: entry.blockHeight,
blockTimestamp: new Date(Number(entry.blockTimestampNs.slice(0, 13))).toISOString(),
...entry.value?.txHash ? { txHash: entry.value.txHash } : {},
...entry.value?.publishedAt ? { publishedAt: entry.value.publishedAt } : {}
}))
};
} catch (error) {
return {
status: "error",
registryUrl: "",
error: `Failed to list deploy manifests: ${error instanceof Error ? error.message : String(error)}`
};
}
}
const imageDigestInput = input.imageDigest ?? process.env.BOS_IMAGE_DIGEST;
if (imageDigestInput && !isValidImageDigest(imageDigestInput)) return {
status: "error",
registryUrl: "",
error: `Invalid image digest "${imageDigestInput}" — expected sha256:<64 hex characters> (--image-digest / BOS_IMAGE_DIGEST)`
};
const result = await Effect.runPromise(train.publish({
bosConfig: session.config,
runtimeConfig: session.runtime,
configDir: session.root,
env: input.env,
build: input.build,
dryRun: input.dryRun,
verbose: input.verbose,
packages: input.packages,
network: input.network,
privateKey: input.privateKey,
registry: input.registry,
fullUpload: input.fullUpload
}).pipe(Effect.catchTag("PublishTrainError", mapTrainError)));
if (result.status === "error") return {
status: "error",
registryUrl: result.registryUrl,
txHash: result.txHash,
built: result.built,
skipped: result.skipped,
error: result.error,
deployResults: result.deployResults
};
if (result.status === "dry-run") return {
status: "dry-run",
registryUrl: result.registryUrl,
built: result.built,
skipped: result.skipped
};
let nextSession = {
config: session.config,
runtime: session.runtime,
root: session.root
};
if (result.publishConfig) {
const opened = await openResolution({ cwd: session.root });
if (opened) nextSession = {
config: opened.config,
runtime: opened.runtime,
root: opened.root
};
}
const configPublished = {
registryUrl: result.registryUrl,
txHash: result.txHash,
fingerprint: result.fingerprint,
slotPins: result.slotPins,
built: result.built,
skipped: result.skipped,
deployResults: result.deployResults
};
const legsOutcome = await runPostPublishLegs({
sessionRoot: session.root,
env: input.env,
smoke: input.smoke,
imageDigestInput,
railwayService: input.service ?? nextSession.config?.ci?.railway?.service,
registry: input.registry,
network: input.network,
privateKey: input.privateKey,
verbose: input.verbose,
account: nextSession.config?.account,
domain: nextSession.config?.domain,
runtimeConfig: nextSession.runtime,
localConfig: session.rawConfig ? session.rawConfig : void 0,
repository: nextSession.config?.repository
});
if (legsOutcome.failure) return {
status: "published",
...configPublished,
image: legsOutcome.image,
service: legsOutcome.service,
legs: legsOutcome.legs,
error: legsOutcome.failure
};
return {
status: "deployed",
...configPublished,
image: legsOutcome.image,
service: legsOutcome.service,
legs: legsOutcome.legs
};
}
//#endregion
export { registerDeploy };
//# sourceMappingURL=deploy.mjs.map