fix: show branch follower drilldown gates
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower drill-down rendering.
|
||||
// Responsibility: bounded human summaries for branch-follower events/logs gates.
|
||||
|
||||
export function renderDrillDownHuman(payload: Record<string, unknown>): string {
|
||||
if (payload.follower === undefined) {
|
||||
const followers = arrayRecords(payload.followers);
|
||||
return [
|
||||
`CI/CD BRANCH-FOLLOWER ${String(payload.action ?? "drill-down").toUpperCase()}`,
|
||||
"",
|
||||
table(["FOLLOWER", "ADAPTER", "STATUS_AUTHORITY"], followers.map((item) => [item.id, item.adapter, item.statusAuthority ?? "k8s-native"])),
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
const summary = asOptionalRecord(payload.summary);
|
||||
const native = asOptionalRecord(payload.native);
|
||||
const gateRows = nativeGateRows(native);
|
||||
return [
|
||||
`CI/CD BRANCH-FOLLOWER ${String(payload.action ?? "drill-down").toUpperCase()} (${payload.ok === false ? "failed" : "ok"})`,
|
||||
"",
|
||||
table(
|
||||
["FOLLOWER", "ADAPTER", "AUTHORITY", "PHASE", "OBSERVED", "TARGET", "PIPELINERUN", "MESSAGE"],
|
||||
[[payload.follower, payload.adapter ?? "-", payload.statusAuthority ?? "k8s-native", summary?.phase ?? "-", shortSha(stringOrNull(summary?.observedSha)), shortSha(stringOrNull(summary?.targetSha)), summary?.pipelineRun ?? "-", summary?.message ?? "-"]],
|
||||
),
|
||||
gateRows.length === 0 ? "" : `\nGATES\n${table(["GATE", "STATUS", "DETAIL", "OBJECT"], gateRows)}`,
|
||||
"",
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function nativeGateRows(native: Record<string, unknown> | null): unknown[][] {
|
||||
if (native === null) return [];
|
||||
const rows: unknown[][] = [];
|
||||
const gitMirror = asOptionalRecord(native.gitMirror);
|
||||
if (gitMirror !== null) {
|
||||
const hasGitops = stringOrNull(gitMirror.gitopsBranch) !== null;
|
||||
const status = gitMirror.pendingFlush === true
|
||||
? "pending-flush"
|
||||
: hasGitops
|
||||
? gitMirror.githubInSync === true && gitMirror.sourceSnapshotReady === true ? "ready" : "not-ready"
|
||||
: gitMirror.sourceSnapshotReady === true ? "source-ready" : "source-not-ready";
|
||||
rows.push(["git-mirror", status, `${shortSha(stringOrNull(gitMirror.localSource))}/${shortSha(stringOrNull(gitMirror.githubSource))}`, stringOrNull(gitMirror.gitopsBranch) ?? stringOrNull(gitMirror.sourceBranch) ?? "-"]);
|
||||
}
|
||||
const tekton = asOptionalRecord(native.tekton);
|
||||
if (tekton !== null) {
|
||||
const status = tekton.succeeded === true ? "succeeded" : tekton.succeeded === false ? `failed:${stringOrNull(tekton.reason) ?? "unknown"}` : "running";
|
||||
const duration = numberOrNull(tekton.durationSeconds);
|
||||
rows.push(["tekton", status, duration === null ? stringOrNull(tekton.reason) ?? "-" : `${duration}s`, stringOrNull(tekton.name) ?? "-"]);
|
||||
}
|
||||
const argo = asOptionalRecord(native.argo);
|
||||
if (argo !== null) {
|
||||
rows.push(["argo", `${stringOrNull(argo.syncStatus) ?? "unknown"}/${stringOrNull(argo.healthStatus) ?? "unknown"}`, shortSha(stringOrNull(argo.revision)), stringOrNull(argo.name) ?? "-"]);
|
||||
}
|
||||
const runtime = asOptionalRecord(native.runtime);
|
||||
if (runtime !== null) {
|
||||
const status = runtime.ready === true ? (runtime.aligned === true ? "ready/aligned" : "ready/stale") : "not-ready";
|
||||
rows.push(["runtime", status, `${shortSha(stringOrNull(runtime.targetSha))}/${shortSha(stringOrNull(runtime.expectedSha))}`, stringOrNull(runtime.namespace) ?? "-"]);
|
||||
}
|
||||
for (const error of arrayTextItems(native.errors).slice(0, 5)) rows.push(["error", "present", error, "-"]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
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 arrayTextItems(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map(String) : [];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user