fix: 将只读状态预算收敛到 YAML

This commit is contained in:
Codex
2026-07-15 11:25:24 +02:00
parent fe23d35c6e
commit 18d0bd89b2
7 changed files with 131 additions and 14 deletions
+14
View File
@@ -173,6 +173,12 @@ export interface AgentRunLaneSpec {
readonly nodeKubeRoute: string;
readonly nodeUnideskWorkspace: string;
readonly version: string;
readonly status: {
readonly readOnlyCapture: {
readonly timeoutMs: number;
readonly configPath: string;
};
};
readonly source: {
readonly statusMode: "host-worktree" | "k3s-git-mirror";
readonly repository: string;
@@ -677,6 +683,8 @@ function parseLanes(input: Record<string, unknown>, nodes: Record<string, AgentR
function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, unknown>, configPath: string): AgentRunLaneSpec {
const path = `${configPath}.controlPlane.lanes.${lane}`;
const status = recordField(input, "status", path);
const readOnlyCapture = recordField(status, "readOnlyCapture", `${path}.status`);
const source = recordField(input, "source", path);
const runtime = recordField(input, "runtime", path);
const ci = recordField(input, "ci", path);
@@ -694,6 +702,12 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
nodeKubeRoute: node.kubeRoute,
nodeUnideskWorkspace: node.unideskWorkspace,
version,
status: {
readOnlyCapture: {
timeoutMs: integerField(readOnlyCapture, "timeoutMs", `${path}.status.readOnlyCapture`),
configPath: `${path}.status.readOnlyCapture.timeoutMs`,
},
},
source: {
statusMode,
repository: stringField(source, "repository", `${path}.source`),
+7 -7
View File
@@ -47,8 +47,6 @@ import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./tri
import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils";
import { yamlLaneK3sSourceStatusScript, yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
const agentRunStatusCaptureTimeoutMs = 20_000;
export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
@@ -419,7 +417,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options:
const artifact = record(pacSummary.artifact);
const pacArgo = record(pacSummary.argo);
const pipelineRunName = options.pipelineRun ?? stringOrNull(latestPipelineRun.name);
const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs }));
const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs }));
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const manager = record(runtimePayload.manager);
const database = record(runtimePayload.database);
@@ -530,6 +528,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options:
command: "agentrun control-plane status",
mode: "pipelines-as-code-migrated-lane",
configPath: target.configPath,
readCaptureBudget: spec.status.readOnlyCapture,
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
summary,
alignment: {
@@ -642,8 +641,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
if (deliveryAuthority.kind === "pac-pr-merge") return await statusPipelinesAsCodeLane(config, options, target, deliveryAuthority);
if (deliveryAuthority.kind === "unknown") return unknownDeliveryAuthorityStatus(options, target, deliveryAuthority);
const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror"
? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })
: capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs }));
? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })
: capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs }));
const sourcePayload = captureJsonPayload(sourceProbe.value);
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
const initialSourceCommit = options.sourceCommit
@@ -651,8 +650,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
?? stringOrNull(sourcePayload.localHead);
const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null);
const [runtimeProbe, mirrorProbe] = await Promise.all([
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })),
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })),
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })),
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })),
]);
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
@@ -904,6 +903,7 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
command: "agentrun control-plane status",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
readCaptureBudget: spec.status.readOnlyCapture,
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
summary: options.full || options.raw ? detailedSummary : commanderSummary,
alignment: {
@@ -64,6 +64,7 @@ describe("PaC 失败证据合同", () => {
parsed: null,
elapsedMs: 25001,
timeoutMs: 25000,
configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs",
next: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer agentrun-nc01-v02 --limit 5",
});
expect(observation).toMatchObject({
@@ -74,6 +75,7 @@ describe("PaC 失败证据合同", () => {
stage: "history-remote-observe",
elapsedMs: 25001,
timeoutMs: 25000,
configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs",
exitCode: 124,
firstBreak: { code: "pac-read-timeout", phase: "remote-read" },
});
@@ -81,6 +83,7 @@ describe("PaC 失败证据合同", () => {
expect(output).toContain("READ OBSERVATION");
expect(output).toContain("history-remote-observe");
expect(output).toContain("pac-read-timeout");
expect(output).toContain("config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs");
expect(output).toContain("next-read-only:");
});
@@ -52,7 +52,6 @@ const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as
const evaluatorFile = rootPath("scripts", "native", "cicd", "pac-status-evaluator.cjs");
const sourceArtifactRuntimeObserverFile = rootPath("scripts", "native", "cicd", "pac-source-artifact-runtime.mjs");
const fieldManager = "unidesk-platform-infra-pipelines-as-code";
const pacReadOnlyCaptureTimeoutMs = 25_000;
const y = createYamlFieldReader(configLabel);
function kubernetesLabelValue(value: string): string {
@@ -111,6 +110,10 @@ export interface PacConfig {
deliveryTiming: PacDeliveryTimingPolicy;
observability: {
configRef: string;
readOnlyCapture: {
timeoutMs: number;
configPath: string;
};
tracesEndpoint: string;
serviceName: string;
propagation: string[];
@@ -530,6 +533,7 @@ export function parsePacConfigDocument(
const display = y.objectField(root, "display", "");
const deliveryTiming = y.objectField(root, "deliveryTiming", "");
const observability = y.objectField(root, "observability", "");
const readOnlyCapture = y.objectField(observability, "readOnlyCapture", "observability");
const observabilitySampling = y.objectField(observability, "sampling", "observability");
const exporterFailure = y.objectField(observability, "exporterFailure", "observability");
const admin = y.objectField(gitea, "admin", "gitea");
@@ -569,6 +573,10 @@ export function parsePacConfigDocument(
},
observability: {
configRef: y.stringField(observability, "configRef", "observability"),
readOnlyCapture: {
timeoutMs: integerInRange(readOnlyCapture.timeoutMs, 1_000, 120_000, "observability.readOnlyCapture.timeoutMs"),
configPath: `${configLabel}#observability.readOnlyCapture.timeoutMs`,
},
tracesEndpoint: urlField(observability, "tracesEndpoint", "observability"),
serviceName: y.stringField(observability, "serviceName", "observability"),
propagation: y.stringArrayField(observability, "propagation", "observability"),
@@ -1371,7 +1379,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
const repository = resolveRepository(pac, consumer.repositoryRef);
const secrets = ensureSecrets(pac, false);
const next = nextCommands(target.id, consumer.id, pac.defaults.consumerId);
const observed = await capturePacReadOnly(config, "status", target, consumer, remoteScript("status", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""), `${stringValue(next.status)} --full`);
const observed = await capturePacReadOnly(config, pac, "status", target, consumer, remoteScript("status", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""), `${stringValue(next.status)} --full`);
const { result, parsed, observation } = observed;
const summary = parsed === null ? null : statusSummary(parsed);
const deliveryAuthority = resolveCicdDeliveryAuthority({ consumerId: consumer.id, node: consumer.node, lane: consumer.lane });
@@ -1627,7 +1635,7 @@ 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, "history", target, firstConsumer, remoteScript("history", pac, target, firstRepository, firstConsumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, "", selectedConsumers), stringValue(record(next).history));
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 { result, parsed, observation } = observed;
const remote = parsed ?? compactCapture(result, { full: true });
const historyErrors = arrayRecords(record(remote).historyErrors);
@@ -1723,7 +1731,7 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
const consumer = resolveConsumer(pac, selection.selectedConsumerIds[0] ?? null);
const repository = resolveRepository(pac, consumer.repositoryRef);
const next = nextCommands(target.id, consumer.id, pac.defaults.consumerId);
const observed = await capturePacReadOnly(config, "debug-step", target, consumer, remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), ""), stringValue(next.status));
const observed = await capturePacReadOnly(config, pac, "debug-step", target, consumer, remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), ""), stringValue(next.status));
const { result, parsed, observation } = observed;
const fixtureChecks = parsed === null ? [] : arrayRecords(parsed.checks);
const fixtureDisclosure = pacDebugFixtureDisclosure(fixtureChecks, options.detailId);
@@ -1747,6 +1755,7 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
async function capturePacReadOnly(
config: UniDeskConfig,
pac: PacConfig,
action: PacReadOnlyAction,
target: PacTarget,
consumer: PacConsumer,
@@ -1755,8 +1764,9 @@ async function capturePacReadOnly(
): Promise<{ readonly result: Awaited<ReturnType<typeof capture>>; readonly parsed: Record<string, unknown> | null; readonly observation: PacReadOnlyCaptureObservation }> {
const stage = `${action}-remote-observe`;
const startedAt = Date.now();
printPacReadOnlyProgress(target, consumer, stage, "started", startedAt, { timeoutMs: pacReadOnlyCaptureTimeoutMs });
const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: pacReadOnlyCaptureTimeoutMs });
const captureBudget = pac.observability.readOnlyCapture;
printPacReadOnlyProgress(target, consumer, stage, "started", startedAt, captureBudget);
const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: captureBudget.timeoutMs });
const parsed = parseJsonOutput(result.stdout);
const observation = pacReadOnlyCaptureObservation({
action,
@@ -1765,7 +1775,8 @@ async function capturePacReadOnly(
result,
parsed,
elapsedMs: Date.now() - startedAt,
timeoutMs: pacReadOnlyCaptureTimeoutMs,
timeoutMs: captureBudget.timeoutMs,
configPath: captureBudget.configPath,
next,
});
printPacReadOnlyProgress(target, consumer, stage, observation.status, startedAt, {
@@ -1783,6 +1794,7 @@ export function pacReadOnlyCaptureObservation(options: {
readonly parsed: Record<string, unknown> | null;
readonly elapsedMs: number;
readonly timeoutMs: number;
readonly configPath: string;
readonly next: string;
}): PacReadOnlyCaptureObservation {
const stdoutBytes = Buffer.byteLength(options.result.stdout, "utf8");
@@ -1806,6 +1818,7 @@ export function pacReadOnlyCaptureObservation(options: {
stage: `${options.action}-remote-observe`,
elapsedMs: options.elapsedMs,
timeoutMs: options.timeoutMs,
configPath: options.configPath,
exitCode: options.result.exitCode,
stdoutBytes,
stderrBytes,
@@ -2452,6 +2465,7 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
capabilities: record(result.config).capabilities,
warnings: result.warnings,
deliveryBudget: result.deliveryBudget,
observation: result.observation,
summary: compactStatusSummary(record(result.summary)),
next: result.next,
valuesPrinted: false,
@@ -2575,6 +2589,7 @@ export function compactHistoryJson(result: Record<string, unknown>, includeTaskD
consumers: result.consumers,
detailId: result.detailId,
deliveryBudget: result.deliveryBudget,
observation: result.observation,
rows: arrayRecords(result.rows).map((row) => {
const taskRuns = record(row.taskRuns);
const artifact = record(row.artifact);
@@ -3125,6 +3140,7 @@ function renderPacReadObservation(result: Record<string, unknown>): string[] {
boolText(observation.partial),
stringValue(firstBreak.code),
]]),
` budget-config: ${stringValue(observation.configPath)}`,
` next-read-only: ${stringValue(observation.next)}`,
];
}