fix(cicd): 收敛手动发布就绪与耗时观测
This commit is contained in:
@@ -129,7 +129,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
|
|||||||
- 唯一推荐入口是 `platform-infra pipelines-as-code delivery-timing --target <NODE> --consumer <id>`;
|
- 唯一推荐入口是 `platform-infra pipelines-as-code delivery-timing --target <NODE> --consumer <id>`;
|
||||||
- 一次调用直接披露 GitHub PR `mergedAt`、trigger wait、PipelineRun/TaskRun、端到端耗时、Argo、runtime health 和 `evidenceGaps`;
|
- 一次调用直接披露 GitHub PR `mergedAt`、trigger wait、PipelineRun/TaskRun、端到端耗时、Argo、runtime health 和 `evidenceGaps`;
|
||||||
- 证据不完整时保留已成立的耗时字段并返回 `partial` 与明确缺口,不得手工串联第二状态源;
|
- 证据不完整时保留已成立的耗时字段并返回 `partial` 与明确缺口,不得手工串联第二状态源;
|
||||||
- 端到端耗时严格超过 owning YAML 预算时输出 `pac-automatic-delivery-over-budget`,固定 `blocking=false`、`mutation=false`;
|
- PaC migrated consumer 从 CLI webhook durable admission 开始计时,PR merge 到人工 trigger 的等待不计入发布耗时;
|
||||||
|
- 端到端耗时严格超过 owning YAML 预算时输出 `pac-manual-delivery-over-budget`,固定 `blocking=false`、`mutation=false`,并直接披露最长 TaskRun 与 budget evidence gaps;
|
||||||
- 超预算提示只下钻 env identity、依赖缓存、BuildKit/cache 和 stage timing,不给出人工恢复命令;
|
- 超预算提示只下钻 env identity、依赖缓存、BuildKit/cache 和 stage timing,不给出人工恢复命令;
|
||||||
- 该入口固定只读且 `mutation=false`,不得用于同步、触发、补跑或运行面写入。
|
- 该入口固定只读且 `mutation=false`,不得用于同步、触发、补跑或运行面写入。
|
||||||
- 验收 registry cache 性能时,按 [Gitea + PaC](references/gitea-pac.md) 的冷热两次正常 PR 流程执行,不用人工 PipelineRun 或重复 status/timing 试探。
|
- 验收 registry cache 性能时,按 [Gitea + PaC](references/gitea-pac.md) 的冷热两次正常 PR 流程执行,不用人工 PipelineRun 或重复 status/timing 试探。
|
||||||
|
|||||||
@@ -699,7 +699,9 @@ function evaluatePacStatus(inputValue) {
|
|||||||
const runtimeMatches = expectedDigest === null || runtimeDigest === expectedDigest;
|
const runtimeMatches = expectedDigest === null || runtimeDigest === expectedDigest;
|
||||||
const runtimeReady = Number.isInteger(runtime.replicas)
|
const runtimeReady = Number.isInteger(runtime.replicas)
|
||||||
&& runtime.replicas > 0
|
&& runtime.replicas > 0
|
||||||
&& runtime.readyReplicas === runtime.replicas;
|
&& runtime.readyReplicas === runtime.replicas
|
||||||
|
&& (!Number.isInteger(runtime.updatedReplicas) || runtime.updatedReplicas === runtime.replicas)
|
||||||
|
&& (!Number.isInteger(runtime.generation) || !Number.isInteger(runtime.observedGeneration) || runtime.observedGeneration >= runtime.generation);
|
||||||
const selectedArtifactDigests = [...new Set([
|
const selectedArtifactDigests = [...new Set([
|
||||||
artifactDigest,
|
artifactDigest,
|
||||||
...(Array.isArray(artifact.digests) ? artifact.digests : []),
|
...(Array.isArray(artifact.digests) ? artifact.digests : []),
|
||||||
@@ -772,11 +774,6 @@ function evaluatePacStatus(inputValue) {
|
|||||||
code = "pac-retained-gitops-missing";
|
code = "pac-retained-gitops-missing";
|
||||||
phase = "retained-gitops-missing";
|
phase = "retained-gitops-missing";
|
||||||
hint = "the no-runtime-change observation is valid but the retained Argo GitOps revision is missing";
|
hint = "the no-runtime-change observation is valid but the retained Argo GitOps revision is missing";
|
||||||
} else if (noRuntimeChange && (argo.sync !== "Synced" || argo.health !== "Healthy")) {
|
|
||||||
ok = false;
|
|
||||||
code = "pac-retained-argo-not-ready";
|
|
||||||
phase = "retained-argo-not-ready";
|
|
||||||
hint = "the no-runtime-change observation is valid but retained Argo is not Synced/Healthy";
|
|
||||||
} else if (noRuntimeChange && gitopsRelation !== "retained") {
|
} else if (noRuntimeChange && gitopsRelation !== "retained") {
|
||||||
ok = false;
|
ok = false;
|
||||||
code = "pac-retained-revision-unproven";
|
code = "pac-retained-revision-unproven";
|
||||||
@@ -800,7 +797,7 @@ function evaluatePacStatus(inputValue) {
|
|||||||
} else if (noRuntimeChange) {
|
} else if (noRuntimeChange) {
|
||||||
code = "pac-ready-no-runtime-change";
|
code = "pac-ready-no-runtime-change";
|
||||||
phase = "ready-no-runtime-change";
|
phase = "ready-no-runtime-change";
|
||||||
hint = "the successful source observation requires no artifact, GitOps or runtime change; retained Argo and runtime provenance remain healthy";
|
hint = "the successful source observation requires no artifact, GitOps or runtime change; retained runtime provenance and minimal health remain ready";
|
||||||
} else if (catalogPresent && !catalogSourceMatches) {
|
} else if (catalogPresent && !catalogSourceMatches) {
|
||||||
ok = false;
|
ok = false;
|
||||||
code = "pac-artifact-catalog-source-mismatch";
|
code = "pac-artifact-catalog-source-mismatch";
|
||||||
@@ -836,11 +833,6 @@ function evaluatePacStatus(inputValue) {
|
|||||||
code = "pac-gitops-missing";
|
code = "pac-gitops-missing";
|
||||||
phase = "artifact-ready-gitops-missing";
|
phase = "artifact-ready-gitops-missing";
|
||||||
hint = "PipelineRun completed but its GitOps commit is not visible";
|
hint = "PipelineRun completed but its GitOps commit is not visible";
|
||||||
} else if (argo.sync !== "Synced" || argo.health !== "Healthy") {
|
|
||||||
ok = false;
|
|
||||||
code = "pac-argo-not-ready";
|
|
||||||
phase = "gitops-ready-argo-pending";
|
|
||||||
hint = "GitOps exists but Argo is not Synced/Healthy";
|
|
||||||
} else if (!gitopsRevisionAligned) {
|
} else if (!gitopsRevisionAligned) {
|
||||||
ok = false;
|
ok = false;
|
||||||
code = `pac-argo-revision-${gitopsRelation}`;
|
code = `pac-argo-revision-${gitopsRelation}`;
|
||||||
@@ -881,8 +873,8 @@ function evaluatePacStatus(inputValue) {
|
|||||||
hint = deliverySkipped
|
hint = deliverySkipped
|
||||||
? "YAML-declared runtime inputs are unchanged; the existing image, GitOps manifest, runtime and health remain aligned"
|
? "YAML-declared runtime inputs are unchanged; the existing image, GitOps manifest, runtime and health remain aligned"
|
||||||
: gitopsRelation === "descendant"
|
: gitopsRelation === "descendant"
|
||||||
? "Argo is on a commit-graph-proven GitOps descendant and the selected artifact, runtime, Argo and health are aligned"
|
? "the observed GitOps revision is a commit-graph-proven descendant and the selected artifact, runtime and minimal health are aligned"
|
||||||
: "Argo is on the exact selected GitOps commit and the artifact, runtime, Argo and health are aligned";
|
: "the observed GitOps revision exactly matches and the artifact, runtime and minimal health are aligned";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1149,23 +1141,29 @@ function evaluatePacNodeStatusSnapshot(inputValue) {
|
|||||||
const digest = digestFromImage(image);
|
const digest = digestFromImage(image);
|
||||||
const desired = Number.isInteger(deploymentSpec.replicas) ? deploymentSpec.replicas : null;
|
const desired = Number.isInteger(deploymentSpec.replicas) ? deploymentSpec.replicas : null;
|
||||||
const readyReplicas = Number.isInteger(deploymentStatus.readyReplicas) ? deploymentStatus.readyReplicas : 0;
|
const readyReplicas = Number.isInteger(deploymentStatus.readyReplicas) ? deploymentStatus.readyReplicas : 0;
|
||||||
|
const updatedReplicas = Number.isInteger(deploymentStatus.updatedReplicas) ? deploymentStatus.updatedReplicas : 0;
|
||||||
|
const generation = Number.isInteger(record(deployment?.metadata).generation) ? deployment.metadata.generation : null;
|
||||||
|
const observedGeneration = Number.isInteger(deploymentStatus.observedGeneration) ? deploymentStatus.observedGeneration : null;
|
||||||
const pipelineReadReady = !readErrorStages.has("pipeline-runs");
|
const pipelineReadReady = !readErrorStages.has("pipeline-runs");
|
||||||
const argoReadReady = !readErrorStages.has("argo-applications");
|
const argoReadReady = !readErrorStages.has("argo-applications");
|
||||||
const runtimeReadReady = !readErrorStages.has("deployments");
|
const runtimeReadReady = !readErrorStages.has("deployments");
|
||||||
const ciReady = pipelineReadReady && selected !== null && pipelineCondition.status === "True";
|
const ciReady = pipelineReadReady && selected !== null && pipelineCondition.status === "True";
|
||||||
const argoReady = argoReadReady && application !== null && appSync.status === "Synced" && appHealth.status === "Healthy";
|
const argoReady = argoReadReady && application !== null;
|
||||||
const runtimeNotApplicable = target === null;
|
const runtimeNotApplicable = target === null;
|
||||||
const runtimeReady = runtimeReadReady && (runtimeNotApplicable || (deployment !== null && desired !== null && desired > 0 && readyReplicas === desired));
|
const runtimeReady = runtimeReadReady && (runtimeNotApplicable || (deployment !== null
|
||||||
const ready = ciReady && argoReady && runtimeReady;
|
&& desired !== null
|
||||||
|
&& desired > 0
|
||||||
|
&& readyReplicas === desired
|
||||||
|
&& updatedReplicas === desired
|
||||||
|
&& generation !== null
|
||||||
|
&& observedGeneration !== null
|
||||||
|
&& observedGeneration >= generation));
|
||||||
|
const ready = ciReady && runtimeReady;
|
||||||
const reason = !pipelineReadReady
|
const reason = !pipelineReadReady
|
||||||
? "pipeline-read-failed"
|
? "pipeline-read-failed"
|
||||||
: !ciReady
|
: !ciReady
|
||||||
? selected === null ? "pipeline-missing" : `ci:${pipelineCondition.reason || pipelineCondition.status || "unknown"}`
|
? selected === null ? "pipeline-missing" : `ci:${pipelineCondition.reason || pipelineCondition.status || "unknown"}`
|
||||||
: !argoReadReady
|
: !runtimeReadReady
|
||||||
? "argo-read-failed"
|
|
||||||
: !argoReady
|
|
||||||
? "argo-not-ready"
|
|
||||||
: !runtimeReadReady
|
|
||||||
? "runtime-read-failed"
|
? "runtime-read-failed"
|
||||||
: !runtimeReady
|
: !runtimeReady
|
||||||
? "runtime-not-ready"
|
? "runtime-not-ready"
|
||||||
@@ -1202,7 +1200,10 @@ function evaluatePacNodeStatusSnapshot(inputValue) {
|
|||||||
runtime: {
|
runtime: {
|
||||||
deployment: firstString(record(deployment?.metadata).name),
|
deployment: firstString(record(deployment?.metadata).name),
|
||||||
readyReplicas,
|
readyReplicas,
|
||||||
|
updatedReplicas,
|
||||||
replicas: desired,
|
replicas: desired,
|
||||||
|
generation,
|
||||||
|
observedGeneration,
|
||||||
digest,
|
digest,
|
||||||
image,
|
image,
|
||||||
...(expectedSourceCommit === null ? {} : {
|
...(expectedSourceCommit === null ? {} : {
|
||||||
@@ -1360,6 +1361,12 @@ function runPacStatusFixtureChecks() {
|
|||||||
expectedCode: "pac-ready-gitops-exact",
|
expectedCode: "pac-ready-gitops-exact",
|
||||||
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
|
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "delivery-ready-ignores-argo-aggregate-health",
|
||||||
|
expectedOk: true,
|
||||||
|
expectedCode: "pac-ready-gitops-exact",
|
||||||
|
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: { ...exactArgo, sync: "OutOfSync", health: "Degraded" }, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "equal-revisions-ignore-owning-git-fetch-failure",
|
id: "equal-revisions-ignore-owning-git-fetch-failure",
|
||||||
expectedOk: true,
|
expectedOk: true,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { observePacDeliveryBudget, observePacHistoryDeliveryBudget, observePacStatusDeliveryBudget } from "./platform-infra-pac-delivery-timing";
|
import { observePacDeliveryBudget, observePacHistoryDeliveryBudget, observePacStatusDeliveryBudget, pacDeliveryTimingWindow } from "./platform-infra-pac-delivery-timing";
|
||||||
|
|
||||||
const policy = {
|
const policy = {
|
||||||
endToEndBudgetSeconds: 120,
|
endToEndBudgetSeconds: 120,
|
||||||
@@ -17,7 +17,31 @@ function observe(endToEndSeconds: number | null, complete: boolean) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("PaC automatic delivery budget warning", () => {
|
describe("PaC manual delivery budget warning", () => {
|
||||||
|
test("manual delivery timing starts at PaC webhook admission instead of PR merge", () => {
|
||||||
|
expect(pacDeliveryTimingWindow({
|
||||||
|
triggeredAt: "2026-07-21T11:35:42Z",
|
||||||
|
startTime: "2026-07-21T11:35:52Z",
|
||||||
|
completionTime: "2026-07-21T11:37:42Z",
|
||||||
|
durationSeconds: 110,
|
||||||
|
})).toEqual({
|
||||||
|
deliveryStart: "2026-07-21T11:35:42Z",
|
||||||
|
deliveryStartSource: "pac-webhook-admission",
|
||||||
|
pipelineStart: "2026-07-21T11:35:52Z",
|
||||||
|
pipelineCompletion: "2026-07-21T11:37:42Z",
|
||||||
|
triggerWaitSeconds: 10,
|
||||||
|
pipelineDurationSeconds: 110,
|
||||||
|
endToEndSeconds: 120,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing webhook admission stays unknown instead of falling back to PR merge", () => {
|
||||||
|
expect(pacDeliveryTimingWindow({
|
||||||
|
startTime: "2026-07-21T11:35:52Z",
|
||||||
|
completionTime: "2026-07-21T11:37:42Z",
|
||||||
|
})).toMatchObject({ deliveryStart: null, deliveryStartSource: null, triggerWaitSeconds: null, endToEndSeconds: null });
|
||||||
|
});
|
||||||
|
|
||||||
test("120 seconds stays within budget without a warning", () => {
|
test("120 seconds stays within budget without a warning", () => {
|
||||||
const result = observe(120, true);
|
const result = observe(120, true);
|
||||||
expect(result).toMatchObject({ state: "complete", overBudget: false, warning: null, blocking: false, mutation: false });
|
expect(result).toMatchObject({ state: "complete", overBudget: false, warning: null, blocking: false, mutation: false });
|
||||||
@@ -27,7 +51,7 @@ describe("PaC automatic delivery budget warning", () => {
|
|||||||
const result = observe(121, true);
|
const result = observe(121, true);
|
||||||
expect(result.overBudget).toBe(true);
|
expect(result.overBudget).toBe(true);
|
||||||
expect(result.warning).toMatchObject({
|
expect(result.warning).toMatchObject({
|
||||||
code: "pac-automatic-delivery-over-budget",
|
code: "pac-manual-delivery-over-budget",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
blocking: false,
|
blocking: false,
|
||||||
mutation: false,
|
mutation: false,
|
||||||
|
|||||||
@@ -49,11 +49,48 @@ function durationBetween(start: unknown, end: unknown): number | null {
|
|||||||
return Math.max(0, Math.round((b - a) / 1000));
|
return Math.max(0, Math.round((b - a) / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pacDeliveryTimingWindow(row: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const deliveryStart = typeof row.triggeredAt === "string" ? row.triggeredAt : null;
|
||||||
|
const pipelineStart = typeof row.startTime === "string" ? row.startTime : null;
|
||||||
|
const pipelineCompletion = typeof row.completionTime === "string" ? row.completionTime : null;
|
||||||
|
return {
|
||||||
|
deliveryStart,
|
||||||
|
deliveryStartSource: deliveryStart === null ? null : "pac-webhook-admission",
|
||||||
|
pipelineStart,
|
||||||
|
pipelineCompletion,
|
||||||
|
triggerWaitSeconds: durationBetween(deliveryStart, pipelineStart),
|
||||||
|
pipelineDurationSeconds: typeof row.durationSeconds === "number"
|
||||||
|
? row.durationSeconds
|
||||||
|
: durationBetween(pipelineStart, pipelineCompletion),
|
||||||
|
endToEndSeconds: durationBetween(deliveryStart, pipelineCompletion),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeTaskRuns(value: unknown): Record<string, unknown> {
|
||||||
|
const taskRuns = record(value);
|
||||||
|
const details = records(taskRuns.details).map((item) => ({
|
||||||
|
name: item.name ?? null,
|
||||||
|
status: item.status ?? null,
|
||||||
|
reason: item.reason ?? null,
|
||||||
|
durationSeconds: item.durationSeconds ?? null,
|
||||||
|
}));
|
||||||
|
const timed = details.filter((item) => typeof item.durationSeconds === "number");
|
||||||
|
const longest = timed.reduce<Record<string, unknown> | null>((selected, item) => (
|
||||||
|
selected === null || Number(item.durationSeconds) > Number(selected.durationSeconds) ? item : selected
|
||||||
|
), null);
|
||||||
|
return {
|
||||||
|
count: typeof taskRuns.count === "number" ? taskRuns.count : details.length,
|
||||||
|
stages: details,
|
||||||
|
longest,
|
||||||
|
valuesPrinted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function observePacDeliveryBudget(input: PacDeliveryBudgetObservationInput): Record<string, unknown> {
|
export function observePacDeliveryBudget(input: PacDeliveryBudgetObservationInput): Record<string, unknown> {
|
||||||
const evidenceState = input.complete && input.endToEndSeconds !== null ? "complete" : "partial";
|
const evidenceState = input.complete && input.endToEndSeconds !== null ? "complete" : "partial";
|
||||||
const overBudget = evidenceState === "complete" && (input.endToEndSeconds as number) > input.policy.endToEndBudgetSeconds;
|
const overBudget = evidenceState === "complete" && (input.endToEndSeconds as number) > input.policy.endToEndBudgetSeconds;
|
||||||
const warning = overBudget ? {
|
const warning = overBudget ? {
|
||||||
code: "pac-automatic-delivery-over-budget",
|
code: "pac-manual-delivery-over-budget",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
blocking: false,
|
blocking: false,
|
||||||
mutation: false,
|
mutation: false,
|
||||||
@@ -161,10 +198,12 @@ export async function observePacDeliveryTiming(
|
|||||||
const latest = record(statusSummary.latestPipelineRun);
|
const latest = record(statusSummary.latestPipelineRun);
|
||||||
const commit = typeof row.commit === "string" ? row.commit : typeof latest.sourceCommit === "string" ? latest.sourceCommit : null;
|
const commit = typeof row.commit === "string" ? row.commit : typeof latest.sourceCommit === "string" ? latest.sourceCommit : null;
|
||||||
const evidenceGaps: string[] = [];
|
const evidenceGaps: string[] = [];
|
||||||
|
const budgetEvidenceGaps: string[] = [];
|
||||||
let pullRequest: Record<string, unknown> | null = null;
|
let pullRequest: Record<string, unknown> | null = null;
|
||||||
let githubEvidence: Record<string, unknown> = { repository: binding.consumer.repository, source: "github-commit-pulls", mutation: false };
|
let githubEvidence: Record<string, unknown> = { repository: binding.consumer.repository, source: "github-commit-pulls", mutation: false };
|
||||||
if (commit === null) {
|
if (commit === null) {
|
||||||
evidenceGaps.push("source-commit-missing");
|
evidenceGaps.push("source-commit-missing");
|
||||||
|
budgetEvidenceGaps.push("source-commit-missing");
|
||||||
} else {
|
} else {
|
||||||
const tokenInfo = resolveToken(binding.consumer.repository, false);
|
const tokenInfo = resolveToken(binding.consumer.repository, false);
|
||||||
if (tokenInfo.token === null) {
|
if (tokenInfo.token === null) {
|
||||||
@@ -190,19 +229,25 @@ export async function observePacDeliveryTiming(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const mergedAt = pullRequest?.mergedAt ?? null;
|
const mergedAt = pullRequest?.mergedAt ?? null;
|
||||||
const startTime = row.startTime ?? null;
|
const timing = pacDeliveryTimingWindow(row);
|
||||||
const completionTime = row.completionTime ?? null;
|
const deliveryStart = timing.deliveryStart;
|
||||||
|
const startTime = timing.pipelineStart;
|
||||||
|
const completionTime = timing.pipelineCompletion;
|
||||||
const argo = record(statusSummary.argo);
|
const argo = record(statusSummary.argo);
|
||||||
const runtime = record(statusSummary.runtime);
|
const runtime = record(statusSummary.runtime);
|
||||||
const provenance = record(statusSummary.provenance);
|
const provenance = record(statusSummary.provenance);
|
||||||
if (mergedAt === null) evidenceGaps.push("mergedAt-missing");
|
if (mergedAt === null) evidenceGaps.push("mergedAt-missing");
|
||||||
if (startTime === null || completionTime === null) evidenceGaps.push("pipeline-timestamps-missing");
|
if (deliveryStart === null) budgetEvidenceGaps.push("manual-webhook-admission-missing");
|
||||||
if (argo.sync === undefined || argo.health === undefined) evidenceGaps.push("argo-closeout-missing");
|
if (startTime === null || completionTime === null) budgetEvidenceGaps.push("pipeline-timestamps-missing");
|
||||||
if (runtime.readyReplicas === undefined) evidenceGaps.push("runtime-health-missing");
|
if (runtime.readyReplicas === undefined) budgetEvidenceGaps.push("runtime-health-missing");
|
||||||
if (provenance.relation === "unknown") evidenceGaps.push("runtime-provenance-unknown");
|
const replicas = typeof runtime.replicas === "number" ? runtime.replicas : null;
|
||||||
if (statusSummary.ready !== true) evidenceGaps.push("runtime-closeout-not-ready");
|
const readyReplicas = typeof runtime.readyReplicas === "number" ? runtime.readyReplicas : null;
|
||||||
const state = evidenceGaps.length === 0 ? "complete" : "partial";
|
if (replicas !== null && (replicas <= 0 || readyReplicas === null || readyReplicas < replicas)) {
|
||||||
const endToEndSeconds = durationBetween(mergedAt, completionTime);
|
budgetEvidenceGaps.push("runtime-minimal-readiness-not-ready");
|
||||||
|
}
|
||||||
|
evidenceGaps.push(...budgetEvidenceGaps);
|
||||||
|
const state = budgetEvidenceGaps.length === 0 ? "complete" : "partial";
|
||||||
|
const endToEndSeconds = typeof timing.endToEndSeconds === "number" ? timing.endToEndSeconds : null;
|
||||||
const pipelineRunId = typeof row.id === "string"
|
const pipelineRunId = typeof row.id === "string"
|
||||||
? row.id
|
? row.id
|
||||||
: typeof latest.name === "string"
|
: typeof latest.name === "string"
|
||||||
@@ -225,19 +270,17 @@ export async function observePacDeliveryTiming(
|
|||||||
target: binding.target,
|
target: binding.target,
|
||||||
consumer: binding.consumer,
|
consumer: binding.consumer,
|
||||||
source: { commit, pullRequest, github: githubEvidence },
|
source: { commit, pullRequest, github: githubEvidence },
|
||||||
timing: {
|
timing: { mergedAt, ...timing },
|
||||||
mergedAt,
|
|
||||||
pipelineStart: startTime,
|
|
||||||
pipelineCompletion: completionTime,
|
|
||||||
triggerWaitSeconds: durationBetween(mergedAt, startTime),
|
|
||||||
pipelineDurationSeconds: row.durationSeconds ?? durationBetween(startTime, completionTime),
|
|
||||||
endToEndSeconds,
|
|
||||||
},
|
|
||||||
deliveryBudget,
|
deliveryBudget,
|
||||||
warnings: Object.keys(warning).length === 0 ? [] : [warning],
|
warnings: Object.keys(warning).length === 0 ? [] : [warning],
|
||||||
pipeline: { id: pipelineRunId, status: row.status ?? latest.status ?? null, taskRuns: row.taskRuns ?? statusSummary.taskRuns ?? null },
|
pipeline: {
|
||||||
|
id: pipelineRunId,
|
||||||
|
status: row.status ?? latest.status ?? null,
|
||||||
|
taskRuns: summarizeTaskRuns(row.taskRuns ?? statusSummary.taskRuns ?? null),
|
||||||
|
},
|
||||||
runtime: { argo, health: runtime, provenance },
|
runtime: { argo, health: runtime, provenance },
|
||||||
evidenceGaps: [...new Set(evidenceGaps)],
|
evidenceGaps: [...new Set(evidenceGaps)],
|
||||||
|
budgetEvidenceGaps: [...new Set(budgetEvidenceGaps)],
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1537,7 +1537,10 @@ const workload = (deploy) => {
|
|||||||
namespace: deploy.metadata?.namespace || null,
|
namespace: deploy.metadata?.namespace || null,
|
||||||
deployment: deploy.metadata?.name || null,
|
deployment: deploy.metadata?.name || null,
|
||||||
readyReplicas: deploy.status?.readyReplicas ?? 0,
|
readyReplicas: deploy.status?.readyReplicas ?? 0,
|
||||||
|
updatedReplicas: deploy.status?.updatedReplicas ?? 0,
|
||||||
replicas: deploy.spec?.replicas ?? null,
|
replicas: deploy.spec?.replicas ?? null,
|
||||||
|
generation: deploy.metadata?.generation ?? null,
|
||||||
|
observedGeneration: deploy.status?.observedGeneration ?? null,
|
||||||
sourceCommit: sourceCommit(deploy),
|
sourceCommit: sourceCommit(deploy),
|
||||||
image,
|
image,
|
||||||
digest,
|
digest,
|
||||||
@@ -2061,20 +2064,23 @@ const argo = JSON.parse(process.env.UNIDESK_PAC_ARGO_JSON || '{}');
|
|||||||
const runtime = JSON.parse(process.env.UNIDESK_PAC_RUNTIME_JSON || '{}');
|
const runtime = JSON.parse(process.env.UNIDESK_PAC_RUNTIME_JSON || '{}');
|
||||||
const output = { ...diagnostics, admissionProvenance, consumerBootstrap, valuesPrinted: false };
|
const output = { ...diagnostics, admissionProvenance, consumerBootstrap, valuesPrinted: false };
|
||||||
const latest = Array.isArray(pipelineRuns) ? pipelineRuns[0] || {} : {};
|
const latest = Array.isArray(pipelineRuns) ? pipelineRuns[0] || {} : {};
|
||||||
const runtimeReady = Number.isInteger(runtime.readyReplicas)
|
const sourceWorkloads = Array.isArray(runtime.workloads)
|
||||||
&& Number.isInteger(runtime.replicas)
|
? runtime.workloads.filter((item) => item?.sourceCommit === latest.sourceCommit)
|
||||||
&& runtime.replicas > 0
|
: [];
|
||||||
&& runtime.readyReplicas >= runtime.replicas;
|
const runtimeReady = sourceWorkloads.length > 0 && sourceWorkloads.every((item) => Number.isInteger(item.readyReplicas)
|
||||||
|
&& Number.isInteger(item.replicas)
|
||||||
|
&& item.replicas > 0
|
||||||
|
&& item.readyReplicas >= item.replicas
|
||||||
|
&& item.updatedReplicas >= item.replicas
|
||||||
|
&& item.observedGeneration >= item.generation);
|
||||||
if (['pac-gitops-missing', 'pac-source-observation-inconsistent'].includes(output.code)
|
if (['pac-gitops-missing', 'pac-source-observation-inconsistent'].includes(output.code)
|
||||||
&& artifact.collector?.mode === 'taskrun-status-only'
|
&& artifact.collector?.mode === 'taskrun-status-only'
|
||||||
&& latest.status === 'True'
|
&& latest.status === 'True'
|
||||||
&& argo.sync === 'Synced'
|
|
||||||
&& argo.health === 'Healthy'
|
|
||||||
&& runtimeReady) {
|
&& runtimeReady) {
|
||||||
output.ok = true;
|
output.ok = true;
|
||||||
output.code = 'pac-artifact-log-evidence-deferred';
|
output.code = 'pac-artifact-log-evidence-deferred';
|
||||||
output.phase = 'read-only-evidence-deferred';
|
output.phase = 'read-only-evidence-deferred';
|
||||||
output.hint = 'PipelineRun, Argo, and runtime are ready; use id-specific history when synchronous TaskRun log evidence is required';
|
output.hint = 'PipelineRun and exact-source workloads are minimally ready; use id-specific history when synchronous TaskRun log evidence is required';
|
||||||
output.warnings = [...(Array.isArray(output.warnings) ? output.warnings : []), {
|
output.warnings = [...(Array.isArray(output.warnings) ? output.warnings : []), {
|
||||||
code: 'pac-artifact-log-evidence-deferred',
|
code: 'pac-artifact-log-evidence-deferred',
|
||||||
blocking: false,
|
blocking: false,
|
||||||
|
|||||||
@@ -3656,16 +3656,21 @@ function renderDeliveryTiming(result: Record<string, unknown>): RenderedCliResul
|
|||||||
const runtime = record(result.runtime);
|
const runtime = record(result.runtime);
|
||||||
const argo = record(runtime.argo);
|
const argo = record(runtime.argo);
|
||||||
const health = record(runtime.health);
|
const health = record(runtime.health);
|
||||||
|
const taskRuns = record(record(result.pipeline).taskRuns);
|
||||||
|
const longestTask = record(taskRuns.longest);
|
||||||
const deliveryBudget = record(result.deliveryBudget);
|
const deliveryBudget = record(result.deliveryBudget);
|
||||||
const warnings = arrayRecords(result.warnings);
|
const warnings = arrayRecords(result.warnings);
|
||||||
const lines = [
|
const lines = [
|
||||||
"PLATFORM-INFRA DELIVERY TIMING",
|
"PLATFORM-INFRA DELIVERY TIMING",
|
||||||
`STATE: ${stringValue(result.state)} MUTATION: ${boolText(result.mutation)}`,
|
`STATE: ${stringValue(result.state)} MUTATION: ${boolText(result.mutation)}`,
|
||||||
`SOURCE: ${stringValue(record(result.consumer).repository)}@${stringValue(source.commit)} PR#${stringValue(pr.number)}`,
|
`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)}`,
|
`MERGED_AT: ${stringValue(timing.mergedAt)} DELIVERY_START: ${stringValue(timing.deliveryStart)} (${stringValue(timing.deliveryStartSource)})`,
|
||||||
|
`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)}`,
|
`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)}`,
|
`BUDGET_S: ${stringValue(deliveryBudget.budgetSeconds)} OVER_BUDGET: ${stringValue(deliveryBudget.overBudget)} BUDGET_STATE: ${stringValue(deliveryBudget.state)}`,
|
||||||
|
`LONGEST_TASK: ${stringValue(longestTask.name)} ${stringValue(longestTask.durationSeconds)}s status=${stringValue(longestTask.reason ?? longestTask.status)}`,
|
||||||
`ARGO: ${stringValue(argo.sync)}/${stringValue(argo.health)} RUNTIME_READY: ${stringValue(health.readyReplicas)}/${stringValue(health.replicas)}`,
|
`ARGO: ${stringValue(argo.sync)}/${stringValue(argo.health)} RUNTIME_READY: ${stringValue(health.readyReplicas)}/${stringValue(health.replicas)}`,
|
||||||
|
`BUDGET_EVIDENCE_GAPS: ${(Array.isArray(result.budgetEvidenceGaps) ? result.budgetEvidenceGaps.map(String) : []).join(", ") || "-"}`,
|
||||||
`EVIDENCE_GAPS: ${(Array.isArray(result.evidenceGaps) ? result.evidenceGaps.map(String) : []).join(", ") || "-"}`,
|
`EVIDENCE_GAPS: ${(Array.isArray(result.evidenceGaps) ? result.evidenceGaps.map(String) : []).join(", ") || "-"}`,
|
||||||
...renderPacConfigWarnings(warnings),
|
...renderPacConfigWarnings(warnings),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user