// SPEC: PJ2026-01060703 CI/CD branch follower TaskRun drill-down. // Responsibility: follower-scoped read-only TaskRun/Pod/container/log-tail visibility. import type { CommandResult } from "./command"; import type { BranchFollowerRegistry, FollowerSpec, ParsedOptions } from "./cicd-types"; import { nativeCicdScriptLoadShell } from "./cicd-native-bundle"; import { redactText, shQuote } from "./platform-infra-ops-library"; type KubeScriptRunner = (registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number) => CommandResult; export async function runBranchFollowerTaskRunDrillDown( registry: BranchFollowerRegistry, follower: FollowerSpec, options: ParsedOptions, runKubeScript: KubeScriptRunner, ): Promise> { if (follower.nativeStatus.tekton === null) throw new Error(`follower ${follower.id} has no Tekton native status config`); const taskRun = options.taskRunName; if (taskRun === null) throw new Error("taskrun drill-down requires --taskrun "); if (follower.drillDown === null) { return { ok: false, action: "taskrun", follower: follower.id, adapter: follower.adapter, degradedReason: "drilldown-policy-missing", message: `follower ${follower.id} registry is missing drillDown policy; apply the current config before TaskRun drill-down`, query: { taskRun, pipelineRun: options.pipelineRunName, tektonNamespace: follower.nativeStatus.tekton.namespace, pipelineRunPrefix: follower.nativeStatus.tekton.pipelineRunPrefix, }, policy: null, result: null, statusAuthority: options.inCluster ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw", parsedDownstreamCliOutput: false, next: { apply: "bun scripts/cli.ts cicd branch-follower apply --confirm --wait", status: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id}`, }, }; } const policy = { taskRunTimeoutSeconds: options.timeoutSeconds ?? follower.drillDown.taskRunTimeoutSeconds, logsTailLines: options.logsTailLines ?? follower.drillDown.logsTailLines, maxLogBytes: options.maxLogBytes ?? follower.drillDown.maxLogBytes, maxMessageBytes: follower.drillDown.maxMessageBytes, maxContainers: follower.drillDown.maxContainers, }; const script = [ "set -eu", "tmpdir=$(mktemp -d)", "cleanup() { rm -rf \"$tmpdir\"; }", "trap cleanup EXIT INT TERM", nativeCicdScriptLoadShell(["taskrun-drilldown.mjs"]), `TEKTON_NAMESPACE=${shQuote(follower.nativeStatus.tekton.namespace)}`, `TASKRUN_QUERY=${shQuote(taskRun)}`, `PIPELINE_RUN_NAME=${shQuote(options.pipelineRunName ?? "")}`, `PIPELINE_RUN_PREFIX=${shQuote(follower.nativeStatus.tekton.pipelineRunPrefix)}`, `LOGS_TAIL_LINES=${policy.logsTailLines}`, `MAX_LOG_BYTES=${policy.maxLogBytes}`, `MAX_MESSAGE_BYTES=${policy.maxMessageBytes}`, `MAX_CONTAINERS=${policy.maxContainers}`, "export TEKTON_NAMESPACE TASKRUN_QUERY PIPELINE_RUN_NAME PIPELINE_RUN_PREFIX LOGS_TAIL_LINES MAX_LOG_BYTES MAX_MESSAGE_BYTES MAX_CONTAINERS", "node \"$tmpdir/taskrun-drilldown.mjs\"", ].join("\n"); const startedAt = Date.now(); const result = runKubeScript(registry, options, script, "", policy.taskRunTimeoutSeconds * 1000); const parsedResult = result.exitCode === 0 ? parseJsonObject(result.stdout) : { value: null, error: "target-command-failed" }; const parsed = parsedResult.value; const includeTails = result.exitCode !== 0 || parsed === null || parsed.ok === false; return { ok: result.exitCode === 0 && parsed !== null && parsed.ok !== false, action: "taskrun", follower: follower.id, adapter: follower.adapter, statusAuthority: options.inCluster ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw", parsedDownstreamCliOutput: false, query: { taskRun, pipelineRun: options.pipelineRunName, tektonNamespace: follower.nativeStatus.tekton.namespace, pipelineRunPrefix: follower.nativeStatus.tekton.pipelineRunPrefix, }, policy, result: parsed, command: { identity: { route: options.inCluster ? "in-cluster" : registry.controller.kubeRoute, script: "scripts/native/cicd/taskrun-drilldown.mjs", namespace: follower.nativeStatus.tekton.namespace, taskRun, pipelineRun: options.pipelineRunName, }, exitCode: result.exitCode, timedOut: result.timedOut, elapsedMs: Date.now() - startedAt, parseError: parsedResult.error, stdoutTail: includeTails ? redactText(tailText(result.stdout, 1600)) : "", stderrTail: includeTails ? redactText(tailText(result.stderr, 1200)) : "", }, next: { status: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id}`, taskRunJson: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id} --taskrun ${taskRun} --logs-tail ${policy.logsTailLines} --json`, }, }; } function parseJsonObject(text: string): { value: Record | null; error: string | null } { const trimmed = text.trim(); if (trimmed.length === 0) return { value: null, error: "empty-stdout" }; try { const parsed = JSON.parse(trimmed) as unknown; return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { value: parsed as Record, error: null } : { value: null, error: "stdout-json-not-object" }; } catch (error) { return { value: null, error: `stdout-json-parse-failed: ${error instanceof Error ? error.message : String(error)}` }; } } function tailText(text: string, maxChars: number): string { return text.length <= maxChars ? text : text.slice(text.length - maxChars); }