feat: add follower controller source debug step
This commit is contained in:
@@ -56,6 +56,7 @@ export function cicdHelp(): unknown {
|
||||
"bun scripts/cli.ts cicd branch-follower status --live",
|
||||
"bun scripts/cli.ts cicd branch-follower run-once --all --dry-run",
|
||||
"bun scripts/cli.ts cicd branch-follower run-once --follower hwlab-jd01-v03 --confirm --wait",
|
||||
"bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step controller-source",
|
||||
"bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-read",
|
||||
"bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-write --confirm",
|
||||
"bun scripts/cli.ts cicd branch-follower cleanup-state --follower web-probe-sentinel-master --confirm",
|
||||
@@ -181,8 +182,8 @@ function parseOptions(args: string[]): ParsedOptions {
|
||||
}
|
||||
|
||||
function debugStepOption(value: string): BranchFollowerDebugStep {
|
||||
if (value === "state-read" || value === "status-read" || value === "decide" || value === "state-write") return value;
|
||||
throw new Error("--step must be state-read, status-read, decide, or state-write");
|
||||
if (value === "state-read" || value === "controller-source" || value === "status-read" || value === "decide" || value === "state-write") return value;
|
||||
throw new Error("--step must be state-read, controller-source, status-read, decide, or state-write");
|
||||
}
|
||||
|
||||
function isInClusterRuntime(): boolean {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower debug steps.
|
||||
// Responsibility: bounded single-step debugging for branch follower state and decision paths.
|
||||
import type { CommandResult } from "./command";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { runCommand, type CommandResult } from "./command";
|
||||
import { repoRoot, rootPath } from "./config";
|
||||
import type { AdapterSummary, BranchFollowerDebugStep, BranchFollowerRegistry, FollowerSpec, FollowerState, K8sStateRead, ParsedOptions } from "./cicd-types";
|
||||
import { renderControllerDebugJob, waitForJobShell } from "./cicd-controller-render";
|
||||
import { redactText, shQuote } from "./platform-infra-ops-library";
|
||||
@@ -30,7 +33,11 @@ export async function buildDebugStep(registry: BranchFollowerRegistry, options:
|
||||
let decided: FollowerState | null = null;
|
||||
let write: Record<string, unknown> | null = null;
|
||||
let after: K8sStateRead | null = null;
|
||||
let controllerSource: Record<string, unknown> | null = null;
|
||||
|
||||
if (step === "controller-source") {
|
||||
controllerSource = controllerSourceSnapshot(registry);
|
||||
}
|
||||
if (step === "status-read" || step === "decide") {
|
||||
live = await deps.readAdapterStatus(registry, follower, options);
|
||||
}
|
||||
@@ -69,6 +76,7 @@ export async function buildDebugStep(registry: BranchFollowerRegistry, options:
|
||||
execution: "k8s-native-in-cluster",
|
||||
dryRun: !options.confirm,
|
||||
stateBefore: stateSnapshot(before, follower.id),
|
||||
controllerSource,
|
||||
status: live === null ? null : compactAdapterStatus(live),
|
||||
decision: decided === null ? null : compactFollowerDecision(decided),
|
||||
stateWrite: write,
|
||||
@@ -84,6 +92,7 @@ export function renderDebugStepHuman(payload: Record<string, unknown>): string {
|
||||
const write = asOptionalRecord(payload.stateWrite);
|
||||
const status = asOptionalRecord(payload.status);
|
||||
const decision = asOptionalRecord(payload.decision);
|
||||
const controllerSource = asOptionalRecord(payload.controllerSource);
|
||||
const target = asOptionalRecord(payload.target);
|
||||
const next = asOptionalRecord(payload.next);
|
||||
const rows = [[
|
||||
@@ -109,11 +118,13 @@ export function renderDebugStepHuman(payload: Record<string, unknown>): string {
|
||||
`CI/CD BRANCH-FOLLOWER DEBUG-STEP (${payload.ok === false ? "failed" : "ok"})`,
|
||||
"",
|
||||
table(["FOLLOWER", "STEP", "EXECUTION", "DRY_RUN", "BEFORE", "AFTER", "BEFORE_SHA", "AFTER_SHA"], rows),
|
||||
controllerSource === null ? "" : `\nCONTROLLER SOURCE\n${table(["HEAD", "BRANCH", "REGISTRY", "REREAD_MARKER", "FILE_SHA"], [[shortSha(stringOrNull(controllerSource.head)), controllerSource.branch ?? "-", shortSha(stringOrNull(controllerSource.registrySha256)), controllerSource.closeoutRereadMarker === true ? "present" : "missing", shortSha(stringOrNull(asOptionalRecord(controllerSource.branchFollowerFile)?.sha256))]])}`,
|
||||
target === null ? "" : `\nTARGET JOB\n${table(["JOB", "EXIT", "TIMED_OUT", "PARSED"], [[target.name ?? "-", target.exitCode ?? "-", target.timedOut ?? "-", target.parsed === true ? "yes" : "no"]])}`,
|
||||
writeRows.length === 0 ? "" : `\nSTATE WRITE\n${table(["STATUS", "MODE", "INPUT", "BEFORE_RV", "AFTER_RV", "EXIT", "MESSAGE"], writeRows)}`,
|
||||
"",
|
||||
"NEXT",
|
||||
`state-read: ${next?.stateRead ?? "-"}`,
|
||||
`controller-source: ${next?.controllerSource ?? "-"}`,
|
||||
`status-read: ${next?.statusRead ?? "-"}`,
|
||||
`decide: ${next?.decide ?? "-"}`,
|
||||
`state-write: ${next?.stateWrite ?? "-"}`,
|
||||
@@ -152,6 +163,7 @@ function runTargetDebugStepJob(registry: BranchFollowerRegistry, options: Parsed
|
||||
execution: "k8s-native-debug-job",
|
||||
dryRun: !options.confirm,
|
||||
stateBefore: compact?.stateBefore ?? compactStateLike(asOptionalRecord(parsed?.stateBefore)),
|
||||
controllerSource: compact?.controllerSource ?? asOptionalRecord(parsed?.controllerSource),
|
||||
status: compact?.status ?? null,
|
||||
decision: compact?.decision ?? null,
|
||||
stateWrite: compact?.stateWrite ?? null,
|
||||
@@ -173,6 +185,7 @@ function runTargetDebugStepJob(registry: BranchFollowerRegistry, options: Parsed
|
||||
function compactTargetDebugResult(parsed: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (parsed === null) return null;
|
||||
const stateBefore = asOptionalRecord(parsed.stateBefore);
|
||||
const controllerSource = asOptionalRecord(parsed.controllerSource);
|
||||
const status = asOptionalRecord(parsed.status);
|
||||
const decision = asOptionalRecord(parsed.decision);
|
||||
const stateWrite = asOptionalRecord(parsed.stateWrite);
|
||||
@@ -183,6 +196,16 @@ function compactTargetDebugResult(parsed: Record<string, unknown> | null): Recor
|
||||
step: stringOrNull(parsed.step),
|
||||
follower: stringOrNull(parsed.follower),
|
||||
stateBefore: compactStateLike(stateBefore),
|
||||
controllerSource: controllerSource === null ? null : {
|
||||
ok: controllerSource.ok === true,
|
||||
repository: stringOrNull(controllerSource.repository),
|
||||
branch: stringOrNull(controllerSource.branch),
|
||||
head: stringOrNull(controllerSource.head),
|
||||
registrySha256: stringOrNull(controllerSource.registrySha256),
|
||||
closeoutRereadMarker: controllerSource.closeoutRereadMarker === true,
|
||||
branchFollowerFile: asOptionalRecord(controllerSource.branchFollowerFile),
|
||||
errors: Array.isArray(controllerSource.errors) ? controllerSource.errors.map(String).slice(0, 5) : [],
|
||||
},
|
||||
status: status === null ? null : {
|
||||
ok: status.ok === true,
|
||||
phase: stringOrNull(status.phase),
|
||||
@@ -220,6 +243,61 @@ function compactStateLike(value: Record<string, unknown> | null): Record<string,
|
||||
};
|
||||
}
|
||||
|
||||
function controllerSourceSnapshot(registry: BranchFollowerRegistry): Record<string, unknown> {
|
||||
const head = commandText(["git", "rev-parse", "HEAD"]);
|
||||
const branch = commandText(["git", "symbolic-ref", "--short", "HEAD"]);
|
||||
const file = fileSnapshot("scripts/src/cicd-branch-follower.ts");
|
||||
const text = file.text;
|
||||
const errors = [
|
||||
head.ok ? null : `git head: ${head.error}`,
|
||||
branch.ok ? null : `git branch: ${branch.error}`,
|
||||
file.ok ? null : `branch follower file: ${file.error}`,
|
||||
].filter((item): item is string => item !== null);
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
repository: registry.controller.source.repository,
|
||||
branch: branch.value,
|
||||
head: head.value,
|
||||
registrySha256: registry.rawSha256,
|
||||
controllerConfigMap: registry.controller.configMapName,
|
||||
closeoutRereadMarker: text.includes("post-closeout status re-read") && text.includes("automaticCloseoutAccelerated"),
|
||||
branchFollowerFile: {
|
||||
path: file.path,
|
||||
present: file.present,
|
||||
bytes: file.bytes,
|
||||
sha256: file.sha256,
|
||||
},
|
||||
statusAuthority: "target-controller-checkout",
|
||||
parsedDownstreamCliOutput: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
function commandText(command: string[]): { ok: boolean; value: string | null; error: string | null } {
|
||||
const result = runCommand(command, repoRoot, { timeoutMs: 5_000 });
|
||||
const value = result.exitCode === 0 ? result.stdout.trim() : null;
|
||||
return {
|
||||
ok: result.exitCode === 0 && value !== null && value.length > 0,
|
||||
value: value === null || value.length === 0 ? null : value,
|
||||
error: result.exitCode === 0 ? null : redactText(tailText(result.stderr || result.stdout, 300)),
|
||||
};
|
||||
}
|
||||
|
||||
function fileSnapshot(path: string): { ok: boolean; path: string; present: boolean; bytes: number | null; sha256: string | null; text: string; error: string | null } {
|
||||
const absolute = rootPath(path);
|
||||
if (!existsSync(absolute)) return { ok: false, path, present: false, bytes: null, sha256: null, text: "", error: "missing" };
|
||||
const text = readFileSync(absolute, "utf8");
|
||||
return {
|
||||
ok: true,
|
||||
path,
|
||||
present: true,
|
||||
bytes: Buffer.byteLength(text),
|
||||
sha256: createHash("sha256").update(text).digest("hex"),
|
||||
text,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function debugDecisionOptions(options: ParsedOptions): ParsedOptions {
|
||||
return { ...options, confirm: false, dryRun: true, wait: false, recordState: false };
|
||||
}
|
||||
@@ -407,6 +485,7 @@ function compactDebugTimings(timings: FollowerState["timings"]): Record<string,
|
||||
function debugNext(followerId: string): Record<string, string> {
|
||||
return {
|
||||
stateRead: `bun scripts/cli.ts cicd branch-follower debug-step --follower ${followerId} --step state-read`,
|
||||
controllerSource: `bun scripts/cli.ts cicd branch-follower debug-step --follower ${followerId} --step controller-source`,
|
||||
statusRead: `bun scripts/cli.ts cicd branch-follower debug-step --follower ${followerId} --step status-read`,
|
||||
decide: `bun scripts/cli.ts cicd branch-follower debug-step --follower ${followerId} --step decide`,
|
||||
stateWrite: `bun scripts/cli.ts cicd branch-follower debug-step --follower ${followerId} --step state-write --confirm`,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
export type OutputMode = "human" | "json" | "yaml";
|
||||
export type BranchFollowerAction = "help" | "plan" | "apply" | "status" | "run-once" | "debug-step" | "cleanup-state" | "events" | "logs";
|
||||
export type BranchFollowerDebugStep = "state-read" | "status-read" | "decide" | "state-write";
|
||||
export type BranchFollowerDebugStep = "state-read" | "controller-source" | "status-read" | "decide" | "state-write";
|
||||
export type BranchFollowerPhase =
|
||||
| "Observed"
|
||||
| "Noop"
|
||||
|
||||
Reference in New Issue
Block a user