125 lines
5.7 KiB
TypeScript
125 lines
5.7 KiB
TypeScript
// 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>");
|
|
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<string, unknown> | 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<string, unknown>, 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);
|
|
}
|