// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. logs module for scripts/src/ci.ts. // SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:3168-3272 for #903. import { randomUUID } from "node:crypto"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, posix as posixPath } from "node:path"; import { blockedCatalogArtifactIds, catalogSummary, findCiCatalogArtifact, loadCiCatalog, supportedSourceBuildArtifactIds, type CiCatalogArtifact, type CiSourceBuildCatalogArtifact, type CiUpstreamImageCatalogArtifact } from "../ci-catalog"; import { runCommand } from "../command"; import { type UniDeskConfig, repoRoot, rootPath } from "../config"; import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "../deploy-ssh-identity"; import { jobWithTail, listJobs, readJob, startJob } from "../jobs"; import { coreInternalFetch } from "../microservices"; import { artifactRegistryReadonlyResultFromCommand, buildArtifactRegistryReadonlyProbe, parseArtifactRegistryOptions, type ArtifactRegistryReadonlyProbe, } from "../artifact-registry"; import { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard"; import { runSshCommandCapture } from "../ssh"; import type { CiLogsOptions, CiTarget } from "./types"; import { boundedHeadTail, countTextLines, extractCiLogConditionHints, extractCiLogFailureHints, shellQuote, tailTextLines } from "./options"; import { dispatchSsh, runRemoteKubectl } from "./remote"; import { ciTarget } from "./types"; export function remoteRunLogsScript(name: string, target: CiTarget, options: CiLogsOptions): string { return [ "set -eu", `run_id=${shellQuote(name)}`, `result_root=${shellQuote(`${target.homeDir}/.unidesk/runs`)}`, `tail_lines=${shellQuote(String(options.tailLines))}`, "result_dir=\"$result_root/$run_id\"", "printf 'result_dir=%s\\n' \"$result_dir\"", "found=0", "if [ -f \"$result_dir/result.json\" ]; then found=1; echo '===== result.json'; cat \"$result_dir/result.json\"; fi", "if [ -f \"$result_dir/launcher.log\" ]; then found=1; echo '===== launcher.log'; tail -n \"$tail_lines\" \"$result_dir/launcher.log\"; fi", "if [ -f \"$result_dir/runner.log\" ]; then found=1; echo '===== runner.log'; tail -n \"$tail_lines\" \"$result_dir/runner.log\"; fi", "if [ -f \"$result_dir/pods.log\" ]; then found=1; echo '===== pods.log'; tail -n \"$tail_lines\" \"$result_dir/pods.log\"; fi", "if [ \"$found\" = \"0\" ]; then echo \"no_run_files=$result_dir\" >&2; exit 42; fi", ].join("\n"); } export function pipelineRunLogsScript(name: string, options: CiLogsOptions): string { return [ "set -eu", `tail_lines=${shellQuote(String(options.tailLines))}`, `kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`, `kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`, "echo '===== pipelinerun conditions'", `kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o jsonpath='{range .status.conditions[*]}condition={.type} status={.status} reason={.reason} message={.message}{"\\n"}{end}' || true`, "echo '===== taskrun conditions'", `kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o jsonpath='{range .items[*]}taskrun={.metadata.name} phase={.status.conditions[0].type} status={.status.conditions[0].status} reason={.status.conditions[0].reason} message={.status.conditions[0].message}{"\\n"}{end}' || true`, "echo '===== pod log tails'", `for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail="$tail_lines" || true; done`, ].join("\n"); } export function ciLogsResultFromCapture(args: { ok: boolean; providerId: string; targetId: string; kind: "run" | "pipelinerun"; name: string; options: CiLogsOptions; stdout: string; stderr: string; exitCode: number | null; dispatchTaskId?: string | null; }): Record { const combined = `${args.stdout}\n${args.stderr}`.trim(); const nextName = args.kind === "run" ? "runId" : "pipelineRun"; return { ok: args.ok, providerId: args.providerId, [nextName]: args.name, capture: args.options.capture, tailLines: args.options.tailLines, lineCount: countTextLines(combined), outputTail: boundedHeadTail(tailTextLines(args.stdout, args.options.tailLines * 4), 2400, 2400), stderrTail: boundedHeadTail(tailTextLines(args.stderr, Math.min(args.options.tailLines, 80)), 1200, 1200), conditionHints: extractCiLogConditionHints(combined), failureHints: extractCiLogFailureHints(combined), exitCode: args.exitCode, ...(args.dispatchTaskId === undefined ? {} : { dispatchTaskId: args.dispatchTaskId }), fullOutputAvailable: args.options.capture === "ssh-stream", next: [ `bun scripts/cli.ts ci logs ${args.name} --target ${args.targetId} --tail-lines ${Math.min(args.options.tailLines * 2, 2000)}`, `bun scripts/cli.ts ci status --target ${args.targetId}`, ], }; } export async function logs(config: UniDeskConfig, name: string, target = ciTarget(null), options: CiLogsOptions = { tailLines: 160, capture: "ssh-stream" }): Promise> { if (name.length === 0) throw new Error("ci logs requires run id or PipelineRun name"); if (/^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$/u.test(name)) { const runScript = remoteRunLogsScript(name, target, options); const result = options.capture === "ssh-stream" ? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], runScript) : await dispatchSsh(runScript, 60_000, 45_000, true, target); const resultOk = "ok" in result ? result.ok : result.exitCode === 0; if (resultOk || (result.exitCode !== 42 && !result.stderr.includes("no_run_files="))) { return ciLogsResultFromCapture({ ok: resultOk, providerId: target.providerId, targetId: target.targetId, kind: "run", name, options, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, dispatchTaskId: "taskId" in result ? result.taskId : undefined, }); } } const script = pipelineRunLogsScript(name, options); const result = options.capture === "ssh-stream" ? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], script) : await runRemoteKubectl(script, 60_000, 45_000, target); return ciLogsResultFromCapture({ ok: result.exitCode === 0, providerId: target.providerId, targetId: target.targetId, kind: "pipelinerun", name, options, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, dispatchTaskId: "taskId" in result ? result.taskId : undefined, }); }