feat: add PaC delivery timing query

This commit is contained in:
Codex
2026-07-14 10:38:42 +02:00
parent d97605d8f2
commit 7e8b83962e
6 changed files with 217 additions and 1 deletions
@@ -0,0 +1,108 @@
import { resolveToken } from "./gh/auth-and-safety";
import { githubRequest, isGitHubError } from "./gh/client";
export interface PacDeliveryTimingBinding {
target: Record<string, unknown>;
consumer: {
id: string;
node: string;
lane: string;
repository: string;
branch: string;
pipeline: string;
};
}
interface PacDeliveryTimingObservers {
history: () => Promise<Record<string, unknown>>;
status: () => Promise<Record<string, unknown>>;
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function records(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.map(record) : [];
}
function durationBetween(start: unknown, end: unknown): number | null {
if (typeof start !== "string" || typeof end !== "string") return null;
const a = Date.parse(start);
const b = Date.parse(end);
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
return Math.max(0, Math.round((b - a) / 1000));
}
export async function observePacDeliveryTiming(
binding: PacDeliveryTimingBinding,
observers: PacDeliveryTimingObservers,
): Promise<Record<string, unknown>> {
const [sourceOwner, sourceRepo] = binding.consumer.repository.split("/");
if (!sourceOwner || !sourceRepo) throw new Error(`invalid owning YAML source repository ${binding.consumer.repository}`);
const [historyResult, statusResult] = await Promise.all([observers.history(), observers.status()]);
const row = records(historyResult.rows)[0] ?? {};
const statusSummary = record(statusResult.summary);
const latest = record(statusSummary.latestPipelineRun);
const commit = typeof row.commit === "string" ? row.commit : typeof latest.sourceCommit === "string" ? latest.sourceCommit : null;
const evidenceGaps: string[] = [];
let pullRequest: Record<string, unknown> | null = null;
let githubEvidence: Record<string, unknown> = { repository: binding.consumer.repository, source: "github-commit-pulls", mutation: false };
if (commit === null) {
evidenceGaps.push("source-commit-missing");
} else {
const tokenInfo = resolveToken(binding.consumer.repository, false);
if (tokenInfo.token === null) {
evidenceGaps.push("github-token-unavailable");
githubEvidence = { ...githubEvidence, ok: false, token: tokenInfo.probe };
} else {
const response = await githubRequest<unknown>(tokenInfo.token, "GET", `/repos/${sourceOwner}/${sourceRepo}/commits/${commit}/pulls`);
if (isGitHubError(response)) {
evidenceGaps.push("github-pr-lookup-failed");
githubEvidence = { ...githubEvidence, ok: false, error: response };
} else {
const prs = Array.isArray(response) ? response : [];
const merged = prs.find((item) => record(item).merged_at !== null && record(item).merged_at !== undefined) ?? prs[0];
if (merged === undefined) {
evidenceGaps.push("github-pr-not-found");
} else {
const item = record(merged);
pullRequest = { number: item.number ?? null, title: item.title ?? null, url: item.html_url ?? null, mergedAt: item.merged_at ?? null, mergeCommit: commit };
if (typeof item.merged_at !== "string") evidenceGaps.push("github-mergedAt-missing");
}
githubEvidence = { ...githubEvidence, ok: true, count: prs.length, token: tokenInfo.probe };
}
}
}
const mergedAt = pullRequest?.mergedAt ?? null;
const startTime = row.startTime ?? null;
const completionTime = row.completionTime ?? null;
const argo = record(statusSummary.argo);
const runtime = record(statusSummary.runtime);
const provenance = record(statusSummary.provenance);
if (mergedAt === null) evidenceGaps.push("mergedAt-missing");
if (startTime === null || completionTime === null) evidenceGaps.push("pipeline-timestamps-missing");
if (argo.sync === undefined || argo.health === undefined) evidenceGaps.push("argo-closeout-missing");
if (runtime.readyReplicas === undefined) evidenceGaps.push("runtime-health-missing");
if (provenance.relation === "unknown") evidenceGaps.push("runtime-provenance-unknown");
return {
ok: commit !== null && startTime !== null && completionTime !== null,
action: "platform-infra-pipelines-as-code-delivery-timing",
mutation: false,
state: evidenceGaps.length === 0 ? "complete" : "partial",
target: binding.target,
consumer: binding.consumer,
source: { commit, pullRequest, github: githubEvidence },
timing: {
mergedAt,
pipelineStart: startTime,
pipelineCompletion: completionTime,
triggerWaitSeconds: durationBetween(mergedAt, startTime),
pipelineDurationSeconds: row.durationSeconds ?? durationBetween(startTime, completionTime),
endToEndSeconds: durationBetween(mergedAt, completionTime),
},
pipeline: { id: row.id ?? latest.name ?? null, status: row.status ?? latest.status ?? null, taskRuns: row.taskRuns ?? statusSummary.taskRuns ?? null },
runtime: { argo, health: runtime, provenance },
evidenceGaps: [...new Set(evidenceGaps)],
};
}
@@ -32,6 +32,7 @@ import { runPlatformInfraGiteaCommand } from "./platform-infra-gitea";
import { readGiteaConfig } from "./platform-infra-gitea-config";
import { resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { featureConfigSchemaHistoryFields, renderFeatureConfigSchemaLine } from "./platform-infra-pac-feature-config-projection";
import { observePacDeliveryTiming } from "./platform-infra-pac-delivery-timing";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
@@ -273,6 +274,11 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
const result = await history(config, options);
return options.raw ? result : options.full ? compactHistoryJson(result, true) : options.json ? compactHistoryJson(result) : renderHistory(result);
}
if (action === "delivery-timing" || action === "timing") {
const options = parseCommonOptions(args.slice(1));
const result = await deliveryTiming(config, options);
return options.full || options.raw || options.json ? result : renderDeliveryTiming(result);
}
if (action === "debug-step") {
const options = parseHistoryOptions(args.slice(1));
const result = await debugStep(config, options);
@@ -395,12 +401,13 @@ function help(scope: string | null): Record<string, unknown> {
};
}
return {
command: "platform-infra pipelines-as-code plan|status|history|debug-step|source-artifact",
command: "platform-infra pipelines-as-code plan|status|history|delivery-timing|debug-step|source-artifact",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--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 delivery-timing --target NC01 --consumer selfmedia-nc01 [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--consumer <consumer>] [--id <pipelinerun>] [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code source-artifact check --target NC01 --consumer agentrun-nc01-v02 --source-worktree /abs/worktree",
@@ -2423,6 +2430,49 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
return rendered(result, "platform-infra pipelines-as-code closeout", lines);
}
async function deliveryTiming(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const consumer = resolveConsumer(pac, options.consumerId);
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
const authority = resolveCicdDeliveryAuthority({ consumerId: consumer.id, node: consumer.node, lane: consumer.lane });
if (authority.kind !== "pac-pr-merge") throw new Error(`delivery authority is ${authority.kind}: ${authority.reason}`);
const historyOptions: HistoryOptions = { ...options, limit: 1, detailId: null };
return await observePacDeliveryTiming({
target: targetSummary(target),
consumer: {
id: consumer.id,
node: consumer.node,
lane: consumer.lane,
repository: authority.consumer.sourceRepository,
branch: authority.consumer.sourceBranch,
pipeline: consumer.pipeline,
},
}, {
history: async () => await history(config, historyOptions),
status: async () => await status(config, options),
});
}
function renderDeliveryTiming(result: Record<string, unknown>): RenderedCliResult {
const timing = record(result.timing);
const source = record(result.source);
const pr = record(source.pullRequest);
const runtime = record(result.runtime);
const argo = record(runtime.argo);
const health = record(runtime.health);
const lines = [
"PLATFORM-INFRA DELIVERY TIMING",
`STATE: ${stringValue(result.state)} MUTATION: ${boolText(result.mutation)}`,
`SOURCE: ${stringValue(record(result.consumer).repository)}@${stringValue(source.commit)} PR#${stringValue(pr.number)}`,
`MERGED_AT: ${stringValue(timing.mergedAt)} PIPELINE_START: ${stringValue(timing.pipelineStart)} PIPELINE_DONE: ${stringValue(timing.pipelineCompletion)}`,
`TRIGGER_WAIT_S: ${stringValue(timing.triggerWaitSeconds)} PIPELINE_S: ${stringValue(timing.pipelineDurationSeconds)} END_TO_END_S: ${stringValue(timing.endToEndSeconds)}`,
`ARGO: ${stringValue(argo.sync)}/${stringValue(argo.health)} RUNTIME_READY: ${stringValue(health.readyReplicas)}/${stringValue(health.replicas)}`,
`EVIDENCE_GAPS: ${(Array.isArray(result.evidenceGaps) ? result.evidenceGaps.map(String) : []).join(", ") || "-"}`,
];
return rendered(result, "platform-infra pipelines-as-code delivery-timing", lines);
}
export function renderHistory(result: Record<string, unknown>): RenderedCliResult {
const rows = arrayRecords(result.rows);
const config = record(result.config);