Files
pikasTech-unidesk/scripts/src/artifact-registry.ts
T
2026-05-20 02:25:54 +00:00

2142 lines
100 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { runCommand, type CommandResult } from "./command";
import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "./config";
import { resolveComposeCommand, writeComposeEnv } from "./docker";
import { startJob } from "./jobs";
type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install" | "deploy-backend-core" | "deploy-service";
type ArtifactDeployEnvironment = "prod" | "dev";
interface ArtifactRegistryOptions {
environment: ArtifactDeployEnvironment | null;
providerId: string;
host: string;
port: number;
image: string;
baseDir: string;
storageDir: string;
unitName: string;
composeProject: string;
serviceName: string;
containerName: string;
timeoutMs: number;
dryRun: boolean;
runNow: boolean;
commit: string | null;
targetImage: string | null;
serviceId: string | null;
sourceRepo: string;
sourceRepoExplicit: boolean;
deployRef: string | null;
}
interface RenderedFile {
path: string;
mode: string;
sha256: string;
content: string;
}
interface RenderedBundle {
files: RenderedFile[];
paths: {
unit: string;
compose: string;
config: string;
storage: string;
baseDir: string;
};
}
const defaultOptions: ArtifactRegistryOptions = {
environment: null,
providerId: "D601",
host: "127.0.0.1",
port: 5000,
image: "registry:2.8.3",
baseDir: "/home/ubuntu/.unidesk/artifact-registry",
storageDir: "/home/ubuntu/.unidesk/registry-storage",
unitName: "unidesk-artifact-registry.service",
composeProject: "unidesk-artifact-registry",
serviceName: "registry",
containerName: "unidesk-artifact-registry",
timeoutMs: 30_000,
dryRun: false,
runNow: false,
commit: null,
targetImage: null,
serviceId: null,
sourceRepo: "https://github.com/pikasTech/unidesk",
sourceRepoExplicit: false,
deployRef: null,
};
const supportedArtifactConsumerServices = [
"backend-core",
"baidu-netdisk",
"code-queue-mgr",
"decision-center",
"findjob",
"frontend",
"k3sctl-adapter",
"met-nonlinear",
"oa-event-flow",
"pipeline",
"project-manager",
"todo-note",
] as const;
type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number];
const legacyDeployBackendCoreDisabled = true;
interface ArtifactConsumerSpec {
serviceId: SupportedArtifactConsumerService;
environment?: ArtifactDeployEnvironment;
kind: "compose" | "d601-compose" | "d601-k3s";
registryRepository: string;
sourceRepo?: string;
dockerfile: string;
targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>;
prodLiveApply: "enabled" | "supervisor-only" | "unsupported";
prodLiveBlockReason?: string;
runtimeVerification?: "strict" | "blocked";
runtimeVerificationBlockReason?: string;
}
interface ArtifactConsumerTarget {
targetImage: string;
targetCommitImage: (commit: string) => string;
deployRef: string;
compose?: {
serviceName: string;
containerName: string;
deployEnvPrefix: string;
healthProbeCommand: string;
requireHealthCommit: boolean;
workDir?: string;
composeFile?: string;
projectHint?: string;
};
k3s?: {
namespace: string;
manifestRepoPath: string;
deploymentName: string;
serviceName: string;
servicePort: number;
containerName: string;
healthPath: string;
applySelector?: string;
podLabelSelector?: string;
};
}
function todoNoteHealthProbeCommand(): string {
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
}
const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
"backend-core": {
serviceId: "backend-core",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/backend-core",
dockerfile: "src/components/backend-core/Dockerfile",
prodLiveApply: "enabled",
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": {
serviceId: "baidu-netdisk",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/baidu-netdisk",
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.dev.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,
},
},
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,
},
},
},
},
"code-queue-mgr": {
serviceId: "code-queue-mgr",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/code-queue-mgr",
dockerfile: "src/components/microservices/code-queue-mgr/Dockerfile",
prodLiveApply: "supervisor-only",
prodLiveBlockReason: "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation.",
targets: {
dev: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.dev.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
prod: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.prod.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
},
},
"decision-center": {
serviceId: "decision-center",
environment: "prod",
kind: "d601-k3s",
registryRepository: "unidesk/decision-center",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
prodLiveApply: "enabled",
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": {
serviceId: "frontend",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
prodLiveApply: "enabled",
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,
},
},
},
},
"findjob": {
serviceId: "findjob",
environment: "prod",
kind: "d601-compose",
sourceRepo: "https://gitee.com/Lyon1998/findjob",
registryRepository: "unidesk/findjob",
dockerfile: "Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "findjob-server",
targetCommitImage: (commit: string) => `findjob-server:${commit}`,
deployRef: "deploy.json#environments.dev.services.findjob",
compose: {
serviceName: "server",
containerName: "findjob-server",
deployEnvPrefix: "UNIDESK_FINDJOB_DEPLOY",
workDir: "/home/ubuntu/findjob",
composeFile: "docker-compose.yml",
projectHint: "findjob",
healthProbeCommand: "curl -fsS --max-time 12 http://127.0.0.1:3254/api/health",
requireHealthCommit: false,
},
},
prod: {
targetImage: "findjob-server",
targetCommitImage: (commit: string) => `findjob-server:${commit}`,
deployRef: "deploy.json#environments.prod.services.findjob",
compose: {
serviceName: "server",
containerName: "findjob-server",
deployEnvPrefix: "UNIDESK_FINDJOB_DEPLOY",
workDir: "/home/ubuntu/findjob",
composeFile: "docker-compose.yml",
projectHint: "findjob",
healthProbeCommand: "curl -fsS --max-time 12 http://127.0.0.1:3254/api/health",
requireHealthCommit: false,
},
},
},
},
"k3sctl-adapter": {
serviceId: "k3sctl-adapter",
environment: "prod",
kind: "d601-compose",
registryRepository: "unidesk/k3sctl-adapter",
dockerfile: "src/components/microservices/k3sctl-adapter/Dockerfile",
prodLiveApply: "supervisor-only",
prodLiveBlockReason: "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.",
targets: {
prod: {
targetImage: "unidesk-k3sctl-adapter:d601",
targetCommitImage: (commit: string) => `unidesk-k3sctl-adapter:${commit}`,
deployRef: "deploy.json#environments.prod.services.k3sctl-adapter",
compose: {
serviceName: "k3sctl-adapter",
containerName: "k3sctl-adapter",
deployEnvPrefix: "UNIDESK_K3SCTL_ADAPTER_DEPLOY",
workDir: "/home/ubuntu/cq-deploy",
composeFile: "src/components/microservices/k3sctl-adapter/docker-compose.d601.yml",
projectHint: "k3sctl-adapter",
healthProbeCommand: "curl -fsS --max-time 10 http://127.0.0.1:4266/health",
requireHealthCommit: false,
},
},
},
},
"met-nonlinear": {
serviceId: "met-nonlinear",
environment: "prod",
kind: "d601-compose",
sourceRepo: "https://github.com/pikasTech/met_nonlinear",
registryRepository: "unidesk/met-nonlinear",
dockerfile: "docker/unidesk/Dockerfile.ml",
prodLiveApply: "unsupported",
prodLiveBlockReason: "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. The current compose contract does not let CD prove that the recreated long-running container image label equals the requested commit.",
runtimeVerification: "blocked",
runtimeVerificationBlockReason: "D601 direct artifact consumer is implemented, but this service's registered Dockerfile is the ML image contract while the long-running Compose service is met-nonlinear-ts. Publish a labeled artifact that matches the running service image contract before live deploy, or update the service contract to a server Dockerfile.",
targets: {
dev: {
targetImage: "met-nonlinear-ml:tf26",
targetCommitImage: (commit: string) => `met-nonlinear-ml:${commit}`,
deployRef: "deploy.json#environments.dev.services.met-nonlinear",
compose: {
serviceName: "met-nonlinear-ts",
containerName: "met-nonlinear-ts",
deployEnvPrefix: "UNIDESK_MET_NONLINEAR_DEPLOY",
workDir: "/home/ubuntu/met_nonlinear",
composeFile: "docker-compose.unidesk.yml",
projectHint: "met-nonlinear",
healthProbeCommand: "curl -fsS --max-time 20 http://127.0.0.1:3288/health",
requireHealthCommit: false,
},
},
prod: {
targetImage: "met-nonlinear-ml:tf26",
targetCommitImage: (commit: string) => `met-nonlinear-ml:${commit}`,
deployRef: "deploy.json#environments.prod.services.met-nonlinear",
compose: {
serviceName: "met-nonlinear-ts",
containerName: "met-nonlinear-ts",
deployEnvPrefix: "UNIDESK_MET_NONLINEAR_DEPLOY",
workDir: "/home/ubuntu/met_nonlinear",
composeFile: "docker-compose.unidesk.yml",
projectHint: "met-nonlinear",
healthProbeCommand: "curl -fsS --max-time 20 http://127.0.0.1:3288/health",
requireHealthCommit: false,
},
},
},
},
"oa-event-flow": {
serviceId: "oa-event-flow",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/oa-event-flow",
dockerfile: "src/components/microservices/oa-event-flow/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.dev.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
prod: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.prod.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
},
},
"project-manager": {
serviceId: "project-manager",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/project-manager",
dockerfile: "src/components/microservices/project-manager/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.dev.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
prod: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.prod.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
},
},
"pipeline": {
serviceId: "pipeline",
environment: "prod",
kind: "d601-compose",
sourceRepo: "https://github.com/pikasTech/pipeline",
registryRepository: "unidesk/pipeline",
dockerfile: "Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "pipeline-v2-control",
targetCommitImage: (commit: string) => `pipeline-v2-control:${commit}`,
deployRef: "deploy.json#environments.dev.services.pipeline",
compose: {
serviceName: "pipeline-control",
containerName: "pipeline-v2-control",
deployEnvPrefix: "UNIDESK_PIPELINE_DEPLOY",
workDir: "/home/ubuntu/pipeline",
composeFile: "docker-compose.yml",
projectHint: "pipeline",
healthProbeCommand: "curl -fsS --max-time 20 http://127.0.0.1:18082/health",
requireHealthCommit: false,
},
},
prod: {
targetImage: "pipeline-v2-control",
targetCommitImage: (commit: string) => `pipeline-v2-control:${commit}`,
deployRef: "deploy.json#environments.prod.services.pipeline",
compose: {
serviceName: "pipeline-control",
containerName: "pipeline-v2-control",
deployEnvPrefix: "UNIDESK_PIPELINE_DEPLOY",
workDir: "/home/ubuntu/pipeline",
composeFile: "docker-compose.yml",
projectHint: "pipeline",
healthProbeCommand: "curl -fsS --max-time 20 http://127.0.0.1:18082/health",
requireHealthCommit: false,
},
},
},
},
"todo-note": {
serviceId: "todo-note",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/todo-note",
sourceRepo: "https://gitee.com/Lyon1998/todo_note",
dockerfile: "Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.dev.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: todoNoteHealthProbeCommand(),
requireHealthCommit: true,
},
},
prod: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.prod.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: todoNoteHealthProbeCommand(),
requireHealthCommit: true,
},
},
},
},
"dev:frontend": {
serviceId: "frontend",
environment: "dev",
kind: "d601-k3s",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
prodLiveApply: "enabled",
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",
},
},
},
},
};
function isHelpArg(value: string | undefined): boolean {
return value === undefined || value === "help" || value === "--help" || value === "-h";
}
function requireValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`${option} requires a non-empty value`);
return value;
}
function positiveInt(value: string, option: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
return parsed;
}
function absolutePath(value: string, option: string): string {
if (!value.startsWith("/")) throw new Error(`${option} must be an absolute path`);
if (value.includes("\n") || value.includes("\0")) throw new Error(`${option} must not contain control characters`);
return value.replace(/\/+$/u, "");
}
function commitValue(value: string, option: string): string {
const normalized = value.toLowerCase();
if (!/^[0-9a-f]{40}$/u.test(normalized)) throw new Error(`${option} must be a full 40-character commit SHA`);
return normalized;
}
function environmentValue(value: string, option: string): ArtifactDeployEnvironment {
if (value === "prod" || value === "dev") return value;
throw new Error(`${option} must be one of: prod, dev`);
}
function parseOptions(args: string[]): ArtifactRegistryOptions {
const options = { ...defaultOptions };
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--dry-run") {
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;
} else if (arg === "--host") {
options.host = requireValue(args, index, arg);
index += 1;
} else if (arg === "--port") {
options.port = positiveInt(requireValue(args, index, arg), arg);
index += 1;
} else if (arg === "--image") {
options.image = requireValue(args, index, arg);
index += 1;
} else if (arg === "--base-dir") {
options.baseDir = absolutePath(requireValue(args, index, arg), arg);
index += 1;
} else if (arg === "--storage-dir") {
options.storageDir = absolutePath(requireValue(args, index, arg), arg);
index += 1;
} else if (arg === "--timeout-ms") {
options.timeoutMs = positiveInt(requireValue(args, index, arg), arg);
index += 1;
} else if (arg === "--commit") {
options.commit = commitValue(requireValue(args, index, arg), arg);
index += 1;
} else if (arg === "--target-image") {
options.targetImage = requireValue(args, index, arg);
index += 1;
} else if (arg === "--service" || arg === "--service-id") {
options.serviceId = requireValue(args, index, arg);
index += 1;
} else if (arg === "--source-repo") {
options.sourceRepo = requireValue(args, index, arg);
options.sourceRepoExplicit = true;
index += 1;
} else if (arg === "--deploy-ref") {
options.deployRef = requireValue(args, index, arg);
index += 1;
} else if (arg === "--env" || arg === "--environment") {
options.environment = environmentValue(requireValue(args, index, arg), arg);
index += 1;
} else {
throw new Error(`unknown artifact-registry option: ${arg}`);
}
}
if (options.host !== "127.0.0.1") throw new Error("--host is first-stage restricted to 127.0.0.1");
if (options.port !== 5000) throw new Error("--port is first-stage restricted to 5000");
return options;
}
function sha256(text: string): string {
return createHash("sha256").update(text).digest("hex");
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function base64(value: string): string {
return Buffer.from(value, "utf8").toString("base64");
}
function safeName(value: string): string {
return value.replace(/[^A-Za-z0-9_.-]/gu, "-");
}
function rootExecPrelude(): string {
return [
"root_exec() {",
" if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return; fi",
" if sudo -n true >/dev/null 2>&1; then sudo -n \"$@\"; return; fi",
" if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return; fi",
" echo 'artifact_registry_root_access=missing' >&2",
" return 1",
"}",
].join("\n");
}
function file(path: string, content: string, mode = "0644"): RenderedFile {
return { path, mode, sha256: sha256(content), content };
}
function registryConfig(): string {
return `version: 0.1
log:
fields:
service: unidesk-artifact-registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: false
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
`;
}
function composeFile(options: ArtifactRegistryOptions): string {
return `name: ${options.composeProject}
services:
${options.serviceName}:
image: ${options.image}
container_name: ${options.containerName}
restart: unless-stopped
ports:
- "${options.host}:${options.port}:5000"
volumes:
- "${options.baseDir}/config.yml:/etc/docker/registry/config.yml:ro"
- "${options.storageDir}:/var/lib/registry"
labels:
unidesk.ai/service-id: artifact-registry
unidesk.ai/managed-by: host-systemd-docker-compose
unidesk.ai/provider-id: ${options.providerId}
`;
}
function systemdUnit(options: ArtifactRegistryOptions): string {
return `[Unit]
Description=UniDesk D601 Artifact Registry (CNCF Distribution)
Documentation=https://github.com/pikasTech/unidesk/blob/master/docs/reference/artifact-registry.md
After=network-online.target
Wants=network-online.target
ConditionPathExists=/var/run/docker.sock
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=${options.baseDir}
ExecStartPre=/bin/mkdir -p ${options.baseDir} ${options.storageDir}
ExecStartPre=/bin/sh -lc 'docker info >/dev/null'
ExecStart=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml up -d --remove-orphans'
ExecStop=/bin/sh -lc 'docker compose -p ${options.composeProject} -f ${options.baseDir}/compose.yml down'
TimeoutStartSec=120
TimeoutStopSec=120
[Install]
WantedBy=multi-user.target
`;
}
function renderBundle(options: ArtifactRegistryOptions): RenderedBundle {
const paths = {
unit: `/etc/systemd/system/${options.unitName}`,
compose: `${options.baseDir}/compose.yml`,
config: `${options.baseDir}/config.yml`,
storage: options.storageDir,
baseDir: options.baseDir,
};
return {
paths,
files: [
file(paths.config, registryConfig()),
file(paths.compose, composeFile(options)),
file(paths.unit, systemdUnit(options)),
],
};
}
function plan(options: ArtifactRegistryOptions): Record<string, unknown> {
const bundle = renderBundle(options);
return {
ok: true,
providerId: options.providerId,
mode: "d601-host-managed",
firstStage: true,
runtime: {
implementation: "CNCF Distribution Docker registry",
image: options.image,
endpoint: `http://${options.host}:${options.port}`,
publicExposure: "none",
orchestration: "systemd + Docker Compose on D601 host/WSL OS",
k3sManaged: false,
},
dependencies: [
{ name: "Docker Engine", scope: "D601 host", requiredFor: "running the registry container" },
{ name: "Docker Compose plugin", scope: "D601 host", requiredFor: "systemd ExecStart/ExecStop" },
{ name: "systemd", scope: "D601 host", requiredFor: "host-managed service lifecycle" },
{ name: "registry:2.8.3", scope: "D601 Docker cache / upstream pull", requiredFor: "CNCF Distribution runtime image" },
{ name: "provider-gateway Host SSH", scope: "UniDesk control bridge", requiredFor: "readonly status and health checks" },
{ name: "local filesystem storage", scope: bundle.paths.storage, requiredFor: "artifact persistence outside k3s" },
],
boundaries: [
"listen only on D601 host loopback 127.0.0.1:5000",
"do not expose a public port, NodePort, hostPort, or third-party registry",
"do not run inside k3s; keep the registry outside the native k3s failure domain",
"CI builds and publishes backend-core, frontend, and reviewed user-service artifacts on D601",
"CD only pulls, retags, recreates/imports artifacts, and verifies live commit",
"master server CD must not compile Rust or run docker compose build for artifact consumers",
],
renderedPaths: bundle.paths,
artifactConsumerFlow: [
"D601 CI builds unidesk/<service-id>:<commit>",
"D601 CI pushes 127.0.0.1:5000/unidesk/<service-id>:<commit>",
"Compose consumers pull via a controlled provider-gateway SSH image stream",
"k3s consumers pull on D601 and import into native k3s containerd",
"CD retags/recreates or updates Deployment images and verifies live commit metadata",
],
frontendArtifactFlow: [
"D601 CI builds and pushes 127.0.0.1:5000/unidesk/frontend:<commit>",
"dev CD imports the image into D601 native k3s and verifies frontend-dev /health deploy.commit",
"prod CD pulls via provider-gateway image stream, recreates Compose frontend with no build, and verifies /health deploy.commit",
],
};
}
function statusScript(options: ArtifactRegistryOptions, bundle: RenderedBundle): string {
const hashes = Object.fromEntries(bundle.files.map((item) => [item.path, item.sha256]));
return `set -u
kv() { printf '%s=%s\\n' "$1" "$2"; }
bool_file() { [ -f "$1" ] && printf true || printf false; }
bool_dir() { [ -d "$1" ] && printf true || printf false; }
hash_file() { sha256sum "$1" 2>/dev/null | awk '{print $1}' || true; }
unit=${shellQuote(bundle.paths.unit)}
compose=${shellQuote(bundle.paths.compose)}
config=${shellQuote(bundle.paths.config)}
storage=${shellQuote(bundle.paths.storage)}
container=${shellQuote(options.containerName)}
expected_image=${shellQuote(options.image)}
port=${options.port}
kv readonly true
kv unit_path "$unit"
kv compose_path "$compose"
kv config_path "$config"
kv storage_path "$storage"
kv unit_exists "$(bool_file "$unit")"
kv compose_exists "$(bool_file "$compose")"
kv config_exists "$(bool_file "$config")"
kv storage_exists "$(bool_dir "$storage")"
if command -v systemctl >/dev/null 2>&1; then
kv systemctl_available true
kv unit_active "$(systemctl is-active "${options.unitName}" 2>/dev/null || true)"
kv unit_enabled "$(systemctl is-enabled "${options.unitName}" 2>/dev/null || true)"
else
kv systemctl_available false
kv unit_active unknown
kv unit_enabled unknown
fi
if command -v docker >/dev/null 2>&1; then
kv docker_available true
container_running="$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || true)"
container_status="$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || true)"
container_image="$(docker inspect -f '{{.Config.Image}}' "$container" 2>/dev/null || true)"
container_restart_policy="$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$container" 2>/dev/null || true)"
kv container_running "$([ -n "$container_running" ] && printf '%s' "$container_running" || printf false)"
kv container_status "$container_status"
kv container_image "$container_image"
kv container_restart_policy "$container_restart_policy"
else
kv docker_available false
kv container_running false
kv container_status unknown
kv container_image ""
kv container_restart_policy unknown
fi
if command -v ss >/dev/null 2>&1; then
listeners="$(ss -ltnH 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" {print $4}' || true)"
else
listeners=""
fi
listener_count="$(printf '%s\\n' "$listeners" | sed '/^$/d' | wc -l | tr -d ' ')"
bad_listener_count="$(printf '%s\\n' "$listeners" | sed '/^$/d' | grep -Ev "^(127[.]0[.]0[.]1|localhost):${options.port}$|^\\[::1\\]:${options.port}$|^::1:${options.port}$" | wc -l | tr -d ' ')"
loopback_only=false
if [ "\${listener_count:-0}" -gt 0 ] && [ "\${bad_listener_count:-0}" -eq 0 ]; then loopback_only=true; fi
kv listener_count "\${listener_count:-0}"
kv bad_listener_count "\${bad_listener_count:-0}"
kv loopback_only "$loopback_only"
if command -v curl >/dev/null 2>&1; then
kv curl_available true
kv v2_http_code "$(timeout 3 curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:${options.port}/v2/ 2>/dev/null || true)"
else
kv curl_available false
kv v2_http_code ""
fi
config_hash="$(hash_file "$config")"
compose_hash="$(hash_file "$compose")"
unit_hash="$(hash_file "$unit")"
kv config_hash "$config_hash"
kv compose_hash "$compose_hash"
kv unit_hash "$unit_hash"
kv expected_config_hash ${shellQuote(hashes[bundle.paths.config] ?? "")}
kv expected_compose_hash ${shellQuote(hashes[bundle.paths.compose] ?? "")}
kv expected_unit_hash ${shellQuote(hashes[bundle.paths.unit] ?? "")}
kv config_hash_matches "$([ -n "$config_hash" ] && [ "$config_hash" = ${shellQuote(hashes[bundle.paths.config] ?? "")} ] && printf true || printf false)"
kv compose_hash_matches "$([ -n "$compose_hash" ] && [ "$compose_hash" = ${shellQuote(hashes[bundle.paths.compose] ?? "")} ] && printf true || printf false)"
kv unit_hash_matches "$([ -n "$unit_hash" ] && [ "$unit_hash" = ${shellQuote(hashes[bundle.paths.unit] ?? "")} ] && printf true || printf false)"
kv image_matches "$([ "\${container_image:-}" = "$expected_image" ] && printf true || printf false)"
`;
}
function parseKeyValueOutput(stdout: string): Record<string, string> {
const values: Record<string, string> = {};
for (const line of stdout.split(/\r?\n/u)) {
const match = /^([A-Za-z0-9_]+)=(.*)$/u.exec(line);
if (match !== null) values[match[1]] = match[2];
}
return values;
}
function asBool(value: string | undefined): boolean {
return value === "true";
}
function commandTail(result: CommandResult): Record<string, unknown> {
return {
command: result.command.length > 7 ? [...result.command.slice(0, 7), "<readonly-script>"] : result.command,
exitCode: result.exitCode,
signal: result.signal,
timedOut: result.timedOut,
stdoutTail: result.stdout.slice(-4000),
stderrTail: result.stderr.slice(-4000),
};
}
function artifactConsumerSpec(serviceId: string, environment: ArtifactDeployEnvironment | null): ArtifactConsumerSpec | null {
const key = environment === null || environment === "prod" ? serviceId : `${environment}:${serviceId}`;
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).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> {
return {
ok: false,
supported: false,
error: "unsupported",
serviceId,
environment: options.environment ?? "prod",
providerId: options.providerId,
reason: "No standardized D601 registry CD consumer is implemented for this service.",
supportedServices: supportedArtifactConsumerServices,
supportedConsumers: supportedArtifactConsumers(),
policy: "unsupported services must not silently fall back to maintenance-channel source builds or legacy direct deployment",
};
}
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 artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> | null {
const environment = options.environment ?? "prod";
if (spec.runtimeVerification === "blocked") {
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "runtime-verification-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.runtimeVerificationBlockReason ?? `${spec.serviceId} does not yet satisfy strict runtime deploy commit verification.`,
requiredBeforeLiveApply: [
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "artifact CD must not accept a healthy old service or silently fall back to legacy rebuild paths",
};
}
if (environment !== "prod" || spec.prodLiveApply === "enabled") return null;
if (spec.prodLiveApply === "supervisor-only") {
return {
ok: false,
supported: true,
liveApplyAllowed: false,
error: "supervisor-confirmation-required",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} production artifact apply requires supervisor confirmation.`,
dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env prod --service ${spec.serviceId} --commit <full-sha> --dry-run`,
policy: "worker automation must not perform live production apply for this infrastructure control-plane service",
};
}
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "artifact-consumer-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`,
requiredBeforeLiveApply: [
"CI can publish a commit-pinned image with matching service id, source commit, and Dockerfile labels",
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "do not silently fall back to server rebuild, dirty worktrees, mutable tags, or source builds on the runtime target",
};
}
function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
}
function sourceRepoFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string {
return options.sourceRepoExplicit ? options.sourceRepo : spec.sourceRepo ?? options.sourceRepo;
}
function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string {
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 {
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
return runCommand(command, repoRoot, { timeoutMs });
}
function statusFromValues(options: ArtifactRegistryOptions, values: Record<string, string>, command: CommandResult, healthMode: boolean): Record<string, unknown> {
const commandOk = command.exitCode === 0 && !command.timedOut;
const checks = {
systemctlAvailable: asBool(values.systemctl_available),
dockerAvailable: asBool(values.docker_available),
curlAvailable: asBool(values.curl_available),
unitExists: asBool(values.unit_exists),
unitActive: values.unit_active === "active",
unitEnabled: values.unit_enabled === "enabled",
composeExists: asBool(values.compose_exists),
configExists: asBool(values.config_exists),
storageExists: asBool(values.storage_exists),
containerRunning: asBool(values.container_running),
loopbackOnly: asBool(values.loopback_only),
v2Ok: values.v2_http_code === "200",
imageMatches: asBool(values.image_matches),
configHashMatches: asBool(values.config_hash_matches),
composeHashMatches: asBool(values.compose_hash_matches),
unitHashMatches: asBool(values.unit_hash_matches),
};
const installed = checks.unitExists || checks.composeExists || checks.configExists || checks.storageExists || checks.containerRunning;
const healthy = commandOk
&& checks.systemctlAvailable
&& checks.dockerAvailable
&& checks.unitExists
&& checks.unitActive
&& checks.composeExists
&& checks.configExists
&& checks.storageExists
&& checks.containerRunning
&& checks.loopbackOnly
&& checks.v2Ok
&& checks.imageMatches
&& checks.configHashMatches
&& checks.composeHashMatches
&& checks.unitHashMatches;
return {
ok: healthMode ? healthy : commandOk,
readonly: true,
installed,
healthy,
checks,
observed: {
unit: { path: values.unit_path, active: values.unit_active, enabled: values.unit_enabled },
compose: { path: values.compose_path, sha256: values.compose_hash },
config: { path: values.config_path, sha256: values.config_hash },
storage: { path: values.storage_path },
container: {
name: options.containerName,
running: values.container_running,
status: values.container_status,
image: values.container_image,
restartPolicy: values.container_restart_policy,
},
listener: {
count: Number(values.listener_count ?? 0),
badCount: Number(values.bad_listener_count ?? 0),
loopbackOnly: checks.loopbackOnly,
},
registryApi: { v2HttpCode: values.v2_http_code },
},
expected: {
unitHash: values.expected_unit_hash,
composeHash: values.expected_compose_hash,
configHash: values.expected_config_hash,
image: options.image,
endpoint: `http://${options.host}:${options.port}`,
},
command: commandTail(command),
};
}
function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean): Record<string, unknown> {
const bundle = renderBundle(options);
const script = statusScript(options, bundle);
const result = runRemoteScript(options, script);
if (result.exitCode !== 0 || result.timedOut) {
return {
ok: false,
readonly: true,
installed: false,
healthy: false,
checks: {},
expected: {
endpoint: `http://${options.host}:${options.port}`,
image: options.image,
paths: bundle.paths,
},
command: commandTail(result),
};
}
return statusFromValues(options, parseKeyValueOutput(result.stdout), result, healthMode);
}
function remoteWriteFileCommand(item: RenderedFile): string {
const encoded = Buffer.from(item.content, "utf8").toString("base64");
const rootOwned = item.path.startsWith("/etc/");
return [
`target=${shellQuote(item.path)}`,
"tmp=$(mktemp /tmp/unidesk-artifact-registry.XXXXXX)",
"trap 'rm -f \"$tmp\"' EXIT",
`printf %s ${shellQuote(encoded)} | base64 -d > "$tmp"`,
`if [ ${rootOwned ? "1" : "0"} = 1 ]; then`,
" root_exec mkdir -p \"$(dirname \"$target\")\"",
` root_exec install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
"else",
" mkdir -p \"$(dirname \"$target\")\"",
` install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
"fi",
`echo ${shellQuote(`artifact_registry_file_written path=${item.path} sha256=${item.sha256}`)}`,
].join("\n");
}
function install(options: ArtifactRegistryOptions): Record<string, unknown> {
const bundle = renderBundle(options);
const script = [
"set -euo pipefail",
"command -v docker >/dev/null",
"docker compose version >/dev/null",
"command -v systemctl >/dev/null",
rootExecPrelude(),
`mkdir -p ${shellQuote(bundle.paths.baseDir)} ${shellQuote(bundle.paths.storage)}`,
...bundle.files.map(remoteWriteFileCommand),
"root_exec systemctl daemon-reload",
`root_exec systemctl enable --now ${shellQuote(options.unitName)}`,
"sleep 2",
`curl -fsS http://${options.host}:${options.port}/v2/ >/dev/null`,
].join("\n");
const command = runRemoteScript(options, script, Math.max(options.timeoutMs, 120_000));
const status = runReadonlyStatus(options, true);
return {
ok: command.exitCode === 0 && !command.timedOut && status.ok === true,
dryRun: false,
mutation: true,
providerId: options.providerId,
render: {
paths: bundle.paths,
files: bundle.files.map((item) => ({ path: item.path, mode: item.mode, sha256: item.sha256 })),
},
installCommand: commandTail(command),
health: status,
};
}
function installDryRun(options: ArtifactRegistryOptions): Record<string, unknown> {
const bundle = renderBundle(options);
return {
ok: true,
dryRun: true,
readonly: true,
mutation: false,
providerId: options.providerId,
plan: plan(options),
render: bundle,
intendedRemoteActions: [
`mkdir -p ${options.baseDir} ${options.storageDir}`,
`write ${bundle.paths.config}`,
`write ${bundle.paths.compose}`,
`write ${bundle.paths.unit}`,
"systemctl daemon-reload",
`systemctl enable --now ${options.unitName}`,
`curl -fsS http://${options.host}:${options.port}/v2/`,
],
note: "Dry run only; no D601 runtime files or services were changed.",
};
}
function composeLockScript(innerScript: string): string {
const lockDir = rootPath(".state", "locks");
const lockPath = rootPath(".state", "locks", "server-compose.lock");
return [
"set -euo pipefail",
`mkdir -p ${shellQuote(lockDir)}`,
`echo ${shellQuote(`compose_lock_wait ${lockPath}`)}`,
`flock ${shellQuote(lockPath)} bash -lc ${shellQuote(innerScript)}`,
].join("; ");
}
function upsertEnvFileValues(path: string, values: Record<string, string>): void {
const existing = readFileSync(path, "utf8");
const seen = new Set<string>();
const lines = existing.split(/\n/u).filter((line, index, array) => index < array.length - 1 || line.length > 0).map((line) => {
const match = /^([A-Za-z0-9_]+)=/u.exec(line);
if (match === null || values[match[1]] === undefined) return line;
seen.add(match[1]);
return `${match[1]}=${values[match[1]]}`;
});
for (const [key, value] of Object.entries(values)) {
if (!seen.has(key)) lines.push(`${key}=${value}`);
}
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
}
function pullArtifactFromD601(options: ArtifactRegistryOptions, sourceImage: string): CommandResult {
const remoteScript = [
"set -euo pipefail",
`image=${shellQuote(sourceImage)}`,
"export DOCKER_CONFIG=$(mktemp -d /tmp/unidesk-artifact-docker-config.XXXXXX)",
"trap 'rm -rf \"$DOCKER_CONFIG\"' EXIT",
"printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"",
"docker pull -q \"$image\" >/dev/null",
"docker image inspect \"$image\" --format 'remote_source={{ index .Config.Labels \"unidesk.ai/source-commit\" }} remote_service={{ index .Config.Labels \"unidesk.ai/service-id\" }} remote_dockerfile={{ index .Config.Labels \"unidesk.ai/dockerfile\" }}' >&2",
"docker save \"$image\" | gzip -1",
].join("\n");
const sshCommand = [
process.execPath,
rootPath("scripts", "cli.ts"),
"ssh",
options.providerId,
"argv",
"bash",
"-lc",
remoteScript,
].map(shellQuote).join(" ");
const pipeline = [
"set -euo pipefail",
`${sshCommand} | gzip -dc | docker load`,
].join("\n");
return runCommand(["bash", "-lc", pipeline], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 900_000) });
}
function registryArtifactProbeScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
const sourceImage = artifactImageRef(options, spec, commit);
return [
"set -euo pipefail",
`registry_image=${shellQuote(sourceImage)}`,
`manifest_url=${shellQuote(`http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`)}`,
"headers=$(mktemp /tmp/unidesk-artifact-manifest.XXXXXX.headers)",
"trap 'rm -f \"$headers\"' EXIT",
"curl -fsSI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -D \"$headers\" -o /dev/null \"$manifest_url\"",
"manifest_digest=$(awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ {gsub(/\\r/, \"\", $2); print $2; exit}' \"$headers\")",
"printf 'registry_image=%s\\nmanifest_url=%s\\nmanifest_digest=%s\\n' \"$registry_image\" \"$manifest_url\" \"$manifest_digest\"",
].join("\n");
}
function registryArtifactMissingMessage(spec: ArtifactConsumerSpec): string {
return `${spec.serviceId} image artifact is missing from D601 registry; run CI artifact publication first`;
}
function verifyLocalArtifactLabels(
localLoadedImage: string,
spec: ArtifactConsumerSpec,
commit: string,
): Record<string, unknown> | null {
const inspectPulled = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{json .Config.Labels}}"], repoRoot);
const labelCommit = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot);
const labelService = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }}"], repoRoot);
const labelDockerfile = runCommand(["docker", "image", "inspect", localLoadedImage, "--format", "{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}"], repoRoot);
const observed = {
commit: labelCommit.stdout.trim(),
serviceId: labelService.stdout.trim(),
dockerfile: labelDockerfile.stdout.trim(),
labels: inspectPulled.stdout.trim(),
};
const ok = observed.commit === commit
&& observed.serviceId === spec.serviceId
&& observed.dockerfile === spec.dockerfile;
if (ok) return null;
return {
ok: false,
step: "image-label-verify",
expected: { commit, serviceId: spec.serviceId, dockerfile: spec.dockerfile },
observed,
};
}
function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
const secretData = {
AUTH_USERNAME: base64(config.auth.username),
AUTH_PASSWORD: base64(config.auth.password),
SESSION_SECRET: base64(config.auth.sessionSecret),
};
const configData = {
SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds),
};
return [
`secret_patch=${shellQuote(JSON.stringify({ data: secretData }))}`,
`config_patch=${shellQuote(JSON.stringify({ data: configData }))}`,
"kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p \"$secret_patch\"",
"kubectl -n unidesk-dev patch configmap unidesk-dev-runtime-config --type merge -p \"$config_patch\"",
"echo artifact_cd_dev_frontend_auth_synced=ok",
].join("\n");
}
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),
[`${prefix}_REPO`]: sourceRepoFor(options, spec),
[`${prefix}_COMMIT`]: commit,
[`${prefix}_REQUESTED_COMMIT`]: commit,
};
}
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 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 ?? 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 = 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);
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) };
}
const config = readConfig();
const runtimeEnv = writeComposeEnv(config, false);
upsertEnvFileValues(runtimeEnv.envFile, {
...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 = 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\")",
`test "$actual_commit" = ${shellQuote(commit)}`,
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
"cat \"$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),
deploy: commandTail(deploy),
};
}
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",
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}`,
},
};
}
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);
}
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_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')",
"test \"$label_commit\" = \"$commit\"",
"test \"$label_service\" = \"$service_id\"",
"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",
"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");
}
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),
};
}
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.",
};
}
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.",
};
}
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod";
const verificationBlocked = spec.runtimeVerification === "blocked";
const sourceImage = artifactImageRef(options, spec, commit);
const common = {
ok: !verificationBlocked,
supported: !verificationBlocked,
dryRun: true,
mutation: false,
error: verificationBlocked ? "runtime-verification-blocked" : undefined,
environment,
providerId: options.providerId,
serviceId: spec.serviceId,
commit,
sourceRepo: sourceRepoFor(options, spec),
deployRef: deployRefFor(options, spec),
sourceImage,
requiredLabels: {
"unidesk.ai/service-id": spec.serviceId,
"unidesk.ai/source-commit": commit,
"unidesk.ai/dockerfile": spec.dockerfile,
},
registryProbe: {
method: "HEAD",
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
},
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: spec.prodLiveApply,
allowed: !verificationBlocked && (environment !== "prod" || spec.prodLiveApply === "enabled"),
reason: spec.runtimeVerificationBlockReason ?? spec.prodLiveBlockReason ?? null,
},
};
if (spec.kind === "compose" || spec.kind === "d601-compose") {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
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}`,
},
validation: [
"D601 registry /v2 manifest exists for the commit tag",
spec.kind === "d601-compose"
? "D601-pulled image labels match service id, source commit, and Dockerfile"
: "loaded image labels match service id, 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
? `${spec.serviceId} runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
],
rollback: rollbackInfo(spec, target, environment, commit),
};
}
return {
...common,
target: {
kind: "d601-k3s",
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: [
"D601 registry /v2 manifest exists for the commit tag before mutation",
"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 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),
};
}
function rollbackInfo(spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, environment: ArtifactDeployEnvironment, commit: string): Record<string, unknown> {
if (spec.kind === "compose" || spec.kind === "d601-compose") {
const compose = target.compose;
return {
type: spec.kind === "d601-compose" ? "d601-compose-retag-recreate" : "compose-retag-recreate",
composeService: compose?.serviceName,
containerName: compose?.containerName,
workDir: compose?.workDir,
composeFile: compose?.composeFile,
previousImageHint: `Use docker image ls / docker inspect to find the previous labeled ${spec.serviceId} image id; Compose volumes are unchanged.`,
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit <previous-full-sha>" : ""}`,
};
}
return {
type: "d601-k3s-previous-commit",
serviceId: spec.serviceId,
environment,
currentCommit: commit,
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, 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 sourceRepo = sourceRepoFor(options, spec);
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)}`,
`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=${environment}`,
];
return [
"set -euo pipefail",
rootExecPrelude(),
`registry_image=${shellQuote(sourceImage)}`,
`stable_image=${shellQuote(target.targetImage)}`,
`commit_image=${shellQuote(commitImage)}`,
`environment=${shellQuote(environment)}`,
`service_id=${shellQuote(spec.serviceId)}`,
`source_repo=${shellQuote(sourceRepo)}`,
`deploy_ref=${shellQuote(deployRefFor(options, spec))}`,
`commit=${shellQuote(commit)}`,
`dockerfile=${shellQuote(spec.dockerfile)}`,
`namespace=${shellQuote(k3s.namespace)}`,
`deployment=${shellQuote(k3s.deploymentName)}`,
`container_name=${shellQuote(k3s.containerName)}`,
`service_name=${shellQuote(k3s.serviceName)}`,
`service_port=${shellQuote(String(k3s.servicePort))}`,
`health_path=${shellQuote(k3s.healthPath)}`,
`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",
"command -v ctr >/dev/null",
"test -S /run/k3s/containerd/containerd.sock",
`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_dockerfile=$(docker image inspect \"$registry_image\" --format '{{ index .Config.Labels \"unidesk.ai/dockerfile\" }}')",
"test \"$label_commit\" = \"$commit\"",
"test \"$label_service\" = \"$service_id\"",
"test \"$label_dockerfile\" = \"$dockerfile\"",
"docker tag \"$registry_image\" \"$stable_image\"",
"docker tag \"$registry_image\" \"$commit_image\"",
"archive=$(mktemp /tmp/unidesk-artifact-k3s-image.XXXXXX.tar)",
"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",
"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\"",
`kubectl -n "$namespace" annotate "deployment/$deployment" ${labels.map(shellQuote).join(" ")} --overwrite`,
"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() {",
" ref=\"$1\"",
" key=\"$2\"",
" target_digest=$(root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images info \"$ref\" | python3 -c 'import json,sys; print(((json.load(sys.stdin).get(\"Target\") or {}).get(\"Digest\")) or \"\")')",
" test -n \"$target_digest\"",
" python3 - \"$target_digest\" \"$key\" <<'PY'",
"import json",
"import subprocess",
"import sys",
"",
"digest, key = sys.argv[1:3]",
"base = ['ctr', '--address', '/run/k3s/containerd/containerd.sock', '-n', 'k8s.io', 'content', 'get']",
"",
"def content(ref):",
" return subprocess.check_output(base + [ref])",
"",
"def labels_from(ref):",
" obj = json.loads(content(ref))",
" if isinstance(obj.get('manifests'), list):",
" for item in obj['manifests']:",
" platform = item.get('platform') or {}",
" if platform.get('architecture') in ('amd64', '') or not platform:",
" value = labels_from(item.get('digest', ''))",
" if value:",
" return value",
" return ''",
" config = obj.get('config') or {}",
" config_digest = config.get('digest') or ''",
" if not config_digest:",
" return ''",
" cfg = json.loads(content(config_digest))",
" return str(((cfg.get('config') or {}).get('Labels') or {}).get(key) or '')",
"",
"print(labels_from(digest))",
"PY",
"}",
"image_label_commit=$(containerd_config_label \"$commit_image\" 'unidesk.ai/source-commit')",
"image_label_service=$(containerd_config_label \"$commit_image\" 'unidesk.ai/service-id')",
"test \"$image_label_commit\" = \"$commit\"",
"test \"$image_label_service\" = \"$service_id\"",
"pod=$(kubectl -n \"$namespace\" get pod -l \"$pod_selector\" -o jsonpath='{.items[0].metadata.name}')",
"test -n \"$pod\"",
"pod_image=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')",
"test \"$pod_image\" = \"$commit_image\"",
"pod_image_id=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.containerStatuses[?(@.name==\"'\"$container_name\"'\")].imageID}')",
"if [ -z \"$pod_image_id\" ]; then pod_image_id=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.status.containerStatuses[0].imageID}'); fi",
"case \"$health_path\" in /*) ;; *) health_path=\"/$health_path\" ;; esac",
"proxy_path=\"/api/v1/namespaces/$namespace/services/http:$service_name:$service_port/proxy$health_path\"",
"health_tmp=$(mktemp /tmp/unidesk-artifact-health.XXXXXX.json)",
"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 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_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, 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);
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 deployScript = d601K3sArtifactDeployScript(options, spec, target, commit);
const deploy = runRemoteScript(options, deployScript, Math.max(options.timeoutMs, 420_000));
if (deploy.exitCode !== 0 || deploy.timedOut) {
return {
ok: false,
supported: true,
serviceId: spec.serviceId,
step: "d601-k3s-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,
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, target, environment, commit),
};
}
async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
if (options.serviceId === null) throw new Error("artifact-registry deploy-service requires --service <id>");
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);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target);
if (spec.kind === "d601-compose") return deployD601ComposeArtifactNow(options, spec, target);
return deployD601K3sArtifactNow(options, spec, target);
}
function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
if (options.serviceId === null) throw new Error("artifact-registry deploy-service requires --service <id>");
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);
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
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`);
return {
ok: true,
mode: "async-job",
serviceId: options.serviceId,
environment: options.environment ?? "prod",
job,
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, target, options.environment ?? "prod", options.commit),
};
}
function serviceIdFromDeployBackendCoreArgs(action: ArtifactRegistryAction, options: ArtifactRegistryOptions): string | null {
return action === "deploy-backend-core" ? "backend-core" : options.serviceId;
}
function localHelp(): Record<string, unknown> {
return {
ok: true,
command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service",
output: "json",
usage: [
"bun scripts/cli.ts artifact-registry plan [--provider-id D601]",
"bun scripts/cli.ts artifact-registry render [--provider-id D601]",
"bun scripts/cli.ts artifact-registry status [--provider-id D601]",
"bun scripts/cli.ts artifact-registry health [--provider-id D601]",
"bun scripts/cli.ts artifact-registry install [--provider-id D601]",
"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 --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]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service todo-note --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service findjob --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service pipeline --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env dev --service met-nonlinear --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service k3sctl-adapter --commit <full-sha> --dry-run [--provider-id D601]",
],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: {
supportedServices: supportedArtifactConsumerServices,
supportedConsumers: supportedArtifactConsumers(),
unsupportedPolicy: "return structured unsupported; never fall back to legacy maintenance-channel source builds",
prodCommands: [
"bun scripts/cli.ts deploy apply --env prod --service backend-core",
"bun scripts/cli.ts deploy apply --env prod --service baidu-netdisk",
"bun scripts/cli.ts deploy apply --env prod --service frontend",
"bun scripts/cli.ts deploy apply --env prod --service decision-center",
"bun scripts/cli.ts deploy apply --env prod --service project-manager",
"bun scripts/cli.ts deploy apply --env prod --service oa-event-flow",
"bun scripts/cli.ts deploy apply --env prod --service code-queue-mgr --dry-run",
"bun scripts/cli.ts deploy apply --env prod --service todo-note",
"bun scripts/cli.ts deploy apply --env prod --service findjob --dry-run",
"bun scripts/cli.ts deploy apply --env prod --service pipeline --dry-run",
"bun scripts/cli.ts deploy apply --env prod --service met-nonlinear --dry-run",
"bun scripts/cli.ts deploy apply --env prod --service k3sctl-adapter --dry-run",
],
devCommands: [
"bun scripts/cli.ts deploy apply --env dev --service frontend",
"bun scripts/cli.ts deploy apply --env dev --service decision-center",
"bun scripts/cli.ts deploy apply --env dev --service project-manager --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service oa-event-flow --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service code-queue-mgr --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service todo-note",
"bun scripts/cli.ts deploy apply --env dev --service findjob --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service pipeline --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service met-nonlinear --dry-run",
],
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
},
legacyEntrypoints: {
"deploy-backend-core": {
deprecated: true,
enabled: !legacyDeployBackendCoreDisabled,
replacement: "bun scripts/cli.ts deploy apply --env prod --service backend-core --commit <full-sha>",
},
},
defaults: defaultOptions,
};
}
export async function runArtifactRegistryCommand(args: string[]): Promise<unknown> {
const action = args[0];
if (isHelpArg(action)) return localHelp();
if (action !== "plan" && action !== "render" && action !== "status" && action !== "health" && action !== "install" && action !== "deploy-backend-core" && action !== "deploy-service") {
throw new Error("artifact-registry usage: plan|render|status|health|install|deploy-backend-core|deploy-service");
}
const typedAction = action as ArtifactRegistryAction;
const options = parseOptions(args.slice(1));
if (action === "plan") return plan(options);
if (action === "render") return { ok: true, providerId: options.providerId, render: renderBundle(options) };
if (action === "status") return runReadonlyStatus(options, false);
if (action === "health") return runReadonlyStatus(options, true);
if (action === "install") {
return options.dryRun ? installDryRun(options) : install(options);
}
if (action === "deploy-backend-core") {
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
if (legacyDeployBackendCoreDisabled) return legacyDeployBackendCoreResult(options);
return options.runNow ? await deployBackendCoreNow(options) : deployBackendCoreJob(args, options);
}
if (action === "deploy-service") {
const serviceId = serviceIdFromDeployBackendCoreArgs(typedAction, options);
return options.runNow || options.dryRun ? await deployServiceNow({ ...options, serviceId }) : deployServiceJob(args, { ...options, serviceId });
}
throw new Error("unreachable artifact-registry action");
}