172 lines
6.9 KiB
TypeScript
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)];
|
|
}
|