// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. remote 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:1659-1933 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 { ArtifactRegistryCommandRuntime, ArtifactRegistryOptions } from "./types"; import { renderBundle, safeName, shellQuote } from "./bundle"; import { artifactRegistryCommandTail } from "./consumer"; import { classifyProviderSshCommandFailure, isLocalProvider, providerSshCommandFailureScopes, readonlyRemoteCommandShape, registryRecommendedAction } from "./status"; export function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs, runtime: ArtifactRegistryCommandRuntime = {}): CommandResult { if (runtime.runRemoteScriptForTest !== undefined) return runtime.runRemoteScriptForTest(options, script, timeoutMs); if (isLocalProvider(options.providerId)) { return runCommand(["bash", "-lc", script], repoRoot, { timeoutMs }); } const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script]; return runCommand(command, repoRoot, { timeoutMs }); } export function combineCommandResults(command: string[], parts: CommandResult[]): CommandResult { const failed = parts.find((part) => part.exitCode !== 0 || part.timedOut) ?? parts.at(-1) ?? null; return { command, cwd: repoRoot, exitCode: failed === null ? 0 : failed.exitCode, signal: failed?.signal ?? null, timedOut: parts.some((part) => part.timedOut), stdout: parts.map((part) => part.stdout).filter((part) => part.length > 0).join("\n"), stderr: parts.map((part) => part.stderr).filter((part) => part.length > 0).join("\n"), }; } export function downloadRemoteFile(options: ArtifactRegistryOptions, remotePath: string, localPath: string, timeoutMs = options.timeoutMs): CommandResult { const transferRuntimeTimeoutMs = Math.max(timeoutMs * 4, 60 * 60_000); const command = [ process.execPath, "scripts/cli.ts", "ssh", options.providerId, "download", "--inactivity-timeout-ms", String(timeoutMs), remotePath, localPath, ]; const stdoutFile = process.env.UNIDESK_JOB_STDOUT_FILE; const stderrFile = process.env.UNIDESK_JOB_STDERR_FILE; if (!stdoutFile || !stderrFile) return runCommand(command, repoRoot, { timeoutMs: transferRuntimeTimeoutMs }); return runCommand([ "bash", "-lc", `set -o pipefail; "$@" > >(tee -a ${shellQuote(stdoutFile)}) 2> >(tee -a ${shellQuote(stderrFile)} >&2)`, "unidesk-artifact-download", ...command, ], repoRoot, { timeoutMs: transferRuntimeTimeoutMs }); } export async function runRemoteScriptBackground( options: ArtifactRegistryOptions, script: string, timeoutMs: number, name: string, ): Promise { const token = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`; const prefix = `/tmp/unidesk-artifact-${safeName(options.serviceId ?? "service")}-${safeName(name)}-${token}`; const logFile = `${prefix}.log`; const doneFile = `${prefix}.done`; const wrapped = [ `bash -lc ${shellQuote(script)}`, "code=$?", `printf '%s\\n' "$code" > ${shellQuote(doneFile)}`, "exit \"$code\"", ].join("; "); const launchScript = [ `rm -f ${shellQuote(logFile)} ${shellQuote(doneFile)}`, `nohup bash -lc ${shellQuote(wrapped)} > ${shellQuote(logFile)} 2>&1 < /dev/null & echo $!`, ].join("\n"); const launch = runRemoteScript(options, launchScript, 30_000); const pid = launch.stdout.trim().split(/\n/u).pop()?.trim() ?? ""; if (launch.exitCode !== 0 || launch.timedOut || !/^\d+$/u.test(pid)) { return combineCommandResults(["artifact-registry", "remote-background", name], [launch]); } const startedAt = Date.now(); let latest: CommandResult | null = null; while (Date.now() - startedAt < timeoutMs) { await Bun.sleep(2_000); const poll = runRemoteScript(options, [ `if [ -f ${shellQuote(doneFile)} ]; then printf 'SENTINEL:%s\\n' "$(cat ${shellQuote(doneFile)} 2>/dev/null || true)"; else echo RUNNING; fi`, `tail -n 160 ${shellQuote(logFile)} 2>/dev/null | tr -d '\\000' | LC_ALL=C sed 's/[^[:print:]\t]//g' || true`, ].join("; "), 30_000); latest = poll; if (poll.exitCode !== 0 || poll.timedOut) continue; const stdout = poll.stdout.trimEnd(); const [head = "", ...rest] = stdout.split("\n"); if (!head.startsWith("SENTINEL:")) continue; const code = Number(head.slice("SENTINEL:".length).trim()); return { command: ["artifact-registry", "remote-background", name], cwd: repoRoot, exitCode: Number.isInteger(code) ? code : 1, signal: null, timedOut: false, stdout: [ `remote_background_name=${name}`, `remote_background_pid=${pid}`, `remote_background_log=${logFile}`, ...rest, ].join("\n"), stderr: poll.stderr, }; } return { command: ["artifact-registry", "remote-background", name], cwd: repoRoot, exitCode: 124, signal: null, timedOut: true, stdout: [ `remote_background_name=${name}`, `remote_background_pid=${pid}`, `remote_background_log=${logFile}`, latest?.stdout ?? "", ].join("\n"), stderr: latest?.stderr ?? `remote background script ${name} did not finish within ${timeoutMs}ms`, }; } export function readonlyCommandFailureResult( options: ArtifactRegistryOptions, command: CommandResult, action: "status" | "health", ): Record { const bundle = renderBundle(options); const failureClassification = classifyProviderSshCommandFailure(command); return { ok: false, readonly: true, installed: false, healthy: false, decision: failureClassification === "local-docker-required" ? "infra-blocked" : "retryable-transient", retryable: failureClassification !== "ssh-helper-command-shape-incompatible", failureClassification, recommendedAction: registryRecommendedAction(failureClassification), remoteCommandShape: readonlyRemoteCommandShape(action, options), healthyScopes: [], failedScopes: providerSshCommandFailureScopes(failureClassification), runtimeApiHealthy: false, checks: {}, expected: { endpoint: `http://${options.host}:${options.port}`, image: options.image, paths: bundle.paths, }, command: artifactRegistryCommandTail(command), }; } export function remoteFrontendHostFromEnv(env: NodeJS.ProcessEnv = process.env): string | null { for (const key of ["UNIDESK_MAIN_SERVER_IP", "UNIDESK_MAIN_SERVER_HOST", "CODE_QUEUE_DEV_CONTAINER_MASTER_HOST"]) { const value = env[key]?.trim() ?? ""; if (value.length === 0) continue; if (value === "localhost" || value === "127.0.0.1" || value === "::1") continue; return value.replace(/\/+$/u, ""); } if (env.CODE_QUEUE_SERVICE_ROLE || env.CODE_QUEUE_INSTANCE_ID || env.KUBERNETES_SERVICE_HOST) { const publicHost = readConfig().network.publicHost.trim(); if (publicHost.length > 0 && publicHost !== "localhost" && publicHost !== "127.0.0.1" && publicHost !== "::1") return publicHost; } return null; } export function controlPlaneMissingResult( options: ArtifactRegistryOptions, action: "status" | "health", localResult: Record, remoteResult: Record | null, remoteCommand: CommandResult | null, remoteHost: string | null, ): Record { const bundle = renderBundle(options); return { ok: false, readonly: true, installed: false, healthy: false, decision: "infra-blocked", retryable: true, runnerDisposition: "infra-blocked", failureClassification: "control-plane-missing", recommendedAction: registryRecommendedAction("control-plane-missing"), remoteCommandShape: readonlyRemoteCommandShape(action, options), healthyScopes: [], failedScopes: providerSshCommandFailureScopes("control-plane-missing"), runtimeApiHealthy: false, checks: {}, expected: { endpoint: `http://${options.host}:${options.port}`, image: options.image, paths: bundle.paths, }, controlPlane: { preferred: "remote-frontend", remoteFallbackAttempted: remoteHost !== null, remoteFallbackUsed: false, remoteHost, localBackendCoreMissing: true, classification: "control-plane-missing", retryCommand: remoteHost === null ? `bun scripts/cli.ts --main-server-ip artifact-registry ${action} --target ${options.targetId}` : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --target ${options.targetId}`, }, localObservation: localResult, remoteObservation: remoteResult, remoteCommand: remoteCommand === null ? null : artifactRegistryCommandTail(remoteCommand), }; } export function parsedCliData(stdout: string): Record | null { if (stdout.trim().length === 0) return null; try { const parsed = JSON.parse(stdout) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null; const record = parsed as Record; const data = record.data; return typeof data === "object" && data !== null && !Array.isArray(data) ? data as Record : record; } catch { return null; } } export function unwrapRemoteArtifactRegistryResult(data: Record): Record | null { const directResult = data.result; if (typeof directResult === "object" && directResult !== null && !Array.isArray(directResult)) return directResult as Record; const nested = data.data; if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) { const nestedRecord = nested as Record; const nestedResult = nestedRecord.result; if (typeof nestedResult === "object" && nestedResult !== null && !Array.isArray(nestedResult)) return nestedResult as Record; } return null; } export function annotateRemoteReadonlyResult( result: Record, localResult: Record, remoteHost: string, command: CommandResult, ): Record { return { ...result, controlPlane: { preferred: "remote-frontend", remoteFallbackAttempted: true, remoteFallbackUsed: true, remoteHost, localBackendCoreMissing: true, classification: null, }, localObservation: localResult, remoteCommand: artifactRegistryCommandTail(command), }; } export function annotateRemoteFirstReadonlyResult( result: Record, remoteHost: string, command: CommandResult, ): Record { return { ...result, controlPlane: { preferred: "remote-frontend", remoteFallbackAttempted: false, remoteFallbackUsed: true, remoteFirst: true, remoteHost, localBackendCoreMissing: null, classification: null, }, remoteCommand: artifactRegistryCommandTail(command), }; }