// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. readonly module for scripts/src/artifact-registry.ts. // SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/artifact-registry.ts:1934-2116 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 { artifactRegistryOptionsTargetSummary, type ArtifactRegistryCommandRuntime, type ArtifactRegistryOptions, type ArtifactRegistryReadonlyProbe } from "./types"; import { renderBundle } from "./bundle"; import { commandTail } from "./consumer"; import { annotateRemoteFirstReadonlyResult, annotateRemoteReadonlyResult, controlPlaneMissingResult, parsedCliData, readonlyCommandFailureResult, remoteFrontendHostFromEnv, runRemoteScript, unwrapRemoteArtifactRegistryResult } from "./remote"; import { asBool, parseKeyValueOutput, readonlyRemoteCommandShape, registryHealthDecision, statusScript } from "./status"; export function artifactRegistryReadonlyAutoRemotePlan( action: "status" | "health", options: ArtifactRegistryOptions, env: NodeJS.ProcessEnv = process.env, ): Record { const remoteHost = remoteFrontendHostFromEnv(env); return { enabled: remoteHost !== null, action, providerId: options.providerId, host: remoteHost, transport: "frontend", command: remoteHost === null ? null : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --target ${options.targetId}`, failureClassification: remoteHost === null ? "control-plane-missing" : null, }; } export function runReadonlyStatusWithRemoteFallback(options: ArtifactRegistryOptions, healthMode: boolean, runtime: ArtifactRegistryCommandRuntime = {}): Record { const action = healthMode ? "health" : "status"; const remoteHost = remoteFrontendHostFromEnv(runtime.env ?? process.env); const shouldPreferRemote = remoteHost !== null && ( Boolean((runtime.env ?? process.env).CODE_QUEUE_SERVICE_ROLE) || Boolean((runtime.env ?? process.env).CODE_QUEUE_INSTANCE_ID) || Boolean((runtime.env ?? process.env).KUBERNETES_SERVICE_HOST) || Boolean((runtime.env ?? process.env).UNIDESK_ARTIFACT_REGISTRY_REMOTE_FIRST) ); if (shouldPreferRemote) { const remoteResult = runReadonlyStatusViaRemoteFrontend(options, action, remoteHost, null, runtime); if (remoteResult !== null) return remoteResult; } const localResult = runReadonlyStatus(options, healthMode, runtime); if (localResult.failureClassification !== "local-docker-required") return localResult; if (remoteHost === null) return controlPlaneMissingResult(options, action, localResult, null, null, remoteHost); const remoteResult = runReadonlyStatusViaRemoteFrontend(options, action, remoteHost, localResult, runtime); if (remoteResult !== null) return remoteResult; return controlPlaneMissingResult(options, action, localResult, null, null, remoteHost); } export function runReadonlyStatusViaRemoteFrontend( options: ArtifactRegistryOptions, action: "status" | "health", remoteHost: string, localResult: Record | null, runtime: ArtifactRegistryCommandRuntime, ): Record | null { const remoteCommandArgs = [ process.execPath, rootPath("scripts", "cli.ts"), "--main-server-ip", remoteHost, "artifact-registry", action, "--target", options.targetId, "--timeout-ms", String(options.timeoutMs), ]; const remoteTimeoutMs = Math.max(options.timeoutMs + 15_000, 45_000); const remoteCommand = runtime.runCliForTest === undefined ? runCommand(remoteCommandArgs, repoRoot, { timeoutMs: remoteTimeoutMs }) : runtime.runCliForTest(remoteCommandArgs, remoteTimeoutMs); const parsed = parsedCliData(remoteCommand.stdout); const remoteResult = parsed === null ? null : unwrapRemoteArtifactRegistryResult(parsed); if (remoteCommand.exitCode === 0 && !remoteCommand.timedOut && remoteResult !== null) { return localResult === null ? annotateRemoteFirstReadonlyResult(remoteResult, remoteHost, remoteCommand) : annotateRemoteReadonlyResult(remoteResult, localResult, remoteHost, remoteCommand); } if (localResult !== null) return controlPlaneMissingResult(options, action, localResult, remoteResult, remoteCommand, remoteHost); return null; } export function statusFromValues(options: ArtifactRegistryOptions, values: Record, command: CommandResult, healthMode: boolean): Record { const commandOk = command.exitCode === 0 && !command.timedOut; const bundle = renderBundle(options); const hashes = Object.fromEntries(bundle.files.map((item) => [item.path, item.sha256])); 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; const decision = registryHealthDecision(checks, commandOk); return { ok: healthMode ? healthy : commandOk, readonly: true, installed, healthy, ...decision, target: artifactRegistryOptionsTargetSummary(options), remoteCommandShape: readonlyRemoteCommandShape(healthMode ? "health" : "status", options), checks, observed: { unit: { path: bundle.paths.unit, active: values.unit_active, enabled: values.unit_enabled }, compose: { path: bundle.paths.compose, sha256: values.compose_hash ?? null }, config: { path: bundle.paths.config, sha256: values.config_hash ?? null }, storage: { path: bundle.paths.storage }, 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: hashes[bundle.paths.unit] ?? "", composeHash: hashes[bundle.paths.compose] ?? "", configHash: hashes[bundle.paths.config] ?? "", image: options.image, endpoint: `http://${options.host}:${options.port}`, }, command: commandTail(command), }; } export function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean, runtime: ArtifactRegistryCommandRuntime = {}): Record { const bundle = renderBundle(options); const script = statusScript(options, bundle); const result = runRemoteScript(options, script, options.timeoutMs, runtime); if (result.exitCode !== 0 || result.timedOut) { return readonlyCommandFailureResult(options, result, healthMode ? "health" : "status"); } return statusFromValues(options, parseKeyValueOutput(result.stdout), result, healthMode); } export function buildArtifactRegistryReadonlyProbe(action: "status" | "health", options: ArtifactRegistryOptions): ArtifactRegistryReadonlyProbe { const bundle = renderBundle(options); const healthMode = action === "health"; return { action, providerId: options.providerId, script: statusScript(options, bundle), timeoutMs: options.timeoutMs, healthMode, options, remoteCommandShape: readonlyRemoteCommandShape(action, options), }; } export function artifactRegistryReadonlyResultFromCommand( probe: ArtifactRegistryReadonlyProbe, command: CommandResult, ): Record { if (command.exitCode !== 0 || command.timedOut) { return readonlyCommandFailureResult(probe.options, command, probe.action); } return statusFromValues(probe.options, parseKeyValueOutput(command.stdout), command, probe.healthMode); }