fix: 收敛 PaC 自动交付提示
This commit is contained in:
@@ -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)],
|
||||
|
||||
Reference in New Issue
Block a user