Merge pull request #1508 from pikasTech/fix/1499-hwlab-pipeline-evidence

feat: add bounded branch-follower pipeline evidence
This commit is contained in:
Lyon
2026-07-04 10:56:04 +08:00
committed by GitHub
13 changed files with 287 additions and 3 deletions
+46 -1
View File
@@ -55,7 +55,10 @@ if (key === "pipelineRun") {
apiVersion: input.apiVersion,
kind: input.kind,
metadata: metadata(input),
spec: { params: Array.isArray(input?.spec?.params) ? input.spec.params : [] },
spec: {
pipelineRef: { name: input?.spec?.pipelineRef?.name || null },
params: Array.isArray(input?.spec?.params) ? input.spec.params : [],
},
status: {
conditions: Array.isArray(input?.status?.conditions) ? input.status.conditions : [],
startTime: input?.status?.startTime || null,
@@ -65,6 +68,48 @@ if (key === "pipelineRun") {
reason: succeeded?.reason || null,
},
};
} else if (key === "pipeline") {
const tasks = Array.isArray(input?.spec?.tasks) ? input.spec.tasks : [];
const runtimeReady = tasks.find((item) => item?.name === "runtime-ready") || null;
const gitopsPromote = tasks.find((item) => item?.name === "gitops-promote") || null;
const gitopsResults = Array.isArray(gitopsPromote?.taskSpec?.results) ? gitopsPromote.taskSpec.results : [];
output = {
apiVersion: input.apiVersion,
kind: input.kind,
metadata: {
name: input?.metadata?.name || null,
namespace: input?.metadata?.namespace || null,
annotations: {
sourceConfig: input?.metadata?.annotations?.["hwlab.pikastech.local/source-config"] || null,
ciContract: input?.metadata?.annotations?.["hwlab.pikastech.local/ci-contract"] || null,
policy: input?.metadata?.annotations?.["hwlab.pikastech.local/policy"] || null,
},
},
spec: {
taskCount: tasks.length,
runtimeReadyTask: {
present: runtimeReady !== null,
name: runtimeReady?.name || null,
runAfter: Array.isArray(runtimeReady?.runAfter) ? runtimeReady.runAfter.slice(0, 6) : [],
when: Array.isArray(runtimeReady?.when)
? runtimeReady.when.slice(0, 4).map((item) => ({
input: item?.input || null,
operator: item?.operator || null,
values: Array.isArray(item?.values) ? item.values.slice(0, 6) : [],
}))
: [],
},
gitopsPromoteTask: {
present: gitopsPromote !== null,
name: gitopsPromote?.name || null,
resultNames: gitopsResults
.map((item) => item?.name || null)
.filter((item) => typeof item === "string")
.slice(0, 8),
runtimeReadyRequiredResult: gitopsResults.some((item) => item?.name === "runtime-ready-required"),
},
},
};
} else if (key === "taskRuns") {
const items = (Array.isArray(input?.items) ? input.items : []).map((item) => {
const succeeded = condition(item, "Succeeded");
+25
View File
@@ -121,6 +121,7 @@ while (Date.now() <= deadline) {
const complete = condition(latest, "Complete");
const failed = condition(latest, "Failed");
const logs = await logsTail();
const summary = parseLastJsonSummary(logs);
const timedOut = !complete && !failed;
const output = {
ok: Boolean(complete) && !timedOut,
@@ -137,6 +138,7 @@ const output = {
conditionReason: complete?.reason || failed?.reason || null,
conditionMessage: complete?.message || failed?.message || null,
logsTail: logs || null,
summary,
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
valuesRedacted: true,
@@ -155,3 +157,26 @@ function requiredPositiveNumber(name) {
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
return value;
}
function parseLastJsonSummary(text) {
const lines = String(text || "").split(/\r?\n/u).map((item) => item.trim()).filter(Boolean);
for (let index = lines.length - 1; index >= 0; index -= 1) {
try {
const parsed = JSON.parse(lines[index]);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return compactValue(parsed, 0);
} catch {
continue;
}
}
return null;
}
function compactValue(value, depth) {
if (typeof value === "string") return value.length <= 240 ? value : `${value.slice(0, 160)} ... ${value.slice(-60)}`;
if (typeof value !== "object" || value === null) return value;
if (Array.isArray(value)) return value.slice(0, 8).map((item) => compactValue(item, depth + 1));
if (depth >= 3) return "[bounded-object]";
const output = {};
for (const [key, child] of Object.entries(value).slice(0, 16)) output[key] = compactValue(child, depth + 1);
return output;
}
@@ -95,6 +95,10 @@ if [ -n "${source_commit}" ] && [ -n "${tekton_namespace}" ] && [ -n "${pipeline
sha12=$(printf '%s' "${source_commit}" | cut -c1-12)
pipeline_run="${pipeline_run_prefix}-${sha12}"
emit_kube_json pipelineRun "/apis/tekton.dev/v1/namespaces/${tekton_namespace}/pipelineruns/${pipeline_run}"
pipeline_ref="$(node -e "const fs=require('node:fs'); try { const value=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(value?.spec?.pipelineRef?.name || ''); } catch {}" "${tmpdir}/pipelineRun.raw" 2>/dev/null || true)"
if [ -n "${pipeline_ref}" ]; then
emit_kube_json pipeline "/apis/tekton.dev/v1/namespaces/${tekton_namespace}/pipelines/${pipeline_ref}"
fi
emit_kube_json taskRuns "/apis/tekton.dev/v1/namespaces/${tekton_namespace}/taskruns?labelSelector=tekton.dev%2FpipelineRun%3D${pipeline_run}"
emit_plan_artifacts "${tekton_namespace}" "${pipeline_run}"
fi
@@ -209,10 +209,12 @@ function compactNativePayload(payload) {
gitMirror: compactGitMirror(value.gitMirror),
reuseConfig: compactReuseConfig(value.reuseConfig),
tekton: compactTekton(value.tekton),
pipeline: compactPipeline(value.pipeline),
taskRuns: compactTaskRuns(value.taskRuns),
planArtifacts: compactPlanArtifacts(value.planArtifacts),
argo: compactArgo(value.argo),
runtime: compactRuntime(value.runtime),
refreshEvidence: compactRefreshEvidence(recordOrNull(recordOrNull(value.nativeCapabilities)?.controlPlaneRefresh)),
errors: arrayStrings(value.errors).slice(0, 5),
statusAuthority: stringOrNull(value.statusAuthority),
parsedDownstreamCliOutput: false,
@@ -264,6 +266,7 @@ function compactTekton(tekton) {
if (value === null) return null;
return {
name: stringOrNull(value.name),
pipelineRefName: stringOrNull(value.pipelineRefName),
succeeded: value.succeeded === true ? true : value.succeeded === false ? false : null,
reason: stringOrNull(value.reason),
startTime: stringOrNull(value.startTime),
@@ -272,6 +275,33 @@ function compactTekton(tekton) {
};
}
function compactPipeline(pipeline) {
const value = recordOrNull(pipeline);
if (value === null) return null;
return {
metadata: recordOrNull(value.metadata),
spec: recordOrNull(value.spec),
};
}
function compactRefreshEvidence(refresh) {
const value = recordOrNull(refresh);
const summary = recordOrNull(value?.summary);
if (value === null || summary === null) return null;
return {
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
status: stringOrNull(summary.status),
pipeline: stringOrNull(summary.pipeline),
sourceCommit: stringOrNull(summary.sourceCommit),
sourceStageRef: stringOrNull(summary.sourceStageRef),
elapsedMs: numberOrNull(summary.elapsedMs),
sourceAuthority: stringOrNull(summary.sourceAuthority),
statusAuthority: stringOrNull(summary.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
function compactArgo(argo) {
const value = recordOrNull(argo);
if (value === null) return null;
+17 -2
View File
@@ -25,6 +25,7 @@ import { renderControllerManifests, renderControllerReconcileJob, waitForJobShel
import { buildDebugStep } from "./cicd-debug";
import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
import { compactRefreshEvidence, followerEvidenceSummary } from "./cicd-evidence";
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
import { argoApplicationReady, nativeArgoSummary, nativeGitMirrorReady, nativeGitMirrorRequired, nativeGitMirrorSummary, nativePipelineRunSummary, nativeRuntimeSummary, pipelineRunSucceeded, runtimeTargetShaFromWorkloads, runtimeWorkloadsReady } from "./cicd-native-summary";
import { invalidRuntimeReuseConfig, missingRuntimeReuseConfig, parseRuntimeReuseConfig, RUNTIME_REUSE_CONFIG_PATH, runtimeReuseService, summarizeRuntimeReuseConfig, type RuntimeReuseConfig } from "./cicd-reuse-config";
@@ -634,6 +635,7 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed
async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
let k8s = readK8sState(registry, options);
const beforeRefreshStateByFollower = k8s.stateByFollower;
const wantsLive = options.live || (!options.noLive && Object.keys(k8s.stateByFollower).length === 0);
const refresh = wantsLive && !options.inCluster ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null;
if (refresh !== null) k8s = readK8sState(registry, options);
@@ -643,8 +645,9 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti
const detailedFollowers = options.followerId !== null || options.full;
for (const follower of selected) {
const stored = k8s.stateByFollower[follower.id] ?? {};
const fallbackStored = refresh === null ? {} : beforeRefreshStateByFollower[follower.id] ?? {};
const live = shouldLive && follower.enabled ? await readAdapterStatus(registry, follower, options) : null;
followers.push(mergeFollowerStatus(registry, follower, stored, live, shouldLive, detailedFollowers));
followers.push(mergeFollowerStatus(registry, follower, stored, live, shouldLive, detailedFollowers, fallbackStored));
}
return {
ok: k8s.ok && followers.every((item) => item.ok !== false),
@@ -665,6 +668,7 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti
async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
if (!options.inCluster) {
const before = readK8sState(registry, options);
const refresh = runControllerReconcileJob(registry, options, { dryRun: options.dryRun, wait: true, recordState: true });
const k8s = readK8sState(registry, options);
const selected = selectFollowers(registry, options, { includeDisabled: false });
@@ -678,7 +682,7 @@ async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions)
execution: "k8s-native-reconcile-job",
registry: registrySummary(registry),
job: refresh,
followers: selected.map((follower) => mergeFollowerStatus(registry, follower, k8s.stateByFollower[follower.id] ?? {}, null, false)),
followers: selected.map((follower) => mergeFollowerStatus(registry, follower, k8s.stateByFollower[follower.id] ?? {}, null, false, false, before.stateByFollower[follower.id] ?? {})),
warnings: refresh.ok ? [] : [`reconcile job failed: ${refresh.message}`],
next: {
status: "bun scripts/cli.ts cicd branch-follower status",
@@ -1884,6 +1888,7 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
reuseConfig: observedSha === null ? null : summarizeRuntimeReuseConfig(requireFollowerRuntimeReuseConfig(follower, observedSha, Math.min(timeoutSeconds, 5))),
gitMirror: nativeGitMirrorSummary(bundle.gitMirror),
tekton: nativePipelineRunSummary(bundle.pipelineRun),
pipeline: bundle.pipeline,
taskRuns: bundle.taskRuns,
planArtifacts: bundle.planArtifacts,
argo: nativeArgoSummary(bundle.argoApplication),
@@ -1941,6 +1946,7 @@ function mergeFollowerStatus(
live: AdapterSummary | null,
liveRequested: boolean,
detailed: boolean,
fallbackStored: Record<string, unknown> = {},
): Record<string, unknown> {
const storedSource = asOptionalRecord(stored.source);
const storedTarget = asOptionalRecord(stored.target);
@@ -1950,6 +1956,12 @@ function mergeFollowerStatus(
const lastTriggeredSha = live?.lastTriggeredSha ?? stringOrNull(stored.lastTriggeredSha);
const lastSucceededSha = live?.lastSucceededSha ?? stringOrNull(stored.lastSucceededSha);
const timings = live === null ? storedFollowerTimingsForStatus(follower, asOptionalRecord(stored.timings), phase, observedSha) : buildFollowerTimings(follower, live, undefined, asOptionalRecord(stored.timings), phase);
const evidence = followerEvidenceSummary({
observedSha,
livePayload: asOptionalRecord(live?.payload),
storedCommand: asOptionalRecord(stored.command),
fallbackStoredCommand: asOptionalRecord(fallbackStored.command),
});
const reconcileTimeline = compactReconcileTimeline(asOptionalRecord(stored.command)?.reconcileTimeline, follower.id) ?? {
bounded: true,
missingReason: "stored state lacks reconcileTimeline; old data cannot be reconstructed",
@@ -1981,6 +1993,7 @@ function mergeFollowerStatus(
live: liveRequested,
message: live?.message ?? stringOrNull(stored.decision) ?? "no controller state yet",
timings: detailed ? timings : compactListTimings(timings),
evidence: detailed ? evidence : null,
reconcileTimeline: detailed ? reconcileTimeline : null,
rawStateDiagnostic: detailed ? asOptionalRecord(stored.rawStateDiagnostic) : null,
drilldown: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id} --live`,
@@ -2286,10 +2299,12 @@ function compactNativePayload(payload: Record<string, unknown> | null): Record<s
reuseConfig: summarizeRuntimeReuseConfigFromRecord(asOptionalRecord(payload.reuseConfig)),
gitMirror: asOptionalRecord(payload.gitMirror),
tekton: asOptionalRecord(payload.tekton),
pipeline: asOptionalRecord(payload.pipeline),
taskRuns: compactTaskRunsPayload(asOptionalRecord(payload.taskRuns)),
planArtifacts: compactPlanArtifactsPayload(asOptionalRecord(payload.planArtifacts)),
argo: asOptionalRecord(payload.argo),
runtime: asOptionalRecord(payload.runtime),
refreshEvidence: compactRefreshEvidence(asOptionalRecord(asOptionalRecord(payload.nativeCapabilities)?.controlPlaneRefresh)),
errors: Array.isArray(payload.errors) ? payload.errors.slice(0, 5) : [],
statusAuthority: stringOrNull(payload.statusAuthority),
parsedDownstreamCliOutput: false,
+29
View File
@@ -400,6 +400,7 @@ function compactStatusGates(payload: Record<string, unknown> | null): Record<str
},
tekton: tekton === null ? null : {
name: stringOrNull(tekton.name),
pipelineRefName: stringOrNull(tekton.pipelineRefName),
succeeded: tekton.succeeded === true ? true : tekton.succeeded === false ? false : null,
reason: stringOrNull(tekton.reason),
startTime: stringOrNull(tekton.startTime),
@@ -428,6 +429,8 @@ function compactStatusGates(payload: Record<string, unknown> | null): Record<str
nonReadyResources: Array.isArray(argo.nonReadyResources) ? argo.nonReadyResources.slice(0, 5) : [],
ready: argo.ready === true,
},
pipeline: compactPipeline(payload.pipeline),
refreshEvidence: compactRefreshEvidence(payload.refreshEvidence),
runtime: runtime === null ? null : {
ready: runtime.ready === true,
targetSha: stringOrNull(runtime.targetSha),
@@ -438,6 +441,32 @@ function compactStatusGates(payload: Record<string, unknown> | null): Record<str
};
}
function compactPipeline(value: unknown): Record<string, unknown> | null {
const pipeline = asOptionalRecord(value);
if (pipeline === null) return null;
return {
metadata: asOptionalRecord(pipeline.metadata),
spec: asOptionalRecord(pipeline.spec),
};
}
function compactRefreshEvidence(value: unknown): Record<string, unknown> | null {
const refresh = asOptionalRecord(value);
if (refresh === null) return null;
return {
jobName: stringOrNull(refresh.jobName),
namespace: stringOrNull(refresh.namespace),
status: stringOrNull(refresh.status),
pipeline: stringOrNull(refresh.pipeline),
sourceCommit: stringOrNull(refresh.sourceCommit),
sourceStageRef: stringOrNull(refresh.sourceStageRef),
elapsedMs: numberOrNull(refresh.elapsedMs),
sourceAuthority: stringOrNull(refresh.sourceAuthority),
statusAuthority: stringOrNull(refresh.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
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)) : [];
}
+20
View File
@@ -268,6 +268,26 @@ function nativeGateRows(native: Record<string, unknown> | null): unknown[][] {
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) ?? "-"]);
}
const pipeline = asOptionalRecord(native.pipeline);
if (pipeline !== null) {
const runtimeReady = asOptionalRecord(asOptionalRecord(pipeline.spec)?.runtimeReadyTask);
const when = arrayRecords(runtimeReady?.when)[0];
rows.push([
"pipeline",
runtimeReady?.present === true ? "runtime-ready-present" : "runtime-ready-absent",
when === undefined ? "-" : `${stringOrNull(when.input) ?? "-"} ${stringOrNull(when.operator) ?? "-"} ${arrayTextItems(when.values).join(",") || "-"}`,
stringOrNull(asOptionalRecord(pipeline.metadata)?.name) ?? "-",
]);
}
const refresh = asOptionalRecord(native.refreshEvidence);
if (refresh !== null) {
rows.push([
"control-plane-refresh",
stringOrNull(refresh.status) ?? "-",
`${shortSha(stringOrNull(refresh.sourceCommit))}/${stringOrNull(refresh.pipeline) ?? "-"}`,
stringOrNull(refresh.jobName) ?? "-",
]);
}
for (const error of arrayTextItems(native.errors).slice(0, 5)) rows.push(["error", "present", error, "-"]);
return rows;
}
+71
View File
@@ -0,0 +1,71 @@
// SPEC: PJ2026-01060703 CI/CD branch follower bounded evidence helpers.
// Responsibility: compact pipeline/runtime-ready and refresh evidence for status/drill-down output.
export function compactRefreshEvidence(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
const summary = asOptionalRecord(value.summary);
if (summary === null) return null;
return {
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
status: stringOrNull(summary.status),
pipeline: stringOrNull(summary.pipeline),
sourceCommit: stringOrNull(summary.sourceCommit),
sourceStageRef: stringOrNull(summary.sourceStageRef),
elapsedMs: numberOrNull(summary.elapsedMs),
sourceAuthority: stringOrNull(summary.sourceAuthority),
statusAuthority: stringOrNull(summary.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
export function followerEvidenceSummary(input: {
observedSha: string | null;
livePayload: Record<string, unknown> | null;
storedCommand: Record<string, unknown> | null;
fallbackStoredCommand?: Record<string, unknown> | null;
}): Record<string, unknown> | null {
const livePayload = input.livePayload;
const storedPayload = asOptionalRecord(input.storedCommand?.payload);
const fallbackStoredPayload = asOptionalRecord(input.fallbackStoredCommand?.payload);
const tekton = firstRecord(asOptionalRecord(livePayload?.tekton), asOptionalRecord(storedPayload?.tekton), asOptionalRecord(fallbackStoredPayload?.tekton));
const pipeline = firstRecord(asOptionalRecord(livePayload?.pipeline), asOptionalRecord(storedPayload?.pipeline), asOptionalRecord(fallbackStoredPayload?.pipeline));
const refresh = firstRecord(asOptionalRecord(storedPayload?.refreshEvidence), asOptionalRecord(fallbackStoredPayload?.refreshEvidence), asOptionalRecord(livePayload?.refreshEvidence));
if (tekton === null && pipeline === null && refresh === null) return null;
const pipelineRefName = stringOrNull(tekton?.pipelineRefName);
const pipelineName = stringOrNull(asOptionalRecord(pipeline?.metadata)?.name);
const refreshPipeline = stringOrNull(refresh?.pipeline);
const refreshSourceCommit = stringOrNull(refresh?.sourceCommit);
return {
pipelineRunRefName: pipelineRefName,
pipeline,
refreshBoundedReason: refresh === null ? "missing-from-live-and-stored-evidence" : null,
refresh: refresh === null
? null
: {
...refresh,
pipelineRefMatches: pipelineRefName === null || refreshPipeline === null ? null : pipelineRefName === refreshPipeline,
pipelineSpecMatches: pipelineName === null || refreshPipeline === null ? null : pipelineName === refreshPipeline,
sourceCommitMatches: input.observedSha === null || refreshSourceCommit === null ? null : input.observedSha === refreshSourceCommit,
},
};
}
function firstRecord(...values: Array<Record<string, unknown> | null>): Record<string, unknown> | null {
for (const value of values) {
if (value !== null) return value;
}
return null;
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
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;
}
+1
View File
@@ -59,6 +59,7 @@ export function readNativeObjectBundle(registry: BranchFollowerRegistry, followe
source: sourceRecord,
gitMirror: asOptionalRecord(parsed.objects.gitMirror),
pipelineRun: asOptionalRecord(parsed.objects.pipelineRun),
pipeline: asOptionalRecord(parsed.objects.pipeline),
taskRuns: asOptionalRecord(parsed.objects.taskRuns),
planArtifacts: asOptionalRecord(parsed.objects.planArtifacts),
argoApplication: asOptionalRecord(parsed.objects.argoApplication),
+1
View File
@@ -50,6 +50,7 @@ export function nativePipelineRunSummary(pipelineRun: Record<string, unknown> |
return {
name: stringOrNull(metadata?.name),
namespace: stringOrNull(metadata?.namespace),
pipelineRefName: stringOrNull(asOptionalRecord(pipelineRun.spec)?.pipelineRef?.name),
succeeded: pipelineRunSucceeded(pipelineRun),
reason: stringOrNull(condition?.reason),
startTime: stringOrNull(status?.startTime),
+5
View File
@@ -52,6 +52,7 @@ export function runNativeK8sJob(namespace: string, jobName: string, manifest: Re
polls: numberOrNull(parsed?.polls) ?? 0,
elapsedMs: numberOrNull(parsed?.elapsedMs) ?? 0,
logsTail: stringOrNull(parsed?.logsTail),
summary: asOptionalRecord(parsed?.summary),
conditionReason: stringOrNull(parsed?.conditionReason),
conditionMessage: stringOrNull(parsed?.conditionMessage) ?? (result.exitCode === 0 ? null : tailText(result.stderr || result.stdout, 500)),
statusAuthority: "kubernetes-api-serviceaccount",
@@ -70,6 +71,10 @@ function parseJsonObject(text: string): Record<string, unknown> | null {
}
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
+36
View File
@@ -110,6 +110,7 @@ function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOpt
const next = asOptionalRecord(payload.next);
const errors = Array.isArray(payload.errors) ? payload.errors : [];
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
const evidenceRows = followers.flatMap(evidenceRowsForFollower).slice(0, 48);
const reconcileRows = followers.flatMap(reconcileRowsForFollower).slice(0, 48);
const rawStateRows = followers.flatMap(rawStateRowsForFollower).slice(0, 24);
return [
@@ -122,6 +123,7 @@ function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOpt
"",
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)}`,
evidenceRows.length === 0 ? "" : `\nEVIDENCE\n${table(["FOLLOWER", "TYPE", "STATUS", "DETAIL", "OBJECT"], evidenceRows)}`,
reconcileRows.length === 0 ? "" : `\nRECONCILE TIMELINE\n${table(["FOLLOWER", "STEP", "STATUS", "SECONDS", "STARTED", "OBJECT"], reconcileRows)}`,
rawStateRows.length === 0 ? "" : `\nRAW STATE DIAGNOSTIC\n${table(["FOLLOWER", "STATE_BYTES", "COMMAND", "TIMELINE", "STEPS", "TIMELINE_BYTES", "REASON"], rawStateRows)}`,
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
@@ -235,6 +237,36 @@ function reconcileRowsFromRunOnce(payload: Record<string, unknown>, followers: R
return followers.flatMap(reconcileRowsForFollower);
}
function evidenceRowsForFollower(item: Record<string, unknown>): unknown[][] {
const evidence = asOptionalRecord(item.evidence);
if (evidence === null) return [];
const rows: unknown[][] = [];
rows.push([item.id, "pipelineRef", "observed", stringOrNull(evidence.pipelineRunRefName) ?? "-", stringOrNull(item.pipelineRun) ?? "-"]);
const pipeline = asOptionalRecord(evidence.pipeline);
if (pipeline !== null) {
const runtimeReady = asOptionalRecord(asOptionalRecord(pipeline.spec)?.runtimeReadyTask);
const when = arrayRecords(runtimeReady?.when)[0];
rows.push([
item.id,
"pipelineSpec",
runtimeReady?.present === true ? "runtime-ready-present" : "runtime-ready-absent",
when === undefined ? "-" : `${stringOrNull(when.input) ?? "-"} ${stringOrNull(when.operator) ?? "-"} ${arrayText(when.values) || "-"}`,
stringOrNull(asOptionalRecord(pipeline.metadata)?.name) ?? "-",
]);
}
const refresh = asOptionalRecord(evidence.refresh);
rows.push([
item.id,
"refresh",
refresh === null ? "missing" : stringOrNull(refresh.status) ?? "-",
refresh === null
? stringOrNull(evidence.refreshBoundedReason) ?? "-"
: `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`,
refresh === null ? "-" : stringOrNull(refresh.pipeline) ?? "-",
]);
return rows;
}
function reconcileRowsForFollower(item: Record<string, unknown>): unknown[][] {
return reconcileRowsForTimeline(asOptionalRecord(item.reconcileTimeline), stringOrNull(item.id));
}
@@ -277,6 +309,10 @@ function formatSeconds(value: number | null): string {
return value === null ? "-" : `${value}s`;
}
function boolMatch(value: unknown): string {
return value === true ? "match" : value === false ? "mismatch" : "-";
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
+2
View File
@@ -222,6 +222,7 @@ export interface NativeObjectBundle {
source: Record<string, unknown> | null;
gitMirror: Record<string, unknown> | null;
pipelineRun: Record<string, unknown> | null;
pipeline: Record<string, unknown> | null;
taskRuns: Record<string, unknown> | null;
planArtifacts: Record<string, unknown> | null;
argoApplication: Record<string, unknown> | null;
@@ -267,6 +268,7 @@ export interface NativeK8sJobResult {
polls: number;
elapsedMs: number;
logsTail: string | null;
summary: Record<string, unknown> | null;
conditionReason: string | null;
conditionMessage: string | null;
statusAuthority: "kubernetes-api-serviceaccount";