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
@@ -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