import { execFileSync } from "node:child_process"; const namespace = process.env.NAMESPACE || ""; const configMap = process.env.CONFIGMAP || ""; const followerId = process.env.FOLLOWER_ID || ""; const specRef = process.env.SPEC_REF || ""; const stateJson = Buffer.from(process.env.STATE_B64 || "", "base64").toString("utf8"); function kubectl(args, input) { return execFileSync("kubectl", ["-n", namespace, ...args], { input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], }); } function readConfigMap() { try { return JSON.parse(kubectl(["get", "configmap", configMap, "-o", "json"])); } catch (error) { const stderr = String(error?.stderr || error?.message || ""); if (/not found/i.test(stderr)) return null; throw error; } } function ensureConfigMap() { if (readConfigMap() !== null) return; const object = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: configMap, namespace }, data: { _createdAt: new Date().toISOString(), _specRef: specRef }, }; kubectl(["apply", "-f", "-"], JSON.stringify(object)); } function stringOrNull(value) { return typeof value === "string" && value.length > 0 ? value : null; } function numberOrNull(value) { return typeof value === "number" && Number.isFinite(value) ? value : null; } function timestampMs(value) { const text = stringOrNull(value); if (text === null) return null; const parsed = Date.parse(text); return Number.isFinite(parsed) ? parsed : null; } function roundSeconds(value) { return Math.round(value * 10) / 10; } function totalSecondsFromRange(startedAt, finishedAt) { const startedMs = timestampMs(startedAt); if (startedMs === null) return null; const finishedMs = timestampMs(finishedAt) ?? Date.now(); return finishedMs >= startedMs ? roundSeconds((finishedMs - startedMs) / 1000) : null; } function terminalPhase(phase) { return ["Succeeded", "Failed", "Blocked", "Skipped", "Noop"].includes(phase); } function preserveExistingTiming(state, existing) { if (numberOrNull(state?.timings?.totalSeconds) !== null) return state; const existingTimings = existing?.timings; const sourceCommit = stringOrNull(existingTimings?.sourceCommit); if (sourceCommit === null || sourceCommit !== stringOrNull(state?.source?.observedSha)) return state; const startedAt = stringOrNull(existingTimings?.startedAt); const existingFinishedAt = stringOrNull(existingTimings?.finishedAt); const finishedAt = existingFinishedAt ?? (terminalPhase(state.phase) ? new Date().toISOString() : null); const seconds = totalSecondsFromRange(startedAt, finishedAt) ?? numberOrNull(existingTimings?.totalSeconds); if (seconds === null) return state; const budgetSeconds = numberOrNull(state?.timings?.budgetSeconds); return { ...state, timings: { ...state.timings, totalSeconds: seconds, totalStatus: terminalPhase(state.phase) ? "completed" : String(state.phase || "recorded").toLowerCase(), totalSource: stringOrNull(existingTimings?.totalSource) ?? "stored-state", sourceCommit, startedAt, finishedAt, overBudget: budgetSeconds === null ? null : seconds > budgetSeconds, }, }; } ensureConfigMap(); const current = readConfigMap(); const currentText = current?.data?.[followerId]; const existing = typeof currentText === "string" && currentText.length > 0 ? JSON.parse(currentText) : null; const incomingState = JSON.parse(stateJson); const state = preserveExistingTiming(incomingState, existing); const patch = { data: { [followerId]: JSON.stringify(state), _updatedAt: new Date().toISOString(), _specRef: specRef, }, }; kubectl(["patch", "configmap", configMap, "--type", "merge", "-p", JSON.stringify(patch)]); process.stdout.write(JSON.stringify({ ok: true, followerId, preservedTiming: state !== incomingState, statusAuthority: "target-node-summary", parsedDownstreamCliOutput: false }));