cicd expose branch follower stage timings
This commit is contained in:
@@ -202,6 +202,7 @@ export interface NativeObjectBundle {
|
||||
errors: string[];
|
||||
exitCode: number | null;
|
||||
timedOut: boolean;
|
||||
elapsedMs: number;
|
||||
stdoutTail: string;
|
||||
stderrTail: string;
|
||||
}
|
||||
@@ -244,6 +245,15 @@ export interface NativeK8sJobResult {
|
||||
parsedDownstreamCliOutput: false;
|
||||
}
|
||||
|
||||
export interface StageTiming {
|
||||
stage: string;
|
||||
status: string;
|
||||
seconds: number | null;
|
||||
budgetSeconds: number | null;
|
||||
source: string;
|
||||
object: string | null;
|
||||
}
|
||||
|
||||
export interface FollowerState {
|
||||
id: string;
|
||||
adapter: string;
|
||||
@@ -276,6 +286,14 @@ export interface FollowerState {
|
||||
decision: string;
|
||||
dryRun: boolean;
|
||||
updatedAt: string;
|
||||
timings: {
|
||||
budgetSeconds: number;
|
||||
totalSeconds: number | null;
|
||||
totalStatus: string;
|
||||
totalSource: string;
|
||||
overBudget: boolean | null;
|
||||
stages: StageTiming[];
|
||||
};
|
||||
warnings: string[];
|
||||
next: Record<string, string>;
|
||||
command?: Record<string, unknown>;
|
||||
|
||||
+238
-9
@@ -22,7 +22,7 @@ import { transPath } from "./hwlab-node/runtime-common";
|
||||
import { configRefGraph, resolveConfigRefString } from "./ops/config-refs";
|
||||
import { renderControllerManifests, renderControllerReconcileJob, waitForJobShell } from "./cicd-controller-render";
|
||||
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
|
||||
import type { AdapterSummary, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeObjectBundle, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, TriggerResult } from "./cicd-types";
|
||||
import type { AdapterSummary, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeObjectBundle, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types";
|
||||
import {
|
||||
arrayField,
|
||||
asRecord,
|
||||
@@ -828,6 +828,7 @@ async function decideAndMaybeTrigger(
|
||||
decision,
|
||||
dryRun: options.dryRun,
|
||||
updatedAt: new Date().toISOString(),
|
||||
timings: buildFollowerTimings(follower, live, triggerCommand),
|
||||
warnings,
|
||||
next: followerNextCommands(follower),
|
||||
command: triggerCommand ?? {
|
||||
@@ -890,7 +891,7 @@ async function executeNativeHwlabNodeTrigger(registry: BranchFollowerRegistry, f
|
||||
const startedAt = Date.now();
|
||||
const sync = runNativeGitMirrorStage(follower, observedSha, "sync", Math.min(remainingSeconds(startedAt, timeoutSeconds), Math.max(5, follower.budgets.sourceSyncSeconds)));
|
||||
if (sync !== null && !sync.result.ok) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native git-mirror sync failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native git-mirror sync failed", startedAt);
|
||||
}
|
||||
const result = runNativeTektonPipelineRun(namespace, pipelineRun, manifest, options.wait, remainingSeconds(startedAt, timeoutSeconds));
|
||||
const payload = parseJsonObject(result.stdout) ?? {};
|
||||
@@ -900,7 +901,7 @@ async function executeNativeHwlabNodeTrigger(registry: BranchFollowerRegistry, f
|
||||
? runNativeGitMirrorStage(follower, observedSha, "flush", remainingSeconds(startedAt, timeoutSeconds))
|
||||
: null;
|
||||
if (flush !== null && !flush.result.ok) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-flush", flush.jobName, flush.result, { action: "flush" }, "native git-mirror flush failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-flush", flush.jobName, flush.result, { action: "flush" }, "native git-mirror flush failed", startedAt);
|
||||
}
|
||||
const closeout = !failed && options.wait && pipelineRunCompleted
|
||||
? await waitNativeFollowerCloseout(registry, follower, observedSha, options, remainingSeconds(startedAt, timeoutSeconds))
|
||||
@@ -913,6 +914,7 @@ async function executeNativeHwlabNodeTrigger(registry: BranchFollowerRegistry, f
|
||||
stageRef: `${follower.source.snapshotPrefix.replace(/\/+$/u, "")}/${observedSha}`,
|
||||
wait: options.wait,
|
||||
result,
|
||||
startedAt,
|
||||
payload: {
|
||||
...payload,
|
||||
nativeCapabilities: {
|
||||
@@ -938,7 +940,7 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
|
||||
const jobPrefix = `agentrun-bf-${spec.nodeId.toLowerCase()}-${spec.lane}`;
|
||||
const sync = runNativeGitMirrorStage(follower, observedSha, "sync", Math.min(remainingSeconds(startedAt, timeoutSeconds), Math.max(5, follower.budgets.sourceSyncSeconds)));
|
||||
if (sync !== null && !sync.result.ok) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native AgentRun git-mirror sync failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native AgentRun git-mirror sync failed", startedAt);
|
||||
}
|
||||
const buildJob = `${jobPrefix}-build-${observedSha.slice(0, 12)}`.slice(0, 63);
|
||||
const build = runNativeK8sJob(spec.ci.namespace, buildJob, yamlLaneK3sBuildImageJobManifest(spec, observedSha, buildJob), Math.min(remainingSeconds(startedAt, timeoutSeconds), Math.max(60, spec.deployment.manager.imageBuild.timeoutSeconds)), "buildkit");
|
||||
@@ -946,7 +948,7 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
|
||||
const digest = stringOrNull(buildPayload.digest);
|
||||
const envIdentity = stringOrNull(buildPayload.envIdentity);
|
||||
if (!build.ok || digest === null || envIdentity === null) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "image-build", buildJob, build, buildPayload, "native AgentRun image build failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "image-build", buildJob, build, buildPayload, "native AgentRun image build failed", startedAt);
|
||||
}
|
||||
const image = agentRunImageArtifact(spec, { sourceCommit: observedSha, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
|
||||
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit: observedSha, image });
|
||||
@@ -954,11 +956,11 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
|
||||
const publish = runNativeK8sJob(spec.gitMirror.namespace, publishJob, yamlLaneGitopsPublishJobManifest(spec, renderedFiles, publishJob), remainingSeconds(startedAt, timeoutSeconds), "publish");
|
||||
const publishPayload = yamlLaneGitopsPublishPayloadFromProbe({ logsTail: stringOrNull(publish.logsTail) ?? "" });
|
||||
if (!publish.ok || publishPayload.ok === false || stringOrNull(publishPayload.gitopsCommit) === null) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "gitops-publish", publishJob, publish, publishPayload, "native AgentRun GitOps publish failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "gitops-publish", publishJob, publish, publishPayload, "native AgentRun GitOps publish failed", startedAt);
|
||||
}
|
||||
const flush = runNativeGitMirrorStage(follower, observedSha, "flush", remainingSeconds(startedAt, timeoutSeconds));
|
||||
if (flush !== null && !flush.result.ok) {
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-flush", flush.jobName, flush.result, { action: "flush" }, "native AgentRun git-mirror flush failed");
|
||||
return nativeK8sStageFailure(follower, observedSha, "git-mirror-flush", flush.jobName, flush.result, { action: "flush" }, "native AgentRun git-mirror flush failed", startedAt);
|
||||
}
|
||||
const pipelineRun = agentRunPipelineRunName(spec, observedSha);
|
||||
const tektonResult = runNativeTektonPipelineRun(follower.nativeStatus.tekton.namespace, pipelineRun, yamlLanePipelineRunManifest(spec, observedSha, pipelineRun), options.wait, remainingSeconds(startedAt, timeoutSeconds));
|
||||
@@ -976,13 +978,14 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
|
||||
stageRef,
|
||||
wait: options.wait,
|
||||
result: tektonResult,
|
||||
startedAt,
|
||||
payload: {
|
||||
...tektonPayload,
|
||||
agentrun: {
|
||||
configPath,
|
||||
gitMirrorSync: sync === null ? null : { jobName: sync.jobName, payload: sync.result },
|
||||
imageBuild: { jobName: buildJob, payload: buildPayload },
|
||||
gitopsPublish: { jobName: publishJob, payload: publishPayload },
|
||||
imageBuild: { jobName: buildJob, result: build, payload: buildPayload },
|
||||
gitopsPublish: { jobName: publishJob, result: publish, payload: publishPayload },
|
||||
gitMirrorFlush: flush === null ? null : { jobName: flush.jobName, payload: flush.result },
|
||||
},
|
||||
},
|
||||
@@ -1010,6 +1013,7 @@ function nativeK8sStageFailure(
|
||||
job: NativeK8sJobResult,
|
||||
payload: Record<string, unknown>,
|
||||
message: string,
|
||||
startedAt?: number,
|
||||
): TriggerResult {
|
||||
const detail = [
|
||||
message,
|
||||
@@ -1030,6 +1034,7 @@ function nativeK8sStageFailure(
|
||||
jobName,
|
||||
sourceCommit: observedSha,
|
||||
ok: false,
|
||||
elapsedMs: startedAt === undefined ? job.elapsedMs : Date.now() - startedAt,
|
||||
payload,
|
||||
job,
|
||||
statusAuthority: "kubernetes-api-serviceaccount",
|
||||
@@ -1084,6 +1089,7 @@ function nativeTektonTriggerResult(input: {
|
||||
stageRef: string;
|
||||
wait: boolean;
|
||||
result: CommandResult;
|
||||
startedAt: number;
|
||||
payload: Record<string, unknown>;
|
||||
closeout: NativeCloseoutWaitResult | null;
|
||||
successMessage: string;
|
||||
@@ -1118,6 +1124,7 @@ function nativeTektonTriggerResult(input: {
|
||||
wait: input.wait,
|
||||
pipelineRunCompleted,
|
||||
stillRunning,
|
||||
elapsedMs: Date.now() - input.startedAt,
|
||||
closeout: input.closeout,
|
||||
statusAuthority: "kubernetes-api-serviceaccount",
|
||||
parsedDownstreamCliOutput: false,
|
||||
@@ -1200,6 +1207,7 @@ async function executeNativeSentinelTrigger(registry: BranchFollowerRegistry, fo
|
||||
wait: options.wait,
|
||||
pipelineRunCompleted,
|
||||
stillRunning,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
closeout,
|
||||
statusAuthority: "kubernetes-api-serviceaccount",
|
||||
parsedDownstreamCliOutput: false,
|
||||
@@ -1387,6 +1395,7 @@ async function readAdapterStatus(registry: BranchFollowerRegistry, follower: Fol
|
||||
planArtifacts: bundle.planArtifacts,
|
||||
argo: nativeArgoSummary(bundle.argoApplication),
|
||||
runtime: nativeRuntimeSummary(follower.nativeStatus.runtime, bundle.workloads),
|
||||
timings: { statusRead: { elapsedMs: bundle.elapsedMs, budgetSeconds: timeoutSeconds } },
|
||||
errors: bundle.errors,
|
||||
statusAuthority: "k8s-native",
|
||||
parsedDownstreamCliOutput: false,
|
||||
@@ -1452,6 +1461,7 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
|
||||
"export NATIVE_CICD_SCRIPT_DIR REPO_PATH SOURCE_BRANCH REPOSITORY SNAPSHOT_PREFIX GITOPS_BRANCH TEKTON_NAMESPACE PIPELINE_RUN_PREFIX ARGO_NAMESPACE ARGO_APPLICATION WORKLOAD_REFS_B64",
|
||||
"\"$tmpdir/read-native-bundle.sh\"",
|
||||
].join("\n");
|
||||
const startedAt = Date.now();
|
||||
const result = runKubeScript(registry, options, script, "", Math.max(5, timeoutSeconds) * 1000);
|
||||
const parsed = parseNativeBundleLines(result.stdout);
|
||||
const sourceRecord = asOptionalRecord(parsed.objects.source);
|
||||
@@ -1475,6 +1485,7 @@ function readNativeObjectBundle(registry: BranchFollowerRegistry, follower: Foll
|
||||
],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
stdoutTail: redactText(tailText(result.stdout, 1000)),
|
||||
stderrTail: redactText(tailText(result.stderr, 1000)),
|
||||
};
|
||||
@@ -1633,6 +1644,7 @@ function nativePipelineRunSummary(pipelineRun: Record<string, unknown> | null):
|
||||
reason: stringOrNull(condition?.reason),
|
||||
startTime: stringOrNull(status?.startTime),
|
||||
completionTime: stringOrNull(status?.completionTime),
|
||||
durationSeconds: numberOrNull(status?.durationSeconds),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1763,6 +1775,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)),
|
||||
warnings: Array.isArray(stored.warnings) ? stored.warnings.slice(0, 6) : [],
|
||||
next: followerNextCommands(follower),
|
||||
};
|
||||
@@ -1950,6 +1963,7 @@ function compactFollowerStateForConfigMap(state: FollowerState): Record<string,
|
||||
decision: state.decision,
|
||||
dryRun: state.dryRun,
|
||||
updatedAt: state.updatedAt,
|
||||
timings: compactTimings(state.timings),
|
||||
warnings: compactStateWarnings(state.warnings),
|
||||
stateFormat: "compact-v1",
|
||||
command: compactStateCommand(state.command),
|
||||
@@ -2053,6 +2067,188 @@ function compactSourcePayload(source: Record<string, unknown> | null): Record<st
|
||||
};
|
||||
}
|
||||
|
||||
function buildFollowerTimings(
|
||||
follower: FollowerSpec,
|
||||
live: AdapterSummary,
|
||||
triggerCommand: Record<string, unknown> | undefined,
|
||||
storedTimings?: Record<string, unknown> | null,
|
||||
): FollowerState["timings"] {
|
||||
const total = totalTimingFromCommand(triggerCommand) ?? totalTimingFromStored(storedTimings);
|
||||
const stages = dedupeTimingStages([
|
||||
...stageTimingsFromCommand(triggerCommand),
|
||||
...stageTimingsFromNativePayload(asOptionalRecord(live.payload)),
|
||||
]).slice(0, 24);
|
||||
return {
|
||||
budgetSeconds: follower.budgets.endToEndSeconds,
|
||||
totalSeconds: total?.seconds ?? null,
|
||||
totalStatus: total?.status ?? "unknown",
|
||||
totalSource: total?.source ?? "unavailable",
|
||||
overBudget: total === null ? null : total.seconds > follower.budgets.endToEndSeconds,
|
||||
stages,
|
||||
};
|
||||
}
|
||||
|
||||
function compactTimings(timings: FollowerState["timings"]): FollowerState["timings"] {
|
||||
return {
|
||||
budgetSeconds: timings.budgetSeconds,
|
||||
totalSeconds: timings.totalSeconds,
|
||||
totalStatus: timings.totalStatus,
|
||||
totalSource: timings.totalSource,
|
||||
overBudget: timings.overBudget,
|
||||
stages: timings.stages.slice(0, 24).map((stage) => ({
|
||||
stage: stage.stage,
|
||||
status: stage.status,
|
||||
seconds: stage.seconds,
|
||||
budgetSeconds: stage.budgetSeconds,
|
||||
source: stage.source,
|
||||
object: stage.object,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function totalTimingFromCommand(command: Record<string, unknown> | undefined): { seconds: number; status: string; source: string } | null {
|
||||
if (command === undefined) return null;
|
||||
const seconds = secondsFromMs(numberOrNull(command.elapsedMs));
|
||||
if (seconds === null) return null;
|
||||
const closeout = asOptionalRecord(command.closeout);
|
||||
const exitCode = numberOrNull(command.exitCode);
|
||||
const status = command.ok === false || (exitCode !== null && exitCode !== 0)
|
||||
? "failed"
|
||||
: command.timedOut === true || closeout?.timedOut === true
|
||||
? "timed-out"
|
||||
: closeout?.completed === true || command.completed === true
|
||||
? "completed"
|
||||
: command.stillRunning === true
|
||||
? "running"
|
||||
: command.pipelineRunCompleted === true
|
||||
? "ci-completed"
|
||||
: "submitted";
|
||||
return { seconds, status, source: stringOrNull(command.mode) ?? stringOrNull(command.status) ?? "command" };
|
||||
}
|
||||
|
||||
function totalTimingFromStored(storedTimings: Record<string, unknown> | null | undefined): { seconds: number; status: string; source: string } | null {
|
||||
if (storedTimings === null || storedTimings === undefined) return null;
|
||||
const seconds = numberOrNull(storedTimings.totalSeconds);
|
||||
if (seconds === null) return null;
|
||||
return {
|
||||
seconds,
|
||||
status: stringOrNull(storedTimings.totalStatus) ?? "recorded",
|
||||
source: stringOrNull(storedTimings.totalSource) ?? "stored-state",
|
||||
};
|
||||
}
|
||||
|
||||
function stageTimingsFromNativePayload(payload: Record<string, unknown> | null): StageTiming[] {
|
||||
if (payload === null) return [];
|
||||
const stages: StageTiming[] = [];
|
||||
const statusRead = asOptionalRecord(asOptionalRecord(payload.timings)?.statusRead);
|
||||
stages.push(stageTiming("status-read", "ok", secondsFromMs(numberOrNull(statusRead?.elapsedMs)), numberOrNull(statusRead?.budgetSeconds), "native-status", null));
|
||||
const gitMirror = asOptionalRecord(payload.gitMirror);
|
||||
if (gitMirror !== null) {
|
||||
const status = gitMirror.pendingFlush === true ? "pending-flush" : gitMirror.githubInSync === true && gitMirror.sourceSnapshotReady === true ? "ready" : "not-ready";
|
||||
stages.push(stageTiming("git-mirror", status, null, null, "git-mirror-cache", stringOrNull(gitMirror.gitopsBranch) ?? stringOrNull(gitMirror.sourceBranch)));
|
||||
}
|
||||
const tekton = asOptionalRecord(payload.tekton);
|
||||
if (tekton !== null) {
|
||||
const status = tekton.succeeded === true ? "succeeded" : tekton.succeeded === false ? `failed:${stringOrNull(tekton.reason) ?? "unknown"}` : "running";
|
||||
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;
|
||||
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)));
|
||||
}
|
||||
const argo = asOptionalRecord(payload.argo);
|
||||
if (argo !== null) {
|
||||
stages.push(stageTiming("argo", `${stringOrNull(argo.syncStatus) ?? "unknown"}/${stringOrNull(argo.healthStatus) ?? "unknown"}`, null, null, "argocd", stringOrNull(argo.name)));
|
||||
}
|
||||
const runtime = asOptionalRecord(payload.runtime);
|
||||
if (runtime !== null) {
|
||||
stages.push(stageTiming("runtime", runtime.ready === true ? "ready" : "not-ready", null, null, "kubernetes-workload", stringOrNull(runtime.namespace)));
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
|
||||
function stageTimingsFromCommand(command: Record<string, unknown> | undefined): StageTiming[] {
|
||||
if (command === undefined) return [];
|
||||
const stages: StageTiming[] = [];
|
||||
const phase = stringOrNull(command.phase);
|
||||
const jobStage = phase === null ? null : k8sJobTiming(phase, asOptionalRecord(command.job), stringOrNull(command.jobName));
|
||||
if (jobStage !== null) stages.push(jobStage);
|
||||
const payload = asOptionalRecord(command.payload);
|
||||
if (payload !== null) {
|
||||
const capabilities = asOptionalRecord(payload.nativeCapabilities);
|
||||
for (const stage of [
|
||||
k8sJobTiming("git-mirror-sync", asOptionalRecord(capabilities?.gitMirrorSync)),
|
||||
k8sJobTiming("git-mirror-flush", asOptionalRecord(capabilities?.gitMirrorFlush)),
|
||||
]) {
|
||||
if (stage !== null) stages.push(stage);
|
||||
}
|
||||
const agentrun = asOptionalRecord(payload.agentrun);
|
||||
const agentrunSync = asOptionalRecord(agentrun?.gitMirrorSync);
|
||||
const agentrunFlush = asOptionalRecord(agentrun?.gitMirrorFlush);
|
||||
const imageBuild = asOptionalRecord(agentrun?.imageBuild);
|
||||
const gitopsPublish = asOptionalRecord(agentrun?.gitopsPublish);
|
||||
for (const stage of [
|
||||
k8sJobTiming("git-mirror-sync", asOptionalRecord(agentrunSync?.payload), stringOrNull(agentrunSync?.jobName)),
|
||||
k8sJobTiming("image-build", asOptionalRecord(imageBuild?.result), stringOrNull(imageBuild?.jobName)),
|
||||
k8sJobTiming("gitops-publish", asOptionalRecord(gitopsPublish?.result), stringOrNull(gitopsPublish?.jobName)),
|
||||
k8sJobTiming("git-mirror-flush", asOptionalRecord(agentrunFlush?.payload), stringOrNull(agentrunFlush?.jobName)),
|
||||
]) {
|
||||
if (stage !== null) stages.push(stage);
|
||||
}
|
||||
const tektonSeconds = secondsFromMs(numberOrNull(payload.elapsedMs));
|
||||
if (tektonSeconds !== null) {
|
||||
const status = payload.completed === true ? "completed" : payload.failed === true ? "failed" : payload.stillRunning === true ? "running" : "submitted";
|
||||
stages.push(stageTiming("pipelinerun-wait", status, tektonSeconds, null, "tekton-submit", stringOrNull(command.pipelineRun)));
|
||||
}
|
||||
}
|
||||
const closeout = asOptionalRecord(command.closeout);
|
||||
if (closeout !== null) {
|
||||
const status = closeout.completed === true ? "completed" : closeout.timedOut === true ? "timed-out" : "pending";
|
||||
stages.push(stageTiming("closeout", status, secondsFromMs(numberOrNull(closeout.elapsedMs)), null, "k8s-native-closeout", stringOrNull(command.pipelineRun)));
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
|
||||
function k8sJobTiming(stage: string, job: Record<string, unknown> | null, objectOverride?: string | null): StageTiming | null {
|
||||
if (job === null) return null;
|
||||
const status = job.completed === true
|
||||
? job.reused === true ? "reused" : "completed"
|
||||
: job.failed === true
|
||||
? "failed"
|
||||
: job.timedOut === true
|
||||
? "timed-out"
|
||||
: "running";
|
||||
return stageTiming(stage, status, secondsFromMs(numberOrNull(job.elapsedMs)), null, "kubernetes-job", objectOverride ?? stringOrNull(job.jobName));
|
||||
}
|
||||
|
||||
function stageTiming(stage: string, status: string, seconds: number | null, budgetSeconds: number | null, source: string, object: string | null): StageTiming {
|
||||
return { stage, status, seconds, budgetSeconds, source, object };
|
||||
}
|
||||
|
||||
function dedupeTimingStages(stages: StageTiming[]): StageTiming[] {
|
||||
const byKey = new Map<string, StageTiming>();
|
||||
for (const stage of stages) {
|
||||
if (stage.stage.length === 0) continue;
|
||||
const key = `${stage.stage}\t${stage.object ?? ""}`;
|
||||
const previous = byKey.get(key);
|
||||
if (previous === undefined || previous.seconds === null && stage.seconds !== null) byKey.set(key, stage);
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function secondsFromMs(value: number | null): number | null {
|
||||
return value === null ? null : roundSeconds(value / 1000);
|
||||
}
|
||||
|
||||
function roundSeconds(value: number): number {
|
||||
return Math.round(value * 10) / 10;
|
||||
}
|
||||
|
||||
function writeFollowerState(registry: BranchFollowerRegistry, state: FollowerState, options: ParsedOptions): CommandResult {
|
||||
const json = JSON.stringify(compactFollowerStateForConfigMap(state));
|
||||
const dataPatch = JSON.stringify({ data: { [state.id]: json, _updatedAt: new Date().toISOString(), _specRef: SPEC_REF } });
|
||||
@@ -2450,6 +2646,7 @@ function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOpt
|
||||
});
|
||||
const next = asOptionalRecord(payload.next);
|
||||
const errors = Array.isArray(payload.errors) ? payload.errors : [];
|
||||
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
|
||||
return [
|
||||
`CI/CD BRANCH-FOLLOWER STATUS (${payload.ok === false ? "degraded" : "ok"})`,
|
||||
"",
|
||||
@@ -2459,6 +2656,7 @@ function renderStatusHuman(payload: Record<string, unknown>, _options: ParsedOpt
|
||||
),
|
||||
"",
|
||||
table(["FOLLOWER", "PHASE", "ADAPTER", "OBSERVED", "TARGET", "TRIGGERED", "SUCCEEDED", "IN_FLIGHT", "BUDGET", "MESSAGE"], rows),
|
||||
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
|
||||
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
|
||||
"",
|
||||
"NEXT",
|
||||
@@ -2484,10 +2682,12 @@ function renderRunOnceHuman(payload: Record<string, unknown>): string {
|
||||
];
|
||||
});
|
||||
const next = asOptionalRecord(payload.next);
|
||||
const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48);
|
||||
return [
|
||||
`CI/CD BRANCH-FOLLOWER RUN-ONCE (${payload.ok === false ? "blocked" : payload.dryRun === true ? "dry-run" : "ok"})`,
|
||||
"",
|
||||
table(["FOLLOWER", "PHASE", "OBSERVED", "TARGET", "TRIGGERED", "IN_FLIGHT", "DECISION"], rows),
|
||||
timingRows.length === 0 ? "" : `\nSTAGE TIMINGS\n${table(["FOLLOWER", "STAGE", "STATUS", "SECONDS", "BUDGET", "OBJECT"], timingRows)}`,
|
||||
"",
|
||||
"NEXT",
|
||||
`status: ${next?.status ?? "-"}`,
|
||||
@@ -2496,6 +2696,35 @@ function renderRunOnceHuman(payload: Record<string, unknown>): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function timingRowsForFollower(item: Record<string, unknown>): unknown[][] {
|
||||
const timings = asOptionalRecord(item.timings);
|
||||
if (timings === null) return [];
|
||||
const budget = numberOrNull(timings.budgetSeconds);
|
||||
const rows: unknown[][] = [[
|
||||
item.id,
|
||||
"total",
|
||||
stringOrNull(timings.totalStatus) ?? "unknown",
|
||||
formatSeconds(numberOrNull(timings.totalSeconds)),
|
||||
formatSeconds(budget),
|
||||
stringOrNull(timings.totalSource) ?? "-",
|
||||
]];
|
||||
for (const stage of arrayRecords(timings.stages)) {
|
||||
rows.push([
|
||||
item.id,
|
||||
stage.stage,
|
||||
stage.status,
|
||||
formatSeconds(numberOrNull(stage.seconds)),
|
||||
formatSeconds(numberOrNull(stage.budgetSeconds)),
|
||||
stringOrNull(stage.object) ?? "-",
|
||||
]);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function formatSeconds(value: number | null): string {
|
||||
return value === null ? "-" : `${value}s`;
|
||||
}
|
||||
|
||||
function renderCleanupStateHuman(payload: Record<string, unknown>): string {
|
||||
const controller = asOptionalRecord(payload.controller);
|
||||
const command = asOptionalRecord(payload.command);
|
||||
|
||||
Reference in New Issue
Block a user