import { resolveToken } from "./gh/auth-and-safety"; import { githubRequest, isGitHubError } from "./gh/client"; export interface PacDeliveryTimingBinding { target: Record; consumer: { id: string; node: string; lane: string; repository: string; branch: string; pipeline: string; }; policy: PacDeliveryTimingPolicy; } export interface PacDeliveryTimingPolicy { endToEndBudgetSeconds: number; configPath: string; } export interface PacDeliveryBudgetObservationInput { policy: PacDeliveryTimingPolicy; targetId: string; consumerId: string; pipelineRunId: string | null; endToEndSeconds: number | null; complete: boolean; } interface PacDeliveryTimingObservers { history: () => Promise>; status: () => Promise>; } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function records(value: unknown): Record[] { 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 function observePacDeliveryBudget(input: PacDeliveryBudgetObservationInput): Record { const evidenceState = input.complete && input.endToEndSeconds !== null ? "complete" : "partial"; const overBudget = evidenceState === "complete" && (input.endToEndSeconds as number) > input.policy.endToEndBudgetSeconds; const warning = overBudget ? { code: "pac-automatic-delivery-over-budget", severity: "warning", blocking: false, mutation: false, object: { kind: "consumer", id: input.consumerId }, configPath: input.policy.configPath, metric: "end-to-end-seconds", observedSeconds: input.endToEndSeconds, budgetSeconds: input.policy.endToEndBudgetSeconds, hint: { summary: "优先检查并优化 env reuse;对照 env identity、依赖缓存、BuildKit/cache 和 stage timing 定位慢阶段。", status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${input.targetId} --consumer ${input.consumerId} --json`, history: input.pipelineRunId === null ? `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${input.targetId} --consumer ${input.consumerId} --limit 10 --json` : `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${input.targetId} --consumer ${input.consumerId} --id ${input.pipelineRunId} --full`, focus: ["env-identity", "dependency-cache", "buildkit-cache", "stage-timing"], mutation: false, valuesPrinted: false, }, valuesPrinted: false, } : null; return { state: evidenceState, metric: "end-to-end-seconds", observedSeconds: input.endToEndSeconds, budgetSeconds: input.policy.endToEndBudgetSeconds, overBudget: evidenceState === "complete" ? overBudget : null, configPath: input.policy.configPath, warning, blocking: false, mutation: false, valuesPrinted: false, }; } export function observePacStatusDeliveryBudget(input: { policy: PacDeliveryTimingPolicy; targetId: string; consumerId: string; summary: Record | null; }): Record { const latest = record(input.summary?.latestPipelineRun); const observedEndToEnd = typeof latest.endToEndSeconds === "number" ? latest.endToEndSeconds : null; return { ...observePacDeliveryBudget({ policy: input.policy, targetId: input.targetId, consumerId: input.consumerId, pipelineRunId: typeof latest.name === "string" ? latest.name : null, endToEndSeconds: observedEndToEnd, complete: input.summary?.ready === true && observedEndToEnd !== null, }), evidenceSource: "status-summary", pipelineDurationSeconds: typeof latest.durationSeconds === "number" ? latest.durationSeconds : null, exactCommand: `bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target ${input.targetId} --consumer ${input.consumerId} --json`, }; } export function observePacHistoryDeliveryBudget(input: { policy: PacDeliveryTimingPolicy; targetId: string; consumerIds: readonly string[]; defaultConsumerId: string; rows: readonly Record[]; }): Record { const observations = input.rows.map((row) => { const consumerId = typeof row.consumer === "string" ? row.consumer : input.consumerIds[0] ?? input.defaultConsumerId; const observedEndToEnd = typeof row.endToEndSeconds === "number" ? row.endToEndSeconds : null; return { ...observePacDeliveryBudget({ policy: input.policy, targetId: input.targetId, consumerId, pipelineRunId: typeof row.id === "string" ? row.id : typeof row.pipelineRun === "string" ? row.pipelineRun : null, endToEndSeconds: observedEndToEnd, complete: row.deliveryComplete === true && observedEndToEnd !== null, }), consumer: consumerId, pipelineRun: row.id ?? row.pipelineRun ?? null, evidenceSource: "history-summary", pipelineDurationSeconds: typeof row.durationSeconds === "number" ? row.durationSeconds : null, }; }); return { state: observations.length > 0 && observations.every((item) => item.state === "complete") ? "complete" : "partial", policy: input.policy, observations, exactCommand: input.consumerIds.length === 1 ? `bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target ${input.targetId} --consumer ${input.consumerIds[0]} --json` : null, blocking: false, mutation: false, valuesPrinted: false, }; } export async function observePacDeliveryTiming( binding: PacDeliveryTimingBinding, observers: PacDeliveryTimingObservers, ): Promise> { 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 | null = null; let githubEvidence: Record = { 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(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"); if (statusSummary.ready !== true) evidenceGaps.push("runtime-closeout-not-ready"); const state = evidenceGaps.length === 0 ? "complete" : "partial"; const endToEndSeconds = durationBetween(mergedAt, completionTime); const pipelineRunId = typeof row.id === "string" ? row.id : typeof latest.name === "string" ? latest.name : null; const deliveryBudget = observePacDeliveryBudget({ policy: binding.policy, targetId: String(binding.target.id ?? binding.consumer.node), consumerId: binding.consumer.id, pipelineRunId, endToEndSeconds, complete: state === "complete", }); const warning = record(deliveryBudget.warning); return { ok: commit !== null && startTime !== null && completionTime !== null, action: "platform-infra-pipelines-as-code-delivery-timing", mutation: false, state, 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, }, deliveryBudget, warnings: Object.keys(warning).length === 0 ? [] : [warning], pipeline: { id: pipelineRunId, status: row.status ?? latest.status ?? null, taskRuns: row.taskRuns ?? statusSummary.taskRuns ?? null }, runtime: { argo, health: runtime, provenance }, evidenceGaps: [...new Set(evidenceGaps)], valuesPrinted: false, }; }