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
+87
View File
@@ -0,0 +1,87 @@
// 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<Record<string, unknown>> {
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 <taskrun-name|pipeline-task>");
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 parsed = result.exitCode === 0 ? parseJsonObject(result.stdout) : null;
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: {
exitCode: result.exitCode,
timedOut: result.timedOut,
elapsedMs: Date.now() - startedAt,
stderrTail: result.exitCode === 0 ? "" : redactText(tailText(result.stderr || result.stdout, 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): Record<string, unknown> | null {
const trimmed = text.trim();
if (trimmed.length === 0) return null;
try {
const parsed = JSON.parse(trimmed) as unknown;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch {
return null;
}
}
function tailText(text: string, maxChars: number): string {
return text.length <= maxChars ? text : text.slice(text.length - maxChars);
}