standardize user-service artifact CD
This commit is contained in:
+118
-7
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
|
||||
import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
|
||||
import { runArtifactRegistryCommand } from "./artifact-registry";
|
||||
import { startJob } from "./jobs";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
|
||||
@@ -27,6 +28,7 @@ interface DeployOptions {
|
||||
file: string;
|
||||
environment: DeployEnvironment | null;
|
||||
serviceId: string | null;
|
||||
commitOverride: string | null;
|
||||
runNow: boolean;
|
||||
dryRun: boolean;
|
||||
force: boolean;
|
||||
@@ -133,6 +135,7 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
|
||||
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
|
||||
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "frontend", "k3sctl-adapter", "code-queue"]);
|
||||
const devApplySupportedServiceIds = new Set<string>(["backend-core", "frontend"]);
|
||||
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "decision-center"]);
|
||||
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
|
||||
dev: {
|
||||
environment: "dev",
|
||||
@@ -186,7 +189,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
|
||||
usage: {
|
||||
check: "bun scripts/cli.ts deploy check [--file deploy.json | --env dev|prod] [--service id]",
|
||||
plan: "bun scripts/cli.ts deploy plan [--file deploy.json | --env dev|prod] [--service id]",
|
||||
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev] [--service id] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
|
||||
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
|
||||
},
|
||||
actions: {
|
||||
check: "Validate desired repo+commit state against live service health and commit markers.",
|
||||
@@ -195,8 +198,9 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
|
||||
},
|
||||
options: [
|
||||
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js. Local manifest apply allows k3sctl-adapter and explicit production code-queue controlled rollout on D601." },
|
||||
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply is enabled only for backend-core and frontend in D601 unidesk-dev; prod apply is disabled." },
|
||||
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply is enabled only for backend-core and frontend in D601 unidesk-dev; prod apply uses the D601 registry artifact consumer for backend-core and decision-center." },
|
||||
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
|
||||
{ name: "--commit <full-sha>", description: "Prod artifact rollback/apply override for a selected service; the image must already exist in D601 registry." },
|
||||
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
|
||||
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
|
||||
{ name: "--timeout-ms <n>", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." },
|
||||
@@ -464,10 +468,16 @@ function environmentOption(args: string[]): DeployEnvironment | null {
|
||||
function parseOptions(args: string[]): DeployOptions {
|
||||
const environment = environmentOption(args);
|
||||
if (environment !== null && args.includes("--file")) throw new Error("deploy --env reads deploy.json from the fixed Git ref and cannot be combined with --file");
|
||||
const commitOverride = optionValue(args, ["--commit"]);
|
||||
const serviceId = optionValue(args, ["--service", "--service-id"]) ?? null;
|
||||
if (commitOverride !== undefined && environment !== "prod") throw new Error("deploy --commit is only supported for --env prod artifact rollback/apply");
|
||||
if (commitOverride !== undefined && !isFullGitSha(commitOverride)) throw new Error("deploy --commit must be a full 40-character commit SHA");
|
||||
if (commitOverride !== undefined && serviceId === null) throw new Error("deploy --commit requires --service so prod rollback/apply is unambiguous");
|
||||
return {
|
||||
file: optionValue(args, ["--file"]) ?? defaultDeployFile,
|
||||
environment,
|
||||
serviceId: optionValue(args, ["--service", "--service-id"]) ?? null,
|
||||
serviceId,
|
||||
commitOverride: commitOverride?.toLowerCase() ?? null,
|
||||
runNow: args.includes("--run-now"),
|
||||
dryRun: args.includes("--dry-run"),
|
||||
force: args.includes("--force"),
|
||||
@@ -2416,15 +2426,19 @@ function environmentDryRunPlan(
|
||||
const environment = options.environment;
|
||||
if (environment === null) throw new Error("environment dry-run requires --env");
|
||||
const target = deployEnvironmentTargets[environment];
|
||||
const services = options.serviceId === null
|
||||
const selectedServices = options.serviceId === null
|
||||
? manifest.services
|
||||
: manifest.services.filter((service) => service.id === options.serviceId);
|
||||
if (options.serviceId !== null && services.length === 0) {
|
||||
if (options.serviceId !== null && selectedServices.length === 0) {
|
||||
throw new Error(`deploy manifest ${source.ref}:deploy.json does not contain service: ${options.serviceId}`);
|
||||
}
|
||||
const services = selectedServices.map((service) => ({
|
||||
...service,
|
||||
commitId: options.commitOverride !== null && service.id === options.serviceId ? options.commitOverride : service.commitId,
|
||||
}));
|
||||
const fingerprint = databaseFingerprint(target);
|
||||
return {
|
||||
ok: true,
|
||||
ok: environment === "prod" ? services.every((service) => prodArtifactConsumerServiceIds.has(service.id)) : true,
|
||||
action,
|
||||
mode: "environment-ref-dry-run",
|
||||
dryRun: true,
|
||||
@@ -2458,6 +2472,18 @@ function environmentDryRunPlan(
|
||||
targetNamespace: target.namespace,
|
||||
providerId: target.provider.providerId,
|
||||
databaseFingerprint: fingerprint,
|
||||
deploymentPath: environment === "prod"
|
||||
? prodArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer"
|
||||
: "unsupported"
|
||||
: "d601-dev-target-side-build",
|
||||
unsupported: environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id)
|
||||
? {
|
||||
ok: false,
|
||||
supported: false,
|
||||
reason: "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -2479,6 +2505,86 @@ function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
|
||||
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core/frontend; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
|
||||
}
|
||||
|
||||
function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
|
||||
if (serviceId !== null && services.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
|
||||
return services;
|
||||
}
|
||||
|
||||
function prodArtifactUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
|
||||
}
|
||||
|
||||
function prodArtifactUnsupportedResult(services: DeployManifestService[]): Record<string, unknown> {
|
||||
return {
|
||||
ok: false,
|
||||
supported: false,
|
||||
error: "unsupported",
|
||||
services: services.map((service) => ({
|
||||
id: service.id,
|
||||
repo: service.repo,
|
||||
commitId: service.commitId,
|
||||
supported: false,
|
||||
reason: "No standardized prod D601 registry artifact consumer is implemented for this service.",
|
||||
})),
|
||||
policy: "prod deploy must not silently fall back to legacy D601 maintenance-channel source builds",
|
||||
supportedServices: Array.from(prodArtifactConsumerServiceIds),
|
||||
};
|
||||
}
|
||||
|
||||
async function runProdArtifactApplyNow(manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
|
||||
const selected = selectedEnvironmentServices(manifest, options.serviceId);
|
||||
const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
|
||||
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
|
||||
const startedAt = nowIso();
|
||||
const results = [];
|
||||
for (const service of selected) {
|
||||
const commit = options.commitOverride ?? service.commitId;
|
||||
const artifactArgs = [
|
||||
"deploy-service",
|
||||
"--service", service.id,
|
||||
"--commit", commit,
|
||||
"--source-repo", service.repo,
|
||||
"--timeout-ms", String(options.timeoutMs),
|
||||
"--run-now",
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
];
|
||||
progressLine("prod-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
|
||||
results.push(await runArtifactRegistryCommand(artifactArgs));
|
||||
const latest = results.at(-1) as Record<string, unknown>;
|
||||
if (latest.ok !== true) break;
|
||||
}
|
||||
return {
|
||||
ok: results.every((result) => asRecord(result)?.ok === true),
|
||||
action: "apply",
|
||||
environment: "prod",
|
||||
executor: "d601-registry-artifact-consumer",
|
||||
dryRun: options.dryRun,
|
||||
startedAt,
|
||||
finishedAt: nowIso(),
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
function prodArtifactApplyJob(args: string[], options: DeployOptions): Record<string, unknown> {
|
||||
if (!options.runNow && options.dryRun) {
|
||||
throw new Error("deploy apply --env prod --dry-run should run in the foreground so the plan is visible immediately");
|
||||
}
|
||||
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
|
||||
const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs];
|
||||
const source = `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`;
|
||||
const job = startJob("deploy_prod_artifact_apply", command, `Deploy prod artifact consumer from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "async-job",
|
||||
executor: "d601-registry-artifact-consumer",
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
|
||||
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
|
||||
note: "Production CD continues in the background: D601 registry commit-pinned image check, pull/import, rollout, image-label and live health commit verification.",
|
||||
};
|
||||
}
|
||||
|
||||
async function runApplyNow(config: UniDeskConfig, manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
|
||||
const selected = selectServices(config, manifest, options.serviceId);
|
||||
const startedAt = nowIso();
|
||||
@@ -2525,7 +2631,12 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
|
||||
if (options.environment !== null) {
|
||||
const { manifest, source } = readEnvironmentDeployManifest(options.environment);
|
||||
if (action === "check" || action === "plan") return environmentDryRunPlan(manifest, source, options, action);
|
||||
if (options.environment !== "dev") throw new Error("deploy apply --env prod is not enabled yet");
|
||||
if (options.environment === "prod") {
|
||||
const unsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
|
||||
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
|
||||
if (options.dryRun || options.runNow) return await runProdArtifactApplyNow(manifest, options);
|
||||
return prodArtifactApplyJob(args, options);
|
||||
}
|
||||
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
|
||||
if (unsupported.length > 0) {
|
||||
throw new Error(`deploy apply --env dev currently supports only backend-core and frontend; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
|
||||
|
||||
Reference in New Issue
Block a user