feat(cicd): generate PaC source artifacts from YAML

This commit is contained in:
Codex
2026-07-11 06:10:20 +02:00
parent 06ce779608
commit 0aab8d35ad
16 changed files with 1901 additions and 42 deletions
+185 -2
View File
@@ -1,4 +1,4 @@
import { createHash, randomBytes } from "node:crypto";
import { randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import type { UniDeskConfig } from "./config";
@@ -17,11 +17,21 @@ import {
sha256Fingerprint,
} from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
import {
canonicalizePacPipelineSpec,
parsePacSourceArtifactOptions,
renderPacSourceArtifactResult,
runPacSourceArtifact,
type PacSourceArtifactBinding,
type PacSourceArtifactRuntimeObservation,
type PacSourceArtifactSpec,
} from "./platform-infra-pipelines-as-code-source-artifact";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as-code-remote.sh");
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 y = createYamlFieldReader(configLabel);
@@ -132,6 +142,7 @@ interface PacConsumer {
repositoryRef: string;
closeoutGitOpsMirrorFlush: boolean;
closeoutGitOpsMirrorLane: "v02" | "v03" | null;
sourceArtifact: PacSourceArtifactSpec | null;
}
interface CommonOptions {
@@ -216,12 +227,78 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
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");
try {
const options = parsePacSourceArtifactOptions(args.slice(1));
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const consumer = resolveConsumer(pac, options.consumerId);
if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
if (consumer.sourceArtifact === null) throw new Error(`Pipelines-as-Code consumer ${consumer.id} has no sourceArtifact owner in ${configLabel}`);
const repository = resolveRepository(pac, consumer.repositoryRef);
const binding: PacSourceArtifactBinding = {
target: { id: target.id },
consumer: {
id: consumer.id,
node: consumer.node,
lane: consumer.lane,
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
sourceArtifact: consumer.sourceArtifact,
},
repository: {
id: repository.id,
cloneUrl: repository.cloneUrl,
owner: repository.owner,
repo: repository.repo,
params: repository.params,
},
};
const result = await runPacSourceArtifact(binding, options, async ({ desiredSpec, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, provenance, sourceCommit));
return options.json || options.full ? result : renderPacSourceArtifactResult(result);
} catch (error) {
if (structuredError) throw error;
return sourceArtifactValidationError(error);
}
}
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help() };
}
function sourceArtifactHelp(): RenderedCliResult {
const lines = [
"PAC SOURCE ARTIFACT",
"Generate and verify consumer-declared PaC source artifacts from owning YAML plus the shared domain renderer.",
"",
"Usage:",
" ... source-artifact plan --target <node> --consumer <id> --source-worktree <absolute-path>",
" ... source-artifact check --target <node> --consumer <id> --source-worktree <absolute-path>",
" ... source-artifact write --target <node> --consumer <id> --source-worktree <absolute-path> --confirm",
" ... source-artifact status --target <node> --consumer <id> --source-worktree <absolute-path> [--source-commit <full-sha>]",
" ... source-artifact verify-runtime --target <node> --consumer <id> --source-worktree <absolute-path> --source-commit <full-sha>",
"",
"plan/check/write compare desired YAML-rendered state with source files and never access runtime.",
"status is non-gating runtime diagnostics; verify-runtime is fail-closed and binds the PipelineRun to the exact source commit.",
"Use --json or --full for explicit structured output; source specs and Secret values are never printed.",
];
return { ok: true, command: "platform-infra-pipelines-as-code-source-artifact-help", renderedText: `${lines.join("\n")}\n`, contentType: "text/plain" };
}
function sourceArtifactValidationError(error: unknown): RenderedCliResult {
const reason = String(error instanceof Error ? error.message : error).replace(/[\r\n]+/gu, " ").slice(0, 1000);
return {
ok: false,
command: "platform-infra-pipelines-as-code-source-artifact-validation",
renderedText: `PAC SOURCE ARTIFACT ERROR\nreason=${reason}\nnext=Fix the explicit target, consumer, source worktree, or owning YAML declaration; rerun plan before write.\n`,
contentType: "text/plain",
};
}
function help(): Record<string, unknown> {
return {
command: "platform-infra pipelines-as-code plan|apply|status|closeout|history|debug-step",
command: "platform-infra pipelines-as-code plan|apply|status|closeout|history|debug-step|source-artifact",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
@@ -232,6 +309,9 @@ function help(): Record<string, unknown> {
"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.",
@@ -353,9 +433,112 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
repositoryRef: typeof consumer.repositoryRef === "string" && consumer.repositoryRef.length > 0 ? consumer.repositoryRef : defaultRepositoryRef,
closeoutGitOpsMirrorFlush: y.booleanField(consumer, "closeoutGitOpsMirrorFlush", path),
closeoutGitOpsMirrorLane: consumer.closeoutGitOpsMirrorLane === undefined ? null : y.enumField(consumer, "closeoutGitOpsMirrorLane", path, ["v02", "v03"] as const),
sourceArtifact: consumer.sourceArtifact === undefined ? null : parseSourceArtifact(y.objectField(consumer, "sourceArtifact", path), `${path}.sourceArtifact`),
};
}
function parseSourceArtifact(value: Record<string, unknown>, path: string): PacSourceArtifactSpec {
const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane"] as const);
const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
if (mode === "embedded-pipeline-spec" && pipelinePath !== null) throw new Error(`${path}.pipelinePath is forbidden for embedded-pipeline-spec`);
if (mode === "remote-pipeline-annotation" && pipelinePath === null) throw new Error(`${path}.pipelinePath is required for remote-pipeline-annotation`);
if (renderer === "agentrun-control-plane" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer agentrun-control-plane requires embedded-pipeline-spec`);
if (renderer === "hwlab-runtime-lane" && mode !== "remote-pipeline-annotation") throw new Error(`${path}.renderer hwlab-runtime-lane requires remote-pipeline-annotation`);
return {
mode,
renderer,
configRef: y.stringField(value, "configRef", path),
pipelineRunPath,
pipelinePath,
maxKeepRuns: positiveInteger(value, "maxKeepRuns", path),
};
}
function sourceArtifactPath(value: string, path: string): string {
if (value.startsWith("/") || value.split(/[\\/]/u).includes("..") || !value.endsWith(".yaml")) throw new Error(`${path} must be a worktree-relative .yaml path without ..`);
return value;
}
async function observeSourceArtifactRuntime(
config: UniDeskConfig,
target: PacTarget,
consumer: PacConsumer,
desiredSpec: Record<string, unknown>,
provenance: { readonly configRef: string; readonly effectiveConfigSha256: string; readonly renderer: string },
sourceCommit: string | null,
): Promise<PacSourceArtifactRuntimeObservation> {
const script = renderPacSourceArtifactRuntimeObserverShell({
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
desiredSpec: canonicalizePacPipelineSpec(desiredSpec),
provenance,
sourceCommit,
});
const captured = await capture(config, target.route, ["sh"], script);
if (captured.exitCode !== 0) {
const compact = compactCapture(captured);
const detail = String(compact.stderrTail ?? compact.stdoutTail ?? "").trim().split(/\r?\n/u).filter(Boolean)[0]?.replace(/\s+/gu, " ").slice(0, 240);
return unavailableSourceArtifactRuntime(`runtime-transport-exit-${captured.exitCode}${detail ? `:${detail}` : ""}`, sourceCommit);
}
const parsed = parseJsonOutput(captured.stdout);
if (parsed === null) return unavailableSourceArtifactRuntime("runtime-observer-invalid-json", sourceCommit);
return {
live: runtimeSourceArtifactItem(parsed.live),
embedded: runtimeSourceArtifactItem(parsed.embedded),
};
}
export function renderPacSourceArtifactRuntimeObserverShell(inputValue: Record<string, unknown>): string {
const input = JSON.stringify(inputValue);
const observer = readFileSync(sourceArtifactRuntimeObserverFile, "utf8").trimEnd();
if (observer.includes("NODE_PAC_SOURCE_ARTIFACT_STATUS") || input.includes("JSON_PAC_SOURCE_ARTIFACT_INPUT")) throw new Error("PaC source artifact observer input contains a reserved heredoc delimiter");
return `set -eu
observer_input=$(mktemp)
trap 'rm -f "$observer_input"' EXIT HUP INT TERM
cat >"$observer_input" <<'JSON_PAC_SOURCE_ARTIFACT_INPUT'
${input}
JSON_PAC_SOURCE_ARTIFACT_INPUT
PAC_SOURCE_ARTIFACT_INPUT_FILE="$observer_input" node --input-type=module <<'NODE_PAC_SOURCE_ARTIFACT_STATUS'
${observer}
NODE_PAC_SOURCE_ARTIFACT_STATUS
`;
}
function runtimeSourceArtifactItem(value: unknown): PacSourceArtifactRuntimeObservation["live"] {
if (typeof value !== "object" || value === null || Array.isArray(value)) return unavailableSourceArtifactRuntime("runtime-observer-invalid-item", null).live;
const item = value as Record<string, unknown>;
const status = item.status;
if (status !== "aligned" && status !== "drifted" && status !== "missing" && status !== "unavailable") return unavailableSourceArtifactRuntime("runtime-observer-invalid-status", null).live;
const drift = item.firstDrift;
return {
status,
name: typeof item.name === "string" ? item.name : null,
canonicalSha256: typeof item.canonicalSha256 === "string" ? item.canonicalSha256 : null,
firstDrift: typeof drift === "object" && drift !== null && !Array.isArray(drift)
? {
path: String((drift as Record<string, unknown>).path ?? "$"),
expected: String((drift as Record<string, unknown>).expected ?? ""),
actual: String((drift as Record<string, unknown>).actual ?? ""),
}
: null,
provenanceAligned: item.provenanceAligned === true,
configRef: typeof item.configRef === "string" ? item.configRef : null,
effectiveConfigSha256: typeof item.effectiveConfigSha256 === "string" ? item.effectiveConfigSha256 : null,
sourceCommit: typeof item.sourceCommit === "string" ? item.sourceCommit : null,
reason: typeof item.reason === "string" ? item.reason : null,
candidateCount: typeof item.candidateCount === "number" && Number.isInteger(item.candidateCount) && item.candidateCount >= 0 ? item.candidateCount : 0,
};
}
function unavailableSourceArtifactRuntime(reason: string, sourceCommit: string | null): PacSourceArtifactRuntimeObservation {
const item = { status: "unavailable" as const, name: null, canonicalSha256: null, firstDrift: null, provenanceAligned: false, configRef: null, effectiveConfigSha256: null, sourceCommit, reason, candidateCount: 0 };
return { live: item, embedded: item };
}
function parseTarget(record: Record<string, unknown>, index: number): PacTarget {
const path = `targets[${index}]`;
return {