diff --git a/.agents/skills/unidesk-cicd/references/branch-follower.md b/.agents/skills/unidesk-cicd/references/branch-follower.md index 0250c20d..f892a5d9 100644 --- a/.agents/skills/unidesk-cicd/references/branch-follower.md +++ b/.agents/skills/unidesk-cicd/references/branch-follower.md @@ -66,6 +66,8 @@ Stage timing must be queryable through normal CLI output, not only raw JSON. `st `timings.totalSeconds` is the authoritative end-to-end wall-clock measurement for a triggered run: measure from `timings.startedAt` until `timings.finishedAt`, or until query time while closeout is still running. Do not compute total by summing stage rows, because stage rows can overlap, omit external waiting, or be reported by different native objects. +If a controller loop resumes an already-triggered source change and the stored state predates `startedAt`, it may recover the total timer from the native PipelineRun start time and then persist that recovered start point. + State machine phases are `Observed`, `Noop`, `PendingTrigger`, `Triggering`, `ClosingOut`, `Succeeded`, `Failed`, `Superseded`, `Blocked`, and `Skipped`. Status and decision inputs are Kubernetes-native: diff --git a/scripts/src/cicd.ts b/scripts/src/cicd.ts index 8d7fb280..292e06f7 100644 --- a/scripts/src/cicd.ts +++ b/scripts/src/cicd.ts @@ -2086,10 +2086,11 @@ function buildFollowerTimings( storedTimings?: Record | null, phase?: BranchFollowerPhase, ): FollowerState["timings"] { - const total = totalTimingFromCommand(triggerCommand, phase) ?? totalTimingFromStored(storedTimings, phase); + const nativePayload = asOptionalRecord(live.payload); + const total = totalTimingFromCommand(triggerCommand, phase) ?? totalTimingFromStored(storedTimings, phase) ?? totalTimingFromLivePayload(nativePayload, phase); const stages = dedupeTimingStages([ ...stageTimingsFromCommand(triggerCommand), - ...stageTimingsFromNativePayload(asOptionalRecord(live.payload)), + ...stageTimingsFromNativePayload(nativePayload), ]).slice(0, 24); return { budgetSeconds: follower.budgets.endToEndSeconds, @@ -2162,6 +2163,23 @@ function totalTimingFromStored(storedTimings: Record | null | u }; } +function totalTimingFromLivePayload(payload: Record | null, phase?: BranchFollowerPhase): { seconds: number; status: string; source: string; startedAt: string | null; finishedAt: string | null } | null { + if (payload === null) return null; + const tekton = asOptionalRecord(payload.tekton); + const startedAt = stringOrNull(tekton?.startTime); + if (startedAt === null) return null; + const finishedAt = phase !== undefined && terminalPhase(phase) ? new Date().toISOString() : null; + const seconds = totalSecondsFromRange(startedAt, finishedAt); + if (seconds === null) return null; + return { + seconds, + status: phase === undefined ? "native-observed" : phase.toLowerCase(), + source: "tekton-pipelinerun-start", + startedAt, + finishedAt, + }; +} + function totalSecondsFromRange(startedAt: string | null, finishedAt: string | null): number | null { const startedMs = timestampMs(startedAt); if (startedMs === null) return null;