368 lines
17 KiB
TypeScript
368 lines
17 KiB
TypeScript
// 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<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),
|
|
};
|
|
}
|
|
|
|
export function commandTail(result: CommandResult): Record<string, unknown> {
|
|
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<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",
|
|
};
|
|
}
|
|
|
|
export function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> {
|
|
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<string, unknown>[] {
|
|
const excluded: Record<string, unknown>[] = [
|
|
{
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string, unknown> | 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<string, unknown> | 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 <full-sha> --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;
|
|
}
|