UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

326 lines (324 loc) • 10.7 kB
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