Files
pikasTech-unidesk/scripts/src/cicd-node-status.ts
T
2026-07-11 11:42:16 +02:00

172 lines
6.9 KiB
TypeScript

import type { UniDeskConfig } from "./config";
import { renderMachine } from "./cicd-render";
import type { RenderedCliResult } from "./output";
import { getPlatformInfraPipelinesAsCodeNodeStatus } from "./platform-infra-pipelines-as-code";
interface NodeStatusOptions {
nodeId: string | null;
targetId: string | null;
full: boolean;
raw: boolean;
output: "text" | "json";
}
export function cicdNodeStatusHelp(): Record<string, unknown> {
return {
command: "cicd status --node <NODE>",
description: "只读汇总节点上每个 Pipelines-as-Code consumer 的自动交付状态。",
usage: [
"bun scripts/cli.ts cicd status --node NC01",
"bun scripts/cli.ts cicd status --node NC01 --full",
"bun scripts/cli.ts cicd status --node NC01 --json",
],
output: "compact text by default; --json/--raw returns machine payload",
source: "config/platform-infra/pipelines-as-code.yaml consumers filtered by node",
};
}
export async function runCicdNodeStatusCommand(config: UniDeskConfig | null, args: string[]): Promise<RenderedCliResult> {
if (args[0] === undefined || args.some(isHelpToken)) {
return renderMachine("cicd status", cicdNodeStatusHelp(), "json");
}
const options = parseNodeStatusOptions(args);
if (config === null) throw new Error("cicd status requires UniDesk config; run through bun scripts/cli.ts cicd status --node <NODE>");
const result = await getPlatformInfraPipelinesAsCodeNodeStatus(config, {
targetId: options.targetId,
nodeId: options.nodeId,
full: options.full,
raw: options.raw,
});
if (options.output === "json" || options.raw) return renderMachine("cicd status", result, "json", result.ok !== false);
return renderNodeStatus(result, options.full);
}
function parseNodeStatusOptions(args: string[]): NodeStatusOptions {
let nodeId: string | null = null;
let targetId: string | null = null;
let full = false;
let raw = false;
let output: "text" | "json" = "text";
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "status") continue;
if (arg === "--node" || arg === "--target") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple node id`);
if (arg === "--node") nodeId = value;
else targetId = value;
index += 1;
} else if (arg === "--full") {
full = true;
} else if (arg === "--raw" || arg === "-o=json" || (arg === "-o" && args[index + 1] === "json")) {
raw = true;
full = true;
output = "json";
if (arg === "-o") index += 1;
} else if (arg === "--json") {
output = "json";
} else if (isHelpToken(arg)) {
output = "json";
} else {
throw new Error(`unsupported cicd status option: ${arg}`);
}
}
if (nodeId === null && targetId === null) throw new Error("cicd status requires --node <NODE>");
return { nodeId, targetId, full, raw, output };
}
function renderNodeStatus(result: Record<string, unknown>, full: boolean): RenderedCliResult {
const target = record(result.target);
const summary = record(result.summary);
const consumers = arrayRecords(result.consumers);
const rows = consumers.map((row) => [
stringValue(row.consumer),
boolText(row.ready),
stringValue(row.pipelineStatus),
`${stringValue(row.durationSeconds)}s`,
short(stringValue(row.sourceCommit)),
short(stringValue(row.gitopsCommit)),
`${stringValue(record(row.argo).sync)}/${stringValue(record(row.argo).health)}`,
stringValue(record(record(row.argo).revisionRelation).relation),
runtimeText(record(row.runtime)),
stringValue(row.reason),
]);
const detailRows = consumers.map((row) => [
stringValue(row.consumer),
stringValue(row.latestPipelineRun),
short(stringValue(row.digest), 18),
stringValue(row.imageStatus),
compactLine(stringValue(record(row.diagnostics).hint)),
]);
const lines = [
"CI/CD NODE STATUS",
...table(["NODE", "TARGET", "READY", "READY_COUNT", "BLOCKED", "ELAPSED_MS"], [[
stringValue(result.node),
stringValue(target.id),
boolText(summary.ready),
`${stringValue(summary.readyCount)}/${stringValue(summary.total)}`,
stringValue(summary.blockedCount),
stringValue(summary.elapsedMs),
]]),
"",
"CONSUMERS",
...(rows.length === 0 ? ["-"] : table(["CONSUMER", "READY", "PIPELINE", "DUR", "SOURCE", "GITOPS", "ARGO", "RELATION", "RUNTIME", "REASON"], rows)),
"",
...(full ? [
"DETAIL",
...(detailRows.length === 0 ? ["-"] : table(["CONSUMER", "PIPELINERUN", "DIGEST", "IMAGE_STATUS", "HINT"], detailRows)),
"",
] : []),
"NEXT",
` status: ${stringValue(record(result.next).status)}`,
` history: ${stringValue(record(result.next).history)}`,
` fix-automatic-delivery: ${stringValue(record(record(result.next).fixAutomaticDelivery).reference)}`,
];
return { ok: result.ok !== false, command: "cicd status", renderedText: lines.join("\n"), contentType: "text/plain" };
}
function isHelpToken(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record<string, unknown>[] : [];
}
function stringValue(value: unknown, fallback = "-"): string {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return fallback;
}
function boolText(value: unknown): string {
return value === true ? "true" : "false";
}
function short(value: string, length = 12): string {
if (value === "-" || value.length <= length) return value;
return value.slice(0, length);
}
function runtimeText(runtime: Record<string, unknown>): string {
const ready = `${stringValue(runtime.readyReplicas)}/${stringValue(runtime.replicas)}`;
const digest = short(stringValue(runtime.digest), 18);
return digest === "-" ? ready : `${ready}:${digest}`;
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
return [format(headers), format(headers.map((header, index) => "-".repeat(Math.max(header.length, widths[index])))), ...rows.map(format)];
}