diff --git a/scripts/native/cicd/read-state-summary.mjs b/scripts/native/cicd/read-state-summary.mjs index 740490ae..10eb33f8 100644 --- a/scripts/native/cicd/read-state-summary.mjs +++ b/scripts/native/cicd/read-state-summary.mjs @@ -97,6 +97,7 @@ function compactStateText(text, includeCommand) { timings: compactTimings(state.timings), warnings: arrayStrings(state.warnings).slice(0, 6), stateFormat: stringOrNull(state.stateFormat), + rawStateDiagnostic: rawStateDiagnostic(state, text), }; if (includeCommand) compact.command = compactCommand(state.command); return compact; @@ -119,10 +120,73 @@ function compactCommand(command) { exitCode: numberOrNull(value.exitCode), timedOut: value.timedOut === true, statusAuthority: stringOrNull(value.statusAuthority), + reconcileTimeline: compactReconcileTimeline(value.reconcileTimeline), parsedDownstreamCliOutput: false, }; } +function compactReconcileTimeline(reconcileTimeline) { + const value = recordOrNull(reconcileTimeline); + if (value === null) return null; + const steps = arrayRecords(value.steps).slice(-16).map((step) => ({ + follower: stringOrNull(step.follower), + step: stringOrNull(step.step), + status: stringOrNull(step.status), + startedAt: stringOrNull(step.startedAt), + finishedAt: stringOrNull(step.finishedAt), + elapsedMs: numberOrNull(step.elapsedMs), + observedSha: stringOrNull(step.observedSha), + targetSha: stringOrNull(step.targetSha), + phase: stringOrNull(step.phase), + pipelineRun: stringOrNull(step.pipelineRun), + object: stringOrNull(step.object), + message: stringOrNull(step.message), + reason: stringOrNull(step.reason), + exitCode: numberOrNull(step.exitCode), + })); + return { + startedAt: stringOrNull(value.startedAt), + finishedAt: stringOrNull(value.finishedAt), + elapsedMs: numberOrNull(value.elapsedMs), + controller: value.controller === true, + dryRun: value.dryRun === true, + confirm: value.confirm === true, + wait: value.wait === true, + followerCount: numberOrNull(value.followerCount), + followers: arrayStrings(value.followers).slice(0, 8), + bounded: true, + omittedStepCount: Math.max(0, arrayRecords(value.steps).length - steps.length), + steps, + }; +} + +function rawStateDiagnostic(state, text) { + const command = recordOrNull(state.command); + const reconcileTimeline = recordOrNull(command?.reconcileTimeline); + return { + bounded: true, + valueBytes: Buffer.byteLength(text, "utf8"), + hasCommand: command !== null, + commandBytes: jsonBytes(command), + hasReconcileTimeline: reconcileTimeline !== null, + reconcileTimelineBytes: jsonBytes(reconcileTimeline), + reconcileTimelineStepCount: reconcileTimeline === null ? 0 : arrayRecords(reconcileTimeline.steps).length, + reconcileTimelineStartedAt: stringOrNull(reconcileTimeline?.startedAt), + reconcileTimelineFinishedAt: stringOrNull(reconcileTimeline?.finishedAt), + reconcileTimelineElapsedMs: numberOrNull(reconcileTimeline?.elapsedMs), + missingReason: reconcileTimeline === null ? command === null ? "command missing" : "command.reconcileTimeline missing" : null, + }; +} + +function jsonBytes(value) { + if (value === null) return null; + try { + return Buffer.byteLength(JSON.stringify(value), "utf8"); + } catch { + return null; + } +} + function compactCloseout(closeout) { const value = recordOrNull(closeout); if (value === null) return null; diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index 8d4b5254..8291fbcb 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -1982,6 +1982,7 @@ function mergeFollowerStatus( message: live?.message ?? stringOrNull(stored.decision) ?? "no controller state yet", timings: detailed ? timings : compactListTimings(timings), reconcileTimeline: detailed ? reconcileTimeline : null, + rawStateDiagnostic: detailed ? asOptionalRecord(stored.rawStateDiagnostic) : null, drilldown: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id} --live`, }; if (!detailed) return summary; diff --git a/scripts/src/cicd-debug.ts b/scripts/src/cicd-debug.ts index b0214107..5f0ea85a 100644 --- a/scripts/src/cicd-debug.ts +++ b/scripts/src/cicd-debug.ts @@ -241,6 +241,7 @@ function compactStateLike(value: Record | null): Record, _options: ParsedOpt const errors = Array.isArray(payload.errors) ? payload.errors : []; const timingRows = followers.flatMap(timingRowsForFollower).slice(0, 48); const reconcileRows = followers.flatMap(reconcileRowsForFollower).slice(0, 48); + const rawStateRows = followers.flatMap(rawStateRowsForFollower).slice(0, 24); return [ `CI/CD BRANCH-FOLLOWER STATUS (${payload.ok === false ? "degraded" : "ok"})`, "", @@ -122,6 +123,7 @@ function renderStatusHuman(payload: Record, _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)}`, reconcileRows.length === 0 ? "" : `\nRECONCILE TIMELINE\n${table(["FOLLOWER", "STEP", "STATUS", "SECONDS", "STARTED", "OBJECT"], reconcileRows)}`, + rawStateRows.length === 0 ? "" : `\nRAW STATE DIAGNOSTIC\n${table(["FOLLOWER", "STATE_BYTES", "COMMAND", "TIMELINE", "STEPS", "TIMELINE_BYTES", "REASON"], rawStateRows)}`, errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`, "", "NEXT", @@ -253,6 +255,20 @@ function reconcileRowsForTimeline(timeline: Record | null, fall ]); } +function rawStateRowsForFollower(item: Record): unknown[][] { + const diagnostic = asOptionalRecord(item.rawStateDiagnostic); + if (diagnostic === null) return []; + return [[ + item.id ?? "-", + diagnostic.valueBytes ?? "-", + diagnostic.hasCommand === true ? "yes" : "no", + diagnostic.hasReconcileTimeline === true ? "yes" : "no", + diagnostic.reconcileTimelineStepCount ?? "-", + diagnostic.reconcileTimelineBytes ?? "-", + stringOrNull(diagnostic.missingReason) ?? "-", + ]]; +} + function secondsFromMs(value: number | null): number | null { return value === null ? null : Math.round(value / 100) / 10; }