244 lines
10 KiB
TypeScript
244 lines
10 KiB
TypeScript
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;
|
|
};
|
|
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<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 function observePacDeliveryBudget(input: PacDeliveryBudgetObservationInput): Record<string, unknown> {
|
|
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<string, unknown> | null;
|
|
}): Record<string, unknown> {
|
|
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<string, unknown>[];
|
|
}): Record<string, unknown> {
|
|
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<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");
|
|
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,
|
|
};
|
|
}
|