diff --git a/scripts/native/cicd/taskrun-drilldown.mjs b/scripts/native/cicd/taskrun-drilldown.mjs index 6d8c7786..bc3f45a1 100644 --- a/scripts/native/cicd/taskrun-drilldown.mjs +++ b/scripts/native/cicd/taskrun-drilldown.mjs @@ -36,9 +36,15 @@ async function main() { if (resolution.taskRun === null) { console.log(JSON.stringify({ ok: false, - degradedReason: "taskrun-not-found", + degradedReason: resolution.degradedReason || "taskrun-not-found", + message: resolution.message || null, query: { namespace, taskRun: query, pipelineRun: pipelineRunName || null, pipelineRunPrefix: pipelineRunPrefix || null }, candidates: resolution.candidates, + resolution: { + mode: resolution.mode, + bounded: true, + namespaceTaskRunList: false, + }, statusAuthority: useServiceAccount ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw", parsedDownstreamCliOutput: false, })); @@ -121,10 +127,21 @@ async function main() { async function resolveTaskRun() { const direct = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/taskruns/${encodeURIComponent(query)}`, false); if (direct !== null) return { mode: "direct-name", taskRun: direct, candidates: [] }; + if (!pipelineRunName) { + const fullNameMiss = pipelineRunPrefix.length > 0 && query.startsWith(`${pipelineRunPrefix}-`); + return { + mode: fullNameMiss ? "direct-name-not-found" : "pipeline-run-required", + degradedReason: fullNameMiss ? "taskrun-not-found" : "pipeline-run-required", + message: fullNameMiss + ? `TaskRun ${query} was not found in namespace ${namespace}` + : "--pipeline-run is required for pipeline-task alias lookup; namespace-wide TaskRun listing is disabled", + taskRun: null, + candidates: [], + }; + } const selectors = []; - if (pipelineRunName) selectors.push(`tekton.dev/pipelineRun=${pipelineRunName},tekton.dev/pipelineTask=${query}`); - selectors.push(`tekton.dev/pipelineTask=${query}`); - selectors.push(`tekton.dev/task=${query}`); + selectors.push(`tekton.dev/pipelineRun=${pipelineRunName},tekton.dev/pipelineTask=${query}`); + selectors.push(`tekton.dev/pipelineRun=${pipelineRunName},tekton.dev/task=${query}`); for (const selector of selectors) { const list = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/taskruns?labelSelector=${encodeURIComponent(selector)}`, false); const candidates = taskRunCandidates(list); diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index 31fcd52d..9e997505 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -366,7 +366,7 @@ function parseFollower(root: Record, 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 drillDown = asOptionalRecord(root.drillDown); const closeout = recordField(root, "closeout", label); const configRefs = stringMap(recordField(target, "configRefs", `${label}.target`), `${label}.target.configRefs`); return { @@ -406,7 +406,7 @@ function parseFollower(root: Record, index: number): FollowerSp logs: parseCommand(recordField(commands, "logs", `${label}.commands`), `${label}.commands.logs`), }, nativeStatus: parseNativeStatus(nativeStatus, `${label}.nativeStatus`), - drillDown: parseDrillDown(drillDown, `${label}.drillDown`), + drillDown: drillDown === null ? null : parseDrillDown(drillDown, `${label}.drillDown`), closeoutChecks: stringArrayField(closeout, "checks", `${label}.closeout`), }; } diff --git a/scripts/src/cicd-drilldown-render.ts b/scripts/src/cicd-drilldown-render.ts index 72f8d4ba..977ca738 100644 --- a/scripts/src/cicd-drilldown-render.ts +++ b/scripts/src/cicd-drilldown-render.ts @@ -38,6 +38,7 @@ function renderTaskRunHuman(payload: Record): string { const timing = asOptionalRecord(result?.nodeCicdTiming); const query = asOptionalRecord(payload.query); const command = asOptionalRecord(payload.command); + const identity = asOptionalRecord(command?.identity); return [ `CI/CD BRANCH-FOLLOWER TASKRUN (${payload.ok === false ? "failed" : "ok"})`, "", @@ -59,9 +60,11 @@ function renderTaskRunHuman(payload: Record): string { logs.length === 0 ? "" : `\nLOG TAILS\n${table(["POD", "CONTAINER", "STATUS", "REASON", "LINES", "BYTES", "TIMING", "MESSAGE"], logs.map(logRow))}`, errors.length === 0 ? "" : `\nERRORS\n${table(["POD", "CONTAINER", "REASON", "MESSAGE"], errors.map((item) => [item.pod, item.container, item.degradedReason, item.message]))}`, timing === null ? "" : `\nNODE_CICD_TIMING\n${JSON.stringify(timing, null, 2)}`, + command === null ? "" : `\nTARGET COMMAND\n${table(["ROUTE", "SCRIPT", "NAMESPACE", "TASKRUN", "PIPELINERUN", "EXIT", "PARSE_ERROR"], [[identity?.route ?? "-", identity?.script ?? "-", identity?.namespace ?? "-", identity?.taskRun ?? "-", identity?.pipelineRun ?? "-", command.exitCode ?? "-", command.parseError ?? "-"]])}`, + command?.stdoutTail ? `\nSTDOUT_TAIL\n${command.stdoutTail}` : "", + command?.stderrTail ? `\nSTDERR_TAIL\n${command.stderrTail}` : "", "", `policy: tailLines=${policy?.logsTailLines ?? "-"} maxLogBytes=${policy?.maxLogBytes ?? "-"} timeoutSeconds=${policy?.taskRunTimeoutSeconds ?? "-"} maxContainers=${policy?.maxContainers ?? "-"}`, - command?.stderrTail ? `stderr: ${command.stderrTail}` : "", "", ].filter((line) => line !== "").join("\n"); } diff --git a/scripts/src/cicd-taskrun-drilldown.ts b/scripts/src/cicd-taskrun-drilldown.ts index 7734bbc0..b850870c 100644 --- a/scripts/src/cicd-taskrun-drilldown.ts +++ b/scripts/src/cicd-taskrun-drilldown.ts @@ -16,6 +16,30 @@ export async function runBranchFollowerTaskRunDrillDown( 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, @@ -42,7 +66,9 @@ export async function runBranchFollowerTaskRunDrillDown( ].join("\n"); const startedAt = Date.now(); const result = runKubeScript(registry, options, script, "", policy.taskRunTimeoutSeconds * 1000); - const parsed = result.exitCode === 0 ? parseJsonObject(result.stdout) : null; + 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", @@ -59,10 +85,19 @@ export async function runBranchFollowerTaskRunDrillDown( 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, - stderrTail: result.exitCode === 0 ? "" : redactText(tailText(result.stderr || result.stdout, 1200)), + 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}`, @@ -71,14 +106,16 @@ export async function runBranchFollowerTaskRunDrillDown( }; } -function parseJsonObject(text: string): Record | null { +function parseJsonObject(text: string): { value: Record | null; error: string | null } { const trimmed = text.trim(); - if (trimmed.length === 0) return null; + 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) ? parsed as Record : null; - } catch { - return null; + 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)}` }; } } diff --git a/scripts/src/cicd-types.ts b/scripts/src/cicd-types.ts index bb50b0ad..b26492c1 100644 --- a/scripts/src/cicd-types.ts +++ b/scripts/src/cicd-types.ts @@ -89,7 +89,7 @@ export interface FollowerSpec { maxLogBytes: number; maxMessageBytes: number; maxContainers: number; - }; + } | null; closeoutChecks: string[]; }