feat: 增加 PaC 回退窗口诊断
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
observePacStatusDeliveryBudget,
|
||||
type PacDeliveryTimingPolicy,
|
||||
} from "./platform-infra-pac-delivery-timing";
|
||||
import { diagnosePacRegression, renderPacRegressionDiagnosis } from "./platform-infra-pac-regression-diagnosis";
|
||||
|
||||
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
|
||||
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
|
||||
@@ -263,6 +264,8 @@ export interface PipelinesAsCodeNodeStatusOptions {
|
||||
interface HistoryOptions extends CommonOptions {
|
||||
limit: number;
|
||||
detailId: string | null;
|
||||
summaryOnly?: boolean;
|
||||
diagnosisConsumerIds?: readonly string[];
|
||||
}
|
||||
|
||||
export interface PacHistoryConsumerIdentity {
|
||||
@@ -297,7 +300,7 @@ interface SecretMaterial {
|
||||
webhookPath: string;
|
||||
}
|
||||
|
||||
type PacReadOnlyAction = "status" | "history" | "debug-step";
|
||||
type PacReadOnlyAction = "status" | "history" | "diagnose-regression" | "debug-step";
|
||||
|
||||
interface PacReadOnlyCaptureObservation {
|
||||
readonly ok: boolean;
|
||||
@@ -363,6 +366,12 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
const result = await history(config, options);
|
||||
return options.raw ? result : options.full ? compactHistoryJson(result, true) : options.json ? compactHistoryJson(result) : renderHistory(result);
|
||||
}
|
||||
if (action === "diagnose-regression" || action === "regression") {
|
||||
const options = parseDiagnoseRegressionOptions(args.slice(1));
|
||||
const result = await diagnoseRegression(config, options);
|
||||
if (options.full || options.raw || options.json) return result;
|
||||
return rendered(result, "platform-infra pipelines-as-code diagnose-regression", renderPacRegressionDiagnosis(result).trimEnd().split("\n"));
|
||||
}
|
||||
if (action === "delivery-timing" || action === "timing") {
|
||||
const options = parseCommonOptions(args.slice(1));
|
||||
const result = await deliveryTiming(config, options);
|
||||
@@ -493,12 +502,13 @@ function help(scope: string | null): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: "platform-infra pipelines-as-code plan|status|history|delivery-timing|debug-step|source-artifact",
|
||||
command: "platform-infra pipelines-as-code plan|status|history|diagnose-regression|delivery-timing|debug-step|source-artifact",
|
||||
configTruth: configLabel,
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression --target NC01 --consumer agentrun-nc01-v02 [--limit 20] [--json|--full]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target NC01 --consumer selfmedia-nc01 [--json]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--consumer <consumer>] [--id <pipelinerun>] [--json]",
|
||||
@@ -1675,7 +1685,12 @@ function printCloseoutProgress(target: PacTarget, consumer: PacConsumer, stage:
|
||||
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig({ consumerId: options.consumerId });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const selection = resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId);
|
||||
const selection = options.diagnosisConsumerIds === undefined
|
||||
? resolvePacHistoryConsumerSelection(validPacConsumers(pac), target.id, options.consumerId, options.detailId)
|
||||
: {
|
||||
selectedConsumerIds: [...options.diagnosisConsumerIds],
|
||||
nextConsumerId: options.diagnosisConsumerIds[0] ?? null,
|
||||
};
|
||||
const selectedConsumers = selection.selectedConsumerIds.map((id) => resolveConsumer(pac, id));
|
||||
const firstConsumer = selectedConsumers[0];
|
||||
if (firstConsumer === undefined) throw new Error("no Pipelines-as-Code consumers are configured");
|
||||
@@ -1684,7 +1699,8 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
const next = selection.nextConsumerId === null
|
||||
? pacNodeReadOnlyNext(target.id)
|
||||
: pacReadOnlyNext(target.id, selection.nextConsumerId);
|
||||
const observed = await capturePacReadOnly(config, pac, "history", target, firstConsumer, remoteScript("history", pac, target, firstRepository, firstConsumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, "", selectedConsumers), stringValue(record(next).history));
|
||||
const remoteAction = options.summaryOnly === true ? "diagnose-regression" : "history";
|
||||
const observed = await capturePacReadOnly(config, pac, remoteAction, target, firstConsumer, remoteScript(remoteAction, pac, target, firstRepository, firstConsumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, "", selectedConsumers), stringValue(record(next).history));
|
||||
const { result, parsed, observation } = observed;
|
||||
const remote = parsed ?? compactCapture(result, { full: true });
|
||||
const historyErrors = arrayRecords(record(remote).historyErrors);
|
||||
@@ -1733,6 +1749,53 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
async function diagnoseRegression(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
if (options.consumerId === null) throw new Error("pipelines-as-code diagnose-regression requires --consumer <consumer>");
|
||||
const pac = readPacConfig({ consumerId: options.consumerId });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) {
|
||||
throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
|
||||
}
|
||||
const authority = resolveCicdDeliveryAuthority({ consumerId: consumer.id, node: consumer.node, lane: consumer.lane });
|
||||
if (authority.kind !== "pac-pr-merge") throw new Error(`delivery authority is ${authority.kind}: ${authority.reason}`);
|
||||
const diagnosisConsumers = validPacConsumers(pac)
|
||||
.filter((item) => item.node.toLowerCase() === target.id.toLowerCase() && item.repositoryRef === consumer.repositoryRef);
|
||||
const historyResult = await history(config, {
|
||||
...options,
|
||||
consumerId: null,
|
||||
detailId: null,
|
||||
summaryOnly: true,
|
||||
diagnosisConsumerIds: diagnosisConsumers.map((item) => item.id),
|
||||
});
|
||||
const diagnosis = diagnosePacRegression({
|
||||
targetId: target.id,
|
||||
consumerId: consumer.id,
|
||||
sourceRepository: authority.consumer.sourceRepository,
|
||||
limitPerConsumer: options.limit,
|
||||
rows: arrayRecords(historyResult.rows),
|
||||
consumerBindings: validPacConsumers(pac)
|
||||
.filter((item) => item.node.toLowerCase() === target.id.toLowerCase())
|
||||
.map((item) => ({ id: item.id, repositoryRef: item.repositoryRef })),
|
||||
});
|
||||
const evidence = record(diagnosis.evidence);
|
||||
const readOk = historyResult.ok === true;
|
||||
return {
|
||||
ok: readOk,
|
||||
action: "platform-infra-pipelines-as-code-diagnose-regression",
|
||||
...diagnosis,
|
||||
state: readOk ? diagnosis.state : "read-failed",
|
||||
incident: readOk ? diagnosis.incident : null,
|
||||
evidence: options.full || options.raw
|
||||
? evidence
|
||||
: { matchingFailureCount: arrayRecords(evidence.matchingFailures).length, valuesPrinted: false },
|
||||
observation: historyResult.observation,
|
||||
historyErrors: historyResult.historyErrors,
|
||||
warnings: historyResult.warnings,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePacHistoryConsumerSelection(
|
||||
consumers: readonly PacHistoryConsumerIdentity[],
|
||||
targetId: string,
|
||||
@@ -1966,7 +2029,7 @@ export class PacReleaseManifestRenderError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
|
||||
function remoteScript(action: "apply" | "status" | "history" | "diagnose-regression" | "debug-step", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
|
||||
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
|
||||
const admissionIdentity = pacAdmissionDesiredIdentity(target.id);
|
||||
const rbacIdentity = pacConsumerRbacDesiredIdentity(target.id);
|
||||
@@ -3268,9 +3331,9 @@ function parseCloseoutOptions(args: string[]): CloseoutOptions {
|
||||
return { ...parseCommonOptions(commonArgs), sourceCommit, wait, timeoutMs };
|
||||
}
|
||||
|
||||
function parseHistoryOptions(args: string[]): HistoryOptions {
|
||||
function parseHistoryOptions(args: string[], defaultLimit = 5): HistoryOptions {
|
||||
const commonArgs: string[] = [];
|
||||
let limit = 5;
|
||||
let limit = defaultLimit;
|
||||
let detailId: string | null = null;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
@@ -3298,6 +3361,12 @@ function parseHistoryOptions(args: string[]): HistoryOptions {
|
||||
return { ...parseCommonOptions(commonArgs), limit, detailId };
|
||||
}
|
||||
|
||||
function parseDiagnoseRegressionOptions(args: string[]): HistoryOptions {
|
||||
const options = parseHistoryOptions(args, 20);
|
||||
if (options.detailId !== null) throw new Error("pipelines-as-code diagnose-regression does not accept --id");
|
||||
return options;
|
||||
}
|
||||
|
||||
function parseCommonOptions(args: string[]): CommonOptions {
|
||||
let targetId: string | null = null;
|
||||
let consumerId: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user