fix: keep branch follower output bounded

This commit is contained in:
Codex
2026-07-03 18:48:40 +00:00
parent 668d8317a3
commit 4df3d5d8bd
4 changed files with 334 additions and 252 deletions
@@ -38,6 +38,10 @@ When a repeated runtime pitfall or visibility defect is found during branch-foll
Bounded JSON means the operator-facing `--json` payload must remain below the YAML-configured stdout limit in normal successful debug cases. Do not duplicate the same evidence as full top-level objects, compact `targetResult`, full `stateAfter` and target stdout tail at the same time; choose one compact representation by default and put full payload/log drill-down behind explicit commands.
Target-side state summaries used by `status`, `events`, `logs` and `debug-step state-read` must also remain below the transport stdout limit. When exposing stored native payloads, return gate summaries only: git-mirror, Tekton, Argo, runtime and short errors. Do not include full source objects, TaskRun item arrays, plan-artifact arrays, report payloads or full command payloads in the default state summary; a truncated state summary is a visibility defect because the operator can no longer parse the follower state.
`scripts/src/cicd.ts` should stay a thin branch-follower route/orchestration entry, not a catch-all implementation file. Rendering, debug steps, controller manifests, native K8s helpers, adapter-specific trigger/status logic and large data compactors must live in responsibility-specific modules before `cicd.ts` approaches the 3000-line hard split point.
`status-read`, `events`, `logs` and debug summaries must expose compact closeout gate details when a follower is not aligned: git-mirror readiness, Tekton PipelineRun condition, Argo sync/health, runtime target sha/readiness and short errors. Repeating only phase/observed/target/message is a visibility defect and must be fixed before further rollout tuning.
Stage timing rows must not label optional gates as `not-ready` when they are not part of that follower's closeout contract. For sentinel-like followers without a GitOps branch flush gate, git-mirror source snapshot readiness should render as source-ready/ready, while missing GitOps `githubInSync` remains `-`/not-applicable instead of a failure-looking state.
+62 -11
View File
@@ -141,20 +141,72 @@ function compactNativePayload(payload) {
const value = recordOrNull(payload);
if (value === null) return null;
return {
source: recordOrNull(value.source),
sourceSync: recordOrNull(value.sourceSync),
gitMirror: recordOrNull(value.gitMirror),
tekton: recordOrNull(value.tekton),
gitMirror: compactGitMirror(value.gitMirror),
tekton: compactTekton(value.tekton),
taskRuns: compactTaskRuns(value.taskRuns),
planArtifacts: compactPlanArtifacts(value.planArtifacts),
argo: recordOrNull(value.argo),
runtime: recordOrNull(value.runtime),
argo: compactArgo(value.argo),
runtime: compactRuntime(value.runtime),
errors: arrayStrings(value.errors).slice(0, 5),
statusAuthority: stringOrNull(value.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
function compactGitMirror(gitMirror) {
const value = recordOrNull(gitMirror);
if (value === null) return null;
return {
ok: value.ok === true,
sourceSnapshotReady: value.sourceSnapshotReady === true,
pendingFlush: value.pendingFlush === true,
githubInSync: value.githubInSync === true,
sourceBranch: stringOrNull(value.sourceBranch),
gitopsBranch: stringOrNull(value.gitopsBranch),
localSource: stringOrNull(value.localSource),
githubSource: stringOrNull(value.githubSource),
localGitops: stringOrNull(value.localGitops),
githubGitops: stringOrNull(value.githubGitops),
};
}
function compactTekton(tekton) {
const value = recordOrNull(tekton);
if (value === null) return null;
return {
name: stringOrNull(value.name),
succeeded: value.succeeded === true ? true : value.succeeded === false ? false : null,
reason: stringOrNull(value.reason),
startTime: stringOrNull(value.startTime),
completionTime: stringOrNull(value.completionTime),
durationSeconds: numberOrNull(value.durationSeconds),
};
}
function compactArgo(argo) {
const value = recordOrNull(argo);
if (value === null) return null;
return {
name: stringOrNull(value.name),
syncStatus: stringOrNull(value.syncStatus),
healthStatus: stringOrNull(value.healthStatus),
revision: stringOrNull(value.revision),
ready: value.ready === true,
};
}
function compactRuntime(runtime) {
const value = recordOrNull(runtime);
if (value === null) return null;
return {
namespace: stringOrNull(value.namespace),
ready: value.ready === true,
targetSha: stringOrNull(value.targetSha),
expectedSha: stringOrNull(value.expectedSha),
aligned: value.aligned === true ? true : value.aligned === false ? false : null,
};
}
function compactTaskRuns(taskRuns) {
const value = recordOrNull(taskRuns);
if (value === null) return null;
@@ -165,7 +217,6 @@ function compactTaskRuns(taskRuns) {
failedCount: numberOrNull(value.failedCount),
activeCount: numberOrNull(value.activeCount),
performance: recordOrNull(value.performance),
items: arrayRecords(value.items).slice(0, 16),
};
}
@@ -178,10 +229,10 @@ function compactPlanArtifacts(planArtifacts) {
eventFound: value.eventFound === true,
degradedReason: stringOrNull(value.degradedReason),
sourceCommitId: stringOrNull(value.sourceCommitId),
affectedServices: arrayStrings(value.affectedServices).slice(0, 40),
rolloutServices: arrayStrings(value.rolloutServices).slice(0, 40),
buildServices: arrayStrings(value.buildServices).slice(0, 40),
reusedServices: arrayStrings(value.reusedServices).slice(0, 40),
affectedServicesCount: arrayStrings(value.affectedServices).length,
rolloutServicesCount: arrayStrings(value.rolloutServices).length,
buildServicesCount: arrayStrings(value.buildServices).length,
reusedServicesCount: arrayStrings(value.reusedServices).length,
buildSkippedCount: numberOrNull(value.buildSkippedCount),
summary: stringOrNull(value.summary),
disclosure: stringOrNull(value.disclosure),
+266
View File
@@ -0,0 +1,266 @@
// SPEC: PJ2026-01060703 CI/CD branch follower rendering.
// Responsibility: machine and human output rendering for cicd branch-follower.
import type { RenderedCliResult } from "./output";
import type { ParsedOptions } from "./cicd-types";
import { renderDebugStepHuman } from "./cicd-debug";
import { renderDrillDownHuman } from "./cicd-drilldown-render";
export function renderResult(command: string, payload: Record<string, unknown>, options: ParsedOptions): RenderedCliResult {
const ok = payload.ok !== false;
if (options.output === "json") return renderMachine(command, payload, "json", ok);
if (options.output === "yaml") return renderMachine(command, payload, "yaml", ok);
return rendered(ok, command, renderHuman(command, payload, options));
}
export function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult {
return rendered(ok, command, mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`, mode === "json" ? "application/json" : "application/yaml");
}
function rendered(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult {
return { ok, command, renderedText, contentType };
}
function renderHuman(command: string, payload: Record<string, unknown>, options: ParsedOptions): string {
if (command.endsWith(" plan")) return renderPlanHuman(payload);
if (command.endsWith(" apply")) return renderApplyHuman(payload);
if (command.endsWith(" status")) return renderStatusHuman(payload, options);
if (command.endsWith(" run-once")) return renderRunOnceHuman(payload);
if (command.endsWith(" debug-step")) return renderDebugStepHuman(payload);
if (command.endsWith(" cleanup-state")) return renderCleanupStateHuman(payload);
if (command.endsWith(" events") || command.endsWith(" logs")) return renderDrillDownHuman(payload);
return `${JSON.stringify(payload, null, 2)}\n`;
}
function renderPlanHuman(payload: Record<string, unknown>): string {
const followers = arrayRecords(payload.followers);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
const budgets = asOptionalRecord(item.budgets);
return [
item.id,
item.enabled,
item.adapter,
`${source?.repository ?? "-"}@${source?.branch ?? "-"}`,
`${target?.node ?? "-"}/${target?.lane ?? "-"}`,
budgets?.endToEndSeconds ?? "-",
arrayRecords(item.configRefGraph).length,
arrayText(item.closeoutChecks),
];
});
const next = asOptionalRecord(payload.next);
return [
`CI/CD BRANCH-FOLLOWER PLAN (${payload.ok === false ? "blocked" : "ok"})`,
"",
table(["FOLLOWER", "ENABLED", "ADAPTER", "SOURCE", "TARGET", "BUDGET", "REFS", "CHECKS"], rows),
"",
"SOURCE AUTHORITY",
`hostWorktreeAuthority=${payload.hostWorktreeAuthority === true ? "true" : "false"} mode=${asOptionalRecord(payload.sourceAuthority)?.mode ?? "-"} resolver=${asOptionalRecord(payload.sourceAuthority)?.resolver ?? "-"}`,
"",
"NEXT",
`apply: ${next?.apply ?? "-"}`,
`status: ${next?.status ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].join("\n");
}
function renderApplyHuman(payload: Record<string, unknown>): string {
const controller = asOptionalRecord(payload.controller);
const command = asOptionalRecord(payload.command);
const next = asOptionalRecord(payload.next);
return [
`CI/CD BRANCH-FOLLOWER APPLY (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(
["NAMESPACE", "ROUTE", "DEPLOYMENT", "STATE_CM", "LEASE", "HOST_WORKTREE"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", controller?.stateConfigMapName ?? "-", controller?.leaseName ?? "-", controller?.hostWorktreeMounted === true ? "mounted" : "not-mounted"]],
),
"",
table(["OBJECTS", "MANIFEST_SHA", "EXIT", "TIMED_OUT"], [[arrayRecords(payload.objects).length, shortSha(stringOrNull(payload.manifestSha256)), command?.exitCode ?? "-", command?.timedOut ?? "-"]]),
command?.stderrTail ? `\nSTDERR\n${command.stderrTail}` : "",
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOptions): string {
const controller = asOptionalRecord(payload.controller);
const followers = arrayRecords(payload.followers);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
const budgets = asOptionalRecord(item.budgetSource);
return [
item.id,
item.phase,
item.adapter,
`${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`,
shortSha(stringOrNull(target?.targetSha)),
shortSha(stringOrNull(item.lastTriggeredSha)),
shortSha(stringOrNull(item.lastSucceededSha)),
item.pipelineRun ?? item.inFlightJob ?? "-",
budgets?.endToEndSeconds ?? "-",
item.message ?? "-",
];
});
const next = asOptionalRecord(payload.next);
const errors = Array.isArray(payload.errors) ? payload.errors : [];
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
return [
`CI/CD BRANCH-FOLLOWER STATUS (${payload.ok === false ? "degraded" : "ok"})`,
"",
table(
["CTRL_NS", "ROUTE", "DEPLOY", "READY", "PODS", "STATE_CM", "LEASE"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", `${controller?.availableReplicas ?? 0}/${controller?.replicas ?? 0}`, controller?.pods ?? "-", controller?.stateConfigMapPresent === true ? "present" : "missing", controller?.leaseHolder ?? "-"]],
),
"",
table(["FOLLOWER", "PHASE", "ADAPTER", "OBSERVED", "TARGET", "TRIGGERED", "SUCCEEDED", "IN_FLIGHT", "BUDGET", "MESSAGE"], rows),
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
"",
"NEXT",
`live-status: ${next?.liveStatus ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function renderRunOnceHuman(payload: Record<string, unknown>): string {
const followers = arrayRecords(payload.followers);
const stateWrites = arrayRecords(payload.stateWrites);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
return [
item.id,
item.phase,
`${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`,
shortSha(stringOrNull(target?.targetSha)),
shortSha(stringOrNull(item.lastTriggeredSha)),
item.inFlightJob ?? "-",
item.decision ?? "-",
];
});
const next = asOptionalRecord(payload.next);
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
const writeRows = stateWrites.map((item) => [
item.follower,
item.ok === true ? "ok" : "failed",
item.beforeResourceVersion ?? "-",
item.afterResourceVersion ?? "-",
item.preservedTiming === true ? "yes" : "no",
item.exitCode ?? "-",
item.message ?? "-",
]);
return [
`CI/CD BRANCH-FOLLOWER RUN-ONCE (${payload.ok === false ? "blocked" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(["FOLLOWER", "PHASE", "OBSERVED", "TARGET", "TRIGGERED", "IN_FLIGHT", "DECISION"], rows),
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
writeRows.length === 0 ? "" : `\nSTATE WRITES\n${table(["FOLLOWER", "STATUS", "BEFORE_RV", "AFTER_RV", "PRESERVED", "EXIT", "MESSAGE"], writeRows)}`,
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`live-status: ${next?.liveStatus ?? "-"}`,
"",
].join("\n");
}
function renderCleanupStateHuman(payload: Record<string, unknown>): string {
const controller = asOptionalRecord(payload.controller);
const command = asOptionalRecord(payload.command);
const followers = arrayRecords(payload.followers);
const next = asOptionalRecord(payload.next);
const rows = followers.map((item) => [
item.id,
item.statePresent === true ? "present" : "missing",
item.cleanup ?? "-",
]);
return [
`CI/CD BRANCH-FOLLOWER CLEANUP-STATE (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(
["NAMESPACE", "ROUTE", "STATE_CM", "STATE_CM_PRESENT"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.stateConfigMapName ?? "-", payload.stateConfigMapPresent === true ? "present" : "missing"]],
),
"",
table(["FOLLOWER", "STATE", "CLEANUP"], rows),
command === null ? "" : `\nPATCH\nexit=${command.exitCode ?? "-"} timedOut=${command.timedOut ?? "-"}`,
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`run-once: ${next?.runOnce ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function timingRowsForFollower(item: Record<string, unknown>): unknown[][] {
const timings = asOptionalRecord(item.timings);
if (timings === null) return [];
const budget = numberOrNull(timings.budgetSeconds);
const rows: unknown[][] = [[
item.id,
"total",
stringOrNull(timings.totalStatus) ?? "unknown",
formatSeconds(numberOrNull(timings.totalSeconds)),
formatSeconds(budget),
[stringOrNull(timings.totalSource), shortSha(stringOrNull(timings.sourceCommit))].filter((value) => value !== null && value !== "-").join(":") || "-",
]];
for (const stage of arrayRecords(timings.stages)) {
rows.push([
item.id,
stage.stage,
stage.status,
formatSeconds(numberOrNull(stage.seconds)),
formatSeconds(numberOrNull(stage.budgetSeconds)),
stringOrNull(stage.object) ?? "-",
]);
}
return rows;
}
function formatSeconds(value: number | null): string {
return value === null ? "-" : `${value}s`;
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
}
function arrayText(value: unknown): string {
return Array.isArray(value) ? value.map(String).join(",") : "-";
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function shortSha(value: string | null): string {
if (value === null) return "-";
return value.length > 12 ? value.slice(0, 12) : value;
}
function table(headers: readonly string[], rows: readonly (readonly unknown[])[]): string {
const normalized = rows.map((row) => headers.map((_, index) => cell(row[index])));
const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => row[index]?.length ?? 0)));
const format = (row: readonly string[]) => row.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
return [format(headers), format(headers.map((header) => "-".repeat(header.length))), ...normalized.map(format)].join("\n");
}
function cell(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
const text = String(value).replace(/\s+/gu, " ");
return text.length > 96 ? `${text.slice(0, 93)}...` : text;
}
+2 -241
View File
@@ -7,6 +7,7 @@ import { repoRoot, rootPath, type UniDeskConfig } from "./config";
import { runCommand, type CommandResult } from "./command";
import { startJob } from "./jobs";
import type { RenderedCliResult } from "./output";
import { renderMachine, renderResult } from "./cicd-render";
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
import { agentRunImageArtifact, renderAgentRunGitopsFiles } from "./agentrun-manifests";
import { agentRunPipelineRunName, resolveAgentRunLaneTarget } from "./agentrun-lanes";
@@ -21,8 +22,7 @@ import { sentinelPipelineRunName } from "./hwlab-node-web-sentinel-cicd-shared";
import { transPath } from "./hwlab-node/runtime-common";
import { configRefGraph, resolveConfigRefString } from "./ops/config-refs";
import { renderControllerManifests, renderControllerReconcileJob, waitForJobShell } from "./cicd-controller-render";
import { buildDebugStep, renderDebugStepHuman } from "./cicd-debug";
import { renderDrillDownHuman } from "./cicd-drilldown-render";
import { buildDebugStep } from "./cicd-debug";
import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
@@ -2730,245 +2730,6 @@ function commandLabel(options: ParsedOptions): string {
return `cicd branch-follower ${options.action}`;
}
function renderResult(command: string, payload: Record<string, unknown>, options: ParsedOptions): RenderedCliResult {
const ok = payload.ok !== false;
if (options.output === "json") return renderMachine(command, payload, "json", ok);
if (options.output === "yaml") return renderMachine(command, payload, "yaml", ok);
return rendered(ok, command, renderHuman(command, payload, options));
}
function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult {
return rendered(ok, command, mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`, mode === "json" ? "application/json" : "application/yaml");
}
function rendered(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult {
return { ok, command, renderedText, contentType };
}
function renderHuman(command: string, payload: Record<string, unknown>, options: ParsedOptions): string {
if (command.endsWith(" plan")) return renderPlanHuman(payload);
if (command.endsWith(" apply")) return renderApplyHuman(payload);
if (command.endsWith(" status")) return renderStatusHuman(payload, options);
if (command.endsWith(" run-once")) return renderRunOnceHuman(payload);
if (command.endsWith(" debug-step")) return renderDebugStepHuman(payload);
if (command.endsWith(" cleanup-state")) return renderCleanupStateHuman(payload);
if (command.endsWith(" events") || command.endsWith(" logs")) return renderDrillDownHuman(payload);
return `${JSON.stringify(payload, null, 2)}\n`;
}
function renderPlanHuman(payload: Record<string, unknown>): string {
const followers = arrayRecords(payload.followers);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
const budgets = asOptionalRecord(item.budgets);
return [
item.id,
item.enabled,
item.adapter,
`${source?.repository ?? "-"}@${source?.branch ?? "-"}`,
`${target?.node ?? "-"}/${target?.lane ?? "-"}`,
budgets?.endToEndSeconds ?? "-",
arrayRecords(item.configRefGraph).length,
arrayText(item.closeoutChecks),
];
});
const next = asOptionalRecord(payload.next);
return [
`CI/CD BRANCH-FOLLOWER PLAN (${payload.ok === false ? "blocked" : "ok"})`,
"",
table(["FOLLOWER", "ENABLED", "ADAPTER", "SOURCE", "TARGET", "BUDGET", "REFS", "CHECKS"], rows),
"",
"SOURCE AUTHORITY",
`hostWorktreeAuthority=${payload.hostWorktreeAuthority === true ? "true" : "false"} mode=${asOptionalRecord(payload.sourceAuthority)?.mode ?? "-"} resolver=${asOptionalRecord(payload.sourceAuthority)?.resolver ?? "-"}`,
"",
"NEXT",
`apply: ${next?.apply ?? "-"}`,
`status: ${next?.status ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].join("\n");
}
function renderApplyHuman(payload: Record<string, unknown>): string {
const controller = asOptionalRecord(payload.controller);
const command = asOptionalRecord(payload.command);
const next = asOptionalRecord(payload.next);
return [
`CI/CD BRANCH-FOLLOWER APPLY (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(
["NAMESPACE", "ROUTE", "DEPLOYMENT", "STATE_CM", "LEASE", "HOST_WORKTREE"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", controller?.stateConfigMapName ?? "-", controller?.leaseName ?? "-", controller?.hostWorktreeMounted === true ? "mounted" : "not-mounted"]],
),
"",
table(["OBJECTS", "MANIFEST_SHA", "EXIT", "TIMED_OUT"], [[arrayRecords(payload.objects).length, shortSha(stringOrNull(payload.manifestSha256)), command?.exitCode ?? "-", command?.timedOut ?? "-"]]),
command?.stderrTail ? `\nSTDERR\n${command.stderrTail}` : "",
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOptions): string {
const controller = asOptionalRecord(payload.controller);
const followers = arrayRecords(payload.followers);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
const budgets = asOptionalRecord(item.budgetSource);
return [
item.id,
item.phase,
item.adapter,
`${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`,
shortSha(stringOrNull(target?.targetSha)),
shortSha(stringOrNull(item.lastTriggeredSha)),
shortSha(stringOrNull(item.lastSucceededSha)),
item.pipelineRun ?? item.inFlightJob ?? "-",
budgets?.endToEndSeconds ?? "-",
item.message ?? "-",
];
});
const next = asOptionalRecord(payload.next);
const errors = Array.isArray(payload.errors) ? payload.errors : [];
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
return [
`CI/CD BRANCH-FOLLOWER STATUS (${payload.ok === false ? "degraded" : "ok"})`,
"",
table(
["CTRL_NS", "ROUTE", "DEPLOY", "READY", "PODS", "STATE_CM", "LEASE"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.deploymentName ?? "-", `${controller?.availableReplicas ?? 0}/${controller?.replicas ?? 0}`, controller?.pods ?? "-", controller?.stateConfigMapPresent === true ? "present" : "missing", controller?.leaseHolder ?? "-"]],
),
"",
table(["FOLLOWER", "PHASE", "ADAPTER", "OBSERVED", "TARGET", "TRIGGERED", "SUCCEEDED", "IN_FLIGHT", "BUDGET", "MESSAGE"], rows),
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
"",
"NEXT",
`live-status: ${next?.liveStatus ?? "-"}`,
`dry-run: ${next?.dryRun ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function renderRunOnceHuman(payload: Record<string, unknown>): string {
const followers = arrayRecords(payload.followers);
const stateWrites = arrayRecords(payload.stateWrites);
const rows = followers.map((item) => {
const source = asOptionalRecord(item.source);
const target = asOptionalRecord(item.target);
return [
item.id,
item.phase,
`${source?.branch ?? "-"}:${shortSha(stringOrNull(source?.observedSha))}`,
shortSha(stringOrNull(target?.targetSha)),
shortSha(stringOrNull(item.lastTriggeredSha)),
item.inFlightJob ?? "-",
item.decision ?? "-",
];
});
const next = asOptionalRecord(payload.next);
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
const writeRows = stateWrites.map((item) => [
item.follower,
item.ok === true ? "ok" : "failed",
item.beforeResourceVersion ?? "-",
item.afterResourceVersion ?? "-",
item.preservedTiming === true ? "yes" : "no",
item.exitCode ?? "-",
item.message ?? "-",
]);
return [
`CI/CD BRANCH-FOLLOWER RUN-ONCE (${payload.ok === false ? "blocked" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(["FOLLOWER", "PHASE", "OBSERVED", "TARGET", "TRIGGERED", "IN_FLIGHT", "DECISION"], rows),
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
writeRows.length === 0 ? "" : `\nSTATE WRITES\n${table(["FOLLOWER", "STATUS", "BEFORE_RV", "AFTER_RV", "PRESERVED", "EXIT", "MESSAGE"], writeRows)}`,
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`live-status: ${next?.liveStatus ?? "-"}`,
"",
].join("\n");
}
function timingRowsForFollower(item: Record<string, unknown>): unknown[][] {
const timings = asOptionalRecord(item.timings);
if (timings === null) return [];
const budget = numberOrNull(timings.budgetSeconds);
const rows: unknown[][] = [[
item.id,
"total",
stringOrNull(timings.totalStatus) ?? "unknown",
formatSeconds(numberOrNull(timings.totalSeconds)),
formatSeconds(budget),
[stringOrNull(timings.totalSource), shortSha(stringOrNull(timings.sourceCommit))].filter((value) => value !== null && value !== "-").join(":") || "-",
]];
for (const stage of arrayRecords(timings.stages)) {
rows.push([
item.id,
stage.stage,
stage.status,
formatSeconds(numberOrNull(stage.seconds)),
formatSeconds(numberOrNull(stage.budgetSeconds)),
stringOrNull(stage.object) ?? "-",
]);
}
return rows;
}
function formatSeconds(value: number | null): string {
return value === null ? "-" : `${value}s`;
}
function renderCleanupStateHuman(payload: Record<string, unknown>): string {
const controller = asOptionalRecord(payload.controller);
const command = asOptionalRecord(payload.command);
const followers = arrayRecords(payload.followers);
const next = asOptionalRecord(payload.next);
const rows = followers.map((item) => [
item.id,
item.statePresent === true ? "present" : "missing",
item.cleanup ?? "-",
]);
return [
`CI/CD BRANCH-FOLLOWER CLEANUP-STATE (${payload.ok === false ? "failed" : payload.dryRun === true ? "dry-run" : "ok"})`,
"",
table(
["NAMESPACE", "ROUTE", "STATE_CM", "STATE_CM_PRESENT"],
[[controller?.namespace ?? "-", controller?.route ?? "-", controller?.stateConfigMapName ?? "-", payload.stateConfigMapPresent === true ? "present" : "missing"]],
),
"",
table(["FOLLOWER", "STATE", "CLEANUP"], rows),
command === null ? "" : `\nPATCH\nexit=${command.exitCode ?? "-"} timedOut=${command.timedOut ?? "-"}`,
"",
"NEXT",
`status: ${next?.status ?? "-"}`,
`run-once: ${next?.runOnce ?? "-"}`,
"",
].filter((line) => line !== "").join("\n");
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
}
function arrayText(value: unknown): string {
return Array.isArray(value) ? value.map(String).join(",") : "-";
}
function table(headers: readonly string[], rows: readonly (readonly unknown[])[]): string {
const normalized = rows.map((row) => headers.map((_, index) => cell(row[index])));
const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => row[index]?.length ?? 0)));
const format = (row: readonly string[]) => row.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
return [format(headers), format(headers.map((header) => "-".repeat(header.length))), ...normalized.map(format)].join("\n");
}
function cell(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
const text = String(value).replace(/\s+/gu, " ");
return text.length > 96 ? `${text.slice(0, 93)}...` : text;
}