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
+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,