fix: expose deploy artifact matrix plan fields
This commit is contained in:
+186
-39
@@ -119,6 +119,8 @@ interface DeployEnvironmentManifestSource {
|
||||
fetchedAt: string;
|
||||
}
|
||||
|
||||
type ArtifactConsumerPlanKind = "main-server-compose" | "d601-direct-compose" | "d601-k3s-managed" | "d601-dev-target-side-build" | "unsupported";
|
||||
|
||||
const defaultDeployFile = "deploy.json";
|
||||
const defaultTimeoutMs = 900_000;
|
||||
const resolveFetchTimeout = "120s";
|
||||
@@ -148,6 +150,9 @@ const prodArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
|
||||
["met-nonlinear", "met-nonlinear is blocked for live artifact deploy because config.json points at docker/unidesk/Dockerfile.ml while the compose service is met-nonlinear-ts."],
|
||||
["k3sctl-adapter", "k3sctl-adapter is an infrastructure control bridge; this executor exposes artifact consumer plan/dry-run only. Real production deployment requires supervisor confirmation outside this task."],
|
||||
]);
|
||||
const artifactConsumerDryRunBlockedServiceIds = new Map<string, string>([
|
||||
["met-nonlinear", "runtime-verification-blocked: CI publishes the ML image Dockerfile contract, but the long-running Compose service is met-nonlinear-ts; CD cannot yet prove the running container image label matches the requested commit."],
|
||||
]);
|
||||
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
|
||||
dev: {
|
||||
environment: "dev",
|
||||
@@ -881,6 +886,133 @@ function environmentTargetSummary(target: DeployEnvironmentTarget): Record<strin
|
||||
};
|
||||
}
|
||||
|
||||
function artifactConsumerPlanKind(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): ArtifactConsumerPlanKind {
|
||||
if (environment === "dev" && devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)) {
|
||||
return "d601-dev-target-side-build";
|
||||
}
|
||||
if (service.deployment.mode === "k3sctl-managed") return "d601-k3s-managed";
|
||||
if (service.providerId === "D601" && service.deployment.mode === "unidesk-direct") return "d601-direct-compose";
|
||||
if (targetIsMain(service) && isDirectComposeDeployMode(service)) return "main-server-compose";
|
||||
return "unsupported";
|
||||
}
|
||||
|
||||
function deploymentPathForEnvironmentService(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
|
||||
return deploymentPathForEnvironmentServiceId(service.id, environment);
|
||||
}
|
||||
|
||||
function deploymentPathForEnvironmentServiceId(serviceId: string, environment: DeployEnvironment): string {
|
||||
if (environment === "prod") return prodArtifactConsumerServiceIds.has(serviceId) ? "d601-registry-artifact-consumer" : "unsupported";
|
||||
if (devArtifactConsumerServiceIds.has(serviceId)) return "d601-registry-artifact-consumer";
|
||||
if (devApplySupportedServiceIds.has(serviceId)) return "d601-dev-target-side-build";
|
||||
return "unsupported";
|
||||
}
|
||||
|
||||
function artifactConsumerPlanTarget(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): Record<string, unknown> {
|
||||
const kind = artifactConsumerPlanKind(service, environment);
|
||||
const liveBlockReason = environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null;
|
||||
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
|
||||
const targetImage = artifactConsumerTargetImageHint(service, environment);
|
||||
const common = {
|
||||
consumerKind: kind,
|
||||
runtimeHost: kind === "main-server-compose" ? "main-server" : "D601",
|
||||
deploymentMode: service.deployment.mode,
|
||||
noRuntimeSourceBuild: kind !== "d601-dev-target-side-build",
|
||||
dryRunOnly: dryRunBlockedReason !== null || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)),
|
||||
blockedReason: dryRunBlockedReason ?? liveBlockReason,
|
||||
};
|
||||
if (kind === "d601-k3s-managed") {
|
||||
return {
|
||||
...common,
|
||||
namespace: service.deployment.namespace ?? (environment === "dev" ? "unidesk-dev" : k8sNamespace),
|
||||
deployment: service.repository.composeService,
|
||||
service: service.deployment.k3sServiceId ?? service.repository.composeService,
|
||||
containerName: service.repository.containerName,
|
||||
manifest: service.repository.composeFile,
|
||||
targetImage,
|
||||
deployCommandShape: "registry manifest check + native k3s containerd import + Kubernetes Deployment image/env/annotation update + API service proxy health",
|
||||
forbiddenActions: ["docker build", "docker compose build", "NodePort", "hostPort", "provider-gateway direct business backend"],
|
||||
};
|
||||
}
|
||||
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
|
||||
return {
|
||||
...common,
|
||||
workDir: targetWorkDir(service),
|
||||
composeFile: service.repository.composeFile,
|
||||
composeService: service.repository.composeService,
|
||||
containerName: service.repository.containerName,
|
||||
targetImage,
|
||||
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${service.repository.composeService}`,
|
||||
forbiddenActions: ["docker build", "docker compose build", "docker compose up --build", "dirty worktree source deploy"],
|
||||
};
|
||||
}
|
||||
if (kind === "d601-dev-target-side-build") {
|
||||
return {
|
||||
...common,
|
||||
namespace: service.deployment.namespace ?? "unidesk-dev",
|
||||
deployment: service.repository.composeService,
|
||||
manifest: service.repository.composeFile,
|
||||
workDir: targetWorkDir(service),
|
||||
deployCommandShape: "D601 dev target-side source materialization/build/import/rollout",
|
||||
forbiddenActions: ["production namespace mutation", "production database mutation"],
|
||||
};
|
||||
}
|
||||
return common;
|
||||
}
|
||||
|
||||
function artifactConsumerTargetImageHint(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
|
||||
if (service.id === "frontend" && environment === "prod") return "unidesk-frontend";
|
||||
if (service.id === "frontend" && environment === "dev") return "unidesk-frontend:dev";
|
||||
if (service.id === "k3sctl-adapter") return "unidesk-k3sctl-adapter:d601";
|
||||
if (service.id === "met-nonlinear") return "met-nonlinear-ml:tf26";
|
||||
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:${environment === "dev" ? "dev" : "d601"}`;
|
||||
const container = service.repository.containerName;
|
||||
if (container.endsWith("-backend")) return container.slice(0, -"-backend".length);
|
||||
return container;
|
||||
}
|
||||
|
||||
function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string[] {
|
||||
const kind = artifactConsumerPlanKind(service, environment);
|
||||
if (kind === "d601-dev-target-side-build") {
|
||||
return [
|
||||
"reads origin/master:deploy.json for dev desired state",
|
||||
"materializes and builds only the selected dev service on D601",
|
||||
"rolls out only unidesk-dev resources",
|
||||
];
|
||||
}
|
||||
const base = [
|
||||
"reads origin/master:deploy.json for commit intent",
|
||||
"requires a commit-pinned artifact in 127.0.0.1:5000",
|
||||
"verifies image labels for service id, source commit, and Dockerfile",
|
||||
"does not build source on the runtime target",
|
||||
];
|
||||
if (kind === "d601-k3s-managed") {
|
||||
return [
|
||||
...base,
|
||||
"imports the artifact into native k3s containerd",
|
||||
"verifies Deployment metadata and health through the Kubernetes API service proxy",
|
||||
];
|
||||
}
|
||||
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
|
||||
return [
|
||||
...base,
|
||||
"recreates only the selected Compose service with --no-build --no-deps --force-recreate",
|
||||
"verifies running image labels and private service health",
|
||||
];
|
||||
}
|
||||
return ["unsupported service remains blocked before source materialization or runtime mutation"];
|
||||
}
|
||||
|
||||
function environmentPlanServiceConfig(config: UniDeskConfig | null, service: DeployManifestService, environment: DeployEnvironment): UniDeskMicroserviceConfig | null {
|
||||
if (environment === "dev") {
|
||||
const devService = devK3sDeployService(service.id);
|
||||
if (devService !== undefined) return devService;
|
||||
}
|
||||
const configured = config?.microservices.find((candidate) => candidate.id === service.id);
|
||||
if (configured !== undefined) return configured;
|
||||
if (config !== null) return coreDeployService(config, service.id, environment) ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function targetIsMain(service: UniDeskMicroserviceConfig): boolean {
|
||||
return service.providerId === "main-server";
|
||||
}
|
||||
@@ -2657,6 +2789,7 @@ async function checkOrPlan(config: UniDeskConfig, manifest: DeployManifest, opti
|
||||
}
|
||||
|
||||
function environmentDryRunPlan(
|
||||
config: UniDeskConfig | null,
|
||||
manifest: DeployManifest,
|
||||
source: DeployEnvironmentManifestSource,
|
||||
options: DeployOptions,
|
||||
@@ -2707,49 +2840,63 @@ function environmentDryRunPlan(
|
||||
deployJsonFromWorktree: false,
|
||||
dirtyWorktreeUsed: false,
|
||||
},
|
||||
services: services.map((service) => ({
|
||||
id: service.id,
|
||||
repo: service.repo,
|
||||
commitId: service.commitId,
|
||||
environment,
|
||||
targetNamespace: target.namespace,
|
||||
providerId: target.provider.providerId,
|
||||
databaseFingerprint: fingerprint,
|
||||
deploymentPath: environment === "prod"
|
||||
? prodArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer"
|
||||
: "unsupported"
|
||||
: devArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer"
|
||||
: devApplySupportedServiceIds.has(service.id)
|
||||
? "d601-dev-target-side-build"
|
||||
: "unsupported",
|
||||
unsupported: (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|
||||
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id))
|
||||
? {
|
||||
ok: false,
|
||||
supported: false,
|
||||
reason: environment === "prod"
|
||||
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
|
||||
: "No standardized dev deployment or artifact validation path is implemented for this service.",
|
||||
}
|
||||
: environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)
|
||||
services: services.map((service) => {
|
||||
const serviceConfig = environmentPlanServiceConfig(config, service, environment);
|
||||
const unsupported = (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|
||||
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
|
||||
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
|
||||
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
|
||||
return {
|
||||
id: service.id,
|
||||
repo: service.repo,
|
||||
commitId: service.commitId,
|
||||
environment,
|
||||
targetNamespace: target.namespace,
|
||||
providerId: target.provider.providerId,
|
||||
databaseFingerprint: fingerprint,
|
||||
deploymentPath: serviceConfig === null
|
||||
? deploymentPathForEnvironmentServiceId(service.id, environment)
|
||||
: deploymentPathForEnvironmentService(serviceConfig, environment),
|
||||
artifactConsumer: serviceConfig === null ? {
|
||||
consumerKind: "unsupported",
|
||||
noRuntimeSourceBuild: true,
|
||||
dryRunOnly: true,
|
||||
blockedReason: "service is missing from config.json and no synthetic deploy service is available for this environment",
|
||||
} : {
|
||||
consumerKind: artifactConsumerPlanKind(serviceConfig, environment),
|
||||
registryImage: `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`,
|
||||
noRuntimeSourceBuild: artifactConsumerPlanKind(serviceConfig, environment) !== "d601-dev-target-side-build",
|
||||
dryRunOnly: (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
|
||||
blockedReason: dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
|
||||
},
|
||||
target: planTarget,
|
||||
validation: serviceConfig === null ? undefined : artifactConsumerPlanValidation(serviceConfig, environment),
|
||||
unsupported: unsupported
|
||||
? {
|
||||
ok: false,
|
||||
supported: false,
|
||||
reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
|
||||
reason: environment === "prod"
|
||||
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
|
||||
: "No standardized dev deployment or artifact validation path is implemented for this service.",
|
||||
}
|
||||
: undefined,
|
||||
liveApply: environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)
|
||||
? {
|
||||
allowed: false,
|
||||
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id),
|
||||
dryRunOnly: true,
|
||||
}
|
||||
: environment === "prod"
|
||||
? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
|
||||
: undefined,
|
||||
})),
|
||||
liveApply: dryRunBlockedReason !== null
|
||||
? {
|
||||
allowed: false,
|
||||
reason: dryRunBlockedReason,
|
||||
dryRunOnly: true,
|
||||
}
|
||||
: environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)
|
||||
? {
|
||||
allowed: false,
|
||||
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id),
|
||||
dryRunOnly: true,
|
||||
}
|
||||
: environment === "prod"
|
||||
? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
|
||||
};
|
||||
}
|
||||
@@ -3031,7 +3178,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
|
||||
const commitOverrideError = commitOverrideArtifactConsumerError(options);
|
||||
if (commitOverrideError !== null) throw new Error(commitOverrideError);
|
||||
const { manifest, source } = readEnvironmentDeployManifest(options.environment);
|
||||
if (action === "check" || action === "plan") return environmentDryRunPlan(manifest, source, options, action);
|
||||
if (action === "check" || action === "plan") return environmentDryRunPlan(config, manifest, source, options, action);
|
||||
if (options.environment === "prod") {
|
||||
const unsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
|
||||
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
|
||||
|
||||
Reference in New Issue
Block a user