Files
pikasTech-unidesk/scripts/src/artifact-registry/remote.ts
T
Lyon edfddd2445 fix: YAML-first 治理 CI/CD target (#919)
* docs: specify cicd yaml target governance

* fix: resolve cicd targets from yaml

---------

Co-authored-by: Codex <codex@noreply.local>
2026-06-26 01:14:38 +08:00

306 lines
12 KiB
TypeScript

// 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<CommandResult> {
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<string, unknown> {
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<string, unknown>,
remoteResult: Record<string, unknown> | null,
remoteCommand: CommandResult | null,
remoteHost: string | null,
): Record<string, unknown> {
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 <host> 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<string, unknown> | 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<string, unknown>;
const data = record.data;
return typeof data === "object" && data !== null && !Array.isArray(data) ? data as Record<string, unknown> : record;
} catch {
return null;
}
}
export function unwrapRemoteArtifactRegistryResult(data: Record<string, unknown>): Record<string, unknown> | null {
const directResult = data.result;
if (typeof directResult === "object" && directResult !== null && !Array.isArray(directResult)) return directResult as Record<string, unknown>;
const nested = data.data;
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
const nestedRecord = nested as Record<string, unknown>;
const nestedResult = nestedRecord.result;
if (typeof nestedResult === "object" && nestedResult !== null && !Array.isArray(nestedResult)) return nestedResult as Record<string, unknown>;
}
return null;
}
export function annotateRemoteReadonlyResult(
result: Record<string, unknown>,
localResult: Record<string, unknown>,
remoteHost: string,
command: CommandResult,
): Record<string, unknown> {
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<string, unknown>,
remoteHost: string,
command: CommandResult,
): Record<string, unknown> {
return {
...result,
controlPlane: {
preferred: "remote-frontend",
remoteFallbackAttempted: false,
remoteFallbackUsed: true,
remoteFirst: true,
remoteHost,
localBackendCoreMissing: null,
classification: null,
},
remoteCommand: artifactRegistryCommandTail(command),
};
}