Files
pikasTech-unidesk/scripts/native/cicd/submit-pipelinerun.mjs
T
2026-07-03 14:21:39 +00:00

135 lines
4.9 KiB
JavaScript

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