feat: add pac cicd history table
This commit is contained in:
@@ -258,6 +258,220 @@ NODE
|
||||
rm -f "$payload_file"
|
||||
}
|
||||
|
||||
history_rows() {
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const cp = require('node:child_process');
|
||||
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
|
||||
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
|
||||
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
|
||||
|
||||
function kubectlJson(args, fallback) {
|
||||
try {
|
||||
const out = cp.execFileSync('kubectl', args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 30000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
return JSON.parse(out || JSON.stringify(fallback));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function condition(item) {
|
||||
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
|
||||
return { status: c.status || '', reason: c.reason || '' };
|
||||
}
|
||||
|
||||
function durationSeconds(start, done) {
|
||||
if (!start) return null;
|
||||
const startMs = Date.parse(start);
|
||||
const doneMs = done ? Date.parse(done) : Date.now();
|
||||
if (!Number.isFinite(startMs) || !Number.isFinite(doneMs)) return null;
|
||||
return Math.max(0, Math.round((doneMs - startMs) / 1000));
|
||||
}
|
||||
|
||||
function mapParams(params) {
|
||||
const out = {};
|
||||
for (const item of params || []) {
|
||||
if (!item || typeof item.name !== 'string') continue;
|
||||
if (typeof item.value === 'string') out[item.name] = item.value;
|
||||
else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function firstString(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string' && value.length > 0) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractCommit(item, params) {
|
||||
const labels = item.metadata?.labels || {};
|
||||
const annotations = item.metadata?.annotations || {};
|
||||
return firstString(
|
||||
labels['pipelinesascode.tekton.dev/sha'],
|
||||
labels['pipelinesascode.tekton.dev/commit'],
|
||||
labels['unidesk.ai/source-commit'],
|
||||
labels['hwlab.pikastech.local/source-commit'],
|
||||
labels['agentrun.pikastech.local/source-commit'],
|
||||
annotations['pipelinesascode.tekton.dev/sha'],
|
||||
annotations['pipelinesascode.tekton.dev/commit'],
|
||||
annotations['unidesk.ai/source-commit'],
|
||||
params.revision,
|
||||
params.source_revision,
|
||||
params.sourceCommit,
|
||||
params.source_commit,
|
||||
params.git_sha,
|
||||
params.sha,
|
||||
params.commit
|
||||
);
|
||||
}
|
||||
|
||||
function extractBranch(item, params) {
|
||||
const labels = item.metadata?.labels || {};
|
||||
const annotations = item.metadata?.annotations || {};
|
||||
return firstString(
|
||||
labels['pipelinesascode.tekton.dev/branch'],
|
||||
annotations['pipelinesascode.tekton.dev/branch'],
|
||||
params.source_branch,
|
||||
params.branch,
|
||||
params.git_branch
|
||||
);
|
||||
}
|
||||
|
||||
function recursiveEnvReuse(value, state) {
|
||||
if (value === null || value === undefined) return;
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) recursiveEnvReuse(item, state);
|
||||
return;
|
||||
}
|
||||
if (typeof value !== 'object') return;
|
||||
if (typeof value.buildSkippedCount === 'number') state.buildSkippedCount = value.buildSkippedCount;
|
||||
if (typeof value.serviceReusedCount === 'number') state.serviceReusedCount = value.serviceReusedCount;
|
||||
if (typeof value.reusedServiceCount === 'number') state.serviceReusedCount = value.reusedServiceCount;
|
||||
if (typeof value.envReuse === 'string') state.status = value.envReuse;
|
||||
if (typeof value.envReuseStatus === 'string') state.status = value.envReuseStatus;
|
||||
if (value.envReuse && typeof value.envReuse === 'object') {
|
||||
const env = value.envReuse;
|
||||
if (typeof env.mode === 'string') state.status = env.mode;
|
||||
if (typeof env.status === 'string') state.status = env.status;
|
||||
if (typeof env.dependencyReuse === 'string') state.status = env.dependencyReuse;
|
||||
if (typeof env.buildSkippedCount === 'number') state.buildSkippedCount = env.buildSkippedCount;
|
||||
if (typeof env.serviceReusedCount === 'number') state.serviceReusedCount = env.serviceReusedCount;
|
||||
}
|
||||
if (typeof value.status === 'string' && /reus|skip|built|publish/u.test(value.status)) state.status = value.status;
|
||||
for (const item of Object.values(value)) recursiveEnvReuse(item, state);
|
||||
}
|
||||
|
||||
function envReuseForPipelineRun(namespace, name) {
|
||||
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'logs' };
|
||||
let logs = '';
|
||||
try {
|
||||
logs = cp.execFileSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers', '--tail=320'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 12000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
return { ...state, source: 'logs-unavailable' };
|
||||
}
|
||||
const lines = logs.split(/\r?\n/u);
|
||||
const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
|
||||
if (headerIndex >= 0) {
|
||||
const row = (lines.slice(headerIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
|
||||
if (row.length >= 5) {
|
||||
state.status = row[0];
|
||||
state.buildCache = row[4];
|
||||
}
|
||||
}
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
||||
try {
|
||||
recursiveEnvReuse(JSON.parse(trimmed), state);
|
||||
} catch {}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function taskSummary(taskRuns, name) {
|
||||
const rows = (taskRuns.items || [])
|
||||
.filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name)
|
||||
.map((item) => {
|
||||
const c = condition(item);
|
||||
return {
|
||||
name: item.metadata?.name || '',
|
||||
status: c.status || null,
|
||||
reason: c.reason || null,
|
||||
startTime: item.status?.startTime || null,
|
||||
completionTime: item.status?.completionTime || null,
|
||||
durationSeconds: durationSeconds(item.status?.startTime, item.status?.completionTime),
|
||||
};
|
||||
});
|
||||
const longest = [...rows].sort((a, b) => (b.durationSeconds || 0) - (a.durationSeconds || 0))[0] || null;
|
||||
return { count: rows.length, longest };
|
||||
}
|
||||
|
||||
function rowFor(consumer, item, taskRuns) {
|
||||
const params = mapParams(item.spec?.params);
|
||||
const c = condition(item);
|
||||
const tasks = taskSummary(taskRuns, item.metadata.name);
|
||||
const labels = item.metadata?.labels || {};
|
||||
const annotations = item.metadata?.annotations || {};
|
||||
return {
|
||||
id: item.metadata.name,
|
||||
consumer: consumer.id,
|
||||
repository: consumer.repository,
|
||||
repo: consumer.repo,
|
||||
repoUrl: consumer.repoUrl,
|
||||
pipeline: consumer.pipeline,
|
||||
pipelineRun: item.metadata.name,
|
||||
triggeredAt: item.metadata?.creationTimestamp || item.status?.startTime || null,
|
||||
startTime: item.status?.startTime || null,
|
||||
completionTime: item.status?.completionTime || null,
|
||||
durationSeconds: durationSeconds(item.status?.startTime || item.metadata?.creationTimestamp, item.status?.completionTime),
|
||||
status: c.status || null,
|
||||
reason: c.reason || null,
|
||||
commit: extractCommit(item, params),
|
||||
branch: extractBranch(item, params),
|
||||
eventType: firstString(labels['pipelinesascode.tekton.dev/event-type'], annotations['pipelinesascode.tekton.dev/event-type']),
|
||||
sender: firstString(labels['pipelinesascode.tekton.dev/sender'], annotations['pipelinesascode.tekton.dev/sender']),
|
||||
envReuse: envReuseForPipelineRun(consumer.namespace, item.metadata.name),
|
||||
taskRuns: tasks,
|
||||
source: 'gitea-pac-tekton-live',
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const consumerSummaries = [];
|
||||
for (const consumer of consumers) {
|
||||
const pipelineRuns = kubectlJson(['-n', consumer.namespace, 'get', 'pipelinerun', '-o', 'json'], { items: [] });
|
||||
const taskRuns = kubectlJson(['-n', consumer.namespace, 'get', 'taskrun', '-o', 'json'], { items: [] });
|
||||
let matches = (pipelineRuns.items || []).filter((item) => {
|
||||
const labels = item.metadata?.labels || {};
|
||||
const name = item.metadata?.name || '';
|
||||
if (detailId) return name === detailId;
|
||||
return name.startsWith(consumer.pipelineRunPrefix)
|
||||
|| labels['tekton.dev/pipeline'] === consumer.pipeline
|
||||
|| labels['tekton.dev/pipelineName'] === consumer.pipeline;
|
||||
});
|
||||
matches = matches.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0));
|
||||
if (!detailId) matches = matches.slice(0, limit);
|
||||
for (const item of matches) rows.push(rowFor(consumer, item, taskRuns));
|
||||
consumerSummaries.push({ id: consumer.id, namespace: consumer.namespace, repository: consumer.repository, matched: matches.length });
|
||||
}
|
||||
rows.sort((a, b) => Date.parse(b.triggeredAt || 0) - Date.parse(a.triggeredAt || 0));
|
||||
process.stdout.write(JSON.stringify({ rows, consumers: consumerSummaries }));
|
||||
NODE
|
||||
}
|
||||
|
||||
task_rows() {
|
||||
payload_file=$(mktemp)
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
|
||||
@@ -428,6 +642,27 @@ status_action() {
|
||||
"$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime"
|
||||
}
|
||||
|
||||
history_action() {
|
||||
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
|
||||
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
|
||||
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
|
||||
hooks=$(hook_summary)
|
||||
history=$(history_rows)
|
||||
rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
|
||||
consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
|
||||
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","consumerRows":%s,"rows":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
|
||||
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
|
||||
"$( [ -n "$crd" ] && echo true || echo false )" \
|
||||
"$(json_string "$controller_ready")" \
|
||||
"$(json_string "$repository_condition")" \
|
||||
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
|
||||
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
|
||||
"$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
|
||||
"$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
|
||||
"$(json_string "$UNIDESK_PAC_CONSUMER_ID")" \
|
||||
"$consumers" "$rows" "$hooks"
|
||||
}
|
||||
|
||||
webhook_test_action() {
|
||||
hooks=$(hook_summary)
|
||||
hook_id=$(printf '%s' "$hooks" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(String(a[0]?.id||""))')
|
||||
@@ -439,6 +674,7 @@ webhook_test_action() {
|
||||
case "$UNIDESK_PAC_ACTION" in
|
||||
apply) apply_action ;;
|
||||
status) status_action ;;
|
||||
history) history_action ;;
|
||||
webhook-test) webhook_test_action ;;
|
||||
*) printf '{"ok":false,"error":"unsupported-action","valuesPrinted":false}\n'; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -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) : "-";
|
||||
|
||||
Reference in New Issue
Block a user