From 979bd33a721c3a060f7fade9cb97a644cd00fbe8 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 3 Jul 2026 18:59:30 +0000 Subject: [PATCH] fix: expose argo closeout reasons --- .../unidesk-cicd/references/branch-follower.md | 2 ++ scripts/native/cicd/compact-native-object.mjs | 16 ++++++++++++++++ scripts/native/cicd/read-state-summary.mjs | 5 +++++ scripts/src/cicd-debug.ts | 5 +++++ scripts/src/cicd-drilldown-render.ts | 12 +++++++++++- scripts/src/cicd.ts | 6 ++++++ 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/.agents/skills/unidesk-cicd/references/branch-follower.md b/.agents/skills/unidesk-cicd/references/branch-follower.md index a6610e95..6a0a4dbd 100644 --- a/.agents/skills/unidesk-cicd/references/branch-follower.md +++ b/.agents/skills/unidesk-cicd/references/branch-follower.md @@ -46,6 +46,8 @@ Follower-scoped commands such as `status --follower`, `events --follower`, `logs `status-read`, `events`, `logs` and debug summaries must expose compact closeout gate details when a follower is not aligned: git-mirror readiness, Tekton PipelineRun condition, Argo sync/health, runtime target sha/readiness and short errors. Repeating only phase/observed/target/message is a visibility defect and must be fixed before further rollout tuning. +Argo closeout visibility must include the bounded reason for non-ready health, not only `Synced/Progressing`: health message, operation phase/message, short Application conditions and a small list of non-healthy resources when available. + Stage timing rows must not label optional gates as `not-ready` when they are not part of that follower's closeout contract. For sentinel-like followers without a GitOps branch flush gate, git-mirror source snapshot readiness should render as source-ready/ready, while missing GitOps `githubInSync` remains `-`/not-applicable instead of a failure-looking state. ## Source Authority diff --git a/scripts/native/cicd/compact-native-object.mjs b/scripts/native/cicd/compact-native-object.mjs index 88aea783..bdab2aeb 100644 --- a/scripts/native/cicd/compact-native-object.mjs +++ b/scripts/native/cicd/compact-native-object.mjs @@ -91,6 +91,18 @@ if (key === "pipelineRun") { statusAuthority: "kubernetes-api-serviceaccount", }; } else if (key === "argoApplication") { + const resources = Array.isArray(input?.status?.resources) ? input.status.resources : []; + const nonReadyResources = resources + .filter((item) => item?.health?.status && item.health.status !== "Healthy") + .slice(0, 8) + .map((item) => ({ + kind: item.kind || null, + namespace: item.namespace || null, + name: item.name || null, + status: item.status || null, + healthStatus: item.health?.status || null, + healthMessage: item.health?.message || null, + })); output = { apiVersion: input.apiVersion, kind: input.kind, @@ -98,6 +110,10 @@ if (key === "pipelineRun") { status: { sync: input?.status?.sync || null, health: input?.status?.health || null, + conditions: Array.isArray(input?.status?.conditions) + ? input.status.conditions.slice(0, 8).map((item) => ({ type: item.type || null, message: item.message || null, lastTransitionTime: item.lastTransitionTime || null })) + : [], + nonReadyResources, operationState: input?.status?.operationState ? { phase: input.status.operationState.phase || null, message: input.status.operationState.message || null, finishedAt: input.status.operationState.finishedAt || null } : null, diff --git a/scripts/native/cicd/read-state-summary.mjs b/scripts/native/cicd/read-state-summary.mjs index 79874e30..51f7a048 100644 --- a/scripts/native/cicd/read-state-summary.mjs +++ b/scripts/native/cicd/read-state-summary.mjs @@ -190,7 +190,12 @@ function compactArgo(argo) { name: stringOrNull(value.name), syncStatus: stringOrNull(value.syncStatus), healthStatus: stringOrNull(value.healthStatus), + healthMessage: stringOrNull(value.healthMessage), revision: stringOrNull(value.revision), + operationPhase: stringOrNull(value.operationPhase), + operationMessage: stringOrNull(value.operationMessage), + conditions: arrayRecords(value.conditions).slice(0, 5), + nonReadyResources: arrayRecords(value.nonReadyResources).slice(0, 5), ready: value.ready === true, }; } diff --git a/scripts/src/cicd-debug.ts b/scripts/src/cicd-debug.ts index 3c647add..d5eca3c5 100644 --- a/scripts/src/cicd-debug.ts +++ b/scripts/src/cicd-debug.ts @@ -315,7 +315,12 @@ function compactStatusGates(payload: Record | null): Record | null): unknown[][] { } const argo = asOptionalRecord(native.argo); if (argo !== null) { - rows.push(["argo", `${stringOrNull(argo.syncStatus) ?? "unknown"}/${stringOrNull(argo.healthStatus) ?? "unknown"}`, shortSha(stringOrNull(argo.revision)), stringOrNull(argo.name) ?? "-"]); + rows.push(["argo", `${stringOrNull(argo.syncStatus) ?? "unknown"}/${stringOrNull(argo.healthStatus) ?? "unknown"}`, argoDetail(argo), stringOrNull(argo.name) ?? "-"]); } const runtime = asOptionalRecord(native.runtime); if (runtime !== null) { @@ -58,6 +58,16 @@ function nativeGateRows(native: Record | null): unknown[][] { return rows; } +function argoDetail(argo: Record): string { + const resource = arrayRecords(argo.nonReadyResources)[0]; + const condition = arrayRecords(argo.conditions)[0]; + return stringOrNull(argo.healthMessage) + ?? stringOrNull(argo.operationMessage) + ?? (resource === undefined ? null : `${resource.kind ?? "resource"}/${resource.name ?? "-"} ${asOptionalRecord(resource.health)?.status ?? resource.healthStatus ?? "-"}`) + ?? (condition === undefined ? null : `${condition.type ?? "condition"} ${condition.message ?? ""}`.trim()) + ?? shortSha(stringOrNull(argo.revision)); +} + function asOptionalRecord(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; } diff --git a/scripts/src/cicd.ts b/scripts/src/cicd.ts index 3e913168..d469bac7 100644 --- a/scripts/src/cicd.ts +++ b/scripts/src/cicd.ts @@ -1722,12 +1722,18 @@ function nativeArgoSummary(application: Record | null): Record< const status = asOptionalRecord(application.status); const sync = asOptionalRecord(status?.sync); const health = asOptionalRecord(status?.health); + const operationState = asOptionalRecord(status?.operationState); return { name: stringOrNull(metadata?.name), namespace: stringOrNull(metadata?.namespace), syncStatus: stringOrNull(sync?.status), healthStatus: stringOrNull(health?.status), + healthMessage: stringOrNull(health?.message), revision: stringOrNull(sync?.revision), + operationPhase: stringOrNull(operationState?.phase), + operationMessage: stringOrNull(operationState?.message), + conditions: Array.isArray(status?.conditions) ? status.conditions.slice(0, 5) : [], + nonReadyResources: Array.isArray(status?.nonReadyResources) ? status.nonReadyResources.slice(0, 5) : [], ready: argoApplicationReady(application), }; }