import { readFileSync } from "node:fs"; import https from "node:https"; const namespace = process.env.NAMESPACE || ""; const pipelineRun = process.env.PIPELINERUN || ""; const shouldWait = process.env.WAIT === "true"; const timeoutSeconds = requiredPositiveNumber("TIMEOUT_SECONDS"); const pollIntervalSeconds = requiredPositiveNumber("POLL_INTERVAL_SECONDS"); const logsTailLines = Number(process.env.LOGS_TAIL_LINES || "240"); const maxLogBytes = Number(process.env.MAX_LOG_BYTES || "32000"); const host = process.env.KUBERNETES_SERVICE_HOST; const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443"); const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim(); const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); const manifest = JSON.parse(Buffer.from(readFileSync(0, "utf8").replace(/\s+/g, ""), "base64").toString("utf8")); const startedAt = Date.now(); function request(method, path, body, contentType = "application/json") { return new Promise((resolve, reject) => { const headers = { authorization: `Bearer ${token}` }; const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body); if (payload !== null) { headers["content-type"] = contentType; headers["content-length"] = Buffer.byteLength(payload); } const req = https.request({ host, port, path, method, ca, headers }, (res) => { let text = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { text += chunk; }); res.on("end", () => resolve({ status: res.statusCode || 0, text })); }); req.on("error", reject); if (payload !== null) req.write(payload); req.end(); }); } function parseBody(result) { if (!result.text) return null; try { return JSON.parse(result.text); } catch { return null; } } async function getPipelineRun() { const result = await request("GET", `/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/pipelineruns/${encodeURIComponent(pipelineRun)}`); if (result.status === 404) return { found: false, object: null, status: result.status, text: result.text }; if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api GET pipelinerun status ${result.status}`); return { found: true, object: parseBody(result), status: result.status, text: result.text }; } function succeededCondition(object) { const conditions = Array.isArray(object?.status?.conditions) ? object.status.conditions : []; return conditions.find((item) => item && item.type === "Succeeded") || null; } function compact(object) { const condition = succeededCondition(object); return { name: object?.metadata?.name || pipelineRun, namespace: object?.metadata?.namespace || namespace, generation: object?.metadata?.generation ?? null, startTime: object?.status?.startTime || null, completionTime: object?.status?.completionTime || null, conditionStatus: condition?.status || null, conditionReason: condition?.reason || null, conditionMessage: condition?.message || null, }; } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } let created = false; let reused = false; let latest = await getPipelineRun(); if (latest.found) { reused = true; } else { const result = await request("POST", `/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/pipelineruns`, manifest); if (result.status === 409) { reused = true; } else if (result.status >= 200 && result.status < 300) { created = true; } else { process.stderr.write(result.text || `kube api POST pipelinerun status ${result.status}`); process.exit(1); } latest = await getPipelineRun(); } const deadline = Date.now() + timeoutSeconds * 1000; let polls = 0; while (shouldWait) { const condition = succeededCondition(latest.object); if (condition?.status === "True" || condition?.status === "False") break; if (Date.now() >= deadline) break; polls += 1; process.stderr.write(JSON.stringify({ event: "cicd.branch-follower.native-tekton.wait", pipelineRun, namespace, polls, conditionStatus: condition?.status || null, valuesRedacted: true }) + "\n"); await delay(pollIntervalSeconds * 1000); latest = await getPipelineRun(); } const condition = succeededCondition(latest.object); const completed = condition?.status === "True"; const failed = condition?.status === "False"; const terminal = completed || failed; const logsTail = terminal ? await pipelineRunLogsTail() : ""; const output = { ok: !failed, submitted: true, created, reused, wait: shouldWait, polls, completed, failed, terminal, stillRunning: !terminal, timedOutWait: shouldWait && !terminal, elapsedMs: Date.now() - startedAt, pipelineRun: compact(latest.object), logsTail, statusAuthority: "kubernetes-api-serviceaccount", parsedDownstreamCliOutput: false, valuesRedacted: true, }; process.stdout.write(JSON.stringify(output)); if (failed) process.exit(1); function requiredPositiveNumber(name) { const value = Number(process.env[name]); if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`); return value; } async function pipelineRunLogsTail() { const selector = encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRun}`); const podsResult = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${selector}`); if (podsResult.status < 200 || podsResult.status >= 300) return ""; const pods = parseBody(podsResult); const chunks = []; for (const pod of Array.isArray(pods?.items) ? pods.items : []) { const podName = pod?.metadata?.name; const containers = [ ...(Array.isArray(pod?.spec?.initContainers) ? pod.spec.initContainers : []), ...(Array.isArray(pod?.spec?.containers) ? pod.spec.containers : []), ]; for (const container of containers) { if (!podName || !container?.name) continue; const path = `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}/log?container=${encodeURIComponent(container.name)}&tailLines=${Math.max(1, logsTailLines)}`; const log = await request("GET", path); if (log.status >= 200 && log.status < 300 && log.text) chunks.push(log.text); } } return chunks.join("\n").slice(-Math.max(1, maxLogBytes)); }