Files
pikasTech-unidesk/scripts/src/ci/pipelinerun-runtime.ts
T
2026-06-25 16:16:25 +00:00

105 lines
5.0 KiB
TypeScript

// 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<string> {
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<DispatchResult | null> {
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<PipelineRunCondition> {
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");
}