Files
pikasTech-unidesk/scripts/src/cicd-evidence.ts
T

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) : [];
}