635 lines
28 KiB
TypeScript
635 lines
28 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. service-plan module for scripts/src/deploy.ts.
|
|
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
|
|
|
|
// Moved mechanically from scripts/src/deploy.ts:634-1236 for #903.
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { runCommand } from "../command";
|
|
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "../config";
|
|
import { ensureGithubSshIdentityForProvider } from "../deploy-ssh-identity";
|
|
import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "../artifact-registry";
|
|
import { startJob } from "../jobs";
|
|
import { coreInternalFetch } from "../microservices";
|
|
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard";
|
|
import { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard";
|
|
import { composeRuntimeEnvValue } from "../runtime-env";
|
|
import {
|
|
compareDeployJsonExecutorMirrors,
|
|
deployJsonCommitImage,
|
|
deployJsonDriftResult,
|
|
deployJsonSourceOfTruth,
|
|
encodeDeployJsonServiceContract,
|
|
hasDeployJsonExecutorContract,
|
|
k3sManifestExecutorMirror,
|
|
parseDeployJsonServiceContract,
|
|
type DeployJsonExecutorMirror,
|
|
type DeployJsonServiceContract,
|
|
} from "../deploy-json-contract";
|
|
|
|
import type { ArtifactConsumerPlanKind, DeployEnvironment, DeployEnvironmentTarget, DeployManifest, DeployManifestService } from "./types";
|
|
import { targetIsMain, targetWorkDir } from "./paths";
|
|
import { artifactConsumerDryRunBlockedServiceIds, artifactDeployNodeId, artifactDeployProviderId, targetSideMaintenanceDeployAllowedServiceIds, devApplySupportedServiceIds, devArtifactConsumerServiceIds, k8sNamespace, prodArtifactConsumerServiceIds, prodArtifactLiveApplyBlockedServiceIds, unideskRepoUrl } from "./types";
|
|
|
|
export function frontendCoreDeployService(config: UniDeskConfig): UniDeskMicroserviceConfig {
|
|
return {
|
|
id: "frontend",
|
|
name: "UniDesk Frontend",
|
|
providerId: "main-server",
|
|
description: "UniDesk core frontend entrypoint deployed by the main-server direct Compose executor.",
|
|
repository: {
|
|
url: unideskRepoUrl,
|
|
commitId: "local",
|
|
dockerfile: "src/components/frontend/Dockerfile",
|
|
composeFile: config.docker.composeFile,
|
|
composeService: "frontend",
|
|
containerName: "unidesk-frontend",
|
|
},
|
|
backend: {
|
|
nodeBaseUrl: `http://127.0.0.1:${config.network.frontend.port}`,
|
|
nodeBindHost: "127.0.0.1",
|
|
nodePort: config.network.frontend.port,
|
|
proxyMode: "core-direct",
|
|
frontendOnly: false,
|
|
public: true,
|
|
allowedMethods: ["GET", "HEAD"],
|
|
allowedPathPrefixes: ["/"],
|
|
healthPath: "/health",
|
|
timeoutMs: 8000,
|
|
},
|
|
deployment: { mode: "unidesk-direct" },
|
|
development: {
|
|
providerId: "main-server",
|
|
sshPassthrough: false,
|
|
worktreePath: repoRoot,
|
|
},
|
|
frontend: {
|
|
route: "/",
|
|
integrated: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function devK3sDeployService(id: string): UniDeskMicroserviceConfig | undefined {
|
|
const specs: Record<string, {
|
|
name: string;
|
|
description: string;
|
|
repoUrl?: string;
|
|
dockerfile: string;
|
|
composeService: string;
|
|
composeFile: string;
|
|
containerName: string;
|
|
nodeBaseUrl: string;
|
|
nodePort: number;
|
|
healthPath: string;
|
|
route: string;
|
|
allowedMethods: string[];
|
|
allowedPathPrefixes: string[];
|
|
}> = {
|
|
"backend-core": {
|
|
name: "UniDesk Dev Backend Core",
|
|
description: "Isolated dev backend-core deployed into D601 native k3s namespace unidesk-dev.",
|
|
dockerfile: "src/components/backend-core/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
|
|
composeService: "backend-core-dev",
|
|
containerName: "k3s:backend-core-dev",
|
|
nodeBaseUrl: "k3s://backend-core-dev",
|
|
nodePort: 8080,
|
|
healthPath: "/health",
|
|
route: "/dev/backend-core",
|
|
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
|
|
allowedPathPrefixes: ["/", "/api/", "/logs"],
|
|
},
|
|
frontend: {
|
|
name: "UniDesk Dev Frontend",
|
|
description: "Isolated dev frontend deployed into D601 native k3s namespace unidesk-dev.",
|
|
dockerfile: "src/components/frontend/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
|
|
composeService: "frontend-dev",
|
|
containerName: "k3s:frontend-dev",
|
|
nodeBaseUrl: "k3s://frontend-dev",
|
|
nodePort: 8080,
|
|
healthPath: "/health",
|
|
route: "/dev/frontend",
|
|
allowedMethods: ["GET", "HEAD"],
|
|
allowedPathPrefixes: ["/"],
|
|
},
|
|
"decision-center": {
|
|
name: "UniDesk Dev Decision Center",
|
|
description: "Isolated dev Decision Center deployed into D601 native k3s namespace unidesk-dev.",
|
|
dockerfile: "src/components/microservices/decision-center/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-decision-center.k8s.yaml",
|
|
composeService: "decision-center-dev",
|
|
containerName: "k3s:decision-center-dev",
|
|
nodeBaseUrl: "k3s://decision-center-dev",
|
|
nodePort: 4277,
|
|
healthPath: "/health",
|
|
route: "/dev/decision-center",
|
|
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
|
|
allowedPathPrefixes: ["/", "/api/", "/logs"],
|
|
},
|
|
mdtodo: {
|
|
name: "UniDesk Dev MDTODO",
|
|
description: "Isolated dev MDTODO deployed into D601 native k3s namespace unidesk-dev.",
|
|
dockerfile: "src/components/microservices/mdtodo/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-mdtodo.k8s.yaml",
|
|
composeService: "mdtodo-dev",
|
|
containerName: "k3s:mdtodo-dev",
|
|
nodeBaseUrl: "k3s://mdtodo-dev",
|
|
nodePort: 4267,
|
|
healthPath: "/health",
|
|
route: "/dev/mdtodo",
|
|
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
|
|
allowedPathPrefixes: ["/health", "/live", "/logs", "/api/"],
|
|
},
|
|
claudeqq: {
|
|
name: "UniDesk Dev ClaudeQQ",
|
|
description: "Isolated dev ClaudeQQ deployed into D601 native k3s namespace unidesk-dev.",
|
|
repoUrl: "https://gitee.com/lyon1998/agent_skills",
|
|
dockerfile: "claudeqq/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-claudeqq.k8s.yaml",
|
|
composeService: "claudeqq-dev",
|
|
containerName: "k3s:claudeqq-dev",
|
|
nodeBaseUrl: "k3s://claudeqq-dev",
|
|
nodePort: 3290,
|
|
healthPath: "/health",
|
|
route: "/dev/claudeqq",
|
|
allowedMethods: ["GET", "HEAD", "POST", "DELETE"],
|
|
allowedPathPrefixes: ["/health", "/logs", "/api/"],
|
|
},
|
|
"code-queue": {
|
|
name: "UniDesk Dev Code Queue",
|
|
description: "Isolated dev Code Queue execution plane deployed into D601 native k3s namespace unidesk-dev.",
|
|
dockerfile: "src/components/microservices/code-queue/Dockerfile",
|
|
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml",
|
|
composeService: "code-queue-scheduler-dev",
|
|
containerName: "k3s:code-queue-scheduler-dev",
|
|
nodeBaseUrl: "k3s://code-queue-dev",
|
|
nodePort: 4222,
|
|
healthPath: "/health",
|
|
route: "/dev/code-queue",
|
|
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
|
|
allowedPathPrefixes: ["/", "/api/", "/logs"],
|
|
},
|
|
};
|
|
const spec = specs[id];
|
|
if (spec === undefined) return undefined;
|
|
return {
|
|
id,
|
|
name: spec.name,
|
|
providerId: artifactDeployProviderId,
|
|
description: spec.description,
|
|
repository: {
|
|
url: spec.repoUrl ?? unideskRepoUrl,
|
|
commitId: "deploy-env",
|
|
dockerfile: spec.dockerfile,
|
|
composeFile: spec.composeFile,
|
|
composeService: spec.composeService,
|
|
containerName: spec.containerName,
|
|
},
|
|
backend: {
|
|
nodeBaseUrl: spec.nodeBaseUrl,
|
|
nodeBindHost: `k3s://unidesk-dev/${spec.composeService}`,
|
|
nodePort: spec.nodePort,
|
|
proxyMode: "dev-k3s-direct",
|
|
frontendOnly: true,
|
|
public: false,
|
|
allowedMethods: spec.allowedMethods,
|
|
allowedPathPrefixes: spec.allowedPathPrefixes,
|
|
healthPath: spec.healthPath,
|
|
timeoutMs: 30_000,
|
|
},
|
|
deployment: {
|
|
mode: "k3sctl-managed",
|
|
adapterServiceId: "k3sctl-adapter",
|
|
k3sServiceId: spec.composeService,
|
|
namespace: "unidesk-dev",
|
|
expectedNodeIds: [artifactDeployNodeId],
|
|
activeNodeId: artifactDeployNodeId,
|
|
},
|
|
development: {
|
|
providerId: artifactDeployProviderId,
|
|
sshPassthrough: true,
|
|
worktreePath: id === "code-queue"
|
|
? "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue"
|
|
: `/home/ubuntu/unidesk-dev-core-deploy/${id}`,
|
|
},
|
|
frontend: {
|
|
route: spec.route,
|
|
integrated: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function coreDeployService(config: UniDeskConfig, id: string, environment: DeployEnvironment | null): UniDeskMicroserviceConfig | undefined {
|
|
if (environment === "dev") return devK3sDeployService(id);
|
|
if (id === "frontend") return frontendCoreDeployService(config);
|
|
return undefined;
|
|
}
|
|
|
|
export function isCoreDeployService(service: UniDeskMicroserviceConfig): boolean {
|
|
return service.id === "frontend" && service.providerId === "main-server" && service.backend.proxyMode === "core-direct";
|
|
}
|
|
|
|
export function isDevK3sDeployService(service: UniDeskMicroserviceConfig): boolean {
|
|
return service.deployment.mode === "k3sctl-managed"
|
|
&& (devApplySupportedServiceIds.has(service.id) || devArtifactConsumerServiceIds.has(service.id));
|
|
}
|
|
|
|
export function isDevArtifactConsumerService(service: UniDeskMicroserviceConfig): boolean {
|
|
return (service.deployment.mode === "k3sctl-managed" || isDirectComposeDeployMode(service))
|
|
&& devArtifactConsumerServiceIds.has(service.id);
|
|
}
|
|
|
|
export function isTargetSideMaintenanceDeployBlocked(service: UniDeskMicroserviceConfig): boolean {
|
|
if (targetIsMain(service)) return false;
|
|
if (service.providerId !== "D601") return false;
|
|
return !targetSideMaintenanceDeployAllowedServiceIds.has(service.id);
|
|
}
|
|
|
|
export function isDirectComposeDeployMode(service: UniDeskMicroserviceConfig): boolean {
|
|
return service.deployment.mode === "unidesk-direct" || service.deployment.mode === "internal-sidecar";
|
|
}
|
|
|
|
export function selectServices(config: UniDeskConfig, manifest: DeployManifest, serviceId: string | null): Array<{
|
|
desired: DeployManifestService;
|
|
config: UniDeskMicroserviceConfig;
|
|
}> {
|
|
const configById = new Map(config.microservices.map((service) => [service.id, service]));
|
|
const selected = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
|
|
if (serviceId !== null && selected.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
|
|
return selected.map((desired) => {
|
|
if (manifest.environment === "dev") {
|
|
const service = devK3sDeployService(desired.id);
|
|
if (service !== undefined) return { desired, config: service };
|
|
const directService = configById.get(desired.id);
|
|
if (directService !== undefined && isDirectComposeDeployMode(directService) && devArtifactConsumerServiceIds.has(directService.id)) {
|
|
return { desired, config: directService };
|
|
}
|
|
throw new Error(`deploy --env dev service ${desired.id} is not enabled for direct rollout in the current CI-only phase`);
|
|
}
|
|
const service = configById.get(desired.id) ?? coreDeployService(config, desired.id, manifest.environment);
|
|
if (service === undefined) throw new Error(`deploy manifest service ${desired.id} is not present in config.json microservices or supported core deploy services`);
|
|
return { desired, config: service };
|
|
});
|
|
}
|
|
|
|
export function unsupportedReason(service: UniDeskMicroserviceConfig): string | null {
|
|
if (service.repository.dockerfile.startsWith("docker.io/")) return "image-only service has no Dockerfile source artifact";
|
|
if (service.repository.composeFile.startsWith("docker run")) return "docker-run image-only service has no compose/k8s build path";
|
|
if (isTargetSideMaintenanceDeployBlocked(service)) return "D601 maintenance-channel deployment is not allowed for this service; use reviewed registry artifact consumers instead";
|
|
if (service.repository.commitId === "local") return null;
|
|
return null;
|
|
}
|
|
|
|
export function databaseFingerprint(target: DeployEnvironmentTarget): string {
|
|
const material = [
|
|
target.environment,
|
|
target.namespace,
|
|
target.database.serviceId,
|
|
target.database.host,
|
|
target.database.port.toString(),
|
|
target.database.name,
|
|
target.provider.providerId,
|
|
].join("|");
|
|
return `sha256:${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
}
|
|
|
|
export function environmentTargetSummary(target: DeployEnvironmentTarget): Record<string, unknown> {
|
|
return {
|
|
namespace: target.namespace,
|
|
runtimeScope: target.runtimeScope,
|
|
database: {
|
|
...target.database,
|
|
fingerprint: databaseFingerprint(target),
|
|
},
|
|
provider: target.provider,
|
|
};
|
|
}
|
|
|
|
export function artifactConsumerPlanKind(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): ArtifactConsumerPlanKind {
|
|
if (environment === "dev" && devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)) {
|
|
return "d601-dev-target-side-build";
|
|
}
|
|
if (service.deployment.mode === "k3sctl-managed") return "d601-k3s-managed";
|
|
if (service.providerId === "D601" && service.deployment.mode === "unidesk-direct") return "d601-direct-compose";
|
|
if (targetIsMain(service) && isDirectComposeDeployMode(service)) return "main-server-compose";
|
|
return "unsupported";
|
|
}
|
|
|
|
export function artifactConsumerPlanKindFromDeployJson(service: DeployManifestService, fallback: ArtifactConsumerPlanKind): ArtifactConsumerPlanKind {
|
|
if (service.consumer?.kind === "d601-k3s-managed") return "d601-k3s-managed";
|
|
return fallback;
|
|
}
|
|
|
|
export function deploymentPathForEnvironmentService(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
|
|
return deploymentPathForEnvironmentServiceId(service.id, environment);
|
|
}
|
|
|
|
export function deploymentPathForEnvironmentServiceId(serviceId: string, environment: DeployEnvironment): string {
|
|
if (environment === "prod") return prodArtifactConsumerServiceIds.has(serviceId) ? "d601-registry-artifact-consumer" : "unsupported";
|
|
if (devArtifactConsumerServiceIds.has(serviceId)) return "d601-registry-artifact-consumer";
|
|
if (devApplySupportedServiceIds.has(serviceId)) return "d601-dev-target-side-build";
|
|
return "unsupported";
|
|
}
|
|
|
|
export function artifactConsumerPlanTarget(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): Record<string, unknown> {
|
|
const kind = artifactConsumerPlanKind(service, environment);
|
|
const liveBlockReason = environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null;
|
|
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
|
|
const targetImage = artifactConsumerTargetImageHint(service, environment);
|
|
const common = {
|
|
consumerKind: kind,
|
|
runtimeHost: kind === "main-server-compose" ? "main-server" : "D601",
|
|
deploymentMode: service.deployment.mode,
|
|
noRuntimeSourceBuild: kind !== "d601-dev-target-side-build",
|
|
dryRunOnly: dryRunBlockedReason !== null || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)),
|
|
blockedReason: dryRunBlockedReason ?? liveBlockReason,
|
|
};
|
|
if (kind === "d601-k3s-managed") {
|
|
return {
|
|
...common,
|
|
namespace: service.deployment.namespace ?? (environment === "dev" ? "unidesk-dev" : k8sNamespace),
|
|
deployment: service.repository.composeService,
|
|
service: service.deployment.k3sServiceId ?? service.repository.composeService,
|
|
containerName: service.repository.containerName,
|
|
manifest: service.repository.composeFile,
|
|
targetImage,
|
|
deployCommandShape: "registry manifest check + native k3s containerd import + Kubernetes Deployment image/env/annotation update + API service proxy health",
|
|
forbiddenActions: ["docker build", "docker compose build", "NodePort", "hostPort", "provider-gateway direct business backend"],
|
|
};
|
|
}
|
|
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
|
|
return {
|
|
...common,
|
|
workDir: targetWorkDir(service),
|
|
composeFile: service.repository.composeFile,
|
|
composeService: service.repository.composeService,
|
|
containerName: service.repository.containerName,
|
|
targetImage,
|
|
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${service.repository.composeService}`,
|
|
forbiddenActions: ["docker build", "docker compose build", "docker compose up --build", "dirty worktree source deploy"],
|
|
};
|
|
}
|
|
if (kind === "d601-dev-target-side-build") {
|
|
return {
|
|
...common,
|
|
namespace: service.deployment.namespace ?? "unidesk-dev",
|
|
deployment: service.repository.composeService,
|
|
manifest: service.repository.composeFile,
|
|
workDir: targetWorkDir(service),
|
|
deployCommandShape: "D601 dev target-side source materialization/build/import/rollout",
|
|
forbiddenActions: ["production namespace mutation", "production database mutation"],
|
|
};
|
|
}
|
|
return common;
|
|
}
|
|
|
|
export function artifactConsumerPlanTargetFromDeployJson(
|
|
service: DeployManifestService,
|
|
fallbackService: UniDeskMicroserviceConfig,
|
|
environment: DeployEnvironment,
|
|
fallbackTarget: Record<string, unknown> | null,
|
|
): Record<string, unknown> | null {
|
|
if (service.consumer?.kind !== "d601-k3s-managed") return fallbackTarget;
|
|
const target = service.consumer.target;
|
|
const liveBlockReason = environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null;
|
|
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
|
|
return {
|
|
consumerKind: service.consumer.kind,
|
|
runtimeHost: "D601",
|
|
deploymentMode: fallbackService.deployment.mode,
|
|
noRuntimeSourceBuild: service.consumer.noRuntimeSourceBuild,
|
|
dryRunOnly: dryRunBlockedReason !== null || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)),
|
|
blockedReason: dryRunBlockedReason ?? liveBlockReason,
|
|
namespace: target.namespace,
|
|
deployment: target.deployment,
|
|
service: target.service,
|
|
containerName: target.containerName,
|
|
manifest: target.manifestRepoPath,
|
|
targetImage: target.stableImage,
|
|
deployCommandShape: "registry manifest check + native k3s containerd import + Kubernetes Deployment image/env/annotation update + API service proxy health",
|
|
forbiddenActions: ["docker build", "docker compose build", "NodePort", "hostPort", "provider-gateway direct business backend"],
|
|
sourceOfTruth: deployJsonSourceOfTruth(service, environment),
|
|
};
|
|
}
|
|
|
|
export function artifactConsumerTargetImageHint(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
|
|
if (service.id === "frontend" && environment === "prod") return "unidesk-frontend";
|
|
if (service.id === "frontend" && environment === "dev") return "unidesk-frontend:dev";
|
|
if (service.id === "k3sctl-adapter") return "unidesk-k3sctl-adapter:d601";
|
|
if (service.id === "met-nonlinear") return "met-nonlinear-ml:tf26";
|
|
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:${environment === "dev" ? "dev" : "d601"}`;
|
|
const container = service.repository.containerName;
|
|
if (container.endsWith("-backend")) return container.slice(0, -"-backend".length);
|
|
return container;
|
|
}
|
|
|
|
export function deployJsonExecutorMirrors(
|
|
service: DeployManifestService,
|
|
serviceConfig: UniDeskMicroserviceConfig,
|
|
environment: DeployEnvironment,
|
|
planKind: ArtifactConsumerPlanKind,
|
|
planTarget: Record<string, unknown> | null,
|
|
): DeployJsonExecutorMirror[] {
|
|
const mirrors: DeployJsonExecutorMirror[] = [];
|
|
mirrors.push({
|
|
surface: "deploy-executor-plan",
|
|
artifact: {
|
|
kind: "source-build",
|
|
repository: `unidesk/${service.id}`,
|
|
tag: "commitId",
|
|
},
|
|
consumer: {
|
|
kind: planKind === "d601-k3s-managed" ? "d601-k3s-managed" : planKind,
|
|
noRuntimeSourceBuild: planKind !== "d601-dev-target-side-build",
|
|
target: planTarget === null ? undefined : {
|
|
namespace: typeof planTarget.namespace === "string" ? planTarget.namespace : undefined,
|
|
deployment: typeof planTarget.deployment === "string" ? planTarget.deployment : undefined,
|
|
service: typeof planTarget.service === "string" ? planTarget.service : undefined,
|
|
containerName: typeof planTarget.containerName === "string" ? planTarget.containerName : undefined,
|
|
stableImage: typeof planTarget.targetImage === "string" ? planTarget.targetImage : undefined,
|
|
manifestRepoPath: typeof planTarget.manifest === "string" ? planTarget.manifest : undefined,
|
|
},
|
|
},
|
|
runtime: {
|
|
containerPort: serviceConfig.backend.nodePort,
|
|
servicePort: serviceConfig.backend.nodePort,
|
|
healthPath: serviceConfig.backend.healthPath,
|
|
health: service.runtime?.health === undefined ? undefined : {
|
|
deployMetadataRequired: true,
|
|
},
|
|
},
|
|
});
|
|
const manifestMirror = k3sManifestExecutorMirror(service);
|
|
if (manifestMirror !== null) mirrors.push(manifestMirror);
|
|
return mirrors;
|
|
}
|
|
|
|
export function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string[] {
|
|
const kind = artifactConsumerPlanKind(service, environment);
|
|
if (kind === "d601-dev-target-side-build") {
|
|
return [
|
|
"reads origin/master:deploy.json for dev desired state",
|
|
"materializes and builds only the selected dev service on D601",
|
|
"rolls out only unidesk-dev resources",
|
|
];
|
|
}
|
|
const base = [
|
|
"reads origin/master:deploy.json for commit intent",
|
|
"requires a commit-pinned artifact in 127.0.0.1:5000",
|
|
"verifies image labels for service id, source repo, source commit, and Dockerfile",
|
|
"does not build source on the runtime target",
|
|
];
|
|
if (kind === "d601-k3s-managed") {
|
|
return [
|
|
...base,
|
|
"imports the artifact into native k3s containerd",
|
|
"verifies Deployment metadata and health through the Kubernetes API service proxy",
|
|
];
|
|
}
|
|
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
|
|
return [
|
|
...base,
|
|
"recreates only the selected Compose service with --no-build --no-deps --force-recreate",
|
|
"verifies running image labels and private service health",
|
|
];
|
|
}
|
|
return ["unsupported service remains blocked before source materialization or runtime mutation"];
|
|
}
|
|
|
|
export function unsupportedEnvironmentPlanReason(serviceId: string, environment: DeployEnvironment): string {
|
|
if (environment === "prod" && serviceId === "code-queue") {
|
|
return "Code Queue production CD is intentionally unsupported in this phase: only CI image publication and dev artifact-consumer validation are implemented; prod artifact deploy, rollout and manifest mutation are blocked.";
|
|
}
|
|
return environment === "prod"
|
|
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
|
|
: "No standardized dev deployment or artifact validation path is implemented for this service.";
|
|
}
|
|
|
|
export function unsupportedPlanTarget(serviceId: string, environment: DeployEnvironment, reason: string): Record<string, unknown> {
|
|
const forbiddenActions = [
|
|
"docker build",
|
|
"docker compose build",
|
|
"docker compose up --build",
|
|
"maintenance-channel source deploy",
|
|
"dirty worktree source deploy",
|
|
];
|
|
if (serviceId === "code-queue") {
|
|
forbiddenActions.push(
|
|
"codex deploy",
|
|
"server rebuild code-queue",
|
|
"production namespace mutation",
|
|
"production manifest mutation",
|
|
"interrupt running Code Queue tasks",
|
|
"cancel running Code Queue tasks",
|
|
);
|
|
}
|
|
return {
|
|
consumerKind: "unsupported",
|
|
runtimeHost: null,
|
|
deploymentMode: "none",
|
|
noRuntimeSourceBuild: true,
|
|
dryRunOnly: true,
|
|
blockedReason: reason,
|
|
deployCommandShape: "none",
|
|
forbiddenActions,
|
|
...(serviceId === "code-queue"
|
|
? {
|
|
excludedTargets: [
|
|
{
|
|
namespace: "unidesk",
|
|
deployments: ["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"],
|
|
reason: `${environment} dry-run must not mutate production Code Queue execution-plane objects.`,
|
|
},
|
|
{
|
|
operations: ["interrupt", "cancel", "restart scheduler", "restart runner"],
|
|
reason: "Code Queue cannot self-deploy or alter active task control state from this runner path.",
|
|
},
|
|
],
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function codeQueueSelfBootstrapGuard(environment: DeployEnvironment): Record<string, unknown> | undefined {
|
|
if (environment !== "dev" && environment !== "prod") return undefined;
|
|
return {
|
|
check: "code-queue-self-bootstrap-guard",
|
|
selfBootstrapBlocked: true,
|
|
requiresSupervisorApproval: true,
|
|
actorBoundary: "a running Code Queue task may publish contract evidence and dry-run plans only; it must not deploy Code Queue itself",
|
|
devApply: "requires explicit supervisor or human authorization outside the running Code Queue task",
|
|
prodApply: "unsupported until a separate supervisor-approved production Code Queue CD design exists",
|
|
allowedWithoutApproval: [
|
|
"CI artifact publication",
|
|
"deploy plan",
|
|
"deploy apply --dry-run",
|
|
"artifact-registry deploy-service --dry-run",
|
|
],
|
|
forbiddenActions: [
|
|
"self-deploy Code Queue from Code Queue",
|
|
"run a non-dry-run DEV apply from Code Queue",
|
|
"production namespace mutation",
|
|
"production manifest mutation",
|
|
"scheduler or runner restart",
|
|
"active task interrupt",
|
|
"active task cancel",
|
|
],
|
|
environment,
|
|
};
|
|
}
|
|
|
|
export function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment): Record<string, unknown> | undefined {
|
|
if (serviceId !== "code-queue") return undefined;
|
|
return {
|
|
serviceId,
|
|
selfDeployBlocked: true,
|
|
requiresSupervisorApproval: true,
|
|
selfBootstrapGuard: codeQueueSelfBootstrapGuard(environment),
|
|
ciProducer: {
|
|
command: "bun scripts/cli.ts ci publish-user-service --service code-queue --commit <full-sha>",
|
|
allowed: true,
|
|
outputImage: "127.0.0.1:5000/unidesk/code-queue:<full-sha>",
|
|
responsibility: "build a commit-pinned Code Queue image from pushed Git source, push it to the D601 registry, and report digest/label evidence",
|
|
forbiddenActions: ["deploy apply", "kubectl apply", "rollout restart", "interrupt", "cancel"],
|
|
},
|
|
cdConsumer: environment === "dev"
|
|
? {
|
|
environment: "dev",
|
|
dryRunCommand: "bun scripts/cli.ts deploy apply --env dev --service code-queue --commit <full-sha> --dry-run",
|
|
liveApplyCommandShape: null,
|
|
liveApplyAllowed: false,
|
|
requiresSupervisorApproval: true,
|
|
allowedTarget: "unidesk-dev Code Queue scheduler/read/write/provider-egress-proxy only",
|
|
manualAuthorizationPoint: "operator reviews CI artifact summary and dev dry-run plan, then explicitly authorizes DEV apply outside this Code Queue task",
|
|
prodMutationAllowed: false,
|
|
}
|
|
: {
|
|
environment: "prod",
|
|
dryRunCommand: "bun scripts/cli.ts deploy plan --env prod --service code-queue",
|
|
liveApplyCommandShape: null,
|
|
liveApplyAllowed: false,
|
|
requiresSupervisorApproval: true,
|
|
allowedTarget: null,
|
|
manualAuthorizationPoint: "PROD requires a future supervisor-approved Code Queue CD design; current runner output is plan/unsupported only",
|
|
prodMutationAllowed: false,
|
|
},
|
|
manualAuthorization: {
|
|
dev: "DEV live apply is a human operator decision after dry-run evidence; this task may not execute it.",
|
|
prod: "PROD live apply is not implemented and must remain unsupported, even with manual intent, until a separate reviewed CD consumer exists.",
|
|
},
|
|
forbiddenActions: [
|
|
"codex deploy",
|
|
"server rebuild code-queue",
|
|
"deploy apply --env prod --service code-queue",
|
|
"artifact-registry deploy-service --env prod --service code-queue",
|
|
"kubectl apply to namespace unidesk",
|
|
"rollout restart production Code Queue scheduler/read/write",
|
|
"interrupt or cancel active Code Queue tasks",
|
|
],
|
|
};
|
|
}
|