everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
346 lines (344 loc) • 12.7 kB
JavaScript
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
const require_types = require('../types.cjs');
const require_fastkv = require('../fastkv.cjs');
const require_config = require('../config.cjs');
const require_theme = require('../utils/theme.cjs');
const require_image_deploy = require('../image-deploy.cjs');
const require_resolution_session = require('../resolution/session.cjs');
const require_shared = require('./shared.cjs');
const require_rollback = require('../rollback.cjs');
const require_publish = require('../publish.cjs');
let node_fs = require("node:fs");
let node_path = require("node:path");
let effect = require("effect");
let node_process = require("node:process");
node_process = require_runtime.__toESM(node_process, 1);
//#region src/commands/deploy.ts
function registerDeploy(builder) {
return {
publish: builder.publish.handler(async ({ input, context }) => {
const session = effect.Context.get(context["effect/context"], require_shared.BosDepsTag).session;
if (!session?.config) return {
status: "error",
registryUrl: "",
error: require_config.MISSING_CONFIG_MESSAGE
};
const result = await require_publish.publishToFastKv({
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
});
return {
status: result.status,
registryUrl: result.registryUrl,
txHash: result.txHash,
error: result.error,
built: result.built,
skipped: result.skipped,
deployResults: result.deployResults
};
}),
rollback: builder.rollback.handler(async ({ input, context }) => {
const session = effect.Context.get(context["effect/context"], require_shared.BosDepsTag).session;
if (!session) return {
status: "error",
registryUrl: "",
error: require_config.MISSING_CONFIG_MESSAGE
};
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 require_fastkv.fetchConfigHistory({
accountId: account,
gatewayId: 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: require_rollback.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)"
};
let liveValue;
try {
liveValue = await require_fastkv.fetchBosConfigFromFastKv(`bos://${account}/${domain}`, input.registry);
} catch {
liveValue = void 0;
}
if (liveValue && JSON.stringify(liveValue) === JSON.stringify(target.value)) return {
status: "error",
registryUrl: "",
error: "The selected snapshot is identical to the live config — nothing to do"
};
const verification = await require_rollback.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 parsedTarget = require_types.BosConfigSchema.parse(target.value);
const payload = require_rollback.buildRollbackPayload(parsedTarget, (/* @__PURE__ */ new Date()).toISOString());
const result = await require_publish.publishToFastKv({
bosConfig: payload,
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
});
return {
status: result.status,
registryUrl: result.registryUrl,
txHash: result.txHash,
error: result.error,
history: historyEntries,
...slotChecks.length > 0 ? { verification: slotChecks } : {}
};
}),
deploy: builder.deploy.handler(async ({ input, context }) => {
const session = effect.Context.get(context["effect/context"], require_shared.BosDepsTag).session;
if (!session?.config) return {
status: "error",
registryUrl: "",
error: require_config.MISSING_CONFIG_MESSAGE
};
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 require_fastkv.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 ?? node_process.default.env.BOS_IMAGE_DIGEST;
if (imageDigestInput && !require_image_deploy.isValidImageDigest(imageDigestInput)) return {
status: "error",
registryUrl: "",
error: `Invalid image digest "${imageDigestInput}" — expected sha256:<64 hex characters> (--image-digest / BOS_IMAGE_DIGEST)`
};
const result = await require_publish.publishToFastKv({
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
});
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 = session;
if (result.publishConfig) {
const opened = await require_resolution_session.openResolution({ cwd: session.root });
if (opened) nextSession = opened;
}
const configPublished = {
registryUrl: result.registryUrl,
txHash: result.txHash,
fingerprint: result.fingerprint,
slotPins: result.slotPins,
built: result.built,
skipped: result.skipped,
deployResults: result.deployResults
};
let image;
let imageDigest;
let service;
const imagePlan = require_image_deploy.resolveDeployImagePlan({
imageRef: require_image_deploy.resolveImageRef({
ciImage: imageDigestInput ? void 0 : nextSession.config?.ci?.image,
repository: nextSession.config?.repository,
env: node_process.default.env
}),
imageDigest: imageDigestInput,
hasDocker: await require_image_deploy.hasDocker(),
hasDockerfile: (0, node_fs.existsSync)((0, node_path.join)(session.root, "Dockerfile"))
});
if (imagePlan.kind === "skip") {
console.log();
console.log(require_theme.colors.yellow(` Image skipped: ${imagePlan.reason}`));
} else if (imagePlan.kind === "prebuilt") {
image = imagePlan.image;
imageDigest = imagePlan.digest;
console.log();
console.log(require_theme.colors.green(` ${require_theme.icons.ok} Using pre-pushed image ${imagePlan.image}@${imagePlan.digest.slice(0, 19)}…`));
} else {
console.log();
try {
const imageResult = await require_image_deploy.buildAndPushImage({
image: imagePlan.image,
configDir: session.root,
verbose: input.verbose
});
image = imageResult.image;
imageDigest = imageResult.digest;
console.log(require_theme.colors.green(` ${require_theme.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}`;
console.log(require_theme.colors.yellow(` ${imageError}`));
return {
status: "published",
...configPublished,
error: `Config published but ${imageError}`
};
}
}
if (node_process.default.env.RAILWAY_TOKEN) {
const railwayService = input.service ?? nextSession.config?.ci?.railway?.service;
if (!railwayService) {
console.log();
console.log(require_theme.colors.yellow(" Railway deploy skipped: ci.railway.service is not configured in bos.config.json"));
return {
status: "published",
...configPublished,
image,
error: "Config published but Railway deploy failed: ci.railway.service is not configured in bos.config.json"
};
}
if (!image || !imageDigest) {
console.log();
console.log(require_theme.colors.yellow(" Railway deploy skipped: no runtime image digest — set ci.image (or pass BOS_IMAGE_DIGEST) and make the image available"));
return {
status: "published",
...configPublished,
image,
service: railwayService,
error: "Config published but Railway deploy requires a runtime image (set ci.image, or pass BOS_IMAGE_DIGEST for a pre-pushed image)"
};
}
service = railwayService;
try {
await require_image_deploy.deployImageToRailway({
image,
digest: imageDigest,
service: railwayService,
configDir: session.root
});
} 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}`;
console.log(require_theme.colors.yellow(` ${railError}`));
return {
status: "published",
...configPublished,
image,
service,
error: `Config published but ${railError}`
};
}
} else {
console.log();
console.log(require_theme.colors.yellow(" Railway deploy skipped (RAILWAY_TOKEN not set)"));
}
return {
status: "deployed",
...configPublished,
image,
service
};
})
};
}
//#endregion
exports.registerDeploy = registerDeploy;
//# sourceMappingURL=deploy.cjs.map