feat: 增加自动交付超预算非阻塞告警
This commit is contained in:
@@ -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 的首次引导:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Record<string, unknown>>)[0]).toMatchObject({ state: "partial", warning: null, pipelineDurationSeconds: 193 });
|
||||
});
|
||||
});
|
||||
@@ -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<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,
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<Re
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const summary = parsed === null ? null : statusSummary(parsed);
|
||||
const deliveryAuthority = resolveCicdDeliveryAuthority({ consumerId: consumer.id, node: consumer.node, lane: consumer.lane });
|
||||
const deliveryBudget = observePacStatusDeliveryBudget({ policy: pac.deliveryTiming, targetId: target.id, consumerId: consumer.id, summary });
|
||||
const deliveryWarning = record(deliveryBudget.warning);
|
||||
return {
|
||||
ok: result.exitCode === 0 && summary?.ready === true && deliveryAuthority.kind === "pac-pr-merge",
|
||||
action: "platform-infra-pipelines-as-code-status",
|
||||
@@ -1307,8 +1320,9 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
deliveryAuthority,
|
||||
coverage: consumerCoverage(pac, target.id),
|
||||
summary,
|
||||
deliveryBudget,
|
||||
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw ? parsed : undefined,
|
||||
warnings: pac.validationWarnings,
|
||||
warnings: [...pac.validationWarnings, ...(Object.keys(deliveryWarning).length === 0 ? [] : [deliveryWarning])],
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
};
|
||||
}
|
||||
@@ -1513,6 +1527,17 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const remote = parsed ?? compactCapture(result, { full: true });
|
||||
const historyErrors = arrayRecords(record(remote).historyErrors);
|
||||
const rows = arrayRecords(record(remote).rows);
|
||||
const deliveryBudget = observePacHistoryDeliveryBudget({
|
||||
policy: pac.deliveryTiming,
|
||||
targetId: target.id,
|
||||
consumerIds: selectedConsumers.map((consumer) => 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<string, unknown>): Record<string, unknow
|
||||
capabilities: config.capabilities,
|
||||
gitea: config.gitea,
|
||||
display: config.display,
|
||||
deliveryTiming: config.deliveryTiming,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
repository: result.repository,
|
||||
@@ -2202,6 +2231,7 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
|
||||
deliveryAuthority: result.deliveryAuthority,
|
||||
capabilities: record(result.config).capabilities,
|
||||
warnings: result.warnings,
|
||||
deliveryBudget: result.deliveryBudget,
|
||||
summary: compactStatusSummary(record(result.summary)),
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
@@ -2321,6 +2351,7 @@ export function compactHistoryJson(result: Record<string, unknown>, 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<string, unknown>, includeTaskD
|
||||
};
|
||||
}),
|
||||
historyErrors: result.historyErrors,
|
||||
warnings: result.warnings,
|
||||
next: result.next,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
@@ -2476,6 +2508,7 @@ export function renderStatus(result: Record<string, unknown>): 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<string, unknown>): 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<string, unknown>[]):
|
||||
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<string, unknown>): 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<string, unknown>): 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",
|
||||
|
||||
Reference in New Issue
Block a user