fix: expose deploy artifact matrix plan fields

This commit is contained in:
Codex
2026-05-20 21:04:57 +00:00
parent a8e7cb8a13
commit f4c0f88312
4 changed files with 265 additions and 39 deletions
+186 -39
View File
@@ -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);