fix: show branch follower drilldown gates
This commit is contained in:
@@ -97,6 +97,94 @@ function compactStateText(text) {
|
||||
timings: compactTimings(state.timings),
|
||||
warnings: arrayStrings(state.warnings).slice(0, 6),
|
||||
stateFormat: stringOrNull(state.stateFormat),
|
||||
command: compactCommand(state.command),
|
||||
};
|
||||
}
|
||||
|
||||
function compactCommand(command) {
|
||||
const value = recordOrNull(command);
|
||||
if (value === null) return null;
|
||||
return {
|
||||
mode: stringOrNull(value.mode) ?? stringOrNull(value.status),
|
||||
namespace: stringOrNull(value.namespace),
|
||||
pipelineRun: stringOrNull(value.pipelineRun),
|
||||
sourceCommit: stringOrNull(value.sourceCommit),
|
||||
sourceStageRef: stringOrNull(value.sourceStageRef),
|
||||
wait: value.wait === true ? true : null,
|
||||
pipelineRunCompleted: value.pipelineRunCompleted === true ? true : null,
|
||||
stillRunning: value.stillRunning === true ? true : null,
|
||||
closeout: compactCloseout(value.closeout),
|
||||
payload: compactNativePayload(value.payload),
|
||||
exitCode: numberOrNull(value.exitCode),
|
||||
timedOut: value.timedOut === true,
|
||||
statusAuthority: stringOrNull(value.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactCloseout(closeout) {
|
||||
const value = recordOrNull(closeout);
|
||||
if (value === null) return null;
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
completed: value.completed === true,
|
||||
timedOut: value.timedOut === true,
|
||||
polls: numberOrNull(value.polls),
|
||||
elapsedMs: numberOrNull(value.elapsedMs),
|
||||
summary: compactNativePayload(value.summary),
|
||||
statusAuthority: stringOrNull(value.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
taskRuns: compactTaskRuns(value.taskRuns),
|
||||
planArtifacts: compactPlanArtifacts(value.planArtifacts),
|
||||
argo: recordOrNull(value.argo),
|
||||
runtime: recordOrNull(value.runtime),
|
||||
errors: arrayStrings(value.errors).slice(0, 5),
|
||||
statusAuthority: stringOrNull(value.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactTaskRuns(taskRuns) {
|
||||
const value = recordOrNull(taskRuns);
|
||||
if (value === null) return null;
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
count: numberOrNull(value.count),
|
||||
succeededCount: numberOrNull(value.succeededCount),
|
||||
failedCount: numberOrNull(value.failedCount),
|
||||
activeCount: numberOrNull(value.activeCount),
|
||||
performance: recordOrNull(value.performance),
|
||||
items: arrayRecords(value.items).slice(0, 16),
|
||||
};
|
||||
}
|
||||
|
||||
function compactPlanArtifacts(planArtifacts) {
|
||||
const value = recordOrNull(planArtifacts);
|
||||
if (value === null) return null;
|
||||
return {
|
||||
ok: value.ok === true,
|
||||
pipelineRun: stringOrNull(value.pipelineRun),
|
||||
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),
|
||||
buildSkippedCount: numberOrNull(value.buildSkippedCount),
|
||||
summary: stringOrNull(value.summary),
|
||||
disclosure: stringOrNull(value.disclosure),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+1
-22
@@ -22,6 +22,7 @@ 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 { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
|
||||
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
|
||||
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
|
||||
@@ -2951,28 +2952,6 @@ function renderCleanupStateHuman(payload: Record<string, unknown>): string {
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
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);
|
||||
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 ?? "-"]],
|
||||
),
|
||||
"",
|
||||
].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)) : [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user