// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. pipelinerun-runtime module for scripts/src/ci.ts. // Moved mechanically from scripts/src/ci.ts:1899-1968 for #903. import { randomUUID } from "node:crypto"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, posix as posixPath } from "node:path"; import { blockedCatalogArtifactIds, catalogSummary, findCiCatalogArtifact, loadCiCatalog, supportedSourceBuildArtifactIds, type CiCatalogArtifact, type CiSourceBuildCatalogArtifact, type CiUpstreamImageCatalogArtifact } from "../ci-catalog"; import { runCommand } from "../command"; import { type UniDeskConfig, repoRoot, rootPath } from "../config"; import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "../deploy-ssh-identity"; import { jobWithTail, listJobs, readJob, startJob } from "../jobs"; import { coreInternalFetch } from "../microservices"; import { artifactRegistryReadonlyResultFromCommand, buildArtifactRegistryReadonlyProbe, parseArtifactRegistryOptions, type ArtifactRegistryReadonlyProbe, } from "../artifact-registry"; import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard"; import { runSshCommandCapture } from "../ssh"; import type { DispatchResult, PipelineRunCondition } from "./types"; import { status } from "./cleanup"; import { ciTargetGuardShellLines, shellQuote } from "./options"; import { dispatchSsh, runRemoteKubectl, runRemoteKubectlRaw, uploadRemoteBase64 } from "./remote"; import { ciTarget } from "./types"; export async function remoteCreatePipelineRun(manifest: string, target = ciTarget(null)): Promise { const encoded = Buffer.from(manifest, "utf8").toString("base64"); const token = randomUUID().replace(/-/gu, "").slice(0, 12); const b64Path = `/tmp/unidesk-ci-pipelinerun-${token}.b64`; const upload = await uploadRemoteBase64(b64Path, encoded, target); if (!upload.ok) throw new Error(`failed to upload PipelineRun manifest: ${upload.stderr || upload.stdout}`); const result = await runRemoteKubectl([ "tmp=$(mktemp /tmp/unidesk-ci-run.XXXXXX.yaml)", `b64_path=${shellQuote(b64Path)}`, "trap 'rm -f \"$tmp\" \"$b64_path\"' EXIT", "base64 -d \"$b64_path\" > \"$tmp\"", "kubectl create -f \"$tmp\" -o jsonpath='{.metadata.name}'", ].join("\n"), 60_000, 45_000, target); return result.stdout.trim(); } export async function waitForPipelineRun(name: string, waitMs: number, target = ciTarget(null)): Promise { if (waitMs <= 0) return null; const command = [ "set -euo pipefail", ...ciTargetGuardShellLines(target), `printf 'waiting_pipelinerun=%s\\n' ${shellQuote(name)}`, `deadline=$((SECONDS + ${Math.ceil(waitMs / 1000)}))`, "while [ \"$SECONDS\" -lt \"$deadline\" ]; do", ` condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`, " case \"$condition\" in", " True*)", " printf 'condition=%s\\n' \"$condition\"", ` kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} --no-headers 2>/dev/null || true`, " exit 0", " ;;", " False*)", " printf 'condition=%s\\n' \"$condition\"", ` kubectl get taskrun,pod -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} 2>/dev/null || true`, " exit 1", " ;;", " esac", " sleep 2", "done", `echo "Timed out waiting for pipelinerun/${name}" >&2`, `kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o json`, "exit 124", ].join("\n"); return dispatchSsh(command, waitMs + 30_000, waitMs + 20_000, true, target); } export async function readPipelineRunCondition(name: string, target = ciTarget(null)): Promise { const result = await runRemoteKubectlRaw([ "set -euo pipefail", `condition="$(kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[?(@.type==\"Succeeded\")]}{.status}{\"\\t\"}{.reason}{\"\\t\"}{.message}{end}' 2>/dev/null || true)"`, "printf '%s\\n' \"$condition\"", ].join("\n"), 30_000, 15_000, target); const [status = "", reason = "", ...messageParts] = result.stdout.trim().split("\t"); const message = messageParts.join("\t"); return { ok: status === "True" ? true : status === "False" ? false : null, status, reason, message, query: { ok: result.ok, status: result.status, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200), }, }; } export function pipelineRunWaitSucceeded(wait: DispatchResult | null, condition: PipelineRunCondition | null): boolean { if (wait === null) return true; if (condition?.ok === true) return true; if (condition?.ok === false) return false; return wait.ok || wait.exitCode === 0 || wait.stdout.includes("condition=True\tSucceeded\t"); }