feat(cicd): add branch follower taskrun drilldown

This commit is contained in:
Codex
2026-07-03 23:32:39 +00:00
parent 2a5c8644dd
commit 7004f8f7ab
7 changed files with 574 additions and 5 deletions
+53 -3
View File
@@ -29,6 +29,7 @@ import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
import { argoApplicationReady, nativeArgoSummary, nativeGitMirrorReady, nativeGitMirrorRequired, nativeGitMirrorSummary, nativePipelineRunSummary, nativeRuntimeSummary, pipelineRunSucceeded, runtimeTargetShaFromWorkloads, runtimeWorkloadsReady } from "./cicd-native-summary";
import { invalidRuntimeReuseConfig, missingRuntimeReuseConfig, parseRuntimeReuseConfig, RUNTIME_REUSE_CONFIG_PATH, runtimeReuseService, summarizeRuntimeReuseConfig, type RuntimeReuseConfig } from "./cicd-reuse-config";
import { prioritizedTaskRunItems } from "./cicd-taskruns";
import { runBranchFollowerTaskRunDrillDown } from "./cicd-taskrun-drilldown";
import type { AdapterSummary, BranchFollowerAction, BranchFollowerDebugStep, BranchFollowerPhase, BranchFollowerRegistry, ControllerSpec, FollowerSpec, FollowerState, K8sFollowerStateRead, K8sStateRead, NativeCloseoutWaitResult, NativeK8sJobResult, NativeStatusSpec, NativeWorkloadSpec, OutputMode, ParsedOptions, StageTiming, TriggerResult } from "./cicd-types";
import {
arrayField,
@@ -49,7 +50,7 @@ const SPEC_VERSION = "draft-2026-07-03-p0-branch-follower";
export function cicdHelp(): unknown {
return {
command: "cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs",
command: "cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs|taskrun",
output: "text by default; use --json, --raw, or -o json|yaml for machine output",
usage: [
"bun scripts/cli.ts cicd branch-follower plan",
@@ -64,6 +65,8 @@ export function cicdHelp(): unknown {
"bun scripts/cli.ts cicd branch-follower cleanup-state --follower web-probe-sentinel-master --confirm",
"bun scripts/cli.ts cicd branch-follower events --follower agentrun-jd01-v02",
"bun scripts/cli.ts cicd branch-follower logs --follower web-probe-sentinel-master",
"bun scripts/cli.ts cicd branch-follower status --follower hwlab-jd01-v03 --taskrun runtime-ready --logs-tail 120 --json",
"bun scripts/cli.ts cicd branch-follower taskrun --follower hwlab-jd01-v03 --taskrun runtime-ready --logs-tail 120 --json",
],
config: DEFAULT_CONFIG_PATH,
spec: `${SPEC_REF} ${SPEC_VERSION}`,
@@ -75,7 +78,7 @@ export async function runCicdCommand(_config: UniDeskConfig | null, args: string
const top = args[0];
if (top === undefined || isHelpToken(top)) return renderMachine("cicd", cicdHelp(), "json");
if (top !== "branch-follower") {
throw new Error("cicd usage: cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs");
throw new Error("cicd usage: cicd branch-follower plan|apply|status|run-once|debug-step|cleanup-state|events|logs|taskrun");
}
const options = parseOptions(args.slice(1));
const command = commandLabel(options);
@@ -87,6 +90,7 @@ export async function runCicdCommand(_config: UniDeskConfig | null, args: string
case "apply":
return renderResult(command, await applyController(registry, options), options);
case "status":
if (options.taskRunName !== null) return renderResult(command, await runTaskRunDrillDown(registry, options), options);
return renderResult(command, await buildStatus(registry, options), options);
case "run-once":
return renderResult(command, await runOnce(registry, options), options);
@@ -97,6 +101,8 @@ export async function runCicdCommand(_config: UniDeskConfig | null, args: string
case "events":
case "logs":
return renderResult(command, await runFollowerDrillDown(registry, options), options);
case "taskrun":
return renderResult(command, await runTaskRunDrillDown(registry, options), options);
case "help":
return renderMachine(command, cicdHelp(), "json");
}
@@ -107,7 +113,7 @@ function parseOptions(args: string[]): ParsedOptions {
if (actionToken === undefined || isHelpToken(actionToken)) {
return defaultOptions("help", args.slice(actionToken === undefined ? 0 : 1));
}
if (!["plan", "apply", "status", "run-once", "debug-step", "cleanup-state", "events", "logs"].includes(actionToken)) {
if (!["plan", "apply", "status", "run-once", "debug-step", "cleanup-state", "events", "logs", "taskrun"].includes(actionToken)) {
throw new Error(`cicd branch-follower unknown action: ${actionToken}`);
}
const action = actionToken as BranchFollowerAction;
@@ -147,6 +153,14 @@ function parseOptions(args: string[]): ParsedOptions {
options.recordState = true;
} else if (arg === "--step") {
options.debugStep = debugStepOption(valueOption(rest, ++index, arg));
} else if (arg === "--taskrun" || arg === "--task-run") {
options.taskRunName = simpleK8sObjectName(valueOption(rest, ++index, arg), arg);
} else if (arg === "--pipelinerun" || arg === "--pipeline-run") {
options.pipelineRunName = simpleK8sObjectName(valueOption(rest, ++index, arg), arg);
} else if (arg === "--logs-tail") {
options.logsTailLines = positiveInt(valueOption(rest, ++index, arg), arg);
} else if (arg === "--max-log-bytes") {
options.maxLogBytes = positiveInt(valueOption(rest, ++index, arg), arg);
} else if (arg === "-o" || arg === "--output") {
const value = valueOption(rest, ++index, arg);
if (value !== "json" && value !== "yaml" && value !== "wide" && value !== "text") throw new Error(`${arg} must be json, yaml, wide, or text`);
@@ -178,6 +192,12 @@ function parseOptions(args: string[]): ParsedOptions {
if (options.action === "debug-step" && options.followerId === null) {
throw new Error("debug-step requires --follower <id>");
}
if (options.action === "taskrun" && options.taskRunName === null) {
throw new Error("taskrun requires --taskrun <taskrun-name|pipeline-task>");
}
if (options.taskRunName !== null && options.followerId === null) {
throw new Error("--taskrun requires --follower <id>");
}
return options;
}
@@ -206,6 +226,10 @@ function defaultOptions(action: BranchFollowerAction, _args: string[]): ParsedOp
raw: false,
recordState: false,
debugStep: null,
taskRunName: null,
pipelineRunName: null,
logsTailLines: null,
maxLogBytes: null,
output: "human",
limit: 20,
tailBytes: 12000,
@@ -224,6 +248,11 @@ function simpleId(value: string, option: string): string {
return value;
}
function simpleK8sObjectName(value: string, option: string): string {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${option} must be a Kubernetes object or task id`);
return value;
}
function positiveInt(value: string, option: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
@@ -337,6 +366,7 @@ function parseFollower(root: Record<string, unknown>, index: number): FollowerSp
const budgets = recordField(root, "budgets", label);
const commands = recordField(root, "commands", label);
const nativeStatus = recordField(root, "nativeStatus", label);
const drillDown = recordField(root, "drillDown", label);
const closeout = recordField(root, "closeout", label);
const configRefs = stringMap(recordField(target, "configRefs", `${label}.target`), `${label}.target.configRefs`);
return {
@@ -376,6 +406,7 @@ function parseFollower(root: Record<string, unknown>, index: number): FollowerSp
logs: parseCommand(recordField(commands, "logs", `${label}.commands`), `${label}.commands.logs`),
},
nativeStatus: parseNativeStatus(nativeStatus, `${label}.nativeStatus`),
drillDown: parseDrillDown(drillDown, `${label}.drillDown`),
closeoutChecks: stringArrayField(closeout, "checks", `${label}.closeout`),
};
}
@@ -438,6 +469,16 @@ function parseCommand(root: Record<string, unknown>, label: string): CommandSpec
};
}
function parseDrillDown(root: Record<string, unknown>, label: string): FollowerSpec["drillDown"] {
return {
taskRunTimeoutSeconds: integerField(root, "taskRunTimeoutSeconds", label),
logsTailLines: integerField(root, "logsTailLines", label),
maxLogBytes: integerField(root, "maxLogBytes", label),
maxMessageBytes: integerField(root, "maxMessageBytes", label),
maxContainers: integerField(root, "maxContainers", label),
};
}
function optionalStringArrayField(root: Record<string, unknown>, key: string, label: string): string[] {
return root[key] === undefined ? [] : stringArrayField(root, key, label);
}
@@ -477,6 +518,7 @@ function buildPlan(registry: BranchFollowerRegistry, options: ParsedOptions): Re
},
target: follower.target,
budgets: follower.budgets,
drillDown: follower.drillDown,
commands: redactCommands(follower),
nativeStatus: nativeStatusPlan(follower.nativeStatus),
closeoutChecks: follower.closeoutChecks,
@@ -765,6 +807,13 @@ async function runFollowerDrillDown(registry: BranchFollowerRegistry, options: P
};
}
async function runTaskRunDrillDown(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
if (options.followerId === null) throw new Error("--taskrun requires --follower <id>");
const follower = registry.followers.find((item) => item.id === options.followerId);
if (follower === undefined) throw new Error(`unknown follower ${options.followerId}`);
return runBranchFollowerTaskRunDrillDown(registry, follower, options, runKubeScript);
}
async function decideAndMaybeTrigger(
registry: BranchFollowerRegistry,
follower: FollowerSpec,
@@ -2776,6 +2825,7 @@ function tailText(text: string, maxChars: number): string {
}
function commandLabel(options: ParsedOptions): string {
if (options.taskRunName !== null) return "cicd branch-follower taskrun";
return `cicd branch-follower ${options.action}`;
}