fix: 收敛 PaC 自动交付提示

This commit is contained in:
Codex
2026-07-11 11:42:16 +02:00
parent b12dd85375
commit b607c3a996
55 changed files with 2953 additions and 1472 deletions
+186 -306
View File
@@ -3,9 +3,6 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
import { dirname, join } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { parseNodeScopedDelegatedOptions } from "./hwlab-node/plan";
import { nodeRuntimeEnsureGitMirrorFlushed } from "./hwlab-node/status";
import { runGitMirrorJob as runAgentRunGitMirrorJob } from "./agentrun/rest-bridge";
import type { RenderedCliResult } from "./output";
import {
capture,
@@ -17,6 +14,7 @@ import {
sha256Fingerprint,
} from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
import { pacNodeReadOnlyNext, pacReadOnlyNext, resolveCicdDeliveryAuthority } from "./cicd-delivery-authority";
import {
canonicalizePacPipelineSpec,
pacSourceArtifactSafeError,
@@ -78,6 +76,13 @@ interface PacConfig {
controllerServicePort: number;
waitTimeoutSeconds: number;
};
capabilities: {
sentinelInternalPublish: {
enabled: boolean;
admissionProvenance: "unavailable";
trackingIssue: string;
};
};
closeout: {
gitOpsMirrorFlush: {
enabled: boolean;
@@ -167,6 +172,17 @@ interface HistoryOptions extends CommonOptions {
detailId: string | null;
}
export interface PacHistoryConsumerIdentity {
readonly id: string;
readonly node: string;
readonly pipelineRunPrefix: string;
}
export interface PacHistoryConsumerSelection {
readonly selectedConsumerIds: readonly string[];
readonly nextConsumerId: string | null;
}
interface CloseoutOptions extends CommonOptions {
sourceCommit: string | null;
wait: boolean;
@@ -178,10 +194,6 @@ interface ApplyOptions extends CommonOptions {
wait: boolean;
}
interface WebhookTestOptions extends CommonOptions {
confirm: boolean;
}
interface SecretMaterial {
adminUsername: string;
adminPassword: string;
@@ -193,7 +205,7 @@ interface SecretMaterial {
export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const [action = "plan"] = args;
if (action === "help" || action === "--help") return help();
if (action === "help" || action === "--help") return help(args[1] ?? null);
if (action === "plan") {
const options = parseCommonOptions(args.slice(1));
const result = plan(options);
@@ -224,11 +236,6 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
const result = await debugStep(config, options);
return options.full || options.raw ? result : options.json ? result : renderDebugStep(result);
}
if (action === "webhook-test") {
const options = parseWebhookTestOptions(args.slice(1));
const result = await webhookTest(config, options);
return options.json || options.full || options.raw ? result : renderWebhookTest(result);
}
if (action === "source-artifact") {
if (args.length === 1 || args.slice(1).includes("--help") || args.slice(1).includes("-h")) return sourceArtifactHelp();
const structuredError = args.includes("--json") || args.includes("--full");
@@ -275,7 +282,7 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
return sourceArtifactValidationError(safeError);
}
}
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help() };
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help(null) };
}
function sourceArtifactHelp(): RenderedCliResult {
@@ -307,31 +314,57 @@ function sourceArtifactValidationError(error: ReturnType<typeof pacSourceArtifac
};
}
function help(): Record<string, unknown> {
function help(scope: string | null): Record<string, unknown> {
if (scope === "platform-bootstrap") {
return {
command: "platform-infra pipelines-as-code help platform-bootstrap",
scope: "explicit-initial-platform-bootstrap",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target <NODE> --dry-run",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target <NODE> --confirm",
],
boundary: "仅用于首次安装或显式平台 bootstrap;不得在 source PR 合并后用于交付、恢复或补跑。",
mutation: "explicit-confirm-required",
};
}
if (scope === "compatibility-diagnostics") {
return {
command: "platform-infra pipelines-as-code help compatibility-diagnostics",
scope: "explicit-readonly-or-connectivity-diagnostics",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target <NODE> --consumer <consumer> --source-commit <sha> --wait",
"bun scripts/cli.ts platform-infra pipelines-as-code status --target <NODE> --consumer <consumer> --full",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target <NODE> --consumer <consumer> --limit 10 --full",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target <NODE> --consumer <consumer> --json",
],
boundary: "本 scope 只允许读取现有 PaC/Tekton/Argo 证据;不得 POST hook test、伪造 push、交付、恢复或补跑。",
};
}
return {
command: "platform-infra pipelines-as-code plan|apply|status|closeout|history|debug-step|source-artifact",
command: "platform-infra pipelines-as-code plan|status|history|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 apply --target JD01 --dry-run",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait",
"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 history --target JD01 --id <pipelinerun>",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--consumer <consumer>] [--id <pipelinerun>] [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code source-artifact check --target NC01 --consumer agentrun-nc01-v02 --source-worktree /abs/worktree",
"bun scripts/cli.ts platform-infra pipelines-as-code source-artifact write --target NC01 --consumer agentrun-nc01-v02 --source-worktree /abs/worktree --confirm",
"bun scripts/cli.ts platform-infra pipelines-as-code source-artifact verify-runtime --target NC01 --consumer agentrun-nc01-v02 --source-worktree /abs/worktree --source-commit <full-sha>",
],
diagnostics: "webhook-test exists only for bounded connectivity diagnosis and must not be used as delivery evidence.",
boundary: "Sole CI trigger path for GH-1552/GH-1607: GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime.",
maintenanceHelp: "bun scripts/cli.ts platform-infra pipelines-as-code help platform-bootstrap",
compatibilityHelp: "bun scripts/cli.ts platform-infra pipelines-as-code help compatibility-diagnostics",
boundary: "唯一正式触发链:GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime。",
};
}
function readPacConfig(): PacConfig {
const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code"), { label: configLabel }).value;
const release = y.objectField(root, "release", "");
const capabilities = y.objectField(root, "capabilities", "");
const sentinelInternalPublish = y.objectField(capabilities, "sentinelInternalPublish", "capabilities");
const closeout = y.objectField(root, "closeout", "");
const gitOpsMirrorFlush = y.objectField(closeout, "gitOpsMirrorFlush", "closeout");
const gitea = y.objectField(root, "gitea", "");
@@ -369,6 +402,13 @@ function readPacConfig(): PacConfig {
controllerServicePort: y.portField(release, "controllerServicePort", "release"),
waitTimeoutSeconds: positiveInteger(release, "waitTimeoutSeconds", "release"),
},
capabilities: {
sentinelInternalPublish: {
enabled: y.booleanField(sentinelInternalPublish, "enabled", "capabilities.sentinelInternalPublish"),
admissionProvenance: y.enumField(sentinelInternalPublish, "admissionProvenance", "capabilities.sentinelInternalPublish", ["unavailable"] as const),
trackingIssue: urlField(sentinelInternalPublish, "trackingIssue", "capabilities.sentinelInternalPublish"),
},
},
closeout: {
gitOpsMirrorFlush: {
enabled: y.booleanField(gitOpsMirrorFlush, "enabled", "closeout.gitOpsMirrorFlush"),
@@ -712,8 +752,7 @@ function plan(options: CommonOptions): Record<string, unknown> {
config: configSummary(pac, consumer, repository),
release: pac.release,
repository: repositorySummary(repository),
consumer,
closeout: closeoutDeclaration(pac, consumer),
consumer: consumerObservationSummary(consumer),
secrets,
policy: policyChecks(repository),
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
@@ -738,7 +777,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
config: compactConfigSummary(pac, consumer, repository),
release: { version: pac.release.version, manifestUrl: pac.release.manifestUrl },
repository: repositorySummary(repository),
consumer,
consumer: consumerObservationSummary(consumer),
secrets: secretSummaries(pac),
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
@@ -754,14 +793,15 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
const result = await capture(config, target.route, ["sh"], remoteScript("status", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""));
const parsed = parseJsonOutput(result.stdout);
const summary = parsed === null ? null : statusSummary(parsed);
const deliveryAuthority = resolveCicdDeliveryAuthority({ consumerId: consumer.id, node: consumer.node, lane: consumer.lane });
return {
ok: result.exitCode === 0 && summary?.ready === true,
ok: result.exitCode === 0 && summary?.ready === true && deliveryAuthority.kind === "pac-pr-merge",
action: "platform-infra-pipelines-as-code-status",
mutation: false,
target: targetSummary(target),
config: compactConfigSummary(pac, consumer, repository),
consumer,
closeout: closeoutDeclaration(pac, consumer),
consumer: consumerObservationSummary(consumer),
deliveryAuthority,
coverage: consumerCoverage(pac, target.id),
summary,
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw ? parsed : undefined,
@@ -827,12 +867,7 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
elapsedMs: Date.now() - startedAt,
valuesPrinted: false,
},
next: {
status: `bun scripts/cli.ts cicd status --node ${nodeId}`,
history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${target.id} --limit 10`,
closeout: `bun scripts/cli.ts platform-infra pipelines-as-code closeout --target ${target.id} --wait`,
valuesPrinted: false,
},
next: pacNodeReadOnlyNext(target.id),
details: options.full || options.raw ? statuses : undefined,
valuesPrinted: false,
};
@@ -859,20 +894,20 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
let observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
let sourceMatched = options.sourceCommit === null || observedSourceCommit === options.sourceCommit;
const ciReady = closeoutCiReady(current, options.sourceCommit);
printCloseoutProgress(target, consumer, "gitops-mirror-flush", "started", startedAt, {
required: pac.closeout.gitOpsMirrorFlush.enabled && consumer.closeoutGitOpsMirrorFlush,
ciReady,
sourceCommit: observedSourceCommit,
});
let gitOpsMirrorFlush = await runCloseoutGitOpsMirrorFlush(config, pac, consumer, summary, latest, ciReady, observedSourceCommit);
printCloseoutProgress(target, consumer, "gitops-mirror-flush", record(gitOpsMirrorFlush).ok === false ? "failed" : "completed", startedAt, {
mode: record(gitOpsMirrorFlush).mode,
executed: record(gitOpsMirrorFlush).executed === true,
required: record(gitOpsMirrorFlush).required === true,
});
const gitOpsMirrorFlushReady = record(gitOpsMirrorFlush).ok !== false;
let gitOpsMirrorFlush: Record<string, unknown> = {
ok: true,
enabled: false,
configured: consumer.closeoutGitOpsMirrorFlush,
required: false,
applicable: false,
mode: "automatic-chain-owned-observation",
executed: false,
reason: "PaC migrated delivery owns GitOps publication and mirror persistence; compatibility closeout is read-only.",
configSource: configLabel,
valuesPrinted: false,
};
const runtimeStartedAt = Date.now();
if (ciReady && gitOpsMirrorFlushReady) {
if (ciReady) {
current = await observeCloseoutStatus(config, options, target, consumer, "runtime", 1, startedAt);
runtimeAttempts += 1;
while (options.wait && !closeoutReady(current, options.sourceCommit) && Date.now() - runtimeStartedAt < waitMs) {
@@ -892,7 +927,7 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
afterSummary: summary,
valuesPrinted: false,
};
const ready = baseReady && gitOpsMirrorFlushReady;
const ready = baseReady;
printCloseoutProgress(target, consumer, "closeout", ready ? "completed" : "blocked", startedAt, {
ready,
sourceMatched,
@@ -903,7 +938,7 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
return {
ok: ready,
action: "platform-infra-pipelines-as-code-closeout",
mutation: record(gitOpsMirrorFlush).executed === true,
mutation: false,
target: targetSummary(target),
consumer,
sourceCommit: options.sourceCommit,
@@ -962,13 +997,8 @@ function printCloseoutProgress(target: PacTarget, consumer: PacConsumer, stage:
async function history(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const targetConsumers = pac.consumers.filter((consumer) => consumer.node.toLowerCase() === target.id.toLowerCase());
const inferredDetailConsumers = options.detailId === null
? []
: targetConsumers.filter((consumer) => options.detailId?.startsWith(consumer.pipelineRunPrefix));
const selectedConsumers = options.consumerId === null
? inferredDetailConsumers.length === 1 ? inferredDetailConsumers : targetConsumers
: [resolveConsumer(pac, options.consumerId)];
const selection = resolvePacHistoryConsumerSelection(pac.consumers, target.id, options.consumerId, options.detailId);
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");
const firstRepository = resolveRepository(pac, firstConsumer.repositoryRef);
@@ -1003,7 +1033,46 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
valuesPrinted: false,
},
remote: parsed === null || options.raw ? remote : undefined,
next: nextCommands(target.id, options.consumerId ?? pac.defaults.consumerId, pac.defaults.consumerId),
next: selection.nextConsumerId === null
? pacNodeReadOnlyNext(target.id)
: pacReadOnlyNext(target.id, selection.nextConsumerId),
};
}
export function resolvePacHistoryConsumerSelection(
consumers: readonly PacHistoryConsumerIdentity[],
targetId: string,
requestedConsumerId: string | null,
detailId: string | null,
): PacHistoryConsumerSelection {
const targetConsumers = consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase());
if (targetConsumers.length === 0) throw new Error(`no Pipelines-as-Code consumers are configured for target ${targetId}`);
const requestedConsumer = requestedConsumerId === null
? null
: consumers.find((consumer) => consumer.id.toLowerCase() === requestedConsumerId.toLowerCase()) ?? null;
if (requestedConsumerId !== null && requestedConsumer === null) {
throw new Error(`unknown Pipelines-as-Code consumer ${requestedConsumerId}`);
}
if (requestedConsumer !== null && requestedConsumer.node.toLowerCase() !== targetId.toLowerCase()) {
throw new Error(`Pipelines-as-Code consumer ${requestedConsumer.id} belongs to ${requestedConsumer.node}, not target ${targetId}`);
}
if (detailId !== null) {
const matches = targetConsumers.filter((consumer) => detailId.startsWith(consumer.pipelineRunPrefix));
if (matches.length !== 1) {
throw new Error(`PipelineRun ${detailId} must match exactly one consumer on target ${targetId}; matched ${matches.length}`);
}
const matched = matches[0] as PacHistoryConsumerIdentity;
if (requestedConsumer !== null && requestedConsumer.id.toLowerCase() !== matched.id.toLowerCase()) {
throw new Error(`PipelineRun ${detailId} belongs to ${matched.id}, not requested consumer ${requestedConsumer.id}`);
}
return { selectedConsumerIds: [matched.id], nextConsumerId: matched.id };
}
if (requestedConsumer !== null) {
return { selectedConsumerIds: [requestedConsumer.id], nextConsumerId: requestedConsumer.id };
}
return {
selectedConsumerIds: targetConsumers.map((consumer) => consumer.id),
nextConsumerId: targetConsumers.length === 1 ? (targetConsumers[0] as PacHistoryConsumerIdentity).id : null,
};
}
@@ -1030,26 +1099,6 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
};
}
async function webhookTest(config: UniDeskConfig, options: WebhookTestOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const consumer = resolveConsumer(pac, options.consumerId);
const repository = resolveRepository(pac, consumer.repositoryRef);
if (!options.confirm) return { ok: false, action: "platform-infra-pipelines-as-code-webhook-test", mutation: false, mode: "missing-confirm", error: "webhook-test requires --confirm" };
const secrets = ensureSecrets(pac, false);
const result = await capture(config, target.route, ["sh"], remoteScript("webhook-test", pac, target, repository, consumer, { ...options, dryRun: false, wait: false }, secrets, ""));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-pipelines-as-code-webhook-test",
mutation: true,
target: targetSummary(target),
consumer,
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
};
}
async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
const response = await fetch(pac.release.manifestUrl);
if (!response.ok) throw new Error(`failed to fetch ${pac.release.manifestUrl}: HTTP ${response.status}`);
@@ -1058,7 +1107,7 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
return text;
}
function remoteScript(action: "apply" | "status" | "history" | "debug-step" | "webhook-test", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | WebhookTestOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
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 {
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 env: Record<string, string> = {
UNIDESK_PAC_ACTION: action,
@@ -1385,11 +1434,11 @@ function configSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRep
path: configLabel,
metadata: pac.metadata,
release: pac.release,
closeout: pac.closeout,
capabilities: capabilitySummary(pac),
gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch },
display: pac.display,
repositories: pac.repositories.map(repositorySummary),
consumers: pac.consumers,
consumers: pac.consumers.map(consumerObservationSummary),
repository: repositorySummary(repository),
consumer,
valuesPrinted: false,
@@ -1400,7 +1449,7 @@ function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository:
return {
path: configLabel,
releaseVersion: pac.release.version,
closeout: pac.closeout,
capabilities: capabilitySummary(pac),
repository: repository.name,
providerType: repository.providerType,
sourceUrl: repository.cloneUrl,
@@ -1412,15 +1461,13 @@ function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository:
};
}
function closeoutDeclaration(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
const config = pac.closeout.gitOpsMirrorFlush;
function capabilitySummary(pac: PacConfig): Record<string, unknown> {
return {
enabled: config.enabled,
configured: consumer.closeoutGitOpsMirrorFlush,
required: config.enabled && consumer.closeoutGitOpsMirrorFlush,
lane: consumer.closeoutGitOpsMirrorLane ?? consumer.lane,
waitTimeoutSeconds: config.waitTimeoutSeconds,
maxAttempts: config.maxAttempts,
sentinelInternalPublish: {
...pac.capabilities.sentinelInternalPublish,
configRef: `${configLabel}#capabilities.sentinelInternalPublish`,
valuesPrinted: false,
},
valuesPrinted: false,
};
}
@@ -1441,34 +1488,38 @@ function repositorySummary(repository: PacRepository): Record<string, unknown> {
};
}
function consumerObservationSummary(consumer: PacConsumer): Record<string, unknown> {
return {
id: consumer.id,
node: consumer.node,
lane: consumer.lane,
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
argoNamespace: consumer.argoNamespace,
argoApplication: consumer.argoApplication,
repositoryRef: consumer.repositoryRef,
valuesPrinted: false,
};
}
function consumerCoverage(pac: PacConfig, targetId: string): Record<string, unknown>[] {
return pac.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase()).map((consumer) => {
const repository = resolveRepository(pac, consumer.repositoryRef);
const suffix = consumerSuffix(consumer.id, pac.defaults.consumerId);
return {
consumer: consumer.id,
source: `${repository.owner}/${repository.repo}@${repository.params.source_branch ?? "-"}`,
namespace: consumer.namespace,
pipeline: consumer.pipeline,
argoApplication: consumer.argoApplication,
statusCommand: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}${suffix}`,
historyCommand: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId}${suffix}`,
statusCommand: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumer.id}`,
historyCommand: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --consumer ${consumer.id}`,
};
});
}
function consumerSuffix(consumerId: string, defaultConsumerId: string): string {
return consumerId === defaultConsumerId ? "" : ` --consumer ${consumerId}`;
}
function nextCommands(targetId: string, consumerId: string, defaultConsumerId: string): Record<string, string> {
const suffix = consumerSuffix(consumerId, defaultConsumerId);
return {
apply: `bun scripts/cli.ts platform-infra pipelines-as-code apply --target ${targetId}${suffix} --confirm`,
closeout: `bun scripts/cli.ts platform-infra pipelines-as-code closeout --target ${targetId}${suffix} --wait`,
status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}${suffix}`,
history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId}${suffix}`,
};
function nextCommands(targetId: string, consumerId: string, _defaultConsumerId: string): Record<string, unknown> {
return pacReadOnlyNext(targetId, consumerId);
}
function compactPlanJson(result: Record<string, unknown>): Record<string, unknown> {
@@ -1483,7 +1534,7 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
path: config.path,
metadata: config.metadata,
release: config.release,
closeout: config.closeout,
capabilities: config.capabilities,
gitea: config.gitea,
display: config.display,
valuesPrinted: false,
@@ -1500,7 +1551,6 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
argoApplication: consumer.argoApplication,
repositoryRef: consumer.repositoryRef,
},
closeout: result.closeout,
secrets: result.secrets,
policy: result.policy,
next: result.next,
@@ -1523,7 +1573,8 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
pipeline: consumer.pipeline,
argoApplication: consumer.argoApplication,
},
closeout: result.closeout,
deliveryAuthority: result.deliveryAuthority,
capabilities: record(result.config).capabilities,
summary: compactStatusSummary(record(result.summary)),
next: result.next,
valuesPrinted: false,
@@ -1711,7 +1762,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
const config = record(result.config);
const repository = record(result.repository);
const consumer = record(result.consumer);
const closeout = record(result.closeout);
const internalPublish = record(record(config.capabilities).sentinelInternalPublish);
const secrets = arrayRecords(result.secrets);
const policy = arrayRecords(result.policy);
const lines = [
@@ -1726,25 +1777,23 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
["cd", "Argo", stringValue(consumer.argoApplication)],
]),
"",
"CLOSEOUT",
...table(["GLOBAL_ENABLED", "CONSUMER_CONFIGURED", "REQUIRED", "LANE", "WAIT_S", "MAX_ATTEMPTS"], [[
boolText(closeout.enabled),
boolText(closeout.configured),
boolText(closeout.required),
stringValue(closeout.lane),
stringValue(closeout.waitTimeoutSeconds),
stringValue(closeout.maxAttempts),
]]),
"",
"SECRETS",
...table(["ID", "PRESENT", "FINGERPRINT", "VALUES"], secrets.map((item) => [stringValue(item.id), boolText(item.present), stringValue(item.fingerprint), "false"])),
"",
"POLICY",
...table(["NAME", "OK", "DETAIL"], policy.map((item) => [stringValue(item.name), boolText(item.ok), stringValue(item.detail)])),
"",
"NEXT",
` apply: ${stringValue(record(result.next).apply)}`,
"CAPABILITIES",
...table(["NAME", "ENABLED", "PROVENANCE", "TRACKING"], [[
"sentinelInternalPublish",
boolText(internalPublish.enabled),
stringValue(internalPublish.admissionProvenance),
stringValue(internalPublish.trackingIssue),
]]),
"",
"READ-ONLY NEXT",
` status: ${stringValue(record(result.next).status)}`,
` history: ${stringValue(record(result.next).history)}`,
];
return rendered(result, "platform-infra pipelines-as-code plan", lines);
}
@@ -1770,7 +1819,8 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const consumer = record(result.consumer);
const closeout = record(result.closeout);
const deliveryAuthority = record(result.deliveryAuthority);
const internalPublish = record(record(record(result.config).capabilities).sentinelInternalPublish);
const coverage = arrayRecords(result.coverage);
const latest = record(summary.latestPipelineRun);
const taskRuns = arrayRecords(summary.taskRuns);
@@ -1788,18 +1838,9 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
...table(["CONSUMER", "SOURCE", "NAMESPACE", "PIPELINE", "ARGO_APP"], coverage.map((item) => [stringValue(item.consumer), stringValue(item.source), stringValue(item.namespace), stringValue(item.pipeline), stringValue(item.argoApplication)])),
"",
]),
...table(["CONSUMER", "READY", "CRD", "CONTROLLER", "WEBHOOKS", "REPO_URL"], [[stringValue(consumer.id), boolText(summary.ready), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), short(stringValue(repository.url), 56)]]),
...table(["CONSUMER", "READY", "AUTHORITY", "CRD", "CONTROLLER", "WEBHOOKS", "REPO_URL"], [[stringValue(consumer.id), boolText(summary.ready), stringValue(deliveryAuthority.kind), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), short(stringValue(repository.url), 56)]]),
` repository-condition: ${compactLine(stringValue(summary.repositoryCondition))}`,
"",
"CLOSEOUT",
...table(["GLOBAL_ENABLED", "CONSUMER_CONFIGURED", "REQUIRED", "LANE", "WAIT_S", "MAX_ATTEMPTS"], [[
boolText(closeout.enabled),
boolText(closeout.configured),
boolText(closeout.required),
stringValue(closeout.lane),
stringValue(closeout.waitTimeoutSeconds),
stringValue(closeout.maxAttempts),
]]),
` sentinel-internal-publish: enabled=${boolText(internalPublish.enabled)} provenance=${stringValue(internalPublish.admissionProvenance)} tracking=${stringValue(internalPublish.trackingIssue)}`,
"",
"GITEA HOOKS",
...(webhooks.length === 0 ? ["-"] : table(["HOOK", "ACTIVE", "EVENTS", "URL"], webhooks.map((item) => [stringValue(item.id), boolText(item.active), Array.isArray(item.events) ? item.events.join(",") : stringValue(item.events), short(stringValue(item.url), 56)]))),
@@ -1852,9 +1893,9 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
` pipeline-run: ${latest.name === undefined ? "-" : `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${stringValue(record(result.target).id)} --id ${stringValue(latest.name)}`}`,
"",
"NEXT",
` closeout: ${stringValue(record(result.next).closeout)}`,
` full: ${stringValue(record(result.next).status)} --full`,
` all-history: ${stringValue(record(result.next).history).replace(/ --consumer [^ ]+/u, "")} --limit 10`,
` fix-automatic-delivery: ${stringValue(record(record(result.next).fixAutomaticDelivery).reference)}`,
];
return rendered(result, "platform-infra pipelines-as-code status", lines);
}
@@ -1905,7 +1946,7 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
"CICD DIAGNOSIS",
...table(["OK", "CODE", "PHASE", "HINT"], [[boolText(diagnostics.ok !== false), stringValue(diagnostics.code), stringValue(diagnostics.phase), compactLine(stringValue(diagnostics.hint))]]),
"",
"GITOPS MIRROR FLUSH",
"AUTOMATIC DELIVERY OBSERVATION",
...table(["ENABLED", "REQUIRED", "OK", "MODE", "EXECUTED", "PENDING", "IN_SYNC"], [[
boolText(gitOpsMirrorFlush.enabled),
boolText(gitOpsMirrorFlush.required),
@@ -1945,10 +1986,11 @@ function renderHistory(result: Record<string, unknown>): RenderedCliResult {
]),
"",
"TRIGGERS",
...(rows.length === 0 ? ["-"] : table([timeHeader, "CONSUMER", "REPO", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [
...(rows.length === 0 ? ["-"] : table([timeHeader, "CONSUMER", "CLASS", "OWNER", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [
stringValue(row.displayTime) === "-" ? isoShort(stringValue(row.triggeredAt)) : stringValue(row.displayTime),
stringValue(row.consumer),
stringValue(row.repo),
stringValue(row.deliveryClass),
stringValue(row.deliveryOwner ?? row.executionOwner),
statusText(row),
stringValue(row.durationSeconds),
short(stringValue(row.commit)),
@@ -1964,18 +2006,6 @@ function renderHistory(result: Record<string, unknown>): RenderedCliResult {
return rendered(result, "platform-infra pipelines-as-code history", lines.filter((line): line is string => line !== null));
}
function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
const remote = record(result.remote);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE WEBHOOK TEST",
...table(["OK", "MUTATION", "HOOK", "SENT"], [[boolText(result.ok), boolText(result.mutation), stringValue(remote.hookId), boolText(remote.webhookTestSent)]]),
"",
"NEXT",
` status: ${stringValue(record(result.next).status)}`,
];
return rendered(result, "platform-infra pipelines-as-code webhook-test", lines);
}
function renderDebugStep(result: Record<string, unknown>): RenderedCliResult {
const checks = arrayRecords(result.checks);
const realRun = record(result.realRun);
@@ -1998,8 +2028,10 @@ function renderDebugStep(result: Record<string, unknown>): RenderedCliResult {
...(Object.keys(realRun).length === 0 ? [] : [
"",
"REAL PIPELINERUN EVIDENCE",
...table(["FOUND", "STATUS", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[
...table(["FOUND", "CLASS", "AUTHORITY", "STATUS", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[
boolText(realRun.found),
stringValue(pipelineRun.deliveryClass),
boolText(pipelineRun.deliveryAuthorityEligible),
stringValue(pipelineRun.status ?? pipelineRun.reason),
short(stringValue(pipelineRun.sourceCommit), 16),
stringValue(realRun.mode),
@@ -2064,23 +2096,6 @@ function parseApplyOptions(args: string[]): ApplyOptions {
return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait };
}
function parseWebhookTestOptions(args: string[]): WebhookTestOptions {
const commonArgs: string[] = [];
let confirm = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm") confirm = true;
else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
return { ...parseCommonOptions(commonArgs), confirm };
}
function parseCloseoutOptions(args: string[]): CloseoutOptions {
const commonArgs: string[] = [];
let sourceCommit: string | null = null;
@@ -2250,146 +2265,6 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function runCloseoutGitOpsMirrorFlush(config: UniDeskConfig, pac: PacConfig, consumer: PacConsumer, summary: Record<string, unknown>, latest: Record<string, unknown>, baseReady: boolean, observedSourceCommit: string): Promise<Record<string, unknown>> {
const cfg = pac.closeout.gitOpsMirrorFlush;
const configSource = `${configLabel}.closeout.gitOpsMirrorFlush`;
const required = cfg.enabled && consumer.closeoutGitOpsMirrorFlush;
if (baseReady && stringValue(record(summary.diagnostics).code) === "pac-ready-no-runtime-change") {
return {
ok: true,
enabled: cfg.enabled,
required,
applicable: false,
mode: "retained-no-runtime-change",
executed: false,
reason: "the successful source observation produced no GitOps change, so there is no new revision to flush",
configSource,
valuesPrinted: false,
};
}
if (!required) {
return {
ok: true,
enabled: cfg.enabled,
required: false,
mode: cfg.enabled ? "consumer-not-enabled" : "disabled",
executed: false,
configSource,
valuesPrinted: false,
};
}
if (!baseReady) {
return {
ok: true,
enabled: true,
required: true,
mode: "waiting-for-pac-ready",
executed: false,
configSource,
valuesPrinted: false,
};
}
const pipelineRun = stringValue(latest.name);
if (observedSourceCommit === "-" || pipelineRun === "-") {
return {
ok: false,
enabled: true,
required: true,
mode: "missing-closeout-context",
executed: false,
configSource,
degradedReason: "pac-closeout-gitops-mirror-context-missing",
valuesPrinted: false,
};
}
if (consumer.id.startsWith("agentrun-")) {
const progressLines: string[] = [];
const flush = await captureStderrLines(progressLines, () => runAgentRunGitMirrorJob(config, "flush", {
node: consumer.node,
lane: consumer.lane,
confirm: true,
dryRun: false,
wait: true,
timeoutSeconds: cfg.waitTimeoutSeconds,
}));
return {
ok: flush.ok === true,
enabled: true,
required: true,
mode: stringValue(flush.mode, flush.ok === true ? "flushed" : "failed"),
executed: flush.ok === true,
sourceCommit: observedSourceCommit,
pipelineRun,
configSource,
waitTimeoutSeconds: cfg.waitTimeoutSeconds,
maxAttempts: cfg.maxAttempts,
beforeSummary: record(record(flush.status).summary),
afterSummary: record(record(flush.status).summary),
jobName: flush.jobName ?? null,
degradedReason: flush.ok === true ? undefined : "pac-closeout-agentrun-gitops-mirror-flush-failed",
next: record(flush.next),
flush,
progressLines,
valuesPrinted: false,
};
}
const deadlineMs = Date.now() + (cfg.waitTimeoutSeconds * 1000);
const gitOpsMirrorLane = consumer.closeoutGitOpsMirrorLane ?? consumer.lane;
const scoped = parseNodeScopedDelegatedOptions("git-mirror", [
"flush",
"--node",
consumer.node,
"--lane",
gitOpsMirrorLane,
"--confirm",
"--wait",
"--timeout-seconds",
String(cfg.waitTimeoutSeconds),
]);
const progressLines: string[] = [];
const flush = await captureStderrLines(progressLines, () => nodeRuntimeEnsureGitMirrorFlushed(scoped, "post", observedSourceCommit, pipelineRun, null, {
deadlineMs,
maxAttempts: cfg.maxAttempts,
}));
return {
ok: flush.ok === true,
enabled: true,
required: true,
mode: stringValue(flush.mode, flush.ok === true ? "flushed" : "failed"),
executed: flush.executed === true,
sourceCommit: observedSourceCommit,
pipelineRun,
configSource,
waitTimeoutSeconds: cfg.waitTimeoutSeconds,
maxAttempts: cfg.maxAttempts,
beforeSummary: flush.beforeSummary ?? null,
afterSummary: flush.afterSummary ?? null,
jobName: flush.jobName ?? null,
degradedReason: flush.degradedReason ?? undefined,
next: flush.next ?? undefined,
flush,
progressLines,
valuesPrinted: false,
};
}
async function captureStderrLines<T>(lines: string[], callback: () => T | Promise<T>): Promise<T> {
const stderr = process.stderr as typeof process.stderr & { write: (...args: unknown[]) => boolean };
const originalWrite = stderr.write.bind(process.stderr);
stderr.write = ((chunk: unknown, ...args: unknown[]): boolean => {
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
lines.push(text);
originalWrite(text);
const callbackArg = args.find((arg) => typeof arg === "function") as (() => void) | undefined;
callbackArg?.();
return true;
}) as typeof stderr.write;
try {
return await callback();
} finally {
stderr.write = originalWrite as typeof stderr.write;
}
}
function pipelineRunGate(latest: Record<string, unknown>): Record<string, unknown> {
const name = stringValue(latest.name);
@@ -2567,6 +2442,11 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
"DETAIL",
...table(["FIELD", "VALUE"], [
["id", stringValue(row.id ?? row.pipelineRun)],
["deliveryClass", stringValue(row.deliveryClass)],
["deliveryAuthorityEligible", stringValue(row.deliveryAuthorityEligible)],
["deliveryOwner", stringValue(row.deliveryOwner)],
["executionOwner", stringValue(row.executionOwner)],
["parentRelation", stringValue(row.parentRelation)],
["pipeline", stringValue(row.pipeline)],
["branch", stringValue(row.branch)],
["commit", stringValue(row.commit)],