Add decision-center k3s artifact consumer paths

This commit is contained in:
Codex
2026-05-19 16:22:03 +00:00
parent 2a21f6cda3
commit b2f16f235b
13 changed files with 543 additions and 161 deletions
+218 -121
View File
@@ -78,6 +78,10 @@ interface ArtifactConsumerSpec {
kind: "compose" | "d601-k3s";
registryRepository: string;
dockerfile: string;
targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>;
}
interface ArtifactConsumerTarget {
targetImage: string;
targetCommitImage: (commit: string) => string;
deployRef: string;
@@ -90,7 +94,7 @@ interface ArtifactConsumerSpec {
};
k3s?: {
namespace: string;
manifestPath: string;
manifestRepoPath: string;
deploymentName: string;
serviceName: string;
servicePort: number;
@@ -108,15 +112,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/backend-core",
dockerfile: "src/components/backend-core/Dockerfile",
targetImage: "unidesk-backend-core",
targetCommitImage: (commit: string) => `unidesk-backend-core:${commit}`,
deployRef: "deploy.json#environments.prod.services.backend-core",
compose: {
serviceName: "backend-core",
containerName: "unidesk-backend-core",
deployEnvPrefix: "UNIDESK_DEPLOY",
healthProbeCommand: "if command -v backend-core >/dev/null 2>&1; then backend-core --fetch-json http://127.0.0.1:8080/health --require-ok; elif command -v bun >/dev/null 2>&1; then bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"; else exit 1; fi",
requireHealthCommit: false,
targets: {
prod: {
targetImage: "unidesk-backend-core",
targetCommitImage: (commit: string) => `unidesk-backend-core:${commit}`,
deployRef: "deploy.json#environments.prod.services.backend-core",
compose: {
serviceName: "backend-core",
containerName: "unidesk-backend-core",
deployEnvPrefix: "UNIDESK_DEPLOY",
healthProbeCommand: "if command -v backend-core >/dev/null 2>&1; then backend-core --fetch-json http://127.0.0.1:8080/health --require-ok; elif command -v bun >/dev/null 2>&1; then bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"; else exit 1; fi",
requireHealthCommit: false,
},
},
},
},
"baidu-netdisk": {
@@ -125,15 +133,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/baidu-netdisk",
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.prod.services.baidu-netdisk",
compose: {
serviceName: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
targets: {
prod: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.prod.services.baidu-netdisk",
compose: {
serviceName: "baidu-netdisk",
containerName: "baidu-netdisk-backend",
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
},
},
},
},
"decision-center": {
@@ -142,17 +154,36 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "d601-k3s",
registryRepository: "unidesk/decision-center",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
targetImage: "unidesk-decision-center:d601",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.prod.services.decision-center",
k3s: {
namespace: "unidesk",
manifestPath: "/home/ubuntu/cq-deploy/src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml",
deploymentName: "decision-center",
serviceName: "decision-center",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
targets: {
dev: {
targetImage: "unidesk-decision-center:dev",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.dev.services.decision-center",
k3s: {
namespace: "unidesk-dev",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml",
deploymentName: "decision-center-dev",
serviceName: "decision-center-dev",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
podLabelSelector: "app.kubernetes.io/name=decision-center,unidesk.ai/environment=dev",
},
},
prod: {
targetImage: "unidesk-decision-center:d601",
targetCommitImage: (commit: string) => `unidesk-decision-center:${commit}`,
deployRef: "deploy.json#environments.prod.services.decision-center",
k3s: {
namespace: "unidesk",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml",
deploymentName: "decision-center",
serviceName: "decision-center",
servicePort: 4277,
containerName: "decision-center",
healthPath: "/health",
},
},
},
},
"frontend": {
@@ -161,15 +192,19 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
targetImage: "unidesk-frontend",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "deploy.json#environments.prod.services.frontend",
compose: {
serviceName: "frontend",
containerName: "unidesk-frontend",
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
targets: {
prod: {
targetImage: "unidesk-frontend",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "deploy.json#environments.prod.services.frontend",
compose: {
serviceName: "frontend",
containerName: "unidesk-frontend",
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
},
},
},
},
"dev:frontend": {
@@ -178,19 +213,23 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "d601-k3s",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
targetImage: "unidesk-frontend:dev",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "origin/master:deploy.json#environments.dev.services.frontend",
k3s: {
namespace: "unidesk-dev",
manifestPath: "/home/ubuntu/cq-deploy/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
deploymentName: "frontend-dev",
serviceName: "frontend-dev",
servicePort: 8080,
containerName: "frontend",
healthPath: "/health",
applySelector: "app.kubernetes.io/name=frontend",
podLabelSelector: "app.kubernetes.io/name=frontend,app.kubernetes.io/component=web,unidesk.ai/environment=dev",
targets: {
dev: {
targetImage: "unidesk-frontend:dev",
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
deployRef: "origin/master:deploy.json#environments.dev.services.frontend",
k3s: {
namespace: "unidesk-dev",
manifestRepoPath: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
deploymentName: "frontend-dev",
serviceName: "frontend-dev",
servicePort: 8080,
containerName: "frontend",
healthPath: "/health",
applySelector: "app.kubernetes.io/name=frontend",
podLabelSelector: "app.kubernetes.io/name=frontend,app.kubernetes.io/component=web,unidesk.ai/environment=dev",
},
},
},
},
};
@@ -236,6 +275,11 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
options.dryRun = true;
} else if (arg === "--run-now") {
options.runNow = true;
} else if (arg === "--env" || arg === "--environment") {
const environment = requireValue(args, index, arg);
if (environment !== "dev" && environment !== "prod") throw new Error(`${arg} must be dev or prod`);
options.environment = environment;
index += 1;
} else if (arg === "--provider-id") {
options.providerId = requireValue(args, index, arg);
index += 1;
@@ -556,15 +600,22 @@ function commandTail(result: CommandResult): Record<string, unknown> {
function artifactConsumerSpec(serviceId: string, environment: ArtifactDeployEnvironment | null): ArtifactConsumerSpec | null {
const key = environment === null || environment === "prod" ? serviceId : `${environment}:${serviceId}`;
return artifactConsumerSpecs[key] ?? null;
const explicit = artifactConsumerSpecs[key];
if (explicit !== undefined) return explicit;
const shared = artifactConsumerSpecs[serviceId];
return environment !== null && shared?.targets[environment] !== undefined ? shared : null;
}
function supportedArtifactConsumers(): Array<{ environment: ArtifactDeployEnvironment; serviceId: SupportedArtifactConsumerService; kind: ArtifactConsumerSpec["kind"] }> {
return Object.values(artifactConsumerSpecs).map((spec) => ({
environment: spec.environment ?? "prod",
return Object.values(artifactConsumerSpecs).flatMap((spec) => Object.keys(spec.targets).map((environment) => ({
environment: environment as ArtifactDeployEnvironment,
serviceId: spec.serviceId,
kind: spec.kind,
}));
})));
}
function artifactConsumerTarget(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment | null): ArtifactConsumerTarget | null {
return spec.targets[environment ?? "prod"] ?? null;
}
function unsupportedService(serviceId: string, options: ArtifactRegistryOptions): Record<string, unknown> {
@@ -582,12 +633,28 @@ function unsupportedService(serviceId: string, options: ArtifactRegistryOptions)
};
}
function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> {
const environment = options.environment ?? "prod";
return {
ok: false,
supported: false,
error: "unsupported-environment",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: `No standardized ${environment} registry artifact consumer is implemented for ${spec.serviceId}.`,
supportedEnvironments: Object.keys(spec.targets),
policy: "artifact CD must not silently fall back to maintenance-channel source builds or legacy direct deployment",
};
}
function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
}
function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string {
return options.deployRef ?? spec.deployRef;
const target = artifactConsumerTarget(spec, options.environment);
return options.deployRef ?? target?.deployRef ?? `deploy.json#environments.${options.environment ?? "prod"}.services.${spec.serviceId}`;
}
function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs): CommandResult {
@@ -879,9 +946,9 @@ function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
].join("\n");
}
function composeArtifactEnvValues(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const prefix = spec.compose.deployEnvPrefix;
function composeArtifactEnvValues(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const prefix = target.compose.deployEnvPrefix;
return {
[`${prefix}_SERVICE_ID`]: spec.serviceId,
[`${prefix}_REF`]: deployRefFor(options, spec),
@@ -891,17 +958,17 @@ function composeArtifactEnvValues(spec: ArtifactConsumerSpec, options: ArtifactR
};
}
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): Promise<Record<string, unknown>> {
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 (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
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 composeImage = options.targetImage ?? spec.targetImage;
const commitImage = options.targetImage === null ? spec.targetCommitImage(commit) : `${options.targetImage}:${commit}`;
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 {
@@ -938,13 +1005,13 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
const config = readConfig();
const runtimeEnv = writeComposeEnv(config, false);
upsertEnvFileValues(runtimeEnv.envFile, {
...composeArtifactEnvValues(spec, options, commit),
...composeArtifactEnvValues(spec, target, options, commit),
});
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const projectIndex = compose.indexOf("-p");
const composeProject = projectIndex >= 0 && compose[projectIndex + 1] !== undefined ? compose[projectIndex + 1] : "unidesk";
const serviceName = spec.compose.serviceName;
const containerName = spec.compose.containerName;
const serviceName = target.compose.serviceName;
const containerName = target.compose.containerName;
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
const upScript = [
"set -euo pipefail",
@@ -968,9 +1035,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
`test "$actual_commit" = ${shellQuote(commit)}`,
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(spec.compose.healthProbeCommand)} > "$health_json"`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
"cat \"$health_json\"",
...(spec.compose.requireHealthCommit ? [
...(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\")",
`test "$health_commit" = ${shellQuote(commit)}`,
] : []),
@@ -1005,7 +1072,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
validation: {
liveCommit: commit,
imageLabelCommit: commit,
serviceHealthCommit: spec.compose.requireHealthCommit ? commit : "not-required",
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
healthyOldVersionAccepted: false,
},
rollback: {
@@ -1017,11 +1084,17 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
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>");
return deployComposeArtifactNow(options, artifactConsumerSpecs["backend-core"]);
const spec = artifactConsumerSpecs["backend-core"];
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
return deployComposeArtifactNow(options, spec, target);
}
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`);
@@ -1035,13 +1108,15 @@ function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions):
};
}
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod";
const sourceImage = artifactImageRef(options, spec, commit);
const common = {
ok: true,
supported: true,
dryRun: true,
mutation: false,
environment,
providerId: options.providerId,
serviceId: spec.serviceId,
commit,
@@ -1057,42 +1132,42 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
method: "HEAD",
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
},
boundary: "artifact 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",
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`,
};
if (spec.kind === "compose") {
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
return {
...common,
target: {
kind: "compose",
composeService: spec.compose.serviceName,
containerName: spec.compose.containerName,
targetImage: options.targetImage ?? spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
deployEnvPrefix: spec.compose.deployEnvPrefix,
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${spec.compose.serviceName}`,
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}`,
},
validation: [
"D601 registry /v2 manifest exists for the commit tag",
"loaded image labels match service id, source commit, and Dockerfile",
"running Compose container image label matches the requested commit",
spec.compose.requireHealthCommit
target.compose.requireHealthCommit
? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
],
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
...common,
target: {
kind: "d601-k3s",
namespace: spec.k3s?.namespace,
deployment: spec.k3s?.deploymentName,
service: spec.k3s?.serviceName,
stableImage: spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
manifestPath: spec.k3s?.manifestPath,
namespace: target.k3s?.namespace,
deployment: target.k3s?.deploymentName,
service: target.k3s?.serviceName,
stableImage: target.targetImage,
runtimeImage: target.targetCommitImage(commit),
manifestRepoPath: target.k3s?.manifestRepoPath,
deployCommandShape: "kubectl set image + set env + annotate + rollout status",
},
validation: [
@@ -1100,17 +1175,16 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
"D601 Docker-pulled image labels match service id, 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",
...(spec.environment === "dev" && spec.serviceId === "frontend" ? ["dev frontend auth/session config is synced from main-server config before rollout"] : []),
"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, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
const environment = spec.environment ?? "prod";
function rollbackInfo(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, environment: ArtifactDeployEnvironment, commit: string): Record<string, unknown> {
if (spec.kind === "compose") {
const compose = spec.compose;
const compose = target.compose;
return {
type: "compose-retag-recreate",
composeService: compose?.serviceName,
@@ -1121,18 +1195,22 @@ function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string
return {
type: "d601-k3s-previous-commit",
serviceId: spec.serviceId,
environment,
currentCommit: commit,
discovery: `kubectl -n ${spec.k3s?.namespace} rollout history deployment/${spec.k3s?.deploymentName} && kubectl -n ${spec.k3s?.namespace} get deployment ${spec.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`,
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit <previous-full-sha>" : ""}`,
discovery: `kubectl -n ${target.k3s?.namespace} rollout history deployment/${target.k3s?.deploymentName} && kubectl -n ${target.k3s?.namespace} get deployment ${target.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`,
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId} --commit <previous-full-sha>`,
note: "Rollback is exposed as the same artifact consumer pointed at a previous commit-pinned image that still exists in D601 registry.",
};
}
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string, config: UniDeskConfig): string {
if (spec.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config`);
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): string {
const environment = options.environment ?? "prod";
if (target.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config for ${environment}`);
const manifestText = readFileSync(rootPath(target.k3s.manifestRepoPath), "utf8");
const manifestBase64 = Buffer.from(manifestText, "utf8").toString("base64");
const sourceImage = artifactImageRef(options, spec, commit);
const commitImage = spec.targetCommitImage(commit);
const k3s = spec.k3s;
const commitImage = target.targetCommitImage(commit);
const k3s = target.k3s;
const labels = [
`unidesk.ai/deploy-service-id=${spec.serviceId}`,
`unidesk.ai/deploy-ref=${deployRefFor(options, spec)}`,
@@ -1140,13 +1218,15 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
`unidesk.ai/deploy-commit=${commit}`,
`unidesk.ai/deploy-requested-commit=${commit}`,
`unidesk.ai/image-source=${sourceImage}`,
`unidesk.ai/deploy-environment=${environment}`,
];
return [
"set -euo pipefail",
rootExecPrelude(),
`registry_image=${shellQuote(sourceImage)}`,
`stable_image=${shellQuote(spec.targetImage)}`,
`stable_image=${shellQuote(target.targetImage)}`,
`commit_image=${shellQuote(commitImage)}`,
`environment=${shellQuote(environment)}`,
`service_id=${shellQuote(spec.serviceId)}`,
`source_repo=${shellQuote(options.sourceRepo)}`,
`deploy_ref=${shellQuote(deployRefFor(options, spec))}`,
@@ -1158,10 +1238,11 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
`service_name=${shellQuote(k3s.serviceName)}`,
`service_port=${shellQuote(String(k3s.servicePort))}`,
`health_path=${shellQuote(k3s.healthPath)}`,
`manifest=${shellQuote(k3s.manifestPath)}`,
`apply_selector=${shellQuote(k3s.applySelector ?? "")}`,
`pod_selector=${shellQuote(k3s.podLabelSelector ?? `app.kubernetes.io/name=${k3s.deploymentName}`)}`,
"health_tmp=",
`manifest_repo_path=${shellQuote(k3s.manifestRepoPath)}`,
`manifest_b64=${shellQuote(manifestBase64)}`,
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml",
"command -v docker >/dev/null",
"command -v kubectl >/dev/null",
@@ -1178,13 +1259,16 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"docker tag \"$registry_image\" \"$stable_image\"",
"docker tag \"$registry_image\" \"$commit_image\"",
"archive=$(mktemp /tmp/unidesk-artifact-k3s-image.XXXXXX.tar)",
"trap 'rm -f \"$archive\" \"$health_tmp\"' EXIT",
"manifest=$(mktemp /tmp/unidesk-artifact-k3s-manifest.XXXXXX.yaml)",
"trap 'rm -f \"$archive\" \"$manifest\" \"$health_tmp\"' EXIT",
"docker save \"$commit_image\" \"$stable_image\" -o \"$archive\"",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import \"$archive\" >/dev/null",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$stable_image\" >/dev/null",
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$commit_image\" >/dev/null",
"if [ -f \"$manifest\" ]; then if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi; else echo artifact_cd_manifest_missing=$manifest; fi",
...(spec.environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(config)] : []),
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest\"",
"grep -F \"name: $deployment\" \"$manifest\" >/dev/null",
"if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi",
...(environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(readConfig())] : []),
"previous_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)",
"kubectl -n \"$namespace\" set image \"deployment/$deployment\" \"$container_name=$commit_image\"",
"kubectl -n \"$namespace\" set env \"deployment/$deployment\" \"UNIDESK_DEPLOY_SERVICE_ID=$service_id\" \"UNIDESK_DEPLOY_REF=$deploy_ref\" \"UNIDESK_DEPLOY_REPO=$source_repo\" \"UNIDESK_DEPLOY_COMMIT=$commit\" \"UNIDESK_DEPLOY_REQUESTED_COMMIT=$commit\"",
@@ -1192,7 +1276,9 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"if [ -n \"$previous_commit\" ] && [ \"$previous_commit\" != \"$commit\" ]; then kubectl -n \"$namespace\" annotate \"deployment/$deployment\" \"unidesk.ai/deploy-previous-commit=$previous_commit\" --overwrite; fi",
"kubectl -n \"$namespace\" rollout status \"deployment/$deployment\" --timeout=180s",
"deployment_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}')",
"deployment_requested_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-requested-commit}')",
"test \"$deployment_commit\" = \"$commit\"",
"test \"$deployment_requested_commit\" = \"$commit\"",
"deployment_image=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.spec.template.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')",
"test \"$deployment_image\" = \"$commit_image\"",
"containerd_config_label() {",
@@ -1247,20 +1333,22 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
"for attempt in $(seq 1 60); do",
" if kubectl get --raw \"$proxy_path\" > \"$health_tmp\" 2>/tmp/unidesk-artifact-health.err; then",
" health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)",
" health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_tmp\" 2>/dev/null || true)",
" health_ok=$(python3 -c 'import json,sys; print(\"true\" if json.load(open(sys.argv[1])).get(\"ok\") is True else \"false\")' \"$health_tmp\" 2>/dev/null || true)",
" echo \"artifact_cd_health_probe attempt=$attempt ok=$health_ok commit=$health_commit\"",
" if [ \"$health_ok\" = \"true\" ] && [ \"$health_commit\" = \"$commit\" ]; then break; fi",
" echo \"artifact_cd_health_probe attempt=$attempt ok=$health_ok commit=$health_commit requestedCommit=$health_requested_commit\"",
" if [ \"$health_ok\" = \"true\" ] && [ \"$health_commit\" = \"$commit\" ] && [ \"$health_requested_commit\" = \"$commit\" ]; then break; fi",
" else",
" echo \"artifact_cd_health_probe attempt=$attempt request=failed\"",
" fi",
" if [ \"$attempt\" = \"60\" ]; then echo artifact_cd_health_failed >&2; cat \"$health_tmp\" >&2 || true; exit 1; fi",
" sleep 2",
"done",
"printf 'artifact_cd_service=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_previous_commit=%s\\nartifact_cd_pod=%s\\nartifact_cd_pod_image=%s\\nartifact_cd_pod_image_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_health_commit=%s\\n' \"$service_id\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$previous_commit\" \"$pod\" \"$pod_image\" \"$pod_image_id\" \"$image_label_commit\" \"$health_commit\"",
"printf 'artifact_cd_service=%s\\nartifact_cd_environment=%s\\nartifact_cd_manifest_repo_path=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_requested_commit=%s\\nartifact_cd_previous_commit=%s\\nartifact_cd_pod=%s\\nartifact_cd_pod_image=%s\\nartifact_cd_pod_image_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_health_commit=%s\\nartifact_cd_health_requested_commit=%s\\n' \"$service_id\" \"$environment\" \"$manifest_repo_path\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$deployment_requested_commit\" \"$previous_commit\" \"$pod\" \"$pod_image\" \"$pod_image_id\" \"$image_label_commit\" \"$health_commit\" \"$health_requested_commit\"",
].join("\n");
}
async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): Promise<Record<string, unknown>> {
async function deployD601K3sArtifactNow(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>");
const health = runReadonlyStatus(options, true);
@@ -1278,8 +1366,7 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
registryProbe: commandTail(registryProbe),
};
}
const config = readConfig();
const deployScript = d601K3sArtifactDeployScript(options, spec, commit, config);
const deployScript = d601K3sArtifactDeployScript(options, spec, target, commit);
const deploy = runRemoteScript(options, deployScript, Math.max(options.timeoutMs, 420_000));
if (deploy.exitCode !== 0 || deploy.timedOut) {
return {
@@ -1290,29 +1377,33 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
sourceImage,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
ok: true,
supported: true,
serviceId: spec.serviceId,
environment,
commit,
providerId: options.providerId,
sourceRepo: options.sourceRepo,
deployRef: deployRefFor(options, spec),
sourceImage,
stableImage: spec.targetImage,
runtimeImage: spec.targetCommitImage(commit),
stableImage: target.targetImage,
runtimeImage: target.targetCommitImage(commit),
manifestRepoPath: target.k3s?.manifestRepoPath,
registryProbe: commandTail(registryProbe),
deploy: commandTail(deploy),
validation: {
liveCommit: commit,
liveRequestedCommit: commit,
imageLabelCommit: commit,
serviceHealthCommit: commit,
serviceHealthRequestedCommit: commit,
healthyOldVersionAccepted: false,
},
rollback: rollbackInfo(spec, commit),
rollback: rollbackInfo(spec, target, environment, commit),
};
}
@@ -1321,9 +1412,11 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
const spec = artifactConsumerSpec(options.serviceId, options.environment);
if (spec === null) return unsupportedService(options.serviceId, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec);
return deployD601K3sArtifactNow(options, spec);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target);
return deployD601K3sArtifactNow(options, spec, target);
}
function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
@@ -1331,7 +1424,9 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
const spec = artifactConsumerSpec(options.serviceId, options.environment);
if (spec === null) return unsupportedService(options.serviceId, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
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_service_cd", command, `Pull and deploy ${options.environment ?? "prod"} ${options.serviceId} artifact ${options.commit} from D601 registry`);
@@ -1344,7 +1439,7 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
note: "User-service CD continues in the background: D601 registry health, commit-pinned artifact check, pull/import, rollout, image-label and live health commit verification.",
rollback: rollbackInfo(spec, options.commit),
rollback: rollbackInfo(spec, target, options.environment ?? "prod", options.commit),
};
}
@@ -1367,7 +1462,8 @@ function localHelp(): Record<string, unknown> {
"bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: {
@@ -1382,6 +1478,7 @@ function localHelp(): Record<string, unknown> {
],
devCommands: [
"bun scripts/cli.ts deploy apply --env dev --service frontend",
"bun scripts/cli.ts deploy apply --env dev --service decision-center",
],
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
},
+143 -25
View File
@@ -79,6 +79,7 @@ interface ServiceRuntimeState {
deploymentMode: string;
currentCommit: string | null;
healthCommit: string | null;
healthRequestedCommit: string | null;
imageCommit: string | null;
orchestratorCommit: string | null;
healthOk: boolean;
@@ -135,7 +136,7 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "k3sctl-adapter", "code-queue"]);
const devApplySupportedServiceIds = new Set<string>(["backend-core"]);
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "frontend"]);
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "decision-center", "frontend"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center", "frontend"]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
@@ -199,7 +200,7 @@ 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 supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; prod apply uses the D601 registry artifact consumer for backend-core, frontend, baidu-netdisk, and decision-center." },
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply supports backend-core target-side rollout plus reviewed artifact consumers for frontend, baidu-netdisk, and decision-center; prod apply uses the D601 registry artifact consumer for backend-core, frontend, baidu-netdisk, 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." },
@@ -662,6 +663,20 @@ function devK3sDeployService(id: string): UniDeskMicroserviceConfig | undefined
allowedMethods: ["GET", "HEAD"],
allowedPathPrefixes: ["/"],
},
"decision-center": {
name: "UniDesk Dev Decision Center",
description: "Isolated dev Decision Center deployed into D601 native k3s namespace unidesk-dev.",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml",
composeService: "decision-center-dev",
containerName: "k3s:decision-center-dev",
nodeBaseUrl: "k3s://decision-center-dev",
nodePort: 4277,
healthPath: "/health",
route: "/dev/decision-center",
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
allowedPathPrefixes: ["/", "/api/", "/logs"],
},
"code-queue": {
name: "UniDesk Dev Code Queue",
description: "Isolated dev Code Queue execution plane deployed into D601 native k3s namespace unidesk-dev.",
@@ -738,7 +753,12 @@ function isCoreDeployService(service: UniDeskMicroserviceConfig): boolean {
function isDevK3sDeployService(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "k3sctl-managed"
&& devApplySupportedServiceIds.has(service.id);
&& (devApplySupportedServiceIds.has(service.id) || devArtifactConsumerServiceIds.has(service.id));
}
function isDevArtifactConsumerService(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "k3sctl-managed"
&& devArtifactConsumerServiceIds.has(service.id);
}
function isD601MaintenanceDeployBlocked(service: UniDeskMicroserviceConfig): boolean {
@@ -1822,6 +1842,12 @@ function healthDeployCommit(body: Record<string, unknown> | null): string | null
return commit.length > 0 ? commit : null;
}
function healthDeployRequestedCommit(body: Record<string, unknown> | null): string | null {
const deploy = asRecord(body?.deploy);
const commit = asString(deploy?.requestedCommit).toLowerCase();
return commit.length > 0 ? commit : null;
}
function healthSummary(response: unknown): Record<string, unknown> {
const record = asRecord(response);
const body = asRecord(record?.body);
@@ -1943,23 +1969,29 @@ function commitMatches(actual: string | null, desired: string): boolean {
function runtimeCommitVerified(
service: UniDeskMicroserviceConfig,
healthCommit: string | null,
healthRequestedCommit: string | null,
imageCommit: string | null,
orchestratorCommit: string | null,
desired: string,
): boolean {
if (service.deployment.mode === "k3sctl-managed") return commitMatches(orchestratorCommit, desired);
if (service.deployment.mode === "k3sctl-managed") {
if (healthRequestedCommit !== null && healthRequestedCommit.length > 0 && !commitMatches(healthRequestedCommit, desired)) return false;
return commitMatches(orchestratorCommit, desired);
}
if (healthCommit !== null && healthCommit.length > 0 && !commitMatches(healthCommit, desired)) return false;
if (healthRequestedCommit !== null && healthRequestedCommit.length > 0 && !commitMatches(healthRequestedCommit, desired)) return false;
return commitMatches(imageCommit, desired);
}
function runtimeCurrentCommit(
service: UniDeskMicroserviceConfig,
healthCommit: string | null,
healthRequestedCommit: string | null,
imageCommit: string | null,
orchestratorCommit: string | null,
): string | null {
if (service.deployment.mode === "k3sctl-managed") return orchestratorCommit ?? imageCommit;
return healthCommit ?? imageCommit ?? orchestratorCommit;
if (service.deployment.mode === "k3sctl-managed") return orchestratorCommit ?? healthCommit ?? healthRequestedCommit ?? imageCommit;
return healthCommit ?? healthRequestedCommit ?? imageCommit ?? orchestratorCommit;
}
function coreBody(response: unknown): Record<string, unknown> | null {
@@ -2195,18 +2227,37 @@ async function readK8sCommit(config: UniDeskConfig, service: UniDeskMicroservice
return commit.length > 0 ? commit : null;
}
function manifestEnvironmentForService(service: UniDeskMicroserviceConfig): DeployEnvironment {
return service.id === "decision-center" && service.deployment.namespace === "unidesk-dev" ? "dev" : "prod";
}
function serviceDeployRef(service: UniDeskMicroserviceConfig): string {
return `deploy.json#environments.${manifestEnvironmentForService(service)}.services.${service.id}`;
}
function k3sDeploymentManifestPath(service: UniDeskMicroserviceConfig): string {
if (service.id === "decision-center" && service.deployment.namespace === "unidesk-dev") {
return "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml";
}
if (service.id === "decision-center") {
return "src/components/microservices/k3sctl-adapter/k3s/decision-center.k8s.yaml";
}
return k8sManifestPath(service);
}
async function readRuntimeState(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService): Promise<ServiceRuntimeState> {
const reason = unsupportedReason(service);
const health = await serviceHealth(config, service);
const healthBody = coreBody(health);
const healthCommit = healthDeployCommit(healthBody);
const healthRequestedCommit = healthDeployRequestedCommit(healthBody);
const healthRecord = asRecord(health);
const healthOk = healthRecord?.ok === true && healthBody?.ok !== false;
const [imageCommit, orchestratorCommit] = await Promise.all([
readDockerImageCommit(config, service).catch(() => null),
readK8sCommit(config, service).catch(() => null),
]);
const currentCommit = runtimeCurrentCommit(service, healthCommit, imageCommit, orchestratorCommit);
const currentCommit = runtimeCurrentCommit(service, healthCommit, healthRequestedCommit, imageCommit, orchestratorCommit);
return {
serviceId: service.id,
ok: reason === null,
@@ -2218,10 +2269,11 @@ async function readRuntimeState(config: UniDeskConfig, service: UniDeskMicroserv
deploymentMode: service.deployment.mode,
currentCommit,
healthCommit,
healthRequestedCommit,
imageCommit,
orchestratorCommit,
healthOk,
upToDate: reason === null && healthOk && runtimeCommitVerified(service, healthCommit, imageCommit, orchestratorCommit, desired.commitId),
upToDate: reason === null && healthOk && runtimeCommitVerified(service, healthCommit, healthRequestedCommit, imageCommit, orchestratorCommit, desired.commitId),
raw: { health: healthSummary(health) },
};
}
@@ -2233,9 +2285,9 @@ async function healthVerify(config: UniDeskConfig, service: UniDeskMicroserviceC
let latest: ServiceRuntimeState | null = null;
while (Date.now() < deadline) {
latest = await readRuntimeState(config, service, { ...desired, commitId: resolvedCommit });
const commitOk = runtimeCommitVerified(service, latest.healthCommit, latest.imageCommit, latest.orchestratorCommit, resolvedCommit);
const commitOk = runtimeCommitVerified(service, latest.healthCommit, latest.healthRequestedCommit, latest.imageCommit, latest.orchestratorCommit, resolvedCommit);
const ok = latest.healthOk && commitOk;
progressLine("live-health", "probe", { serviceId: service.id, ok, healthOk: latest.healthOk, expectedCommit: resolvedCommit, healthCommit: latest.healthCommit, imageCommit: latest.imageCommit, orchestratorCommit: latest.orchestratorCommit });
progressLine("live-health", "probe", { serviceId: service.id, ok, healthOk: latest.healthOk, expectedCommit: resolvedCommit, healthCommit: latest.healthCommit, healthRequestedCommit: latest.healthRequestedCommit, imageCommit: latest.imageCommit, orchestratorCommit: latest.orchestratorCommit });
if (ok) {
return {
step: "live-health",
@@ -2297,23 +2349,75 @@ async function ensureGithubSshIdentityStep(config: UniDeskConfig, service: UniDe
}
}
async function runDevArtifactConsumerService(
service: UniDeskMicroserviceConfig,
desired: DeployManifestService,
options: DeployOptions,
before: ServiceRuntimeState,
): Promise<Record<string, unknown>> {
const commit = options.commitOverride ?? desired.commitId;
const artifactArgs = [
"deploy-service",
"--env", "dev",
"--service", service.id,
"--commit", commit,
"--source-repo", desired.repo,
"--timeout-ms", String(options.timeoutMs),
"--run-now",
...(options.dryRun ? ["--dry-run"] : []),
];
progressLine("dev-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
const result = await runArtifactRegistryCommand(artifactArgs);
const ok = asRecord(result)?.ok === true;
return {
ok,
action: ok ? "deployed" : "failed",
environment: "dev",
executor: "d601-registry-artifact-consumer",
resolvedCommit: commit,
before,
after: result,
steps: [{
step: "artifact-registry-consumer",
ok,
detail: JSON.stringify(result),
startedAt: nowIso(),
finishedAt: nowIso(),
raw: result,
}],
};
}
async function applyOneService(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService, options: DeployOptions): Promise<Record<string, unknown>> {
const steps: StepResult[] = [];
const startedAt = nowIso();
const reason = unsupportedReason(service);
if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps };
const before = await readRuntimeState(config, service, desired);
if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps };
if (manifestEnvironmentForService(service) === "dev" && isDevArtifactConsumerService(service)) {
const artifact = await runDevArtifactConsumerService(service, desired, options, before);
return {
...artifact,
before,
serviceId: service.id,
startedAt,
finishedAt: nowIso(),
};
}
if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps };
if (!options.dryRun && isD601MaintenanceDeployBlocked(service)) {
return {
ok: false,
serviceId: service.id,
skipped: true,
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core; ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core; artifact consumers must use registry CD. ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
steps,
};
}
const reason = unsupportedReason(service);
if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps };
const before = await readRuntimeState(config, service, desired);
if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps };
if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps };
const runId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
const exportDir = targetExportDir(service, runId);
@@ -2438,6 +2542,8 @@ function environmentDryRunPlan(
commitId: options.commitOverride !== null && service.id === options.serviceId ? options.commitOverride : service.commitId,
}));
const fingerprint = databaseFingerprint(target);
const devUnsupported = devUnsupportedServices(manifest, options.serviceId);
const prodArtifactUnsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
return {
ok: environment === "prod"
? services.every((service) => prodArtifactConsumerServiceIds.has(service.id))
@@ -2480,7 +2586,7 @@ function environmentDryRunPlan(
? "d601-registry-artifact-consumer"
: "unsupported"
: devArtifactConsumerServiceIds.has(service.id)
? service.id === "frontend" ? "d601-registry-artifact-consumer" : "d601-registry-artifact-consumer-dev-validation"
? "d601-registry-artifact-consumer"
: devApplySupportedServiceIds.has(service.id)
? "d601-dev-target-side-build"
: "unsupported",
@@ -2493,8 +2599,15 @@ function environmentDryRunPlan(
? "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)
? {
ok: false,
supported: false,
reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
}
: undefined,
})),
unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
};
}
@@ -2512,7 +2625,7 @@ function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: D
}
function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core; artifact consumers must use registry CD. Blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
}
function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
@@ -2532,6 +2645,11 @@ function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string
return services;
}
function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return [];
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
}
function prodArtifactUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
}
@@ -2569,7 +2687,7 @@ async function runArtifactConsumerApplyNow(
"--commit", commit,
"--source-repo", service.repo,
"--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`,
...(environment === "prod" || service.id === "frontend" ? ["--env", environment] : []),
...(environment === "prod" || service.id === "frontend" || service.id === "decision-center" ? ["--env", environment] : []),
"--timeout-ms", String(options.timeoutMs),
"--run-now",
...(options.dryRun ? ["--dry-run"] : []),
@@ -2690,7 +2808,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
}
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) {
throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk/decision-center artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
}
const devArtifactServices = selectedDevArtifactServices(manifest, options.serviceId);
const devTargetServices = selectedDevTargetServices(manifest, options.serviceId);
@@ -2703,11 +2821,11 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
}
if (config === null) throw new Error("deploy apply --env dev requires config.json");
if (!options.dryRun) {
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId);
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId).filter((serviceId) => serviceId !== "decision-center");
if (blocked.length > 0) throw new Error(d601MaintenanceDeployBlockMessage(blocked));
}
if (!options.runNow) return applyJob(config, args, options);
return await runApplyNow(config, manifest, options);
if (options.dryRun || options.runNow) return await runApplyNow(config, manifest, options);
return applyJob(config, args, options);
}
if (config === null) throw new Error("deploy local manifest mode requires config.json");
const manifest = resolveManifestCommits(await readDeployManifest(options.file), options.serviceId);
@@ -2716,8 +2834,8 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
const blocked = blockedD601MaintenanceDeployServices(config, manifest, options.serviceId);
if (blocked.length > 0) throw new Error(d601MaintenanceDeployBlockMessage(blocked));
}
if (!options.runNow) return applyJob(config, args, options);
return await runApplyNow(config, manifest, options);
if (options.dryRun || options.runNow) return await runApplyNow(config, manifest, options);
return applyJob(config, args, options);
}
export async function runCodeQueueDeployCompatCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
+2 -2
View File
@@ -36,9 +36,9 @@ export function rootHelp(): unknown {
{ command: "decision list [--type ...] [--status ...] [--level ...] [--linked-goal-id id] [--limit N]", description: "List Decision Center records through the user-service proxy." },
{ command: "decision requirement list|upsert [--id id] [--title text] [--body-file path] [--type goal|decision|blocker|debt|experiment]", description: "Manage requirement records over the existing records model, excluding meeting records." },
{ command: "decision show <id>", description: "Show one Decision Center record." },
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env reads origin/master:deploy.json environments and can apply supported dev services or supported prod artifact consumers." },
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from a repo+commit manifest; --env reads origin/master:deploy.json environments and can apply supported dev target-side builds plus decision-center's dev artifact consumer, or supported prod artifact consumers." },
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including decision-center dev/prod artifact consumers." },
{ command: "schedule list|get|runs|run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N." },
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Compatibility wrapper for deploy apply --service code-queue with a temporary repo+commit manifest." },