fix: prioritize follower taskrun visibility
This commit is contained in:
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -308,10 +308,41 @@ function compactTimings(timings) {
|
||||
startedAt: stringOrNull(value.startedAt),
|
||||
finishedAt: stringOrNull(value.finishedAt),
|
||||
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) {
|
||||
return {
|
||||
stage: stringOrNull(stage.stage),
|
||||
|
||||
@@ -27,6 +27,7 @@ import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
|
||||
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
|
||||
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
|
||||
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 {
|
||||
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)));
|
||||
}
|
||||
const taskRuns = asOptionalRecord(payload.taskRuns);
|
||||
const taskRunItems = taskRuns !== null && Array.isArray(taskRuns.items) ? taskRuns.items : [];
|
||||
for (const item of taskRunItems) {
|
||||
const record = asOptionalRecord(item);
|
||||
if (record === null) continue;
|
||||
for (const record of taskRuns === null ? [] : prioritizedTaskRunItems(taskRuns)) {
|
||||
const name = stringOrNull(record.pipelineTask) ?? stringOrNull(record.name) ?? "unknown";
|
||||
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)));
|
||||
|
||||
@@ -6,6 +6,7 @@ import { runCommand, type CommandResult } from "./command";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import type { AdapterSummary, BranchFollowerDebugStep, BranchFollowerRegistry, FollowerSpec, FollowerState, K8sStateRead, ParsedOptions } from "./cicd-types";
|
||||
import { renderControllerDebugJob, waitForJobShell } from "./cicd-controller-render";
|
||||
import { taskRunItems } from "./cicd-taskruns";
|
||||
import { redactText, shQuote } from "./platform-infra-ops-library";
|
||||
|
||||
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>[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user