Files
pikasTech-unidesk/scripts/src/artifact-registry/consumer.ts
T
2026-06-25 16:16:25 +00:00

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;
}