153 lines
7.0 KiB
TypeScript
153 lines
7.0 KiB
TypeScript
// 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) ?? 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,
|
|
};
|
|
}
|
|
|
|
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 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,
|
|
pipeline: compactPipelineEvidence(pipeline),
|
|
refreshBoundedReason: refresh === null ? "missing-control-plane-refresh-capability-evidence; status refresh job is reported separately at top-level refresh/liveRefresh" : null,
|
|
refresh: refresh === null
|
|
? null
|
|
: {
|
|
...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 compactPipelineEvidence(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
if (value === null) return null;
|
|
const metadata = asOptionalRecord(value.metadata);
|
|
const spec = asOptionalRecord(value.spec);
|
|
return {
|
|
metadata: { name: stringOrNull(metadata?.name) },
|
|
spec: {
|
|
runtimeReadyTask: compactRefreshRuntimeReady(asOptionalRecord(spec?.runtimeReadyTask)),
|
|
},
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
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) : [];
|
|
}
|