import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import http from "node:http"; import https from "node:https"; import { parseRuntimeReuseConfig, summarizeRuntimeReuseConfig } from "./reuse-config-summary.mjs"; const gate = requiredEnv("GATE"); const follower = requiredEnv("FOLLOWER_ID"); const repository = requiredEnv("REPOSITORY"); const repoPath = requiredEnv("REPO_PATH"); const sourceBranch = requiredEnv("SOURCE_BRANCH"); const snapshotPrefix = requiredEnv("SNAPSHOT_PREFIX").replace(/\/+$/u, ""); const selectedSource = process.env.SOURCE_COMMIT || ""; const gitopsBranch = process.env.GITOPS_BRANCH || ""; const tektonNamespace = process.env.TEKTON_NAMESPACE || ""; const pipelineRunPrefix = process.env.PIPELINE_RUN_PREFIX || ""; const argoNamespace = process.env.ARGO_NAMESPACE || ""; const argoApplication = process.env.ARGO_APPLICATION || ""; const runtimeNamespace = process.env.RUNTIME_NAMESPACE || ""; const stateNamespace = process.env.STATE_NAMESPACE || ""; const stateConfigMap = process.env.STATE_CONFIGMAP || ""; const workloads = parseWorkloads(process.env.WORKLOADS_B64 || ""); const healthUrl = process.env.HEALTH_URL || ""; const slowTaskSeconds = requiredPositiveIntEnv("SLOW_TASK_SECONDS"); const healthTimeoutMs = requiredPositiveIntEnv("HEALTH_TIMEOUT_MS"); const gateTimeoutMs = optionalPositiveIntEnv("GATE_TIMEOUT_MS") ?? healthTimeoutMs; const errors = []; const branchCommit = rev(`refs/heads/${sourceBranch}`); const sourceCommit = selectedSource || branchCommit || ""; const sourceStageRef = sourceCommit ? `${snapshotPrefix}/${sourceCommit}` : null; const sourceSnapshot = sourceStageRef ? rev(sourceStageRef) : null; const source = { repository, branch: sourceBranch, sourceCommit: sourceCommit || null, stageRef: sourceStageRef, snapshotReady: Boolean(sourceCommit && sourceSnapshot === sourceCommit), authority: "k8s-git-mirror-snapshot", }; const gitMirror = gitMirrorSummary(sourceCommit); let evidence; if (gate === "reuse-plan") evidence = await reusePlanEvidence(sourceCommit); else if (gate === "ci-taskrun-plan") evidence = await ciTaskRunEvidence(sourceCommit); else if (gate === "cd-rollout-plan") evidence = await cdRolloutEvidence(sourceCommit); else if (gate === "post-deploy-health") evidence = await postDeployHealthEvidence(sourceCommit); else if (gate === "runtime-closeout") evidence = await runtimeCloseoutEvidence(sourceCommit); else fail(`unsupported gate ${gate}`); const ok = errors.length === 0 && evidence?.ok === true; const payload = { ok, gate, follower, source, evidence, policy: { slowTaskSeconds, healthTimeoutMs, }, errors: errors.slice(0, 6), statusAuthority: "kubernetes-api-serviceaccount", parsedDownstreamCliOutput: false, bounded: true, }; console.log(JSON.stringify(payload)); if (!ok) process.exit(2); async function reusePlanEvidence(commit) { const reuse = readReuseConfig(commit); const decisions = reuse.present === true ? reusePlanDecisions(reuse) : []; const summary = reusePlanDecisionSummary(decisions); return { ok: source.snapshotReady && gitMirror.ok === true && reuse.present === true && reuse.serviceCount > 0 && decisions.length > 0, gitMirror: compactGitMirrorEvidence(gitMirror), reuse: compactReuseEvidence(reuse), decisions: boundedDecisions(decisions), decisionSummary: summary, contract: { consumer: "adapter-ci", fields: "serviceId sourceIdentity envIdentity runtimeReuse envReuse skipImageBuild buildDecision reusableImageRef reason", expectation: "CI consumes skipImageBuild from this plan; it does not re-infer build/skip from TaskRun logs or source file scans.", }, }; } async function ciTaskRunEvidence(commit) { if (!commit || !tektonNamespace || !pipelineRunPrefix) return notConfigured("tekton"); const pipelineRunName = `${pipelineRunPrefix}-${commit.slice(0, 12)}`; const reusePlan = await reusePlanEvidence(commit); const pipelineRun = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelineruns/${encodeURIComponent(pipelineRunName)}`, false); const prStatus = pipelineRunStatus(pipelineRun); const pipelineRef = str(pipelineRun?.spec?.pipelineRef?.name); const pipeline = pipelineRef ? await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelines/${encodeURIComponent(pipelineRef)}`, false) : null; const taskRuns = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/taskruns?labelSelector=${encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRunName}`)}`, false); const taskSummary = taskRunsSummary(taskRuns); const planArtifacts = planArtifactsEvidence(pipelineRunName); const storedCiConsumption = await storedAgentRunCiConsumption(commit); const buildTaskRunServices = buildTaskServices(taskRuns); const ciConsumption = ciConsumptionSummary(reusePlan.decisions || [], planArtifacts, buildTaskRunServices, storedCiConsumption); return { ok: prStatus.succeeded === true && taskSummary.failedCount === 0 && taskSummary.activeCount === 0 && ciConsumption.ok === true, reusePlan: { ok: reusePlan.ok, decisionSummary: compactDecisionSummary(reusePlan.decisionSummary), }, planArtifacts, storedCiConsumption, ciConsumption, pipelineRun: prStatus, pipeline: { name: pipelineRef, taskCount: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.length : null, tasks: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.slice(0, 12).map((task) => ({ name: str(task?.name), runAfter: Array.isArray(task?.runAfter) ? task.runAfter.slice(0, 6) : [] })) : [], tasksTruncated: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.length > 12 : false, }, taskRuns: compactCiTaskRuns(taskSummary), }; } function reusePlanDecisions(reuse) { const baseRef = sourceStageRef ? parentRef(sourceStageRef) : null; return (reuse.serviceSpecs || []).map((service) => { const codePaths = service.runtimeReuse?.codeIdentityPaths || []; const envPaths = uniqueStrings([...(service.runtimeReuse?.envIdentityPaths || []), ...(service.envReuse?.envIdentityFiles || [])]); const sourceIdentity = identityComparison(sourceStageRef, baseRef, codePaths); const envIdentity = identityComparison(sourceStageRef, baseRef, envPaths); const runtimeEnabled = service.runtimeReuse?.enabled !== false; const envEnabled = service.envReuse?.enabled !== false; const runtimeHit = runtimeEnabled && sourceIdentity.hit === true && envIdentity.hit === true; const envHit = envEnabled && envIdentity.hit === true; const skipImageBuild = runtimeHit || envHit; return { serviceId: service.id, sourceIdentity, envIdentity, runtimeReuse: { enabled: runtimeEnabled, hit: runtimeHit, reason: runtimeHit ? "source-and-env-identity-hit" : missReason(sourceIdentity, envIdentity, runtimeEnabled), }, envReuse: { enabled: envEnabled, hit: envHit, reason: envHit ? "env-identity-hit" : missReason(null, envIdentity, envEnabled), }, skipImageBuild, buildDecision: skipImageBuild ? "skipImageBuild" : "buildImage", reusableImageRef: null, reusableImageRefKnown: false, reason: decisionReason({ runtimeHit, envHit, runtimeEnabled, envEnabled, sourceIdentity, envIdentity }), }; }); } function reusePlanDecisionSummary(decisions) { const skip = decisions.filter((item) => item.skipImageBuild === true).map((item) => item.serviceId).sort(); const build = decisions.filter((item) => item.skipImageBuild !== true).map((item) => item.serviceId).sort(); return { serviceCount: decisions.length, skipImageBuildCount: skip.length, buildImageCount: build.length, skipImageBuildServices: skip, buildImageServices: build, }; } function compactDecisionSummary(summary) { return { serviceCount: summary?.serviceCount ?? null, skipImageBuildCount: summary?.skipImageBuildCount ?? null, buildImageCount: summary?.buildImageCount ?? null, }; } function identityComparison(currentRef, baseRef, paths) { const current = identityDigest(currentRef, paths); const previous = baseRef ? identityDigest(baseRef, paths) : null; const configured = paths.length > 0; const hit = configured && previous !== null && current.sha256 !== null && previous.sha256 !== null && current.sha256 === previous.sha256; return { configured, paths: paths.slice(0, 12), pathsTruncated: paths.length > 12, pathCount: paths.length, current: current.sha256, previous: previous?.sha256 ?? null, currentMissingCount: current.missingCount, previousMissingCount: previous?.missingCount ?? null, hit, status: !configured ? "not-configured" : previous === null ? "base-missing" : hit ? "hit" : "miss", }; } function identityDigest(ref, paths) { if (!ref || paths.length === 0) return { sha256: null, missingCount: 0 }; const hash = createHash("sha256"); let missingCount = 0; for (const path of paths) { const entries = gitTreeEntries(ref, path); if (entries.length === 0) missingCount += 1; hash.update(path); hash.update("\0"); for (const entry of entries) { hash.update(entry); hash.update("\0"); } } return { sha256: hash.digest("hex"), missingCount }; } function gitTreeEntries(ref, path) { try { const out = execFileSync("git", [`--git-dir=${repoPath}`, "ls-tree", "-r", "-z", "--full-tree", ref, "--", path], { encoding: "utf8", maxBuffer: 1024 * 1024 }); return out.split("\0").filter(Boolean).sort(); } catch { return []; } } function parentRef(ref) { try { const out = execFileSync("git", [`--git-dir=${repoPath}`, "rev-parse", "--verify", `${ref}^`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); return sha(out) ? out : null; } catch { return null; } } function missReason(sourceIdentity, envIdentity, enabled) { if (!enabled) return "reuse-disabled"; if (sourceIdentity?.configured === false || envIdentity?.configured === false) return "identity-not-configured"; if (sourceIdentity?.status === "base-missing" || envIdentity?.status === "base-missing") return "base-identity-missing"; if (sourceIdentity?.status === "miss") return "source-identity-miss"; if (envIdentity?.status === "miss") return "env-identity-miss"; return "identity-miss"; } function decisionReason(input) { if (input.runtimeHit) return "runtime-reuse-hit"; if (input.envHit) return "env-reuse-hit"; if (input.envIdentity.configured === false) return "env-identity-not-configured"; if (input.envIdentity.status === "base-missing") return "previous-env-identity-unavailable"; if (input.envIdentity.status === "miss") return "env-identity-changed"; if (!input.runtimeEnabled && !input.envEnabled) return "reuse-disabled"; return "reuse-miss"; } function planArtifactsEvidence(pipelineRunName) { if (!tektonNamespace || !pipelineRunName) return { ok: false, degradedReason: "tekton-not-configured" }; try { const text = execFileSync("node", ["./plan-artifacts.mjs", tektonNamespace, pipelineRunName], { encoding: "utf8", maxBuffer: 512 * 1024 }); return JSON.parse(text); } catch (error) { return { ok: false, degradedReason: "plan-artifacts-query-failed", reason: shortText(error?.message || String(error)) }; } } function ciConsumptionSummary(decisions, planArtifacts, buildTaskRunServices, storedCiConsumption) { const skipExpected = decisions.filter((item) => item.skipImageBuild === true).map((item) => item.serviceId).sort(); const buildExpected = decisions.filter((item) => item.skipImageBuild !== true).map((item) => item.serviceId).sort(); if (/agentrun/u.test(follower) && storedCiConsumption?.present === true && storedCiConsumption.ok !== true) { return { ok: false, reason: storedCiConsumption.reason || "ci-consumption-evidence-missing", source: "branch-follower-compact-state", expected: { skipImageBuildCount: skipExpected.length, buildImageCount: buildExpected.length, }, observed: { buildServicesCount: 0, buildTaskRunServices, reusedServicesCount: 0, skippedOrReusedServicesCount: 0, buildSkippedCount: null, }, mismatches: [], }; } if (storedCiConsumption?.ok === true) { const observed = storedCiConsumption.observed || {}; const unexpectedStoredBuild = skipExpected.filter((serviceId) => observed.imageBuildDecision === "buildImage" && serviceId === "agentrun-mgr"); const missingStoredBuild = buildExpected.filter((serviceId) => observed.imageBuildDecision !== "buildImage" && serviceId === "agentrun-mgr"); const storedOk = unexpectedStoredBuild.length === 0 && missingStoredBuild.length === 0; return { ok: storedOk, reason: storedOk ? "ci-consumed-reuse-plan" : "ci-consumption-mismatch", source: "branch-follower-compact-state", expected: { skipImageBuildCount: skipExpected.length, buildImageCount: buildExpected.length, }, observed: { buildServicesCount: numberOrNull(observed.buildServicesCount), buildTaskRunServices, reusedServicesCount: numberOrNull(observed.reusedServicesCount), skippedOrReusedServicesCount: numberOrNull(observed.reusedServicesCount), buildSkippedCount: numberOrNull(observed.buildSkippedCount), imageBuildDecision: str(observed.imageBuildDecision), imageBuildStatus: str(observed.imageBuildStatus), imageBuildCreated: observed.imageBuildCreated === true, imageBuildReused: observed.imageBuildReused === true, }, mismatches: compactCiMismatches(unexpectedStoredBuild, missingStoredBuild), }; } const buildObserved = uniqueStrings([...strings(planArtifacts?.buildServices), ...buildTaskRunServices]).sort(); const reusedObserved = strings(planArtifacts?.reusedServices).sort(); const skippedObserved = uniqueStrings(reusedObserved).sort(); const unexpectedBuild = skipExpected.filter((serviceId) => buildObserved.includes(serviceId) && !reusedObserved.includes(serviceId)); const missingBuild = buildExpected.filter((serviceId) => !buildObserved.includes(serviceId) && !reusedObserved.includes(serviceId)); const ok = planArtifacts?.ok === true && unexpectedBuild.length === 0 && missingBuild.length === 0; return { ok, reason: unexpectedBuild.length > 0 || missingBuild.length > 0 ? "ci-consumption-mismatch" : planArtifacts?.ok === true ? "ci-consumed-reuse-plan" : "plan-artifacts-event-missing", source: "reuse-plan-decisions-compared-to-plan-artifacts-g14-ci-plan", expected: { skipImageBuildCount: skipExpected.length, buildImageCount: buildExpected.length, }, observed: { buildServicesCount: buildObserved.length, buildTaskRunServices, reusedServicesCount: reusedObserved.length, skippedOrReusedServicesCount: skippedObserved.length, buildSkippedCount: typeof planArtifacts?.buildSkippedCount === "number" ? planArtifacts.buildSkippedCount : null, }, mismatches: compactCiMismatches(unexpectedBuild, missingBuild), }; } async function storedAgentRunCiConsumption(commit) { if (!stateNamespace || !stateConfigMap || !follower || !commit) return null; const cm = await getJson(`/api/v1/namespaces/${encodeURIComponent(stateNamespace)}/configmaps/${encodeURIComponent(stateConfigMap)}`, false); const raw = cm?.data?.[follower]; if (typeof raw !== "string" || raw.length === 0) return { ok: false, present: false, reason: "compact-state-missing" }; const state = parseJson(raw); if (state === null) return { ok: false, present: true, reason: "state-json-parse-failed" }; const command = state?.command || {}; const payload = command.payload || {}; const agentrun = payload.agentrun || {}; const sourceCommit = str(agentrun.sourceCommit) || str(command.sourceCommit); const ci = agentrun.ciConsumption || null; if (sourceCommit === null && (!ci || typeof ci !== "object")) return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit: null }; if (sourceCommit !== commit) return { ok: false, present: true, reason: "state-source-commit-mismatch", sourceCommit }; if (!ci || typeof ci !== "object") return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit }; return { ok: ci.ok === true, present: true, sourceCommit, source: str(ci.source), expected: ci.expected || null, observed: ci.observed || null, mismatches: Array.isArray(ci.mismatches) ? ci.mismatches.slice(0, 6) : [], valuesRedacted: true, }; } function compactCiMismatches(unexpectedBuild, missingBuild) { return [ ...(unexpectedBuild.length > 0 ? [{ reason: "expected-skipImageBuild-but-ci-buildServices-includes-service", serviceIds: unexpectedBuild }] : []), ...(missingBuild.length > 0 ? [{ reason: "expected-buildImage-but-ci-plan-has-no-build-or-reuse-service", serviceIds: missingBuild }] : []), ]; } function buildTaskServices(list) { const items = Array.isArray(list?.items) ? list.items : []; return uniqueStrings(items.flatMap((item) => { const taskName = str(item?.metadata?.labels?.["tekton.dev/pipelineTask"]) || str(item?.spec?.taskRef?.name) || ""; const match = /^build-(.+)$/u.exec(taskName); return match ? [match[1]] : []; })).sort(); } function compactCiTaskRuns(summary) { return { count: summary.count, slowThresholdSeconds: summary.slowThresholdSeconds, failedCount: summary.failedCount, activeCount: summary.activeCount, slowCount: summary.slowCount, failedItems: summary.failedItems.slice(0, 4).map(compactCiTaskItem), activeItems: summary.activeItems.slice(0, 4).map(compactCiTaskItem), slowItems: summary.slowItems.slice(0, 4).map(compactCiTaskItem), timeline: summary.timeline.slice(0, 10), timelineTruncated: summary.timeline.length > 10 || summary.timelineTruncated === true, performance: summary.performance, }; } function compactCiTaskItem(item) { return { taskName: item.taskName, status: item.status, reason: item.reason, durationSeconds: item.durationSeconds, }; } async function cdRolloutEvidence(commit) { const argo = await argoSummary(); const runtime = await runtimeSummary(commit); return { ok: gitMirror.githubInSync !== false && argo.ready === true && runtime.ready === true && runtime.aligned === true, gitMirror, argo, runtime, }; } async function postDeployHealthEvidence(commit) { const runtime = await runtimeSummary(commit); const health = await healthSummary(); return { ok: runtime.ready === true && runtime.aligned === true && health.ok === true, runtime, health, }; } async function runtimeCloseoutEvidence(commit) { const startedAt = Date.now(); const refresh = await argoRefresh(); const deadline = Date.now() + gateTimeoutMs; let polls = 0; let latest = null; while (Date.now() <= deadline) { polls += 1; latest = await runtimeCloseoutStatus(commit); if (latest.ok === true) break; if (Date.now() + 2000 > deadline) break; await delay(2000); } return { ok: latest?.ok === true, sourceCommit: shortSha(commit), gitMirror: compactGitMirrorEvidence(gitMirror), refresh, closeout: latest, polls, elapsedMs: Date.now() - startedAt, writesState: false, contract: { gate: "argo-runtime-only", excludes: ["git-mirror-flush", "state-write"], expectation: "Argo refresh and runtime readiness are independently triggerable; git-mirror post-flush is validated by the git-mirror-flush gate.", }, }; } async function runtimeCloseoutStatus(commit) { const pipelineRunName = commit && pipelineRunPrefix ? `${pipelineRunPrefix}-${commit.slice(0, 12)}` : ""; const pipelineRun = pipelineRunName && tektonNamespace ? pipelineRunStatus(await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelineruns/${encodeURIComponent(pipelineRunName)}`, false)) : { present: null, succeeded: null, reason: "tekton-not-configured" }; const argo = await argoSummary(); const runtime = await runtimeSummary(commit); return { ok: source.snapshotReady === true && pipelineRun.succeeded === true && argo.ready === true && runtime.ready === true && runtime.aligned === true, source, pipelineRun, argo, runtime, }; } async function argoRefresh() { if (!argoNamespace || !argoApplication) return { ok: true, skipped: true, reason: "argo-not-configured" }; const path = `/apis/argoproj.io/v1alpha1/namespaces/${encodeURIComponent(argoNamespace)}/applications/${encodeURIComponent(argoApplication)}`; const patched = await patchJson(path, { metadata: { annotations: { "argocd.argoproj.io/refresh": "hard" } } }); return { ok: patched !== null, namespace: argoNamespace, application: argoApplication, refresh: "hard", resourceVersion: str(patched?.metadata?.resourceVersion), statusAuthority: "kubernetes-api-serviceaccount", parsedDownstreamCliOutput: false, valuesRedacted: true, }; } function readReuseConfig(commit) { if (!commit || !sourceStageRef) return { present: false, reason: "source-commit-missing" }; try { const text = execFileSync("git", [`--git-dir=${repoPath}`, "show", `${sourceStageRef}:gitops/reuse.ymal`], { encoding: "utf8", maxBuffer: 256 * 1024 }); const parsed = parseRuntimeReuseConfig(text, { sourceCommit: commit, stageRef: sourceStageRef }); const summary = summarizeRuntimeReuseConfig(parsed); return { ...summary, serviceSpecs: parsed.services, bytes: Buffer.byteLength(text, "utf8"), sha256: summary.sha256 ?? createHash("sha256").update(text).digest("hex"), }; } catch (error) { return { present: false, path: "gitops/reuse.ymal", reason: shortText(error?.message || String(error)) }; } } function compactReuseEvidence(reuse) { return { ok: reuse.ok === true, present: reuse.present === true, path: reuse.path || "gitops/reuse.ymal", sha256: shortFingerprint(reuse.sha256), serviceCount: reuse.serviceCount ?? null, errors: Array.isArray(reuse.errors) ? reuse.errors.slice(0, 3) : [], bytes: reuse.bytes ?? null, valuesRedacted: true, }; } function compactGitMirrorEvidence(value) { return { ok: value.ok === true, sourceSnapshotReady: value.sourceSnapshotReady === true, githubInSync: value.githubInSync, pendingFlush: value.pendingFlush, }; } function boundedDecisions(decisions) { return decisions.slice(0, 16).map((item) => ({ serviceId: item.serviceId, sourceIdentity: compactIdentity(item.sourceIdentity), envIdentity: compactIdentity(item.envIdentity), runtimeReuse: compactReuseHit(item.runtimeReuse), envReuse: compactReuseHit(item.envReuse), skipImageBuild: item.skipImageBuild, buildDecision: item.buildDecision, reusableImageRef: item.reusableImageRef, reusableImageRefKnown: item.reusableImageRefKnown, reason: item.reason, })); } function compactIdentity(value) { return { configured: value.configured, pathCount: value.pathCount, hit: value.hit, status: value.status, missing: { current: value.currentMissingCount, previous: value.previousMissingCount, }, }; } function compactReuseHit(value) { return { enabled: value.enabled, hit: value.hit, }; } function gitMirrorSummary(commit) { const localSource = rev(`refs/heads/${sourceBranch}`); const githubSource = rev(`refs/mirror-stage/heads/${sourceBranch}`); const localGitops = gitopsBranch ? rev(`refs/heads/${gitopsBranch}`) : null; const githubGitops = gitopsBranch ? rev(`refs/mirror-stage/heads/${gitopsBranch}`) : null; return { ok: Boolean(localSource) && source.snapshotReady && (!gitopsBranch || !localGitops || localGitops === githubGitops), localSource, githubSource, sourceStageRef, sourceSnapshot, sourceSnapshotReady: source.snapshotReady, gitopsBranch: gitopsBranch || null, localGitops, githubGitops, pendingFlush: gitopsBranch ? Boolean(localGitops && localGitops !== githubGitops) : null, githubInSync: gitopsBranch ? Boolean(!localGitops || localGitops === githubGitops) : null, }; } async function argoSummary() { if (!argoNamespace || !argoApplication) return { ready: null, reason: "argo-not-configured" }; const app = await getJson(`/apis/argoproj.io/v1alpha1/namespaces/${encodeURIComponent(argoNamespace)}/applications/${encodeURIComponent(argoApplication)}`, false); const sync = app?.status?.sync || {}; const health = app?.status?.health || {}; const op = app?.status?.operationState || {}; const problemWorkloads = await argoProblemWorkloadSummaries(app); return { name: argoApplication, namespace: argoNamespace, syncStatus: str(sync.status), healthStatus: str(health.status), revision: str(sync.revision), operationPhase: str(op.phase), operationMessage: str(op.message), conditions: compactArgoConditions(app?.status?.conditions), nonReadyResources: compactArgoNonReadyResources(app?.status?.resources), syncResultResources: compactArgoSyncResultResources(op?.syncResult?.resources), problemWorkloads, ready: sync.status === "Synced" && health.status === "Healthy", }; } async function argoProblemWorkloadSummaries(app) { const resources = Array.isArray(app?.status?.resources) ? app.status.resources : []; const syncResources = Array.isArray(app?.status?.operationState?.syncResult?.resources) ? app.status.operationState.syncResult.resources : []; const names = uniqueStrings([ ...resources .filter((item) => item?.kind === "Deployment" && (item.status === "OutOfSync" || (item.health?.status && item.health.status !== "Healthy"))) .map((item) => str(item.name)) .filter(Boolean), ...syncResources .filter((item) => item?.kind === "Deployment" && problemArgoSyncResource(item)) .map((item) => str(item.name)) .filter(Boolean), ]).slice(0, 8); const rows = []; for (const name of names) rows.push(await deploymentProblemSummary(runtimeNamespace || "default", name)); return rows; } async function deploymentProblemSummary(namespace, name) { const deployment = await getJson(`/apis/apps/v1/namespaces/${encodeURIComponent(namespace)}/deployments/${encodeURIComponent(name)}`, false); const selector = deploymentSelector(deployment); const pods = selector ? await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${encodeURIComponent(selector)}`, false) : null; const conditions = Array.isArray(deployment?.status?.conditions) ? deployment.status.conditions .filter((item) => item?.status !== "True" || item?.type === "Progressing") .slice(-4) .map((item) => ({ type: str(item?.type), status: str(item?.status), reason: str(item?.reason), message: shortText(str(item?.message) || ""), })) : []; return { kind: "Deployment", namespace, name, desired: deployment?.spec?.replicas ?? 1, readyReplicas: deployment?.status?.readyReplicas ?? 0, updatedReplicas: deployment?.status?.updatedReplicas ?? 0, unavailableReplicas: deployment?.status?.unavailableReplicas ?? null, conditions, pods: compactProblemPods(pods), }; } function deploymentSelector(deployment) { const labels = deployment?.spec?.selector?.matchLabels; if (!labels || typeof labels !== "object" || Array.isArray(labels)) return null; const pairs = Object.entries(labels) .filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string") .map(([key, value]) => `${key}=${value}`); return pairs.length === 0 ? null : pairs.join(","); } function compactProblemPods(list) { const items = Array.isArray(list?.items) ? list.items : []; return items.slice(0, 6).map((pod) => { const statuses = Array.isArray(pod?.status?.containerStatuses) ? pod.status.containerStatuses : []; return { name: str(pod?.metadata?.name), phase: str(pod?.status?.phase), ready: statuses.every((item) => item?.ready === true), containers: statuses.slice(0, 4).map((item) => ({ name: str(item?.name), ready: item?.ready === true, restartCount: typeof item?.restartCount === "number" ? item.restartCount : null, waitingReason: str(item?.state?.waiting?.reason), waitingMessage: shortText(str(item?.state?.waiting?.message) || ""), terminatedReason: str(item?.lastState?.terminated?.reason) || str(item?.state?.terminated?.reason), exitCode: typeof item?.lastState?.terminated?.exitCode === "number" ? item.lastState.terminated.exitCode : typeof item?.state?.terminated?.exitCode === "number" ? item.state.terminated.exitCode : null, })), }; }); } function compactArgoConditions(value) { return Array.isArray(value) ? value.slice(0, 5).map((item) => ({ type: str(item?.type), message: shortText(str(item?.message) || ""), lastTransitionTime: str(item?.lastTransitionTime), })) : []; } function compactArgoNonReadyResources(value) { return Array.isArray(value) ? value .filter((item) => item?.health?.status && item.health.status !== "Healthy") .slice(0, 5) .map((item) => ({ kind: str(item?.kind), namespace: str(item?.namespace), name: str(item?.name), status: str(item?.status), healthStatus: str(item?.health?.status), healthMessage: shortText(str(item?.health?.message) || ""), })) : []; } function compactArgoSyncResultResources(value) { return Array.isArray(value) ? value .filter(problemArgoSyncResource) .slice(0, 8) .map((item) => ({ group: str(item?.group), kind: str(item?.kind), namespace: str(item?.namespace), name: str(item?.name), status: str(item?.status), hookPhase: str(item?.hookPhase), syncPhase: str(item?.syncPhase), message: shortText(str(item?.message) || ""), })) : []; } function problemArgoSyncResource(item) { const message = String(item?.message || ""); return (item?.status && item.status !== "Synced") || (item?.hookPhase && item.hookPhase !== "Succeeded") || /fail|error|backoff|forbidden|invalid|denied|exceeded/iu.test(message); } async function runtimeSummary(expected) { if (!runtimeNamespace || workloads.length === 0) return { ready: null, aligned: null, reason: "runtime-not-configured" }; const rows = []; for (const workload of workloads) { const resource = workload.kind === "StatefulSet" ? "statefulsets" : "deployments"; const item = await getJson(`/apis/apps/v1/namespaces/${encodeURIComponent(runtimeNamespace)}/${resource}/${encodeURIComponent(workload.name)}`, false); rows.push(workloadSummary(workload, item, expected)); } return { namespace: runtimeNamespace, expectedSha: shortSha(expected), ready: rows.length > 0 && rows.every((row) => row.ready === true), aligned: rows.length > 0 && rows.every((row) => row.aligned === true), workloads: rows, }; } async function healthSummary() { if (!healthUrl) return { ok: null, reason: "health-url-not-configured" }; const targets = healthProbeTargets(healthUrl); const probes = []; for (const url of targets) probes.push(await httpProbe(url)); return { ok: probes.every((probe) => probe.ok), probes }; } function healthProbeTargets(value) { const trimmed = value.replace(/\/+$/u, ""); try { const parsed = new URL(trimmed); const pathname = parsed.pathname.replace(/\/+$/u, ""); if (pathname.endsWith("/health") || pathname.endsWith("/api/health") || pathname.endsWith("/health/readiness") || pathname.endsWith("/health/live")) { return [trimmed]; } } catch { // Fall back to the historical base-url contract when HEALTH_URL is not a full URL. } return [`${trimmed}/health/readiness`, `${trimmed}/health/live`]; } function workloadSummary(spec, item, expected) { const status = item?.status || {}; const desired = item?.spec?.replicas ?? 1; const readyCount = spec.kind === "StatefulSet" ? status.readyReplicas ?? 0 : status.availableReplicas ?? 0; const updated = status.updatedReplicas ?? readyCount; const commit = workloadCommit(spec, item); return { kind: spec.kind, name: spec.name, ready: readyCount >= desired && updated >= desired, desired, readyReplicas: readyCount, updatedReplicas: updated, sourceCommit: shortSha(commit), aligned: Boolean(expected && commit && commit === expected), }; } function workloadCommit(spec, item) { const meta = item?.metadata || {}; const tmpl = item?.spec?.template || {}; const podMeta = tmpl.metadata || {}; for (const key of spec.sourceCommit.labels || []) if (sha(meta.labels?.[key])) return meta.labels[key]; for (const key of spec.sourceCommit.annotations || []) if (sha(meta.annotations?.[key])) return meta.annotations[key]; for (const key of spec.sourceCommit.podLabels || []) if (sha(podMeta.labels?.[key])) return podMeta.labels[key]; for (const key of spec.sourceCommit.podAnnotations || []) if (sha(podMeta.annotations?.[key])) return podMeta.annotations[key]; for (const envName of spec.sourceCommit.env || []) { for (const container of tmpl.spec?.containers || []) { for (const env of container.env || []) if (env?.name === envName && sha(env.value)) return env.value; } } return null; } function pipelineRunStatus(value) { const condition = (value?.status?.conditions || []).find((item) => item?.type === "Succeeded") || null; return { name: str(value?.metadata?.name), namespace: str(value?.metadata?.namespace), pipelineRefName: str(value?.spec?.pipelineRef?.name), present: value !== null, succeeded: condition?.status === "True" ? true : condition?.status === "False" ? false : null, reason: str(condition?.reason), startTime: str(value?.status?.startTime), completionTime: str(value?.status?.completionTime), durationSeconds: durationSeconds(value?.status?.startTime, value?.status?.completionTime), }; } function taskRunsSummary(list) { const items = Array.isArray(list?.items) ? list.items : []; const rows = items.map((item) => { const condition = (item?.status?.conditions || []).find((entry) => entry?.type === "Succeeded") || {}; return { name: str(item?.metadata?.name), taskName: str(item?.metadata?.labels?.["tekton.dev/pipelineTask"]) || str(item?.spec?.taskRef?.name), status: str(condition.status) || "Unknown", reason: str(condition.reason), startTime: str(item?.status?.startTime), completionTime: str(item?.status?.completionTime), durationSeconds: durationSeconds(item?.status?.startTime, item?.status?.completionTime), }; }); const failed = rows.filter((item) => item.status === "False"); const active = rows.filter((item) => item.status !== "True" && item.status !== "False"); const slow = rows .filter((item) => typeof item.durationSeconds === "number" && item.durationSeconds >= slowTaskSeconds) .sort((left, right) => (right.durationSeconds || 0) - (left.durationSeconds || 0)); const timeline = rows.slice().sort(compareTaskRunRows); const performance = taskRunPerformance(rows); return { count: rows.length, slowThresholdSeconds: slowTaskSeconds, failedCount: failed.length, activeCount: active.length, slowCount: slow.length, failedItems: failed.slice(0, 6).map(compactNamedTaskRunRow), activeItems: active.slice(0, 6).map(compactNamedTaskRunRow), slowItems: slow.slice(0, 6).map(compactNamedTaskRunRow), timeline: timeline.slice(0, 16).map((item) => compactTimelineTaskRunRow(item, performance.firstStart)), timelineTruncated: timeline.length > 16, performance, }; } function taskRunPerformance(rows) { const starts = rows.map((item) => Date.parse(item.startTime || "")).filter((value) => Number.isFinite(value)); const finishes = rows.map((item) => Date.parse(item.completionTime || "")).filter((value) => Number.isFinite(value)); const durations = rows.map((item) => item.durationSeconds).filter((value) => typeof value === "number"); const firstStart = starts.length === 0 ? null : new Date(Math.min(...starts)).toISOString(); const lastCompletion = finishes.length === 0 ? null : new Date(Math.max(...finishes)).toISOString(); return { firstStart, lastCompletion, spanSeconds: firstStart && lastCompletion ? durationSeconds(firstStart, lastCompletion) : null, taskDurationSumSeconds: durations.length === 0 ? null : Math.round(durations.reduce((sum, value) => sum + value, 0) * 10) / 10, maxTaskDurationSeconds: durations.length === 0 ? null : Math.max(...durations), }; } function compareTaskRunRows(left, right) { const leftTime = Date.parse(left.startTime || ""); const rightTime = Date.parse(right.startTime || ""); if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) return leftTime - rightTime; if (Number.isFinite(leftTime)) return -1; if (Number.isFinite(rightTime)) return 1; return String(left.name || "").localeCompare(String(right.name || "")); } function compactNamedTaskRunRow(item) { return { name: item.name, taskName: item.taskName, status: item.status, reason: item.reason, durationSeconds: item.durationSeconds, }; } function compactTimelineTaskRunRow(item, firstStart) { return { taskName: item.taskName, status: item.status, startOffsetSeconds: firstStart && item.startTime ? durationSeconds(firstStart, item.startTime) : null, durationSeconds: item.durationSeconds, }; } async function getJson(path, required) { const host = process.env.KUBERNETES_SERVICE_HOST; const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443"); const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"; const caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; if (!host || !existsSync(tokenPath) || !existsSync(caPath)) fail("kubernetes serviceaccount is unavailable"); const token = readFileSync(tokenPath, "utf8").trim(); const ca = readFileSync(caPath); return await new Promise((resolve, reject) => { const req = https.request({ host, port, path, method: "GET", ca, headers: { authorization: `Bearer ${token}` } }, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { body += chunk; }); res.on("end", () => { if (res.statusCode === 404 && !required) return resolve(null); if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) return reject(new Error(shortText(body || `kube api ${res.statusCode}`))); try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); req.on("error", reject); req.end(); }).catch((error) => { errors.push(`${path}: ${shortText(error?.message || String(error))}`); return null; }); } async function patchJson(path, value) { const host = process.env.KUBERNETES_SERVICE_HOST; const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443"); const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"; const caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; if (!host || !existsSync(tokenPath) || !existsSync(caPath)) fail("kubernetes serviceaccount is unavailable"); const token = readFileSync(tokenPath, "utf8").trim(); const ca = readFileSync(caPath); const payload = JSON.stringify(value); return await new Promise((resolve, reject) => { const req = https.request({ host, port, path, method: "PATCH", ca, headers: { authorization: `Bearer ${token}`, "content-type": "application/merge-patch+json", "content-length": Buffer.byteLength(payload), }, }, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { body += chunk; }); res.on("end", () => { if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) return reject(new Error(shortText(body || `kube api ${res.statusCode}`))); try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); }); req.on("error", reject); req.write(payload); req.end(); }).catch((error) => { errors.push(`${path}: ${shortText(error?.message || String(error))}`); return null; }); } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function httpProbe(url) { const client = url.startsWith("https:") ? https : http; const started = Date.now(); return await new Promise((resolve) => { const req = client.get(url, { timeout: healthTimeoutMs }, (res) => { res.resume(); res.on("end", () => resolve({ url, ok: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300, statusCode: res.statusCode || null, elapsedMs: Date.now() - started })); }); req.on("timeout", () => { req.destroy(); resolve({ url, ok: false, statusCode: null, elapsedMs: Date.now() - started, reason: "timeout" }); }); req.on("error", (error) => resolve({ url, ok: false, statusCode: null, elapsedMs: Date.now() - started, reason: shortText(error?.message || String(error)) })); }); } function parseWorkloads(value) { if (!value) return []; try { const parsed = JSON.parse(Buffer.from(value, "base64").toString("utf8")); return Array.isArray(parsed) ? parsed.filter((item) => item && typeof item === "object").slice(0, 8) : []; } catch { return []; } } function optionalPositiveIntEnv(name) { const raw = process.env[name] || ""; if (!raw) return null; const value = Number.parseInt(raw, 10); if (!Number.isInteger(value) || value <= 0) fail(`${name} must be a positive integer`); return value; } function rev(ref) { try { const out = execFileSync("git", [`--git-dir=${repoPath}`, "rev-parse", "--verify", `${ref}^{commit}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); return sha(out) ? out : null; } catch { return null; } } function durationSeconds(start, finish) { if (!start || !finish) return null; const value = (Date.parse(finish) - Date.parse(start)) / 1000; return Number.isFinite(value) && value >= 0 ? Math.round(value * 10) / 10 : null; } function notConfigured(name) { return { ok: false, reason: `${name}-not-configured` }; } function str(value) { return typeof value === "string" && value.length > 0 ? value : null; } function parseJson(value) { try { const parsed = JSON.parse(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null; } catch { return null; } } function numberOrNull(value) { return typeof value === "number" && Number.isFinite(value) ? value : null; } function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; } function uniqueStrings(value) { return [...new Set(value.filter((item) => typeof item === "string" && item.length > 0))]; } function sha(value) { return typeof value === "string" && /^[0-9a-f]{40}$/iu.test(value); } function shortSha(value) { return sha(value) ? value.slice(0, 12) : null; } function shortFingerprint(value) { return typeof value === "string" && value.length >= 12 ? value.slice(0, 12) : null; } function shortText(value) { const text = String(value || "").replace(/\s+/gu, " ").trim(); return text.length <= 300 ? text : text.slice(0, 300); } function requiredEnv(name) { const value = process.env[name]; if (!value) fail(`${name} is required`); return value; } function requiredPositiveIntEnv(name) { const value = requiredEnv(name); const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) fail(`${name} must be a positive integer`); return parsed; } function fail(message) { console.error(message); process.exit(1); }