fix: prioritize follower taskrun visibility

This commit is contained in:
Codex
2026-07-03 20:17:21 +00:00
parent b3c039c8ca
commit 19d270b44b
5 changed files with 111 additions and 25 deletions
@@ -54,6 +54,8 @@ Argo closeout visibility must include the bounded reason for non-ready health, n
Tekton failure visibility must include bounded TaskRun detail, not only PipelineRun `Failed`: failed TaskRuns, active TaskRuns and slow TaskRuns with task name, reason and duration. Without this, performance/failure work cannot move past the PipelineRun gate. Tekton failure visibility must include bounded TaskRun detail, not only PipelineRun `Failed`: failed TaskRuns, active TaskRuns and slow TaskRuns with task name, reason and duration. Without this, performance/failure work cannot move past the PipelineRun gate.
Default stage timing tables must prioritize failed, active and slow TaskRun rows before ordinary succeeded TaskRuns when the row budget is tight. Do not truncate TaskRuns purely by Kubernetes start time if that hides the first failing or slow task.
When Argo exposes operation start/finish timestamps, stage timing rows should report the Argo operation duration directly. Missing timestamps still render `-`; do not infer Argo duration from total elapsed time or from unrelated runtime polling. When Argo exposes operation start/finish timestamps, stage timing rows should report the Argo operation duration directly. Missing timestamps still render `-`; do not infer Argo duration from total elapsed time or from unrelated runtime polling.
The automatic controller loop is non-blocking, so closeout acceleration cannot live only in the user-facing `--wait` path. Once a triggered PipelineRun has succeeded and required runtime/GitOps gates are not aligned, the in-cluster controller path should perform the same bounded target-side Argo refresh used by wait closeout; otherwise convergence depends on Argo's background poll interval and can exceed the 120s budget even when Tekton finished quickly. The automatic controller loop is non-blocking, so closeout acceleration cannot live only in the user-facing `--wait` path. Once a triggered PipelineRun has succeeded and required runtime/GitOps gates are not aligned, the in-cluster controller path should perform the same bounded target-side Argo refresh used by wait closeout; otherwise convergence depends on Argo's background poll interval and can exceed the 120s budget even when Tekton finished quickly.
+32 -1
View File
@@ -308,10 +308,41 @@ function compactTimings(timings) {
startedAt: stringOrNull(value.startedAt), startedAt: stringOrNull(value.startedAt),
finishedAt: stringOrNull(value.finishedAt), finishedAt: stringOrNull(value.finishedAt),
overBudget: typeof value.overBudget === "boolean" ? value.overBudget : null, overBudget: typeof value.overBudget === "boolean" ? value.overBudget : null,
stages: arrayRecords(value.stages).slice(0, maxTimingStages).map(compactStageTiming), stages: prioritizedStageTimings(arrayRecords(value.stages)).slice(0, maxTimingStages).map(compactStageTiming),
}; };
} }
function prioritizedStageTimings(stages) {
const priority = [];
const rest = [];
for (const stage of stages) {
if (isPriorityTaskStage(stage)) priority.push(stage);
else rest.push(stage);
}
const seen = new Set();
const out = [];
for (const stage of [...priority, ...rest]) {
const key = [
stringOrNull(stage.stage),
stringOrNull(stage.status),
stringOrNull(stage.source),
stringOrNull(stage.object),
].filter((item) => item !== null).join("|");
if (seen.has(key)) continue;
seen.add(key);
out.push(stage);
}
return out;
}
function isPriorityTaskStage(stage) {
const name = stringOrNull(stage.stage) || "";
if (!name.startsWith("task:")) return false;
const status = stringOrNull(stage.status) || "";
const seconds = numberOrNull(stage.seconds);
return status.startsWith("failed") || status === "running" || (seconds !== null && seconds > 60);
}
function compactStageTiming(stage) { function compactStageTiming(stage) {
return { return {
stage: stringOrNull(stage.stage), stage: stringOrNull(stage.stage),
+2 -4
View File
@@ -27,6 +27,7 @@ import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle"; import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native"; import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
import { argoApplicationReady, nativeArgoSummary, nativeGitMirrorReady, nativeGitMirrorRequired, nativeGitMirrorSummary, nativePipelineRunSummary, nativeRuntimeSummary, pipelineRunSucceeded, runtimeTargetShaFromWorkloads, runtimeWorkloadsReady } from "./cicd-native-summary"; import { argoApplicationReady, nativeArgoSummary, nativeGitMirrorReady, nativeGitMirrorRequired, nativeGitMirrorSummary, nativePipelineRunSummary, nativeRuntimeSummary, pipelineRunSucceeded, runtimeTargetShaFromWorkloads, runtimeWorkloadsReady } from "./cicd-native-summary";
import { prioritizedTaskRunItems } from "./cicd-taskruns";
import type { AdapterSummary, BranchFollowerAction, BranchFollowerDebugStep, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types"; import type { AdapterSummary, BranchFollowerAction, BranchFollowerDebugStep, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types";
import { import {
arrayField, arrayField,
@@ -2279,10 +2280,7 @@ function stageTimingsFromNativePayload(payload: Record<string, unknown> | null):
stages.push(stageTiming("pipelinerun", status, numberOrNull(tekton.durationSeconds), null, "tekton", stringOrNull(tekton.name))); stages.push(stageTiming("pipelinerun", status, numberOrNull(tekton.durationSeconds), null, "tekton", stringOrNull(tekton.name)));
} }
const taskRuns = asOptionalRecord(payload.taskRuns); const taskRuns = asOptionalRecord(payload.taskRuns);
const taskRunItems = taskRuns !== null && Array.isArray(taskRuns.items) ? taskRuns.items : []; for (const record of taskRuns === null ? [] : prioritizedTaskRunItems(taskRuns)) {
for (const item of taskRunItems) {
const record = asOptionalRecord(item);
if (record === null) continue;
const name = stringOrNull(record.pipelineTask) ?? stringOrNull(record.name) ?? "unknown"; const name = stringOrNull(record.pipelineTask) ?? stringOrNull(record.name) ?? "unknown";
const status = record.status === "True" ? "succeeded" : record.status === "False" ? `failed:${stringOrNull(record.reason) ?? "unknown"}` : "running"; const status = record.status === "True" ? "succeeded" : record.status === "False" ? `failed:${stringOrNull(record.reason) ?? "unknown"}` : "running";
stages.push(stageTiming(`task:${name}`, status, numberOrNull(record.durationSeconds), null, "tekton-taskrun", stringOrNull(record.name))); stages.push(stageTiming(`task:${name}`, status, numberOrNull(record.durationSeconds), null, "tekton-taskrun", stringOrNull(record.name)));
+1 -20
View File
@@ -6,6 +6,7 @@ import { runCommand, type CommandResult } from "./command";
import { repoRoot, rootPath } from "./config"; import { repoRoot, rootPath } from "./config";
import type { AdapterSummary, BranchFollowerDebugStep, BranchFollowerRegistry, FollowerSpec, FollowerState, K8sStateRead, ParsedOptions } from "./cicd-types"; import type { AdapterSummary, BranchFollowerDebugStep, BranchFollowerRegistry, FollowerSpec, FollowerState, K8sStateRead, ParsedOptions } from "./cicd-types";
import { renderControllerDebugJob, waitForJobShell } from "./cicd-controller-render"; import { renderControllerDebugJob, waitForJobShell } from "./cicd-controller-render";
import { taskRunItems } from "./cicd-taskruns";
import { redactText, shQuote } from "./platform-infra-ops-library"; import { redactText, shQuote } from "./platform-infra-ops-library";
type KubeScriptRunner = (registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number) => CommandResult; type KubeScriptRunner = (registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number) => CommandResult;
@@ -423,26 +424,6 @@ function compactStatusGates(payload: Record<string, unknown> | null): Record<str
}; };
} }
function taskRunItems(taskRuns: Record<string, unknown>, mode: "failed" | "active" | "slow"): Record<string, unknown>[] {
const explicit = mode === "failed" ? taskRuns.failedItems : mode === "active" ? taskRuns.activeItems : taskRuns.slowItems;
const explicitItems = arrayRecords(explicit);
if (explicitItems.length > 0) return explicitItems.slice(0, 5).map(compactTaskRunItem);
const items = arrayRecords(taskRuns.items);
if (mode === "failed") return items.filter((item) => item.status === "False").slice(0, 5).map(compactTaskRunItem);
if (mode === "active") return items.filter((item) => item.status !== "True" && item.status !== "False").slice(0, 5).map(compactTaskRunItem);
return arrayRecords(asOptionalRecord(taskRuns.performance)?.slowTaskRuns).slice(0, 5).map(compactTaskRunItem);
}
function compactTaskRunItem(item: Record<string, unknown>): Record<string, unknown> {
return {
name: stringOrNull(item.name),
pipelineTask: stringOrNull(item.pipelineTask),
status: stringOrNull(item.status),
reason: stringOrNull(item.reason),
durationSeconds: numberOrNull(item.durationSeconds),
};
}
function arrayRecords(value: unknown): Record<string, unknown>[] { 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)) : []; return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
} }
+74
View File
@@ -0,0 +1,74 @@
// SPEC: PJ2026-01060703 CI/CD TaskRun summaries.
// Responsibility: shared bounded TaskRun prioritization for branch-follower status/debug visibility.
export type TaskRunMode = "failed" | "active" | "slow";
export function taskRunItems(taskRuns: Record<string, unknown>, mode: TaskRunMode, limit = 5): Record<string, unknown>[] {
const explicit = mode === "failed" ? taskRuns.failedItems : mode === "active" ? taskRuns.activeItems : taskRuns.slowItems;
const explicitItems = arrayRecords(explicit);
if (explicitItems.length > 0) return explicitItems.slice(0, limit).map(compactTaskRunItem);
const items = arrayRecords(taskRuns.items);
if (mode === "failed") return items.filter((item) => item.status === "False").slice(0, limit).map(compactTaskRunItem);
if (mode === "active") return items.filter((item) => item.status !== "True" && item.status !== "False").slice(0, limit).map(compactTaskRunItem);
const slowItems = arrayRecords(asOptionalRecord(taskRuns.performance)?.slowTaskRuns);
if (slowItems.length > 0) return slowItems.slice(0, limit).map(compactTaskRunItem);
return items.filter((item) => {
const seconds = numberOrNull(item.durationSeconds);
return seconds !== null && seconds > 60;
}).slice(0, limit).map(compactTaskRunItem);
}
export function prioritizedTaskRunItems(taskRuns: Record<string, unknown>, limit = 16): Record<string, unknown>[] {
const prioritized = [
...taskRunItems(taskRuns, "failed", limit),
...taskRunItems(taskRuns, "active", limit),
...taskRunItems(taskRuns, "slow", limit),
...arrayRecords(taskRuns.items).map(compactTaskRunItem),
];
const seen = new Set<string>();
const out: Record<string, unknown>[] = [];
for (const item of prioritized) {
const key = taskRunKey(item);
if (seen.has(key)) continue;
seen.add(key);
out.push(item);
if (out.length >= limit) break;
}
return out;
}
export function compactTaskRunItem(item: Record<string, unknown>): Record<string, unknown> {
return {
name: stringOrNull(item.name),
pipelineTask: stringOrNull(item.pipelineTask),
status: stringOrNull(item.status),
reason: stringOrNull(item.reason),
durationSeconds: numberOrNull(item.durationSeconds),
};
}
function taskRunKey(item: Record<string, unknown>): string {
const key = [
stringOrNull(item.name),
stringOrNull(item.pipelineTask),
stringOrNull(item.status),
stringOrNull(item.reason),
].filter((value) => value !== null).join("|");
return key.length > 0 ? key : JSON.stringify(item);
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
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)) : [];
}
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;
}