From c96cab96f763d5256d1e94f7421b8b0f2f33d06d Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 20:29:54 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=BA=A4=E4=BB=98=E8=B6=85=E9=A2=84=E7=AE=97=E9=9D=9E=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/unidesk-cicd/SKILL.md | 2 + .../unidesk-cicd/references/env-reuse.md | 16 ++ config/platform-infra/pipelines-as-code.yaml | 2 + ...platform-infra-pac-delivery-timing.test.ts | 67 +++++++++ .../src/platform-infra-pac-delivery-timing.ts | 141 +++++++++++++++++- .../src/platform-infra-pipelines-as-code.ts | 62 +++++++- 6 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 scripts/src/platform-infra-pac-delivery-timing.test.ts diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index f195a513..70f4a93b 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -50,6 +50,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 唯一推荐入口是 `platform-infra pipelines-as-code delivery-timing --target NC01 --consumer selfmedia-nc01`; - 一次调用直接披露 GitHub PR `mergedAt`、trigger wait、PipelineRun/TaskRun、端到端耗时、Argo、runtime health 和 `evidenceGaps`; - 证据不完整时保留已成立的耗时字段并返回 `partial` 与明确缺口,不得手工串联第二状态源; + - 端到端耗时严格超过 owning YAML 预算时输出 `pac-automatic-delivery-over-budget`,固定 `blocking=false`、`mutation=false`; + - 超预算提示只下钻 env identity、依赖缓存、BuildKit/cache 和 stage timing,不给出人工恢复命令; - 该入口固定只读且 `mutation=false`,不得用于同步、触发、补跑或运行面写入。 - 新增 PaC consumer 的首次引导: diff --git a/.agents/skills/unidesk-cicd/references/env-reuse.md b/.agents/skills/unidesk-cicd/references/env-reuse.md index b60a61f6..a9a7b32a 100644 --- a/.agents/skills/unidesk-cicd/references/env-reuse.md +++ b/.agents/skills/unidesk-cicd/references/env-reuse.md @@ -62,6 +62,22 @@ The history/status env reuse display is log-derived: - Sentinel's compact table headed `ENV_REUSE NODE_DEPS ...` is parsed as human-readable fallback. - Missing env reuse text is not proof that no reuse happened; first check the consumer type, the relevant TaskRun logs, and whether the pipeline emits the expected fields. +## 自动交付预算告警 + +- 自动交付端到端预算的唯一真相是 `config/platform-infra/pipelines-as-code.yaml#deliveryTiming.endToEndBudgetSeconds`。 +- `delivery-timing` 只有在 PR `mergedAt`、PipelineRun 时间、Argo、runtime、health 和 provenance 证据完整时才判定预算。 +- 证据不完整时状态固定为 `partial`,`overBudget=null`,不得提前输出超预算告警。 +- 严格超过预算时输出 `pac-automatic-delivery-over-budget` typed warning: + - `blocking=false`; + - `mutation=false`; + - 不改变 PipelineRun、artifact、GitOps、Argo、runtime 或 health 的成功终态。 +- 告警只指向现有只读 `status`、`history` 和 `delivery-timing` 入口,并提示检查: + - env identity; + - 依赖缓存; + - BuildKit/cache; + - stage timing。 +- `status/history` 复用同一预算摘要;只有流水线阶段耗时而缺少完整端到端证据时仍显示 `partial`,不得把 PipelineRun 完成等同于 runtime closeout 完成。 + ## Interpreting Results Use these interpretations: diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index e5d04e4a..cea135da 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -19,6 +19,8 @@ defaults: consumerId: hwlab-nc01-v03 display: timeZone: Asia/Shanghai +deliveryTiming: + endToEndBudgetSeconds: 120 observability: configRef: config/platform-infra/observability.yaml tracesEndpoint: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces diff --git a/scripts/src/platform-infra-pac-delivery-timing.test.ts b/scripts/src/platform-infra-pac-delivery-timing.test.ts new file mode 100644 index 00000000..32689513 --- /dev/null +++ b/scripts/src/platform-infra-pac-delivery-timing.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "bun:test"; +import { observePacDeliveryBudget, observePacHistoryDeliveryBudget, observePacStatusDeliveryBudget } from "./platform-infra-pac-delivery-timing"; + +const policy = { + endToEndBudgetSeconds: 120, + configPath: "config/platform-infra/pipelines-as-code.yaml#deliveryTiming.endToEndBudgetSeconds", +}; + +function observe(endToEndSeconds: number | null, complete: boolean) { + return observePacDeliveryBudget({ + policy, + targetId: "NC01", + consumerId: "selfmedia-nc01", + pipelineRunId: "selfmedia-nc01-fixture", + endToEndSeconds, + complete, + }); +} + +describe("PaC automatic delivery budget warning", () => { + test("120 seconds stays within budget without a warning", () => { + const result = observe(120, true); + expect(result).toMatchObject({ state: "complete", overBudget: false, warning: null, blocking: false, mutation: false }); + }); + + test("more than 120 seconds emits a non-blocking env reuse warning", () => { + const result = observe(121, true); + expect(result.overBudget).toBe(true); + expect(result.warning).toMatchObject({ + code: "pac-automatic-delivery-over-budget", + severity: "warning", + blocking: false, + mutation: false, + observedSeconds: 121, + budgetSeconds: 120, + configPath: policy.configPath, + }); + const warningText = JSON.stringify(result.warning); + expect(warningText).toContain("env reuse"); + expect(warningText).toContain("BuildKit/cache"); + expect(warningText).not.toMatch(/trigger-current|refresh|sync|PipelineRun create/iu); + }); + + test("missing timing evidence remains partial and never warns", () => { + const result = observe(null, false); + expect(result).toMatchObject({ state: "partial", overBudget: null, warning: null, blocking: false, mutation: false }); + }); + + test("status and history summaries stay partial without exact end-to-end evidence", () => { + const status = observePacStatusDeliveryBudget({ + policy, + targetId: "NC01", + consumerId: "selfmedia-nc01", + summary: { ready: true, latestPipelineRun: { name: "fixture", durationSeconds: 193 } }, + }); + const history = observePacHistoryDeliveryBudget({ + policy, + targetId: "NC01", + consumerIds: ["selfmedia-nc01"], + defaultConsumerId: "selfmedia-nc01", + rows: [{ id: "fixture", consumer: "selfmedia-nc01", durationSeconds: 193 }], + }); + expect(status).toMatchObject({ state: "partial", overBudget: null, warning: null, pipelineDurationSeconds: 193, mutation: false }); + expect(history).toMatchObject({ state: "partial", blocking: false, mutation: false }); + expect((history.observations as Array>)[0]).toMatchObject({ state: "partial", warning: null, pipelineDurationSeconds: 193 }); + }); +}); diff --git a/scripts/src/platform-infra-pac-delivery-timing.ts b/scripts/src/platform-infra-pac-delivery-timing.ts index 9a68f400..b2de7803 100644 --- a/scripts/src/platform-infra-pac-delivery-timing.ts +++ b/scripts/src/platform-infra-pac-delivery-timing.ts @@ -11,6 +11,21 @@ export interface PacDeliveryTimingBinding { 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 { @@ -34,6 +49,106 @@ function durationBetween(start: unknown, end: unknown): number | 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, @@ -85,11 +200,28 @@ export async function observePacDeliveryTiming( 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: evidenceGaps.length === 0 ? "complete" : "partial", + state, target: binding.target, consumer: binding.consumer, source: { commit, pullRequest, github: githubEvidence }, @@ -99,10 +231,13 @@ export async function observePacDeliveryTiming( pipelineCompletion: completionTime, triggerWaitSeconds: durationBetween(mergedAt, startTime), pipelineDurationSeconds: row.durationSeconds ?? durationBetween(startTime, completionTime), - endToEndSeconds: durationBetween(mergedAt, completionTime), + endToEndSeconds, }, - pipeline: { id: row.id ?? latest.name ?? null, status: row.status ?? latest.status ?? null, taskRuns: row.taskRuns ?? statusSummary.taskRuns ?? null }, + 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, }; } diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 6667168e..df4e35a9 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -37,7 +37,12 @@ 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"; +import { + observePacDeliveryTiming, + observePacHistoryDeliveryBudget, + observePacStatusDeliveryBudget, + type PacDeliveryTimingPolicy, +} 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"; @@ -80,6 +85,7 @@ export interface PacConfig { display: { timeZone: string; }; + deliveryTiming: PacDeliveryTimingPolicy; observability: { configRef: string; tracesEndpoint: string; @@ -461,6 +467,7 @@ export function parsePacConfigDocument( const gitOpsMirrorFlush = y.objectField(closeout, "gitOpsMirrorFlush", "closeout"); const gitea = y.objectField(root, "gitea", ""); const display = y.objectField(root, "display", ""); + const deliveryTiming = y.objectField(root, "deliveryTiming", ""); const observability = y.objectField(root, "observability", ""); const observabilitySampling = y.objectField(observability, "sampling", "observability"); const exporterFailure = y.objectField(observability, "exporterFailure", "observability"); @@ -495,6 +502,10 @@ export function parsePacConfigDocument( display: { timeZone: y.stringField(display, "timeZone", "display"), }, + deliveryTiming: { + endToEndBudgetSeconds: positiveInteger(deliveryTiming, "endToEndBudgetSeconds", "deliveryTiming"), + configPath: `${configLabel}#deliveryTiming.endToEndBudgetSeconds`, + }, observability: { configRef: y.stringField(observability, "configRef", "observability"), tracesEndpoint: urlField(observability, "tracesEndpoint", "observability"), @@ -1297,6 +1308,8 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise consumer.id), + defaultConsumerId: pac.defaults.consumerId, + rows, + }); + const deliveryWarnings = arrayRecords(deliveryBudget.observations) + .map((observation) => record(observation.warning)) + .filter((warning) => Object.keys(warning).length > 0); return { ok: result.exitCode === 0 && parsed?.ok !== false && historyErrors.length === 0, action: "platform-infra-pipelines-as-code-history", @@ -1527,9 +1552,10 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise< }, consumers: selectedConsumers.map((consumer) => consumer.id), detailId: options.detailId, - rows: arrayRecords(record(remote).rows), + rows, + deliveryBudget, historyErrors, - warnings: pac.validationWarnings, + warnings: [...pac.validationWarnings, ...deliveryWarnings], controlPlane: parsed === null ? undefined : { crdPresent: parsed.crdPresent === true, controllerReady: parsed.controllerReady, @@ -2051,6 +2077,7 @@ function configSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRep metadata: pac.metadata, release: pac.release, capabilities: capabilitySummary(pac), + deliveryTiming: pac.deliveryTiming, gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch }, display: pac.display, repositories: pac.repositories.map(repositorySummary), @@ -2070,6 +2097,7 @@ function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository: providerType: repository.providerType, sourceUrl: repository.cloneUrl, displayTimeZone: pac.display.timeZone, + deliveryTiming: pac.deliveryTiming, consumer: `${consumer.node}/${consumer.lane}`, consumerId: consumer.id, pipeline: consumer.pipeline, @@ -2159,6 +2187,7 @@ function compactPlanJson(result: Record): Record): Record, includeTaskD config: result.config, consumers: result.consumers, detailId: result.detailId, + deliveryBudget: result.deliveryBudget, rows: arrayRecords(result.rows).map((row) => { const taskRuns = record(row.taskRuns); const artifact = record(row.artifact); @@ -2393,6 +2424,7 @@ export function compactHistoryJson(result: Record, includeTaskD }; }), historyErrors: result.historyErrors, + warnings: result.warnings, next: result.next, valuesPrinted: false, }; @@ -2476,6 +2508,7 @@ export function renderStatus(result: Record): RenderedCliResult const coverage = arrayRecords(result.coverage); const warnings = arrayRecords(result.warnings); const latest = record(summary.latestPipelineRun); + const deliveryBudget = record(result.deliveryBudget); const taskRuns = arrayRecords(summary.taskRuns); const artifact = record(summary.artifact); const argo = record(summary.argo); @@ -2521,6 +2554,7 @@ export function renderStatus(result: Record): RenderedCliResult "", "LATEST PIPELINERUN", ...table(["NAME", "STATUS", "REASON", "DURATION_S", "SOURCE"], [[stringValue(latest.name), stringValue(latest.status), stringValue(latest.reason), stringValue(latest.durationSeconds), short(stringValue(latest.sourceCommit))]]), + ` delivery-budget: state=${stringValue(deliveryBudget.state)} end-to-end=${stringValue(deliveryBudget.observedSeconds)}s/${stringValue(deliveryBudget.budgetSeconds)}s exact=${stringValue(deliveryBudget.exactCommand)}`, "", "DELIVERY OBSERVATION", ...table(["MODE", "VALID", "SOURCE_MATCHED", "AFFECTED", "ROLLOUT", "BUILD", "REUSED", "REASON"], [[ @@ -2585,12 +2619,21 @@ function renderPacConfigWarnings(warnings: readonly Record[]): return [ "", "NON-BLOCKING WARNINGS", - ...table(["OBJECT", "BLOCKING", "CODE", "CONFIG"], warnings.map((item) => [ + ...table(["OBJECT", "BLOCKING", "CODE", "CONFIG", "OBSERVED/BUDGET"], warnings.map((item) => [ `${stringValue(record(item.object).kind)}:${stringValue(record(item.object).id)}`, boolText(item.blocking), stringValue(item.code), stringValue(item.configPath), + item.observedSeconds === undefined ? "-" : `${stringValue(item.observedSeconds)}s/${stringValue(item.budgetSeconds)}s`, ])), + ...warnings.flatMap((item) => { + const hint = record(item.hint); + return Object.keys(hint).length === 0 ? [] : [ + ` hint: ${compactLine(stringValue(hint.summary))}`, + ` status: ${stringValue(hint.status)}`, + ` history: ${stringValue(hint.history)}`, + ]; + }), ]; } @@ -2681,6 +2724,7 @@ async function deliveryTiming(config: UniDeskConfig, options: CommonOptions): Pr branch: authority.consumer.sourceBranch, pipeline: consumer.pipeline, }, + policy: pac.deliveryTiming, }, { history: async () => await history(config, historyOptions), status: async () => await status(config, options), @@ -2694,14 +2738,18 @@ function renderDeliveryTiming(result: Record): RenderedCliResul const runtime = record(result.runtime); const argo = record(runtime.argo); const health = record(runtime.health); + const deliveryBudget = record(result.deliveryBudget); + const warnings = arrayRecords(result.warnings); 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)}`, + `BUDGET_S: ${stringValue(deliveryBudget.budgetSeconds)} OVER_BUDGET: ${stringValue(deliveryBudget.overBudget)} BUDGET_STATE: ${stringValue(deliveryBudget.state)}`, `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(", ") || "-"}`, + ...renderPacConfigWarnings(warnings), ]; return rendered(result, "platform-infra pipelines-as-code delivery-timing", lines); } @@ -2710,12 +2758,16 @@ export function renderHistory(result: Record): RenderedCliResul const rows = arrayRecords(result.rows); const config = record(result.config); const historyErrors = arrayRecords(result.historyErrors); + const deliveryBudget = record(result.deliveryBudget); + const warnings = arrayRecords(result.warnings); const detailId = stringValue(result.detailId); const timeHeader = `TIME(${stringValue(config.displayTimeZone)})`; const lines = [ "PLATFORM-INFRA PIPELINES-AS-CODE HISTORY", `DATA: ${stringValue(config.source)}`, `ROWS: ${stringValue(rows.length)} LIMIT_PER_CONSUMER: ${stringValue(config.limitPerConsumer)} READ_ERRORS: ${stringValue(historyErrors.length)}`, + `DELIVERY_BUDGET: state=${stringValue(deliveryBudget.state)} budget=${stringValue(record(deliveryBudget.policy).endToEndBudgetSeconds)}s exact=${stringValue(deliveryBudget.exactCommand)}`, + ...renderPacConfigWarnings(warnings), ...(historyErrors.length === 0 ? [] : [ "", "READ ERRORS",