Files
pikasTech-unidesk/scripts/src/cicd-native.ts
T

92 lines
3.9 KiB
TypeScript

// SPEC: PJ2026-01060703 CI/CD branch follower native Kubernetes helpers.
// Responsibility: submit/probe Kubernetes Jobs and Tekton PipelineRuns via file-backed native scripts.
import { repoRoot, rootPath } from "./config";
import { runCommand, type CommandResult } from "./command";
import type { NativeK8sJobResult } from "./cicd-types";
const NATIVE_SCRIPT_DIR = "scripts/native/cicd";
interface NativeTimingOptions {
nativeTransportGraceSeconds: number;
nativePollIntervalSeconds: number;
}
export function runNativeTektonPipelineRun(namespace: string, pipelineRun: string, manifest: Record<string, unknown>, wait: boolean, timeoutSeconds: number, timing: NativeTimingOptions): CommandResult {
return runCommand(["node", rootPath(NATIVE_SCRIPT_DIR, "submit-pipelinerun.mjs")], repoRoot, {
input: Buffer.from(JSON.stringify(manifest), "utf8").toString("base64"),
timeoutMs: (timeoutSeconds + timing.nativeTransportGraceSeconds) * 1000,
env: {
...process.env,
NAMESPACE: namespace,
PIPELINERUN: pipelineRun,
WAIT: wait ? "true" : "false",
TIMEOUT_SECONDS: String(timeoutSeconds),
POLL_INTERVAL_SECONDS: String(timing.nativePollIntervalSeconds),
},
});
}
export function runNativeK8sJob(namespace: string, jobName: string, manifest: Record<string, unknown>, timeoutSeconds: number, logContainer: string, timing: NativeTimingOptions): NativeK8sJobResult {
const result = runCommand(["node", rootPath(NATIVE_SCRIPT_DIR, "native-job.mjs")], repoRoot, {
input: Buffer.from(JSON.stringify(manifest), "utf8").toString("base64"),
timeoutMs: (timeoutSeconds + timing.nativeTransportGraceSeconds) * 1000,
env: {
...process.env,
NAMESPACE: namespace,
JOB_NAME: jobName,
LOG_CONTAINER: logContainer,
TIMEOUT_SECONDS: String(timeoutSeconds),
POLL_INTERVAL_SECONDS: String(timing.nativePollIntervalSeconds),
},
});
const parsed = parseJsonObject(result.stdout);
const nativeWaitTimedOut = parsed?.timedOut === true && parsed?.failed !== true;
return {
ok: result.exitCode === 0 && parsed?.ok === true,
completed: parsed?.completed === true,
failed: parsed?.failed === true || (result.exitCode !== 0 && !nativeWaitTimedOut),
timedOut: nativeWaitTimedOut || result.timedOut,
created: parsed?.created === true,
reused: parsed?.reused === true,
jobName,
namespace,
polls: numberOrNull(parsed?.polls) ?? 0,
elapsedMs: numberOrNull(parsed?.elapsedMs) ?? 0,
startedAt: stringOrNull(parsed?.startedAt),
finishedAt: stringOrNull(parsed?.finishedAt),
logsTail: stringOrNull(parsed?.logsTail),
summary: asOptionalRecord(parsed?.summary),
conditionReason: stringOrNull(parsed?.conditionReason),
conditionMessage: stringOrNull(parsed?.conditionMessage) ?? (result.exitCode === 0 || nativeWaitTimedOut ? null : tailText(result.stderr || result.stdout, 500)),
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
};
}
function parseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (trimmed.length === 0) return null;
try {
const parsed = JSON.parse(trimmed) as unknown;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch {
return null;
}
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function tailText(text: string, maxChars: number): string {
return text.length <= maxChars ? text : text.slice(text.length - maxChars);
}