137 lines
6.9 KiB
TypeScript
137 lines
6.9 KiB
TypeScript
// 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<string, unknown> {
|
|
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<Record<string, unknown>> {
|
|
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,
|
|
});
|
|
}
|