Files
pikasTech-unidesk/scripts/src/artifact-registry/compose-deploy.ts
T
Lyon edfddd2445 fix: YAML-first 治理 CI/CD target (#919)
* docs: specify cicd yaml target governance

* fix: resolve cicd targets from yaml

---------

Co-authored-by: Codex <codex@noreply.local>
2026-06-26 01:14:38 +08:00

613 lines
34 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. compose-deploy module for scripts/src/artifact-registry.ts.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Moved mechanically from scripts/src/artifact-registry.ts:2579-3153 for #903.
import { createHash } from "node:crypto";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { runCommand, type CommandResult } from "../command";
import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "../config";
import { startJob } from "../jobs";
import {
compareDeployJsonExecutorMirrors,
deployJsonCommitImage,
deployJsonDriftResult,
deployJsonSourceOfTruth,
hasDeployJsonExecutorContract,
k3sManifestExecutorMirror,
parseDeployJsonServiceContractBase64,
readDeployJsonServiceContractFromFile,
type DeployJsonExecutorMirror,
type DeployJsonServiceContract,
} from "../deploy-json-contract";
import { d601K3sGuardShellLines } from "../d601-k3s-guard";
import { composeRuntimeEnvValue } from "../runtime-env";
import { artifactRegistryOptionsTargetSummary, type ArtifactConsumerSpec, type ArtifactConsumerTarget, type ArtifactRegistryOptions } from "./types";
import { composeArtifactEnvValues, registryArtifactMissingMessage, registryArtifactProbeScript, syntheticComposeHealthDeployScript, verifyLocalArtifactLabels } from "./artifact-probe";
import { base64, safeName, shellQuote } from "./bundle";
import { artifactConsumerSpecs } from "./catalog";
import { artifactConsumerLiveBlockReason, artifactConsumerLivePolicy, artifactConsumerSelfBootstrapGuard, artifactConsumerTarget, artifactImageRef, artifactRegistryDeployJsonMirrors, codeQueueAffectedRuntime, codeQueueExcludedTargets, commandTail, deployJsonServiceForOptions, deployRefFor, registryRepositoryFor, sourceRepoFor, targetCommitImageFor, targetImageFor, unsupportedEnvironment } from "./consumer";
import { composeLockScript } from "./install";
import { rollbackInfo } from "./k3s-deploy";
import { runReadonlyStatus } from "./readonly";
import { runRemoteScript } from "./remote";
import { authHealthGateScript, composeArtifactRuntime, composeArtifactSecretPreflight, pullArtifactFromD601, runtimeSecretContractForComposeTarget, runtimeSecretRecommendedActionForService, upsertEnvFileValues } from "./runtime-secrets";
export async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const config = readConfig();
const runtime = composeArtifactRuntime(config, target);
const secretPreflight = composeArtifactSecretPreflight(runtime.envFile, target.compose.requiredRuntimeSecrets);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined
? undefined
: runtimeSecretRecommendedActionForService(runtimeSecretsRaw, options.environment ?? "prod", spec.serviceId);
if (!secretPreflight.ok) {
return {
ok: false,
supported: true,
serviceId: spec.serviceId,
step: "runtime-secret-preflight",
error: "runtime-secret-presence-missing",
environment: options.environment ?? "prod",
envFile: runtime.envFile,
requirements: secretPreflight.requirements,
missing: secretPreflight.missing,
secretSource: runtimeSecrets?.secretSource,
requiredSecretsPresent: runtimeSecrets?.requiredSecretsPresent ?? false,
missingSecretKeys: runtimeSecrets?.missingSecretKeys ?? secretPreflight.missing.map((item) => item.sourceEnvName),
recommendedAction: runtimeSecrets?.recommendedAction ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
runtimeSecrets,
valuesLogged: false,
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
};
}
const health = runReadonlyStatus(options, true);
if (health.ok !== true) {
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
}
const sourceImage = artifactImageRef(options, spec, commit);
const sourceRepo = sourceRepoFor(options, spec);
const composeImage = options.targetImage ?? target.targetImage;
const commitImage = `${composeImage}:${commit}`;
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
return {
ok: false,
serviceId: spec.serviceId,
step: "registry-artifact-check",
error: registryArtifactMissingMessage(spec),
sourceImage,
registryProbe: commandTail(registryProbe),
};
}
const pull = await pullArtifactFromD601(options, sourceImage);
if (pull.exitCode !== 0 || pull.timedOut) {
return {
ok: false,
serviceId: spec.serviceId,
step: "docker-load",
sourceImage,
registryProbe: commandTail(registryProbe),
pull: commandTail(pull),
};
}
const localLoadedImage = sourceImage;
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepo);
if (labelFailure !== null) return { ...labelFailure, registryProbe: commandTail(registryProbe) };
const tag = runCommand(["docker", "tag", localLoadedImage, composeImage], repoRoot);
if (tag.exitCode !== 0 || tag.timedOut) {
return { ok: false, step: "docker-tag", targetImage: composeImage, tag: commandTail(tag), registryProbe: commandTail(registryProbe) };
}
const tagCommit = runCommand(["docker", "tag", localLoadedImage, commitImage], repoRoot);
if (tagCommit.exitCode !== 0 || tagCommit.timedOut) {
return { ok: false, step: "docker-tag-commit", targetImage: commitImage, tag: commandTail(tagCommit), registryProbe: commandTail(registryProbe) };
}
upsertEnvFileValues(runtime.envFile, {
...composeArtifactEnvValues(spec, target, options, commit),
});
const compose = runtime.command;
const composeProject = runtime.project;
const serviceName = target.compose.serviceName;
const containerName = target.compose.containerName;
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
const upScript = [
"set -euo pipefail",
`echo ${shellQuote(`${serviceLogPrefix}_artifact_cd source=${sourceImage} target=${composeImage}`)}`,
`${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate ${shellQuote(serviceName)}`,
"ready=0",
"for attempt in $(seq 1 60); do",
` cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1 || true)`,
" if [ -n \"$cid\" ]; then",
" health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)",
` echo "${serviceLogPrefix}_container_probe attempt=$attempt cid=$cid health=$health"`,
" if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
" else",
` echo "${serviceLogPrefix}_container_probe attempt=$attempt cid=missing"`,
" fi",
" sleep 1",
"done",
"test \"$ready\" = \"1\"",
`cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1)`,
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
"actual_service=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/service-id\" }}' \"$image_id\")",
"actual_repo=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}' \"$image_id\")",
`test "$actual_commit" = ${shellQuote(commit)}`,
`test "$actual_service" = ${shellQuote(spec.serviceId)}`,
`test "$actual_repo" = ${shellQuote(sourceRepo)}`,
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
...syntheticComposeHealthDeployScript(target.compose, commit, sourceRepo),
"cat \"$health_json\"",
...authHealthGateScript(target.compose.authHealthGate, "\"$health_json\""),
...(target.compose.requireHealthCommit ? [
"health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
"health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_json\")",
`test "$health_commit" = ${shellQuote(commit)}`,
`test "$health_requested_commit" = ${shellQuote(commit)}`,
] : []),
].join("\n");
const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) });
if (deploy.exitCode !== 0 || deploy.timedOut) {
return {
ok: false,
serviceId: spec.serviceId,
step: "compose-recreate",
sourceImage,
targetImage: composeImage,
registryProbe: commandTail(registryProbe),
runtimeSecretPreflight: secretPreflight,
deploy: commandTail(deploy),
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? undefined,
};
}
const running = runCommand(["docker", "inspect", containerName, "--format", "image={{.Config.Image}} imageId={{.Image}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"], repoRoot);
return {
ok: true,
supported: true,
serviceId: spec.serviceId,
commit,
providerId: options.providerId,
sourceImage,
registryProbe: commandTail(registryProbe),
localLoadedImage,
targetImage: composeImage,
targetCommitImage: commitImage,
pull: commandTail(pull),
deploy: commandTail(deploy),
running: running.stdout.trim(),
validation: {
liveCommit: commit,
liveRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
imageLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? "native-health" : target.compose.syntheticHealthDeployProof,
requiredRuntimeSecrets: secretPreflight.requirements,
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
requiredFields: target.compose.authHealthGate.requiredFields,
passed: true,
},
healthyOldVersionAccepted: false,
},
rollback: {
previousImageHint: `Use docker image ls and docker inspect logs for the previous ${spec.serviceId} image id; Compose named volumes were not changed.`,
commandShape: `${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate ${serviceName}`,
},
};
}
export async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
const spec = artifactConsumerSpecs["backend-core"];
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
return deployComposeArtifactNow(options, spec, target);
}
export function d601ComposeArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): string {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const compose = target.compose;
const sourceImage = artifactImageRef(options, spec, commit);
const sourceRepo = sourceRepoFor(options, spec);
const commitImage = target.targetCommitImage(commit);
const envValues = composeArtifactEnvValues(spec, target, options, commit);
const labels = {
"unidesk.ai/deploy-service-id": spec.serviceId,
"unidesk.ai/deploy-ref": deployRefFor(options, spec),
"unidesk.ai/deploy-repo": sourceRepo,
"unidesk.ai/deploy-commit": commit,
"unidesk.ai/deploy-requested-commit": commit,
"unidesk.ai/image-source": sourceImage,
"unidesk.ai/deploy-environment": options.environment ?? "prod",
};
const override = {
services: {
[compose.serviceName]: {
image: "${UNIDESK_ARTIFACT_STABLE_IMAGE}",
labels,
environment: {
UNIDESK_DEPLOY_SERVICE_ID: spec.serviceId,
UNIDESK_DEPLOY_REF: deployRefFor(options, spec),
UNIDESK_DEPLOY_REPO: sourceRepo,
UNIDESK_DEPLOY_COMMIT: commit,
UNIDESK_DEPLOY_REQUESTED_COMMIT: commit,
...envValues,
},
},
},
};
return [
"set -euo pipefail",
`registry_image=${shellQuote(sourceImage)}`,
`stable_image=${shellQuote(target.targetImage)}`,
`commit_image=${shellQuote(commitImage)}`,
`service_id=${shellQuote(spec.serviceId)}`,
`source_repo=${shellQuote(sourceRepo)}`,
`deploy_ref=${shellQuote(deployRefFor(options, spec))}`,
`commit=${shellQuote(commit)}`,
`dockerfile=${shellQuote(spec.dockerfile)}`,
`work_dir=${shellQuote(compose.workDir ?? "/home/ubuntu")}`,
`compose_file=${shellQuote(compose.composeFile ?? "docker-compose.yml")}`,
`compose_service=${shellQuote(compose.serviceName)}`,
`container=${shellQuote(compose.containerName)}`,
`project_hint=${shellQuote(compose.projectHint ?? "")}`,
`health_probe_b64=${shellQuote(base64(compose.healthProbeCommand))}`,
`override_b64=${shellQuote(base64(JSON.stringify(override, null, 2)))}`,
"command -v docker >/dev/null",
"docker compose version >/dev/null",
`curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' ${shellQuote(`http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`)} >/dev/null`,
"docker pull -q \"$registry_image\" >/dev/null",
"label_commit=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}')",
"label_service=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/service-id\" }}')",
"label_repo=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}')",
"label_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')",
"test \"$label_commit\" = \"$commit\"",
"test \"$label_service\" = \"$service_id\"",
"test \"$label_repo\" = \"$source_repo\"",
"test \"$label_dockerfile\" = \"$dockerfile\"",
"test -d \"$work_dir\"",
"test -f \"$work_dir/$compose_file\"",
"docker tag \"$registry_image\" \"$stable_image\"",
"docker tag \"$registry_image\" \"$commit_image\"",
"override=\"$work_dir/.unidesk-artifact-consumer.override.yml\"",
"printf '%s' \"$override_b64\" | base64 -d > \"$override\"",
"export UNIDESK_ARTIFACT_STABLE_IMAGE=\"$stable_image\"",
"project=$(docker inspect \"$container\" --format '{{ index .Config.Labels \"com.docker.compose.project\" }}' 2>/dev/null || true)",
"if [ -z \"$project\" ]; then project=\"$project_hint\"; fi",
"if [ -z \"$project\" ]; then project=$(basename \"$work_dir\"); fi",
"cd \"$work_dir\"",
"docker compose -p \"$project\" -f \"$compose_file\" -f \"$override\" up -d --no-build --no-deps --force-recreate \"$compose_service\"",
"ready=0",
"for attempt in $(seq 1 90); do",
" cid=$(docker ps -q --filter label=com.docker.compose.project=\"$project\" --filter label=com.docker.compose.service=\"$compose_service\" --filter label=com.docker.compose.oneoff=False | head -1 || true)",
" if [ -n \"$cid\" ]; then",
" health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)",
" echo \"artifact_cd_container_probe attempt=$attempt cid=$cid health=$health\"",
" if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
" else",
" echo \"artifact_cd_container_probe attempt=$attempt cid=missing\"",
" fi",
" sleep 2",
"done",
"test \"$ready\" = \"1\"",
"cid=$(docker ps -q --filter label=com.docker.compose.project=\"$project\" --filter label=com.docker.compose.service=\"$compose_service\" --filter label=com.docker.compose.oneoff=False | head -1)",
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
"actual_service=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/service-id\" }}' \"$image_id\")",
"container_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-commit\" }}' \"$cid\")",
"test \"$actual_commit\" = \"$commit\"",
"test \"$actual_service\" = \"$service_id\"",
"test \"$container_commit\" = \"$commit\"",
"health_probe=$(printf '%s' \"$health_probe_b64\" | base64 -d)",
"docker exec \"$cid\" sh -lc \"$health_probe\" >/tmp/unidesk-artifact-health.out",
"cat /tmp/unidesk-artifact-health.out",
...authHealthGateScript(compose.authHealthGate, shellQuote("/tmp/unidesk-artifact-health.out")),
"printf 'artifact_cd_service=%s\\nartifact_cd_source_repo=%s\\nartifact_cd_deploy_ref=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_container=%s\\nartifact_cd_container_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_container_label_commit=%s\\n' \"$service_id\" \"$source_repo\" \"$deploy_ref\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$container\" \"$cid\" \"$actual_commit\" \"$container_commit\"",
].join("\n");
}
export async function deployD601ComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
const environment = options.environment ?? "prod";
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const health = runReadonlyStatus(options, true);
if (health.ok !== true) return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
const sourceImage = artifactImageRef(options, spec, commit);
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
return {
ok: false,
supported: true,
serviceId: spec.serviceId,
step: "registry-artifact-check",
error: registryArtifactMissingMessage(spec),
sourceImage,
registryProbe: commandTail(registryProbe),
};
}
const deploy = runRemoteScript(options, d601ComposeArtifactDeployScript(options, spec, target, commit), Math.max(options.timeoutMs, 420_000));
if (deploy.exitCode !== 0 || deploy.timedOut) {
return {
ok: false,
supported: true,
serviceId: spec.serviceId,
step: "d601-compose-artifact-deploy",
sourceImage,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
ok: true,
supported: true,
serviceId: spec.serviceId,
environment,
commit,
providerId: options.providerId,
sourceRepo: sourceRepoFor(options, spec),
deployRef: deployRefFor(options, spec),
sourceImage,
targetImage: target.targetImage,
targetCommitImage: target.targetCommitImage(commit),
composeService: target.compose.serviceName,
containerName: target.compose.containerName,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
validation: {
liveCommit: commit,
liveRequestedCommit: commit,
imageLabelCommit: commit,
containerDeployLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
healthyOldVersionAccepted: false,
},
rollback: rollbackInfo(spec, target, environment, commit),
};
}
export function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
const spec = artifactConsumerSpecs["backend-core"];
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs];
const job = startJob("artifact_registry_backend_core_cd", command, `Pull and deploy backend-core artifact ${options.commit} from D601 registry`);
return {
ok: true,
mode: "async-job",
job,
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
note: "Backend-core CD continues in the background: D601 registry health, provider-gateway SSH image stream, docker load, retag, no-build recreate, live commit verification.",
};
}
export function legacyDeployBackendCoreResult(options: ArtifactRegistryOptions): Record<string, unknown> {
return {
ok: false,
supported: false,
deprecated: true,
action: "deploy-backend-core",
replacement: "bun scripts/cli.ts deploy apply --env prod --service backend-core --commit <full-sha>",
serviceId: "backend-core",
environment: options.environment ?? "prod",
commit: options.commit,
policy: "backend-core production CD must enter through deploy apply --env prod so the standard artifact-consumer guardrails run before any mutation; the legacy artifact-registry deploy-backend-core entry is retained only as a documented compatibility name.",
};
}
export function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod";
const deployJsonService = deployJsonServiceForOptions(options, spec, environment);
const effectiveOptions = deployJsonService === options.deployJsonService ? options : { ...options, deployJsonService };
const drifts = deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService)
? compareDeployJsonExecutorMirrors(deployJsonService, environment, artifactRegistryDeployJsonMirrors(effectiveOptions, spec, target))
: [];
if (deployJsonService !== null && drifts.length > 0) return deployJsonDriftResult(deployJsonService, environment, drifts);
const verificationBlocked = spec.runtimeVerification === "blocked";
const livePolicy = artifactConsumerLivePolicy(spec, environment);
const liveReason = artifactConsumerLiveBlockReason(spec, environment);
const requiresSupervisorApproval = livePolicy === "supervisor-only" || spec.serviceId === "code-queue";
const sourceImage = artifactImageRef(effectiveOptions, spec, commit);
const registryEndpoint = `http://127.0.0.1:${effectiveOptions.port}`;
const config = readConfig();
const contractTarget = deployJsonService?.consumer?.target;
const contractRuntime = deployJsonService?.runtime;
const k3sDeployments = target.k3s === undefined
? []
: [
{ name: contractTarget?.deployment ?? target.k3s.deploymentName, containerName: contractTarget?.containerName ?? target.k3s.containerName },
...(contractTarget === undefined ? (target.k3s.extraDeployments ?? []) : []).map((deployment) => typeof deployment === "string"
? { name: deployment, containerName: target.k3s!.containerName }
: { name: deployment.name, containerName: deployment.containerName ?? target.k3s!.containerName }),
];
const common = {
ok: !verificationBlocked,
supported: !verificationBlocked,
dryRun: true,
mutation: false,
error: verificationBlocked ? "runtime-verification-blocked" : undefined,
environment,
providerId: effectiveOptions.providerId,
artifactRegistryTarget: artifactRegistryOptionsTargetSummary(effectiveOptions),
serviceId: spec.serviceId,
commit,
sourceRepo: sourceRepoFor(effectiveOptions, spec),
deployRef: deployRefFor(effectiveOptions, spec),
sourceImage,
source: {
repo: sourceRepoFor(effectiveOptions, spec),
commit,
dockerfile: spec.dockerfile,
},
registry: {
endpoint: registryEndpoint,
repository: registryRepositoryFor(effectiveOptions, spec),
tag: commit,
imageRef: sourceImage,
digest: null,
digestHeader: "Docker-Content-Digest",
digestSource: "planned registry manifest HEAD; live apply must read this digest before mutation",
},
build: {
willCompile: false,
willRunCargoBuild: false,
willRunDockerBuild: false,
willRunDockerComposeBuild: false,
producerBoundary: spec.serviceId === "backend-core" ? "ci publish-backend-core" : "ci publish-user-service",
},
requiredLabels: {
"unidesk.ai/service-id": spec.serviceId,
"unidesk.ai/source-repo": sourceRepoFor(effectiveOptions, spec),
"unidesk.ai/source-commit": commit,
"unidesk.ai/dockerfile": spec.dockerfile,
},
registryProbe: {
method: "HEAD",
url: `${registryEndpoint}/v2/${registryRepositoryFor(effectiveOptions, spec)}/manifests/${commit}`,
digestHeader: "Docker-Content-Digest",
},
boundary: `${environment} CD is artifact-consumer only: verify commit-pinned registry image, pull/import, deploy, then verify live commit/image/health; it never builds source on the runtime target`,
liveApply: {
policy: livePolicy,
allowed: !verificationBlocked && livePolicy === "enabled",
requiresSupervisorApproval,
reason: liveReason,
},
requiresSupervisorApproval,
selfBootstrapGuard: artifactConsumerSelfBootstrapGuard(spec, environment, requiresSupervisorApproval),
affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, true) : undefined,
sourceOfTruth: deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService) ? deployJsonSourceOfTruth(deployJsonService, environment) : undefined,
driftCheck: deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService) ? {
ok: true,
mirrors: ["artifact-registry-executor", ...(deployJsonService.consumer?.kind === "d601-k3s-managed" ? ["k8s-manifest"] : [])],
} : undefined,
};
if (spec.kind === "compose" || spec.kind === "d601-compose") {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined ? undefined : runtimeSecretRecommendedActionForService(runtimeSecretsRaw, environment, spec.serviceId);
return {
...common,
target: {
kind: spec.kind,
runtimeHost: spec.kind === "d601-compose" ? "D601" : "main-server",
workDir: target.compose.workDir,
composeFile: target.compose.composeFile,
composeService: target.compose.serviceName,
containerName: target.compose.containerName,
targetImage: options.targetImage ?? target.targetImage,
runtimeImage: target.targetCommitImage(commit),
deployEnvPrefix: target.compose.deployEnvPrefix,
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${target.compose.serviceName}`,
},
excludedTargets: spec.serviceId === "code-queue-mgr"
? [
{
serviceId: "code-queue",
reason: "code-queue-mgr is the main-server control-plane sidecar only; scheduler/runner live in the separate Code Queue execution plane and are not part of this artifact consumer.",
},
{
services: ["code-queue-scheduler", "code-queue-runner"],
reason: "dry-run does not mutate D601 Code Queue scheduler, runner, tasks, interrupts, or cancellations.",
},
]
: spec.serviceId === "code-queue"
? codeQueueExcludedTargets(environment)
: undefined,
validation: [
"D601 registry /v2 manifest exists for the commit tag",
spec.kind === "d601-compose"
? "D601-pulled image labels match service id, source repo, source commit, and Dockerfile"
: "loaded image labels match service id, source repo, source commit, and Dockerfile",
spec.kind === "d601-compose"
? "running D601 Compose container is recreated with a no-build override that points the service image at the artifact"
: "running Compose container image label matches the requested commit",
...(spec.kind === "d601-compose" ? ["running Compose container image label matches the requested commit"] : []),
verificationBlocked
? `blocked: ${spec.runtimeVerificationBlockReason}`
: target.compose.requireHealthCommit && target.compose.syntheticHealthDeployProof !== undefined
? `${spec.serviceId} runtime health proof synthesizes deploy.commit/deploy.requestedCommit from Compose container runtime metadata and verifies it against image/container labels, not source directory guesses`
: target.compose.requireHealthCommit
? `${spec.serviceId} runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
...(target.compose.requiredRuntimeSecrets === undefined ? [] : [
`${spec.serviceId} live apply checks required runtime secret presence in the canonical Compose env file without reading or printing values`,
]),
...(target.compose.authHealthGate === undefined ? [] : [
`${spec.serviceId} live apply gates success on /health.auth fields: ${target.compose.authHealthGate.requiredFields.join(", ")}`,
]),
],
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? undefined : {
path: "/health",
requiredAuthFields: target.compose.authHealthGate.requiredFields,
dryRunDisposition: "pending-live-check",
},
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? {
kind: "native-health-deploy-metadata",
sourceDirectoryUsed: false,
} : {
...target.compose.syntheticHealthDeployProof,
sourceDirectoryUsed: false,
sources: ["service-health", "container-env", "container-labels", "image-labels"],
requiredEnvKeys: [
"UNIDESK_DEPLOY_SERVICE_ID",
"UNIDESK_DEPLOY_REF",
"UNIDESK_DEPLOY_REPO",
"UNIDESK_DEPLOY_COMMIT",
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
`${target.compose.deployEnvPrefix}_SERVICE_ID`,
`${target.compose.deployEnvPrefix}_REF`,
`${target.compose.deployEnvPrefix}_REPO`,
`${target.compose.deployEnvPrefix}_COMMIT`,
`${target.compose.deployEnvPrefix}_REQUESTED_COMMIT`,
],
},
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
...common,
target: {
kind: "d601-k3s",
namespace: contractTarget?.namespace ?? target.k3s?.namespace,
deployment: contractTarget?.deployment ?? target.k3s?.deploymentName,
deployments: k3sDeployments,
service: contractTarget?.service ?? target.k3s?.serviceName,
stableImage: targetImageFor(effectiveOptions, target),
runtimeImage: targetCommitImageFor(effectiveOptions, target, commit),
manifestRepoPath: contractTarget?.manifestRepoPath ?? target.k3s?.manifestRepoPath,
deployCommandShape: "kubectl set image + set env + annotate + rollout status",
},
runtime: contractRuntime === undefined ? undefined : {
sourceOfTruth: "deploy.json",
containerPort: contractRuntime.containerPort,
healthPath: contractRuntime.healthPath,
memory: contractRuntime.memory,
health: contractRuntime.health,
},
excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined,
validation: [
"D601 registry /v2 manifest exists for the commit tag before mutation",
"D601 Docker-pulled image labels match service id, source repo, source commit, and Dockerfile",
"native k3s containerd has the commit image and stable runtime image tag",
"Deployment annotation and pod image id label match the requested commit",
"service health via Kubernetes API service proxy returns the same deploy.commit and deploy.requestedCommit",
...(environment === "dev" && spec.serviceId === "frontend" ? ["dev frontend auth/session config is synced from main-server config before rollout"] : []),
],
rollback: rollbackInfo(spec, target, environment, commit),
};
}