feat: add pac cicd history table

This commit is contained in:
Codex
2026-07-06 00:43:24 +00:00
parent f7bed4527f
commit ed718f939a
3 changed files with 412 additions and 2 deletions
+174 -2
View File
@@ -116,6 +116,11 @@ interface CommonOptions {
raw: boolean;
}
interface HistoryOptions extends CommonOptions {
limit: number;
detailId: string | null;
}
interface ApplyOptions extends CommonOptions {
confirm: boolean;
dryRun: boolean;
@@ -153,6 +158,11 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
const result = await status(config, options);
return options.full || options.raw ? result : renderStatus(result);
}
if (action === "history" || action === "runs") {
const options = parseHistoryOptions(args.slice(1));
const result = await history(config, options);
return options.full || options.raw ? result : renderHistory(result);
}
if (action === "webhook-test") {
const options = parseWebhookTestOptions(args.slice(1));
const result = await webhookTest(config, options);
@@ -163,13 +173,15 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
function help(): Record<string, unknown> {
return {
command: "platform-infra pipelines-as-code plan|apply|status|webhook-test",
command: "platform-infra pipelines-as-code plan|apply|status|history|webhook-test",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --dry-run",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--full|--raw]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
"bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target JD01 --confirm",
],
boundary: "Sole CI trigger path for GH-1552: Gitea webhook -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime.",
@@ -392,6 +404,38 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
};
}
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const selectedConsumers = options.consumerId === null
? pac.consumers
: [resolveConsumer(pac, options.consumerId)];
const firstConsumer = selectedConsumers[0];
if (firstConsumer === undefined) throw new Error("no Pipelines-as-Code consumers are configured");
const firstRepository = resolveRepository(pac, firstConsumer.repositoryRef);
const secrets = ensureSecrets(pac, false);
const result = await capture(config, target.route, ["sh"], remoteScript("history", pac, target, firstRepository, firstConsumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, "", selectedConsumers));
const parsed = parseJsonOutput(result.stdout);
const remote = parsed ?? compactCapture(result, { full: true });
return {
ok: result.exitCode === 0 && parsed?.ok !== false,
action: "platform-infra-pipelines-as-code-history",
mutation: false,
target: targetSummary(target),
config: {
path: configLabel,
source: "Gitea Repository CR + Tekton PipelineRun/TaskRun live objects; no UniDesk-owned history store is written.",
limitPerConsumer: options.limit,
valuesPrinted: false,
},
consumers: selectedConsumers.map((consumer) => consumer.id),
detailId: options.detailId,
rows: arrayRecords(record(remote).rows),
remote,
next: nextCommands(target.id, options.consumerId ?? pac.defaults.consumerId, pac.defaults.consumerId),
};
}
async function webhookTest(config: UniDeskConfig, options: WebhookTestOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
@@ -420,7 +464,7 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
return text;
}
function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions, secrets: SecretMaterial, releaseManifest: string): string {
function remoteScript(action: "apply" | "status" | "history" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
const env: Record<string, string> = {
UNIDESK_PAC_ACTION: action,
@@ -448,7 +492,11 @@ function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfi
UNIDESK_PAC_WEBHOOK_SECRET_KEY: repository.webhookSecretKey,
UNIDESK_PAC_CONCURRENCY_LIMIT: String(repository.concurrencyLimit),
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
UNIDESK_PAC_PIPELINE_NAME: consumer.pipeline,
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
UNIDESK_PAC_HISTORY_LIMIT: "limit" in options ? String(options.limit) : "5",
UNIDESK_PAC_HISTORY_ID: "detailId" in options && options.detailId !== null ? options.detailId : "",
UNIDESK_PAC_HISTORY_CONSUMERS_B64: Buffer.from(JSON.stringify(historyConsumers.map((item) => historyConsumerConfig(pac, item))), "utf8").toString("base64"),
UNIDESK_PAC_ARGO_NAMESPACE: consumer.argoNamespace,
UNIDESK_PAC_ARGO_APPLICATION: consumer.argoApplication,
UNIDESK_PAC_PART_OF: pacPartOf(consumer.id),
@@ -458,6 +506,19 @@ function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfi
return `${exports}\n${readFileSync(remoteScriptFile, "utf8")}`;
}
function historyConsumerConfig(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
const repository = resolveRepository(pac, consumer.repositoryRef);
return {
id: consumer.id,
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
repository: repository.name,
repo: `${repository.owner}/${repository.repo}`,
repoUrl: repository.url,
};
}
function credentialPath(root: string, sourceRef: string): string {
return sourceRef.startsWith("/") ? sourceRef : join(root, sourceRef);
}
@@ -628,6 +689,7 @@ function nextCommands(targetId: string, consumerId: string, defaultConsumerId: s
return {
apply: `bun scripts/cli.ts platform-infra pipelines-as-code apply --target ${targetId}${suffix} --confirm`,
status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}${suffix}`,
history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId}${suffix}`,
webhookTest: `bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target ${targetId}${suffix} --confirm`,
};
}
@@ -709,6 +771,35 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
return rendered(result, "platform-infra pipelines-as-code status", lines);
}
function renderHistory(result: Record<string, unknown>): RenderedCliResult {
const rows = arrayRecords(result.rows);
const config = record(result.config);
const detailId = stringValue(result.detailId);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE HISTORY",
`DATA: ${stringValue(config.source)}`,
`ROWS: ${stringValue(rows.length)} LIMIT_PER_CONSUMER: ${stringValue(config.limitPerConsumer)}`,
"",
"TRIGGERS",
...(rows.length === 0 ? ["-"] : table(["TIME", "CONSUMER", "REPO", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [
isoShort(stringValue(row.triggeredAt)),
stringValue(row.consumer),
stringValue(row.repo),
statusText(row),
stringValue(row.durationSeconds),
short(stringValue(row.commit)),
envReuseText(record(row.envReuse)),
stringValue(row.id ?? row.pipelineRun),
]))),
...(detailId === "-" || rows.length !== 1 ? [] : renderHistoryDetail(rows[0] ?? {})),
"",
"NEXT",
rows.length > 0 && detailId === "-" ? ` detail: ${stringValue(record(result.next).history)} --id ${stringValue(rows[0]?.id ?? rows[0]?.pipelineRun)}` : null,
` full: ${stringValue(record(result.next).history)}${detailId === "-" ? "" : ` --id ${detailId}`} --full`,
];
return rendered(result, "platform-infra pipelines-as-code history", lines.filter((line): line is string => line !== null));
}
function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
const remote = record(result.remote);
const lines = [
@@ -760,6 +851,36 @@ function parseWebhookTestOptions(args: string[]): WebhookTestOptions {
return { ...parseCommonOptions(commonArgs), confirm };
}
function parseHistoryOptions(args: string[]): HistoryOptions {
const commonArgs: string[] = [];
let limit = 5;
let detailId: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--limit") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value");
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 50) throw new Error("--limit must be an integer from 1 to 50");
limit = parsed;
index += 1;
} else if (arg === "--id" || arg === "--run" || arg === "--pipeline-run") {
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 PipelineRun id`);
detailId = value;
index += 1;
} else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
return { ...parseCommonOptions(commonArgs), limit, detailId };
}
function parseCommonOptions(args: string[]): CommonOptions {
let targetId: string | null = null;
let consumerId: string | null = null;
@@ -839,6 +960,57 @@ function short(value: string, length = 12): string {
return value.slice(0, length);
}
function isoShort(value: string): string {
if (value === "-") return value;
return value.replace(/:\d\d(?:\.\d+)?Z$/u, "Z");
}
function statusText(row: Record<string, unknown>): string {
const status = stringValue(row.status);
const reason = stringValue(row.reason);
if (status === "-" && reason === "-") return "-";
if (status === "True" && reason !== "-") return reason;
if (reason === "-") return status;
if (status === "-") return reason;
return `${status}/${reason}`;
}
function envReuseText(envReuse: Record<string, unknown>): string {
const status = stringValue(envReuse.status);
const skipped = stringValue(envReuse.buildSkippedCount);
const reused = stringValue(envReuse.serviceReusedCount);
const cache = stringValue(envReuse.buildCache);
const parts = [
status === "-" ? null : status,
skipped === "-" ? null : `skip=${skipped}`,
reused === "-" ? null : `reuse=${reused}`,
cache === "-" ? null : `cache=${cache}`,
].filter((item): item is string => item !== null);
return parts.length === 0 ? "-" : parts.join(",");
}
function renderHistoryDetail(row: Record<string, unknown>): string[] {
const envReuse = record(row.envReuse);
const taskRuns = record(row.taskRuns);
const longest = record(taskRuns.longest);
return [
"",
"DETAIL",
...table(["FIELD", "VALUE"], [
["id", stringValue(row.id ?? row.pipelineRun)],
["pipeline", stringValue(row.pipeline)],
["branch", stringValue(row.branch)],
["commit", stringValue(row.commit)],
["started", stringValue(row.startTime)],
["completed", stringValue(row.completionTime)],
["taskRuns", stringValue(taskRuns.count)],
["longestTask", stringValue(longest.name)],
["longestTaskDurationS", stringValue(longest.durationSeconds)],
["envReuseSource", stringValue(envReuse.source)],
]),
];
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";