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 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 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), 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; }