cicd track follower timing start and finish

This commit is contained in:
Codex
2026-07-03 11:31:48 +00:00
parent 63de02e3a2
commit 8557ab3553
3 changed files with 55 additions and 11 deletions
+2
View File
@@ -291,6 +291,8 @@ export interface FollowerState {
totalSeconds: number | null;
totalStatus: string;
totalSource: string;
startedAt: string | null;
finishedAt: string | null;
overBudget: boolean | null;
stages: StageTiming[];
};
+52 -10
View File
@@ -828,7 +828,7 @@ async function decideAndMaybeTrigger(
decision,
dryRun: options.dryRun,
updatedAt: new Date().toISOString(),
timings: buildFollowerTimings(follower, live, triggerCommand),
timings: buildFollowerTimings(follower, live, triggerCommand, null, phase),
warnings,
next: followerNextCommands(follower),
command: triggerCommand ?? {
@@ -1034,6 +1034,8 @@ function nativeK8sStageFailure(
jobName,
sourceCommit: observedSha,
ok: false,
startedAt: startedAt === undefined ? null : new Date(startedAt).toISOString(),
finishedAt: new Date().toISOString(),
elapsedMs: startedAt === undefined ? job.elapsedMs : Date.now() - startedAt,
payload,
job,
@@ -1109,6 +1111,9 @@ function nativeTektonTriggerResult(input: {
? `native PipelineRun ${input.pipelineRun} is still running; query status/events/logs for closeout`
: `native PipelineRun ${input.pipelineRun} submitted`;
const ok = !failed && (input.closeout === null || input.closeout.completed === true);
const finishedAt = failed || input.result.timedOut || input.closeout?.completed === true || input.closeout?.timedOut === true
? new Date().toISOString()
: null;
return {
ok,
completed: input.closeout?.completed === true,
@@ -1124,6 +1129,8 @@ function nativeTektonTriggerResult(input: {
wait: input.wait,
pipelineRunCompleted,
stillRunning,
startedAt: new Date(input.startedAt).toISOString(),
finishedAt,
elapsedMs: Date.now() - input.startedAt,
closeout: input.closeout,
statusAuthority: "kubernetes-api-serviceaccount",
@@ -1192,6 +1199,9 @@ async function executeNativeSentinelTrigger(registry: BranchFollowerRegistry, fo
? `native sentinel PipelineRun ${pipelineRun} is still running; query status/events/logs for closeout`
: `native sentinel PipelineRun ${pipelineRun} submitted`;
const ok = !failed && (closeout === null || closeout.completed === true);
const finishedAt = failed || result.timedOut || closeout?.completed === true || closeout?.timedOut === true
? new Date().toISOString()
: null;
return {
ok,
completed: closeout?.completed === true,
@@ -1207,6 +1217,8 @@ async function executeNativeSentinelTrigger(registry: BranchFollowerRegistry, fo
wait: options.wait,
pipelineRunCompleted,
stillRunning,
startedAt: new Date(startedAt).toISOString(),
finishedAt,
elapsedMs: Date.now() - startedAt,
closeout,
statusAuthority: "kubernetes-api-serviceaccount",
@@ -1775,7 +1787,7 @@ function mergeFollowerStatus(
stateConfigMap: registry.controller.stateConfigMapName,
live: liveRequested,
message: live?.message ?? stringOrNull(stored.decision) ?? "no controller state yet",
timings: live === null ? asOptionalRecord(stored.timings) : buildFollowerTimings(follower, live, undefined, asOptionalRecord(stored.timings)),
timings: live === null ? asOptionalRecord(stored.timings) : buildFollowerTimings(follower, live, undefined, asOptionalRecord(stored.timings), phase),
warnings: Array.isArray(stored.warnings) ? stored.warnings.slice(0, 6) : [],
next: followerNextCommands(follower),
};
@@ -2072,8 +2084,9 @@ function buildFollowerTimings(
live: AdapterSummary,
triggerCommand: Record<string, unknown> | undefined,
storedTimings?: Record<string, unknown> | null,
phase?: BranchFollowerPhase,
): FollowerState["timings"] {
const total = totalTimingFromCommand(triggerCommand) ?? totalTimingFromStored(storedTimings);
const total = totalTimingFromCommand(triggerCommand, phase) ?? totalTimingFromStored(storedTimings, phase);
const stages = dedupeTimingStages([
...stageTimingsFromCommand(triggerCommand),
...stageTimingsFromNativePayload(asOptionalRecord(live.payload)),
@@ -2083,6 +2096,8 @@ function buildFollowerTimings(
totalSeconds: total?.seconds ?? null,
totalStatus: total?.status ?? "unknown",
totalSource: total?.source ?? "unavailable",
startedAt: total?.startedAt ?? null,
finishedAt: total?.finishedAt ?? null,
overBudget: total === null ? null : total.seconds > follower.budgets.endToEndSeconds,
stages,
};
@@ -2094,6 +2109,8 @@ function compactTimings(timings: FollowerState["timings"]): FollowerState["timin
totalSeconds: timings.totalSeconds,
totalStatus: timings.totalStatus,
totalSource: timings.totalSource,
startedAt: timings.startedAt,
finishedAt: timings.finishedAt,
overBudget: timings.overBudget,
stages: timings.stages.slice(0, 24).map((stage) => ({
stage: stage.stage,
@@ -2106,9 +2123,11 @@ function compactTimings(timings: FollowerState["timings"]): FollowerState["timin
};
}
function totalTimingFromCommand(command: Record<string, unknown> | undefined): { seconds: number; status: string; source: string } | null {
function totalTimingFromCommand(command: Record<string, unknown> | undefined, phase?: BranchFollowerPhase): { seconds: number; status: string; source: string; startedAt: string | null; finishedAt: string | null } | null {
if (command === undefined) return null;
const seconds = secondsFromMs(numberOrNull(command.elapsedMs));
const startedAt = stringOrNull(command.startedAt);
const finishedAt = stringOrNull(command.finishedAt);
const seconds = totalSecondsFromRange(startedAt, finishedAt) ?? secondsFromMs(numberOrNull(command.elapsedMs));
if (seconds === null) return null;
const closeout = asOptionalRecord(command.closeout);
const exitCode = numberOrNull(command.exitCode);
@@ -2122,21 +2141,44 @@ function totalTimingFromCommand(command: Record<string, unknown> | undefined): {
? "running"
: command.pipelineRunCompleted === true
? "ci-completed"
: "submitted";
return { seconds, status, source: stringOrNull(command.mode) ?? stringOrNull(command.status) ?? "command" };
: phase === undefined
? "submitted"
: phase.toLowerCase();
return { seconds, status, source: stringOrNull(command.mode) ?? stringOrNull(command.status) ?? "command", startedAt, finishedAt };
}
function totalTimingFromStored(storedTimings: Record<string, unknown> | null | undefined): { seconds: number; status: string; source: string } | null {
function totalTimingFromStored(storedTimings: Record<string, unknown> | null | undefined, phase?: BranchFollowerPhase): { seconds: number; status: string; source: string; startedAt: string | null; finishedAt: string | null } | null {
if (storedTimings === null || storedTimings === undefined) return null;
const seconds = numberOrNull(storedTimings.totalSeconds);
const startedAt = stringOrNull(storedTimings.startedAt);
const finishedAt = stringOrNull(storedTimings.finishedAt);
const seconds = totalSecondsFromRange(startedAt, finishedAt) ?? numberOrNull(storedTimings.totalSeconds);
if (seconds === null) return null;
return {
seconds,
status: stringOrNull(storedTimings.totalStatus) ?? "recorded",
status: finishedAt === null && phase !== undefined && !terminalPhase(phase) ? phase.toLowerCase() : stringOrNull(storedTimings.totalStatus) ?? "recorded",
source: stringOrNull(storedTimings.totalSource) ?? "stored-state",
startedAt,
finishedAt,
};
}
function totalSecondsFromRange(startedAt: string | null, finishedAt: string | null): number | null {
const startedMs = timestampMs(startedAt);
if (startedMs === null) return null;
const finishedMs = timestampMs(finishedAt) ?? Date.now();
return finishedMs >= startedMs ? roundSeconds((finishedMs - startedMs) / 1000) : null;
}
function timestampMs(value: string | null): number | null {
if (value === null) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
function terminalPhase(phase: BranchFollowerPhase): boolean {
return phase === "Succeeded" || phase === "Failed" || phase === "Blocked" || phase === "Skipped" || phase === "Noop";
}
function stageTimingsFromNativePayload(payload: Record<string, unknown> | null): StageTiming[] {
if (payload === null) return [];
const stages: StageTiming[] = [];