Merge pull request #1510 from pikasTech/fix/1499-refresh-render-evidence
fix: persist HWLAB refresh render/apply evidence
This commit is contained in:
@@ -34,6 +34,8 @@ Do not debug the same state/read/write problem by repeatedly pushing empty or ti
|
||||
|
||||
When a branch-follower issue remains ambiguous after a debug step or drill-down, split the CLI into a smaller single-step probe before any new end-to-end run. Add or use a focused `debug-step`, follower-scoped drill-down, or bounded target-side diagnostic for the exact missing edge, such as PipelineRun -> Pipeline spec, controller refresh apply object, state write, closeout re-read, or log/timing extraction. Do not use another source PR, merge, or full automatic follower loop as the next diagnostic action until the narrower step can show the needed evidence.
|
||||
|
||||
For HWLAB native `control-plane-refresh`, the bounded evidence chain must preserve both the rendered Pipeline summary and the applied cluster object summary for the same source commit: rendered Pipeline name, bounded `runtime-ready` task/when summary, source commit/stage ref, applied Pipeline name, resourceVersion, and a short annotation/label subset proving which object was patched. If the Job TTL has already removed the original Job, status/events/logs must show `-` or a bounded missing reason from stored state instead of inferring the missing edge.
|
||||
|
||||
CI/CD validation must be decomposable into ordered single-step gates before a full rollout observation is accepted: first validate the reuse plan, then CI parallelism/TaskRun plan, then CD rollout plan, then post-deploy monitoring/health evidence. Each gate must have a CLI/debug-step/drill-down entry that can be run and fixed independently on the target side. Do not use issue comments, repeated PR merges, or end-to-end follower loops as substitutes for a missing single-step validator; add the missing bounded CLI step first.
|
||||
|
||||
When a repeated runtime pitfall or visibility defect is found during branch-follower work, update this reference or the skill entry first, then continue with the narrow debug step. Do not proceed to `run-once`, controller loop observation, automatic follower validation, or source-commit-driven integration until the relevant `state-read`, `status-read`, `decide`, and `state-write` debug steps pass for the affected follower.
|
||||
|
||||
@@ -26,8 +26,8 @@ try {
|
||||
prepareYamlDependency();
|
||||
applyDeployOverlay();
|
||||
renderControlPlane();
|
||||
await applyPipeline();
|
||||
emit({ ok: true, status: "applied" });
|
||||
const evidence = await applyPipeline();
|
||||
emit({ ok: true, status: "applied", ...evidence });
|
||||
} finally {
|
||||
rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -143,16 +143,21 @@ async function applyPipeline() {
|
||||
if (typeof renderedPipelineName !== "string" || renderedPipelineName.length === 0) {
|
||||
throw new Error(`rendered Pipeline metadata.name missing: ${pipelinePath}`);
|
||||
}
|
||||
const render = summarizeRenderedPipeline(pipeline, renderedPipelineName);
|
||||
const pipelineName = requiredOverlayString("pipelineName");
|
||||
pipeline.metadata = pipeline.metadata && typeof pipeline.metadata === "object" ? pipeline.metadata : {};
|
||||
pipeline.metadata.name = pipelineName;
|
||||
const pipelineText = YAML.stringify(pipeline);
|
||||
await kubeRequest(
|
||||
const applyText = await kubeRequest(
|
||||
"PATCH",
|
||||
`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelines/${encodeURIComponent(pipelineName)}?fieldManager=${encodeURIComponent(fieldManager)}&force=true`,
|
||||
pipelineText,
|
||||
"application/apply-patch+yaml",
|
||||
);
|
||||
return {
|
||||
render,
|
||||
apply: summarizeAppliedPipeline(parseJsonObject(applyText), pipelineName, tektonNamespace),
|
||||
};
|
||||
}
|
||||
|
||||
function yamlModule() {
|
||||
@@ -281,6 +286,102 @@ function requiredNonNegativeNumber(name) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function summarizeRenderedPipeline(pipeline, pipelineName) {
|
||||
const tasks = Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks : [];
|
||||
const runtimeReady = tasks.find((task) => recordOrNull(task)?.name === "runtime-ready");
|
||||
return {
|
||||
pipelineName,
|
||||
taskCount: tasks.length,
|
||||
runtimeReadyTask: summarizeRuntimeReadyTask(runtimeReady),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRuntimeReadyTask(value) {
|
||||
const task = recordOrNull(value);
|
||||
if (task === null) return { present: false, name: null, runAfter: [], when: [] };
|
||||
return {
|
||||
present: true,
|
||||
name: stringOrNull(task.name),
|
||||
runAfter: compactStringArray(task.runAfter, 4),
|
||||
when: compactWhenList(task.when, 4),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeAppliedPipeline(value, pipelineName, namespace) {
|
||||
const metadata = recordOrNull(value?.metadata);
|
||||
return {
|
||||
pipelineName: stringOrNull(metadata?.name) || pipelineName,
|
||||
namespace: stringOrNull(metadata?.namespace) || namespace,
|
||||
resourceVersion: stringOrNull(metadata?.resourceVersion),
|
||||
annotations: compactMetadataMap(metadata?.annotations, [
|
||||
"sourceConfig",
|
||||
"ciContract",
|
||||
"policy",
|
||||
"hwlab.pikastech.local/source-commit",
|
||||
"tekton.dev/pipelines.minVersion",
|
||||
], 6),
|
||||
labels: compactMetadataMap(metadata?.labels, [
|
||||
"hwlab.pikastech.local/source-commit",
|
||||
"app.kubernetes.io/name",
|
||||
"app.kubernetes.io/part-of",
|
||||
"app.kubernetes.io/component",
|
||||
], 6),
|
||||
degradedReason: metadata === null ? "apply-response-metadata-missing" : null,
|
||||
};
|
||||
}
|
||||
|
||||
function compactMetadataMap(value, preferredKeys, limit) {
|
||||
const record = recordOrNull(value);
|
||||
if (record === null) return null;
|
||||
const output = {};
|
||||
for (const key of preferredKeys) {
|
||||
const item = stringOrNull(record[key]);
|
||||
if (item === null || output[key] !== undefined) continue;
|
||||
output[key] = item;
|
||||
if (Object.keys(output).length >= limit) return output;
|
||||
}
|
||||
for (const key of Object.keys(record).sort()) {
|
||||
const item = stringOrNull(record[key]);
|
||||
if (item === null || output[key] !== undefined) continue;
|
||||
output[key] = item;
|
||||
if (Object.keys(output).length >= limit) break;
|
||||
}
|
||||
return Object.keys(output).length === 0 ? null : output;
|
||||
}
|
||||
|
||||
function compactStringArray(value, limit) {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => stringOrNull(item)).filter(Boolean).slice(0, limit)
|
||||
: [];
|
||||
}
|
||||
|
||||
function compactWhenList(value, limit) {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => recordOrNull(item)).filter(Boolean).slice(0, limit).map((item) => ({
|
||||
input: stringOrNull(item.input),
|
||||
operator: stringOrNull(item.operator),
|
||||
values: compactStringArray(item.values, 4),
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
function parseJsonObject(text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
return recordOrNull(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function recordOrNull(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function emit(extra) {
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
...extra,
|
||||
|
||||
@@ -292,16 +292,69 @@ function compactRefreshEvidence(refresh) {
|
||||
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
|
||||
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
|
||||
status: stringOrNull(summary.status),
|
||||
pipeline: stringOrNull(summary.pipeline),
|
||||
pipeline: stringOrNull(summary.pipeline) || stringOrNull(recordOrNull(summary.apply)?.pipelineName) || stringOrNull(recordOrNull(summary.render)?.pipelineName),
|
||||
sourceCommit: stringOrNull(summary.sourceCommit),
|
||||
sourceStageRef: stringOrNull(summary.sourceStageRef),
|
||||
elapsedMs: numberOrNull(summary.elapsedMs),
|
||||
render: compactRefreshRender(recordOrNull(summary.render)),
|
||||
apply: compactRefreshApply(recordOrNull(summary.apply)),
|
||||
sourceAuthority: stringOrNull(summary.sourceAuthority),
|
||||
statusAuthority: stringOrNull(summary.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRender(value) {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
taskCount: numberOrNull(value.taskCount),
|
||||
runtimeReadyTask: compactRefreshRuntimeReady(recordOrNull(value.runtimeReadyTask)),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshApply(value) {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
namespace: stringOrNull(value.namespace),
|
||||
resourceVersion: stringOrNull(value.resourceVersion),
|
||||
annotations: compactStringMap(recordOrNull(value.annotations)),
|
||||
labels: compactStringMap(recordOrNull(value.labels)),
|
||||
degradedReason: stringOrNull(value.degradedReason),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRuntimeReady(value) {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
present: booleanOrNull(value.present),
|
||||
name: stringOrNull(value.name),
|
||||
runAfter: compactStringArray(value.runAfter, 4),
|
||||
when: compactWhenList(value.when, 4),
|
||||
};
|
||||
}
|
||||
|
||||
function compactWhenList(value, limit) {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => recordOrNull(item)).filter(Boolean).slice(0, limit).map((item) => ({
|
||||
input: stringOrNull(item.input),
|
||||
operator: stringOrNull(item.operator),
|
||||
values: compactStringArray(item.values, 4),
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
function compactStringMap(value) {
|
||||
if (value === null) return null;
|
||||
const output = {};
|
||||
for (const [key, item] of Object.entries(value).slice(0, 8)) {
|
||||
const text = stringOrNull(item);
|
||||
if (text !== null) output[key] = text;
|
||||
}
|
||||
return Object.keys(output).length === 0 ? null : output;
|
||||
}
|
||||
|
||||
function compactArgo(argo) {
|
||||
const value = recordOrNull(argo);
|
||||
if (value === null) return null;
|
||||
@@ -492,6 +545,14 @@ function numberOrNull(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function booleanOrNull(value) {
|
||||
return value === true ? true : value === false ? false : null;
|
||||
}
|
||||
|
||||
function compactStringArray(value, limit) {
|
||||
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter(Boolean).slice(0, limit) : [];
|
||||
}
|
||||
|
||||
const result = await readConfigMap();
|
||||
const errors = [];
|
||||
const stateByFollower = {};
|
||||
|
||||
@@ -457,16 +457,73 @@ function compactRefreshEvidence(value: unknown): Record<string, unknown> | null
|
||||
jobName: stringOrNull(refresh.jobName),
|
||||
namespace: stringOrNull(refresh.namespace),
|
||||
status: stringOrNull(refresh.status),
|
||||
pipeline: stringOrNull(refresh.pipeline),
|
||||
pipeline: stringOrNull(refresh.pipeline) ?? stringOrNull(asOptionalRecord(refresh.apply)?.pipelineName) ?? stringOrNull(asOptionalRecord(refresh.render)?.pipelineName),
|
||||
sourceCommit: stringOrNull(refresh.sourceCommit),
|
||||
sourceStageRef: stringOrNull(refresh.sourceStageRef),
|
||||
elapsedMs: numberOrNull(refresh.elapsedMs),
|
||||
render: compactRefreshRender(asOptionalRecord(refresh.render)),
|
||||
apply: compactRefreshApply(asOptionalRecord(refresh.apply)),
|
||||
sourceAuthority: stringOrNull(refresh.sourceAuthority),
|
||||
statusAuthority: stringOrNull(refresh.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRender(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
taskCount: numberOrNull(value.taskCount),
|
||||
runtimeReadyTask: compactRefreshRuntimeReady(asOptionalRecord(value.runtimeReadyTask)),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshApply(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
namespace: stringOrNull(value.namespace),
|
||||
resourceVersion: stringOrNull(value.resourceVersion),
|
||||
annotations: compactStringMap(asOptionalRecord(value.annotations)),
|
||||
labels: compactStringMap(asOptionalRecord(value.labels)),
|
||||
degradedReason: stringOrNull(value.degradedReason),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRuntimeReady(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
present: booleanOrNull(value.present),
|
||||
name: stringOrNull(value.name),
|
||||
runAfter: compactStringArray(value.runAfter, 4),
|
||||
when: compactWhenList(value.when, 4),
|
||||
};
|
||||
}
|
||||
|
||||
function compactWhenList(value: unknown, limit: number): Array<Record<string, unknown>> {
|
||||
return Array.isArray(value)
|
||||
? value
|
||||
.map((item) => asOptionalRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => item !== null)
|
||||
.slice(0, limit)
|
||||
.map((item) => ({
|
||||
input: stringOrNull(item.input),
|
||||
operator: stringOrNull(item.operator),
|
||||
values: compactStringArray(item.values, 4),
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
function compactStringMap(value: Record<string, unknown> | null): Record<string, string> | null {
|
||||
if (value === null) return null;
|
||||
const output: Record<string, string> = {};
|
||||
for (const [key, item] of Object.entries(value).slice(0, 8)) {
|
||||
const text = stringOrNull(item);
|
||||
if (text !== null) output[key] = text;
|
||||
}
|
||||
return Object.keys(output).length === 0 ? null : output;
|
||||
}
|
||||
|
||||
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)) : [];
|
||||
}
|
||||
@@ -555,6 +612,14 @@ function numberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function booleanOrNull(value: unknown): boolean | null {
|
||||
return value === true ? true : value === false ? false : null;
|
||||
}
|
||||
|
||||
function compactStringArray(value: unknown, limit: number): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter((item): item is string => item !== null).slice(0, limit) : [];
|
||||
}
|
||||
|
||||
function shortSha(value: string | null): string {
|
||||
if (value === null) return "-";
|
||||
return value.length > 12 ? value.slice(0, 12) : value;
|
||||
|
||||
@@ -37,6 +37,7 @@ function renderJobHuman(payload: Record<string, unknown>): string {
|
||||
const pods = arrayRecords(result?.pods);
|
||||
const logs = arrayRecords(result?.logs);
|
||||
const errors = arrayRecords(result?.errors);
|
||||
const summaryEvidence = refreshEvidenceRows(asOptionalRecord(result?.summary));
|
||||
const command = asOptionalRecord(payload.command);
|
||||
const identity = asOptionalRecord(command?.identity);
|
||||
return [
|
||||
@@ -59,6 +60,7 @@ function renderJobHuman(payload: Record<string, unknown>): string {
|
||||
pods.length === 0 ? "" : `\nPODS\n${table(["POD", "PHASE", "READY", "START", "CONTAINERS", "REASON"], pods.map(jobPodRow))}`,
|
||||
logs.length === 0 ? "" : `\nLOG TAILS\n${table(["POD", "CONTAINER", "STATUS", "REASON", "LINES", "BYTES", "TIMING", "MESSAGE"], logs.map(logRow))}`,
|
||||
errors.length === 0 ? "" : `\nERRORS\n${table(["POD", "CONTAINER", "REASON", "MESSAGE"], errors.map((item) => [item.pod, item.container, item.degradedReason, item.message]))}`,
|
||||
summaryEvidence.length === 0 ? "" : `\nEVIDENCE\n${table(["TYPE", "STATUS", "DETAIL", "OBJECT"], summaryEvidence)}`,
|
||||
command === null ? "" : `\nTARGET COMMAND\n${table(["ROUTE", "SCRIPT", "EXIT", "PARSE_ERROR"], [[identity?.route ?? "-", identity?.script ?? "-", command.exitCode ?? "-", command.parseError ?? "-"]])}`,
|
||||
command?.stdoutTail ? `\nSTDOUT_TAIL\n${command.stdoutTail}` : "",
|
||||
command?.stderrTail ? `\nSTDERR_TAIL\n${command.stderrTail}` : "",
|
||||
@@ -287,11 +289,37 @@ function nativeGateRows(native: Record<string, unknown> | null): unknown[][] {
|
||||
`${shortSha(stringOrNull(refresh.sourceCommit))}/${stringOrNull(refresh.pipeline) ?? "-"}`,
|
||||
stringOrNull(refresh.jobName) ?? "-",
|
||||
]);
|
||||
rows.push(...refreshEvidenceRows(refresh));
|
||||
}
|
||||
for (const error of arrayTextItems(native.errors).slice(0, 5)) rows.push(["error", "present", error, "-"]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
function refreshEvidenceRows(value: Record<string, unknown> | null): unknown[][] {
|
||||
if (value === null) return [];
|
||||
const rows: unknown[][] = [];
|
||||
const render = asOptionalRecord(value.render);
|
||||
const renderRuntimeReady = asOptionalRecord(render?.runtimeReadyTask);
|
||||
if (render !== null) {
|
||||
rows.push([
|
||||
"control-plane-render",
|
||||
renderRuntimeReady?.present === true ? "runtime-ready-present" : renderRuntimeReady?.present === false ? "runtime-ready-absent" : "-",
|
||||
whenSummary(arrayRecords(renderRuntimeReady?.when)[0]),
|
||||
stringOrNull(render.pipelineName) ?? "-",
|
||||
]);
|
||||
}
|
||||
const apply = asOptionalRecord(value.apply);
|
||||
if (apply !== null) {
|
||||
rows.push([
|
||||
"control-plane-apply",
|
||||
stringOrNull(apply.resourceVersion) ?? stringOrNull(apply.degradedReason) ?? "-",
|
||||
applyMetadataSummary(apply),
|
||||
stringOrNull(apply.pipelineName) ?? "-",
|
||||
]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function taskRunDetail(item: Record<string, unknown> | undefined): string {
|
||||
if (item === undefined) return "-";
|
||||
const duration = numberOrNull(item.durationSeconds);
|
||||
@@ -308,6 +336,24 @@ function argoDetail(argo: Record<string, unknown>): string {
|
||||
?? shortSha(stringOrNull(argo.revision));
|
||||
}
|
||||
|
||||
function whenSummary(value: Record<string, unknown> | undefined): string {
|
||||
if (value === undefined) return "-";
|
||||
const values = arrayTextItems(value.values).join(",");
|
||||
return `${stringOrNull(value.input) ?? "-"} ${stringOrNull(value.operator) ?? "-"} ${values || "-"}`;
|
||||
}
|
||||
|
||||
function applyMetadataSummary(value: Record<string, unknown>): string {
|
||||
const annotations = asOptionalRecord(value.annotations);
|
||||
const labels = asOptionalRecord(value.labels);
|
||||
return `ann:${firstEntry(annotations)} label:${firstEntry(labels)}`;
|
||||
}
|
||||
|
||||
function firstEntry(value: Record<string, unknown> | null): string {
|
||||
if (value === null) return "-";
|
||||
const [key, item] = Object.entries(value)[0] ?? [];
|
||||
return key === undefined ? "-" : `${key}=${stringOrNull(item) ?? "-"}`;
|
||||
}
|
||||
|
||||
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ export function compactRefreshEvidence(value: Record<string, unknown> | null): R
|
||||
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
|
||||
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
|
||||
status: stringOrNull(summary.status),
|
||||
pipeline: stringOrNull(summary.pipeline),
|
||||
pipeline: stringOrNull(summary.pipeline) ?? stringOrNull(asOptionalRecord(summary.apply)?.pipelineName) ?? stringOrNull(asOptionalRecord(summary.render)?.pipelineName),
|
||||
sourceCommit: stringOrNull(summary.sourceCommit),
|
||||
sourceStageRef: stringOrNull(summary.sourceStageRef),
|
||||
elapsedMs: numberOrNull(summary.elapsedMs),
|
||||
render: compactRefreshRender(asOptionalRecord(summary.render)),
|
||||
apply: compactRefreshApply(asOptionalRecord(summary.apply)),
|
||||
sourceAuthority: stringOrNull(summary.sourceAuthority),
|
||||
statusAuthority: stringOrNull(summary.statusAuthority),
|
||||
parsedDownstreamCliOutput: false,
|
||||
@@ -34,7 +36,10 @@ export function followerEvidenceSummary(input: {
|
||||
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 refreshRender = asOptionalRecord(refresh?.render);
|
||||
const refreshApply = asOptionalRecord(refresh?.apply);
|
||||
const refreshRenderedPipeline = stringOrNull(refreshRender?.pipelineName);
|
||||
const refreshPipeline = stringOrNull(refreshApply?.pipelineName) ?? stringOrNull(refresh?.pipeline);
|
||||
const refreshSourceCommit = stringOrNull(refresh?.sourceCommit);
|
||||
return {
|
||||
pipelineRunRefName: pipelineRefName,
|
||||
@@ -46,11 +51,67 @@ export function followerEvidenceSummary(input: {
|
||||
...refresh,
|
||||
pipelineRefMatches: pipelineRefName === null || refreshPipeline === null ? null : pipelineRefName === refreshPipeline,
|
||||
pipelineSpecMatches: pipelineName === null || refreshPipeline === null ? null : pipelineName === refreshPipeline,
|
||||
renderedPipelineMatchesApplied: refreshRenderedPipeline === null || refreshPipeline === null ? null : refreshRenderedPipeline === refreshPipeline,
|
||||
sourceCommitMatches: input.observedSha === null || refreshSourceCommit === null ? null : input.observedSha === refreshSourceCommit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRender(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
taskCount: numberOrNull(value.taskCount),
|
||||
runtimeReadyTask: compactRefreshRuntimeReady(asOptionalRecord(value.runtimeReadyTask)),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshApply(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
pipelineName: stringOrNull(value.pipelineName),
|
||||
namespace: stringOrNull(value.namespace),
|
||||
resourceVersion: stringOrNull(value.resourceVersion),
|
||||
annotations: compactStringMap(asOptionalRecord(value.annotations)),
|
||||
labels: compactStringMap(asOptionalRecord(value.labels)),
|
||||
degradedReason: stringOrNull(value.degradedReason),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRefreshRuntimeReady(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (value === null) return null;
|
||||
return {
|
||||
present: booleanOrNull(value.present),
|
||||
name: stringOrNull(value.name),
|
||||
runAfter: compactStringArray(value.runAfter, 4),
|
||||
when: compactWhenList(value.when, 4),
|
||||
};
|
||||
}
|
||||
|
||||
function compactWhenList(value: unknown, limit: number): Array<Record<string, unknown>> {
|
||||
return Array.isArray(value)
|
||||
? value
|
||||
.map((item) => asOptionalRecord(item))
|
||||
.filter((item): item is Record<string, unknown> => item !== null)
|
||||
.slice(0, limit)
|
||||
.map((item) => ({
|
||||
input: stringOrNull(item.input),
|
||||
operator: stringOrNull(item.operator),
|
||||
values: compactStringArray(item.values, 4),
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
function compactStringMap(value: Record<string, unknown> | null): Record<string, string> | null {
|
||||
if (value === null) return null;
|
||||
const output: Record<string, string> = {};
|
||||
for (const [key, item] of Object.entries(value).slice(0, 8)) {
|
||||
const text = stringOrNull(item);
|
||||
if (text !== null) output[key] = text;
|
||||
}
|
||||
return Object.keys(output).length === 0 ? null : output;
|
||||
}
|
||||
|
||||
function firstRecord(...values: Array<Record<string, unknown> | null>): Record<string, unknown> | null {
|
||||
for (const value of values) {
|
||||
if (value !== null) return value;
|
||||
@@ -69,3 +130,11 @@ function stringOrNull(value: unknown): string | null {
|
||||
function numberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function booleanOrNull(value: unknown): boolean | null {
|
||||
return value === true ? true : value === false ? false : null;
|
||||
}
|
||||
|
||||
function compactStringArray(value: unknown, limit: number): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter((item): item is string => item !== null).slice(0, limit) : [];
|
||||
}
|
||||
|
||||
@@ -264,6 +264,27 @@ function evidenceRowsForFollower(item: Record<string, unknown>): unknown[][] {
|
||||
: `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`,
|
||||
refresh === null ? "-" : stringOrNull(refresh.pipeline) ?? "-",
|
||||
]);
|
||||
const refreshRender = asOptionalRecord(refresh?.render);
|
||||
const refreshRenderRuntimeReady = asOptionalRecord(refreshRender?.runtimeReadyTask);
|
||||
if (refreshRender !== null) {
|
||||
rows.push([
|
||||
item.id,
|
||||
"refresh-render",
|
||||
refreshRenderRuntimeReady?.present === true ? "runtime-ready-present" : refreshRenderRuntimeReady?.present === false ? "runtime-ready-absent" : "-",
|
||||
whenSummary(arrayRecords(refreshRenderRuntimeReady?.when)[0]),
|
||||
stringOrNull(refreshRender.pipelineName) ?? "-",
|
||||
]);
|
||||
}
|
||||
const refreshApply = asOptionalRecord(refresh?.apply);
|
||||
if (refreshApply !== null) {
|
||||
rows.push([
|
||||
item.id,
|
||||
"refresh-apply",
|
||||
stringOrNull(refreshApply.resourceVersion) ?? stringOrNull(refreshApply.degradedReason) ?? "-",
|
||||
applyMetadataSummary(refreshApply),
|
||||
stringOrNull(refreshApply.pipelineName) ?? "-",
|
||||
]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -313,6 +334,25 @@ function boolMatch(value: unknown): string {
|
||||
return value === true ? "match" : value === false ? "mismatch" : "-";
|
||||
}
|
||||
|
||||
function whenSummary(value: Record<string, unknown> | undefined): string {
|
||||
if (value === undefined) return "-";
|
||||
const values = arrayText(value.values);
|
||||
return `${stringOrNull(value.input) ?? "-"} ${stringOrNull(value.operator) ?? "-"} ${values || "-"}`;
|
||||
}
|
||||
|
||||
function applyMetadataSummary(value: Record<string, unknown>): string {
|
||||
const annotations = asOptionalRecord(value.annotations);
|
||||
const labels = asOptionalRecord(value.labels);
|
||||
const annotation = annotations === null ? "-" : `${firstEntry(annotations)}`;
|
||||
const label = labels === null ? "-" : `${firstEntry(labels)}`;
|
||||
return `ann:${annotation} label:${label}`;
|
||||
}
|
||||
|
||||
function firstEntry(value: Record<string, unknown>): string {
|
||||
const [key, item] = Object.entries(value)[0] ?? [];
|
||||
return key === undefined ? "-" : `${key}=${stringOrNull(item) ?? "-"}`;
|
||||
}
|
||||
|
||||
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user