// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. consumer module for scripts/src/artifact-registry.ts. // Moved mechanically from scripts/src/artifact-registry.ts:1319-1658 for #903. import { createHash } from "node:crypto"; import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { runCommand, type CommandResult } from "../command"; import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "../config"; import { startJob } from "../jobs"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, deployJsonDriftResult, deployJsonSourceOfTruth, hasDeployJsonExecutorContract, k3sManifestExecutorMirror, parseDeployJsonServiceContractBase64, readDeployJsonServiceContractFromFile, type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import type { ArtifactConsumerSpec, ArtifactConsumerTarget, ArtifactDeployEnvironment, ArtifactRegistryOptions, SupportedArtifactConsumerService } from "./types"; import { artifactConsumerSpecs } from "./catalog"; import { supportedArtifactConsumerServices } from "./types"; export function artifactRegistryCommandTail(result: CommandResult): Record { return { command: result.command.length > 7 ? [...result.command.slice(0, 7), ""] : result.command, exitCode: result.exitCode, signal: result.signal, timedOut: result.timedOut, stdoutTail: result.stdout.slice(-4000), stderrTail: result.stderr.slice(-4000), }; } export function commandTail(result: CommandResult): Record { return artifactRegistryCommandTail(result); } export 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 shared ?? null; } export 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, }))); } export function artifactConsumerTarget(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment | null): ArtifactConsumerTarget | null { return spec.targets[environment ?? "prod"] ?? null; } export function unsupportedService(serviceId: string, options: ArtifactRegistryOptions): Record { 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", }; } export function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record { const environment = options.environment ?? "prod"; const codeQueueGuard = spec.serviceId === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined; const reason = spec.serviceId === "code-queue" && environment === "prod" ? spec.prodLiveBlockReason ?? "Code Queue production artifact deploy, rollout and manifest mutation are intentionally unsupported in this phase." : `No standardized ${environment} registry artifact consumer is implemented for ${spec.serviceId}.`; return { ok: false, supported: false, error: "unsupported-environment", serviceId: spec.serviceId, environment, providerId: options.providerId, reason, supportedEnvironments: Object.keys(spec.targets), requiresSupervisorApproval: spec.serviceId === "code-queue", selfBootstrapGuard: codeQueueGuard, affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, false) : undefined, excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, policy: "artifact CD must not silently fall back to maintenance-channel source builds or legacy direct deployment", }; } export function artifactConsumerLivePolicy(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): "enabled" | "supervisor-only" | "unsupported" { return environment === "prod" ? spec.prodLiveApply : spec.devLiveApply ?? "enabled"; } export function artifactConsumerLiveBlockReason(spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): string | null { if (spec.runtimeVerificationBlockReason !== undefined) return spec.runtimeVerificationBlockReason; return environment === "prod" ? spec.prodLiveBlockReason ?? null : spec.devLiveBlockReason ?? null; } export function codeQueueExcludedTargets(environment: ArtifactDeployEnvironment): Record[] { const excluded: Record[] = [ { namespace: "unidesk", deployments: ["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"], reason: "production Code Queue execution-plane Deployments are outside this artifact consumer and must not be mutated by dry-run or self-bootstrap automation.", }, { operations: ["restart scheduler", "restart runner", "interrupt active tasks", "cancel active tasks"], reason: "artifact CD planning must not alter scheduler/runner lifecycle or active task control state.", }, ]; if (environment === "dev") { excluded.push({ namespace: "unidesk-dev", nonDryRunMutation: "requires explicit supervisor or human authorization outside the running Code Queue task", reason: "DEV is the only Code Queue artifact consumer target, but self-bootstrap automation may produce dry-run evidence only.", }); } return excluded; } export function codeQueueAffectedRuntime(environment: ArtifactDeployEnvironment, targetPlanned: boolean): Record { return { dryRunMutation: false, plannedTarget: targetPlanned && environment === "dev" ? { namespace: "unidesk-dev", deployments: ["code-queue-scheduler-dev", "code-queue-read-dev", "code-queue-write-dev", "d601-dev-provider-egress-proxy"], } : null, productionNamespaceAffected: false, productionSchedulerRunnerAffected: false, activeTaskControlAffected: false, activeTaskInterruptCancelAffected: false, }; } export function codeQueueSelfBootstrapGuard(environment: ArtifactDeployEnvironment): Record { return { check: "code-queue-self-bootstrap-guard", serviceId: "code-queue", 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 codeQueueMgrSelfBootstrapGuard(environment: ArtifactDeployEnvironment, requiresSupervisorApproval: boolean): Record { return { check: "code-queue-mgr-control-plane-guard", serviceId: "code-queue-mgr", selfBootstrapBlocked: true, requiresSupervisorApproval, actorBoundary: "dry-run is allowed, but a running Code Queue task must not replace the Code Queue control-plane sidecar as part of delivering Code Queue itself", targetScope: "main-server Compose service code-queue-mgr / container code-queue-mgr-backend only", prodApply: "requires explicit supervisor confirmation", environment, forbiddenActions: [ "mutate D601 Code Queue scheduler", "mutate D601 Code Queue runner", "restart active tasks", "interrupt active tasks", "cancel active tasks", ], }; } export function authBrokerCredentialMountGuard(environment: ArtifactDeployEnvironment, requiresSupervisorApproval: boolean): Record { return { check: "auth-broker-credential-mount-guard", serviceId: "auth-broker", requiresSupervisorApproval, actorBoundary: "dry-run and contract evidence are allowed, but live broker startup needs explicit review of credential reference mounting and private exposure", targetScope: "main-server Compose profile auth-broker / container auth-broker-backend only", composeProfileRequired: "auth-broker", publicPortAllowed: false, registryCredentialsProxied: false, deployCredentialsProxied: false, environment, forbiddenActions: [ "write real GitHub token into config.json, deploy.json, or docker-compose.yml", "publish auth-broker on a public port", "restart backend-core, provider-gateway, or Code Queue as part of broker dry-run registration", "grant registry, deploy, database, k3s, provider token, or host SSH permissions", ], }; } export function artifactConsumerSelfBootstrapGuard( spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment, requiresSupervisorApproval: boolean, ): Record | undefined { if (spec.serviceId === "code-queue") return codeQueueSelfBootstrapGuard(environment); if (spec.serviceId === "code-queue-mgr") return codeQueueMgrSelfBootstrapGuard(environment, requiresSupervisorApproval); if (spec.serviceId === "auth-broker") return authBrokerCredentialMountGuard(environment, requiresSupervisorApproval); return undefined; } export function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record | null { const environment = options.environment ?? "prod"; const livePolicy = artifactConsumerLivePolicy(spec, environment); const liveReason = artifactConsumerLiveBlockReason(spec, environment); 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 (livePolicy === "enabled") return null; if (livePolicy === "supervisor-only") { return { ok: false, supported: true, liveApplyAllowed: false, error: "supervisor-confirmation-required", serviceId: spec.serviceId, environment, providerId: options.providerId, reason: liveReason ?? `${spec.serviceId} ${environment} artifact apply requires supervisor confirmation.`, requiresSupervisorApproval: true, selfBootstrapGuard: artifactConsumerSelfBootstrapGuard(spec, environment, true), affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, true) : undefined, excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env ${environment} --service ${spec.serviceId} --commit --dry-run`, policy: "worker automation must not perform live apply for this supervisor-gated artifact consumer", }; } return { ok: false, supported: false, liveApplyAllowed: false, error: "artifact-consumer-blocked", serviceId: spec.serviceId, environment, providerId: options.providerId, reason: liveReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`, requiresSupervisorApproval: spec.serviceId === "code-queue", selfBootstrapGuard: spec.serviceId === "code-queue" ? codeQueueSelfBootstrapGuard(environment) : undefined, affectedRuntime: spec.serviceId === "code-queue" ? codeQueueAffectedRuntime(environment, false) : undefined, excludedTargets: spec.serviceId === "code-queue" ? codeQueueExcludedTargets(environment) : undefined, requiredBeforeLiveApply: [ "CI can publish a commit-pinned image with matching service id, source repo, 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", }; } export function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string { return `127.0.0.1:${options.port}/${(options.dryRun ? options.deployJsonService?.artifact?.repository : undefined) ?? spec.registryRepository}:${commit}`; } export function sourceRepoFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string { return options.sourceRepoExplicit ? options.sourceRepo : spec.sourceRepo ?? options.sourceRepo; } export function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string { const target = artifactConsumerTarget(spec, options.environment); return options.deployRef ?? (options.dryRun ? options.deployJsonService?.consumer?.targetRef : undefined) ?? target?.deployRef ?? `deploy.json#environments.${options.environment ?? "prod"}.services.${spec.serviceId}`; } export function deployJsonServiceForOptions(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, environment: ArtifactDeployEnvironment): DeployJsonServiceContract | null { if (!options.dryRun) return null; if (options.deployJsonService !== null) return options.deployJsonService; if (environment === "dev" && (spec.serviceId === "decision-center" || spec.serviceId === "mdtodo")) { return readDeployJsonServiceContractFromFile(environment, spec.serviceId); } return null; } export function targetImageFor(options: ArtifactRegistryOptions, target: ArtifactConsumerTarget): string { return (options.dryRun ? options.deployJsonService?.consumer?.target.stableImage : undefined) ?? target.targetImage; } export function targetCommitImageFor(options: ArtifactRegistryOptions, target: ArtifactConsumerTarget, commit: string): string { const stableImage = options.dryRun ? options.deployJsonService?.consumer?.target.stableImage : undefined; return stableImage === undefined ? target.targetCommitImage(commit) : deployJsonCommitImage(stableImage, commit); } export function registryRepositoryFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string { return (options.dryRun ? options.deployJsonService?.artifact?.repository : undefined) ?? spec.registryRepository; } export function artifactRegistryDeployJsonMirrors( options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, ): DeployJsonExecutorMirror[] { const mirrors: DeployJsonExecutorMirror[] = [ { surface: "artifact-registry-executor", artifact: { kind: "source-build", repository: spec.registryRepository, tag: "commitId", }, consumer: { kind: spec.kind === "d601-k3s" ? "d601-k3s-managed" : spec.kind, noRuntimeSourceBuild: true, target: target.k3s === undefined ? undefined : { namespace: target.k3s.namespace, deployment: target.k3s.deploymentName, service: target.k3s.serviceName, containerName: target.k3s.containerName, stableImage: target.targetImage, manifestRepoPath: target.k3s.manifestRepoPath, }, }, runtime: target.k3s === undefined ? undefined : { containerPort: target.k3s.servicePort, servicePort: target.k3s.servicePort, healthPath: target.k3s.healthPath, health: options.deployJsonService?.runtime?.health === undefined ? undefined : { deployMetadataRequired: true, }, }, }, ]; if (options.deployJsonService !== null) { const manifestMirror = k3sManifestExecutorMirror(options.deployJsonService); if (manifestMirror !== null) mirrors.push(manifestMirror); } return mirrors; }