// 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, 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, 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, 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 | 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 : null; } catch { return null; } } function asOptionalRecord(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : 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); }