1108 lines
54 KiB
TypeScript
1108 lines
54 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
import { rootPath } from "./config";
|
|
import { AGENTRUN_CONFIG_PATH, resolveAgentRunLaneTarget } from "./agentrun-lanes";
|
|
import { renderAgentRunPipelineManifest } from "./agentrun-manifests";
|
|
import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
|
import { nodeRuntimeGitopsRoot } from "./hwlab-node/cleanup";
|
|
import { nodeRuntimePipelinePostprocessScript } from "./hwlab-node/render";
|
|
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
|
|
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
|
|
import { applyNodeRuntimeDeployYamlOverlay } from "./hwlab-node/deploy-overlay";
|
|
import type { RenderedCliResult } from "./output";
|
|
import { renderedCliResult } from "./agentrun/render";
|
|
import { stableJsonSha256, stableJsonValue } from "./stable-json";
|
|
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
|
|
|
|
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
|
|
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane";
|
|
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
|
|
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
|
|
|
|
export interface PacSourceArtifactTaskRunTemplateSpec {
|
|
readonly hostNetwork: boolean;
|
|
readonly dnsPolicy: "ClusterFirst" | "ClusterFirstWithHostNet" | "Default" | "None";
|
|
readonly fsGroup: number;
|
|
}
|
|
|
|
export interface PacSourceArtifactSpec {
|
|
readonly mode: PacSourceArtifactMode;
|
|
readonly renderer: PacSourceArtifactRenderer;
|
|
readonly configRef: string;
|
|
readonly pipelineRunPath: string;
|
|
readonly pipelinePath: string | null;
|
|
readonly maxKeepRuns: number;
|
|
readonly taskRunTemplate: PacSourceArtifactTaskRunTemplateSpec;
|
|
}
|
|
|
|
export interface PacSourceArtifactBinding {
|
|
readonly target: { readonly id: string };
|
|
readonly consumer: {
|
|
readonly id: string;
|
|
readonly node: string;
|
|
readonly lane: string;
|
|
readonly namespace: string;
|
|
readonly pipeline: string;
|
|
readonly pipelineRunPrefix: string;
|
|
readonly deliveryProvenance?: {
|
|
readonly markerAnnotation: string;
|
|
readonly markerValue: string;
|
|
readonly executionServiceAccountName: string;
|
|
} | null;
|
|
readonly sourceArtifact: PacSourceArtifactSpec;
|
|
};
|
|
readonly repository: {
|
|
readonly id: string;
|
|
readonly url: string;
|
|
readonly cloneUrl: string;
|
|
readonly owner: string;
|
|
readonly repo: string;
|
|
readonly params: Readonly<Record<string, string>>;
|
|
};
|
|
}
|
|
|
|
export interface PacSourceArtifactOptions {
|
|
readonly action: PacSourceArtifactAction;
|
|
readonly targetId: string;
|
|
readonly consumerId: string;
|
|
readonly sourceWorktree: string;
|
|
readonly sourceCommit: string | null;
|
|
readonly confirm: boolean;
|
|
readonly json: boolean;
|
|
readonly full: boolean;
|
|
}
|
|
|
|
export interface PacSourceArtifactRuntimeObservation {
|
|
readonly live: PacSourceArtifactRuntimeItem;
|
|
readonly embedded: PacSourceArtifactRuntimeItem;
|
|
}
|
|
|
|
export interface PacSourceArtifactRuntimeItem {
|
|
readonly status: "aligned" | "drifted" | "missing" | "unavailable";
|
|
readonly name: string | null;
|
|
readonly canonicalSha256: string | null;
|
|
readonly firstDrift: PacSourceArtifactDrift | null;
|
|
readonly provenanceAligned: boolean;
|
|
readonly configRef: string | null;
|
|
readonly effectiveConfigSha256: string | null;
|
|
readonly sourceCommit: string | null;
|
|
readonly reason: string | null;
|
|
readonly candidateCount: number;
|
|
}
|
|
|
|
export interface PacSourceArtifactDrift {
|
|
readonly path: string;
|
|
readonly expected: string;
|
|
readonly actual: string;
|
|
}
|
|
|
|
export type PacSourceArtifactRuntimeObserver = (input: {
|
|
readonly binding: PacSourceArtifactBinding;
|
|
readonly desiredSpec: Record<string, unknown>;
|
|
readonly desiredWrapper: Record<string, unknown> | null;
|
|
readonly provenance: PacSourceArtifactProvenance;
|
|
readonly sourceCommit: string | null;
|
|
}) => Promise<PacSourceArtifactRuntimeObservation>;
|
|
|
|
interface PacSourceArtifactProvenance {
|
|
readonly configRef: string;
|
|
readonly effectiveConfigSha256: string;
|
|
readonly renderer: PacSourceArtifactRenderer;
|
|
readonly mode: PacSourceArtifactMode;
|
|
}
|
|
|
|
interface DesiredArtifact {
|
|
readonly pipeline: Record<string, unknown>;
|
|
readonly pipelineRun: Record<string, unknown>;
|
|
readonly provenance: PacSourceArtifactProvenance;
|
|
readonly desiredSpec: Record<string, unknown>;
|
|
readonly files: readonly { readonly path: string; readonly content: string }[];
|
|
}
|
|
|
|
interface SourceInspection {
|
|
readonly status: "aligned" | "drifted" | "missing";
|
|
readonly aligned: boolean;
|
|
readonly pipelineCanonicalSha256: string | null;
|
|
readonly pipelineRunCanonicalSha256: string | null;
|
|
readonly firstDrift: PacSourceArtifactDrift | null;
|
|
readonly provenanceAligned: boolean;
|
|
}
|
|
|
|
export interface SourceWorktreeState {
|
|
readonly head: string;
|
|
readonly clean: boolean;
|
|
readonly matchesSourceCommit: boolean | null;
|
|
}
|
|
|
|
export class PacSourceArtifactError extends Error {
|
|
readonly code: string;
|
|
readonly safeDetails: Readonly<Record<string, string>>;
|
|
|
|
constructor(code: string, safeDetails: Readonly<Record<string, string>> = {}) {
|
|
super(code);
|
|
this.name = "PacSourceArtifactError";
|
|
this.code = code;
|
|
this.safeDetails = safeDetails;
|
|
}
|
|
}
|
|
|
|
export interface PacSourceArtifactSafeError {
|
|
readonly code: string;
|
|
readonly evidence: string;
|
|
readonly details: Readonly<Record<string, string>>;
|
|
}
|
|
|
|
export function pacSourceArtifactSafeError(error: unknown): PacSourceArtifactSafeError {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const allowedDetailKeys = new Set([
|
|
"expectedUrlFingerprint",
|
|
"observedUrlFingerprint",
|
|
"head",
|
|
"requestedSourceCommit",
|
|
]);
|
|
const details = error instanceof PacSourceArtifactError
|
|
? Object.fromEntries(Object.entries(error.safeDetails).map(([key, value]) => [key, allowedDetailKeys.has(key) ? value : pacValueEvidence(value)]))
|
|
: {};
|
|
return {
|
|
code: error instanceof PacSourceArtifactError ? error.code : "source-artifact-operation-failed",
|
|
evidence: pacValueEvidence(message),
|
|
details,
|
|
};
|
|
}
|
|
|
|
const knownDriftPathKeys = new Set([
|
|
"accessModes", "annotations", "apiVersion", "args", "command", "computeResources", "default", "description", "dnsPolicy", "env", "envFrom",
|
|
"executionMode", "finally", "fsGroup", "generateName", "hostNetwork", "image", "kind", "labels", "length", "metadata", "mode", "mountPath", "name", "namespace", "onError", "operator",
|
|
"optional", "params", "pipeline", "pipelineRef", "pipelineSpec", "podTemplate", "readinessProbe", "readOnly", "requests", "resources",
|
|
"results", "retries", "runAfter", "script", "secret", "secretName", "securityContext", "serviceAccountName", "sidecars", "spec", "steps",
|
|
"storage", "subPath", "taskRunTemplate", "taskSpec", "tasks", "timeout", "timeouts", "type", "value", "values", "volumeClaimTemplate",
|
|
"volumeMounts", "volumes", "when", "workspaces", "workingDir", "wrapper",
|
|
]);
|
|
|
|
export function parsePacSourceArtifactOptions(args: readonly string[]): PacSourceArtifactOptions {
|
|
const action = args[0];
|
|
if (!isSourceArtifactAction(action)) {
|
|
throw new Error("source-artifact requires one of plan, check, write, status, verify-runtime");
|
|
}
|
|
let targetId: string | null = null;
|
|
let consumerId: string | null = null;
|
|
let sourceWorktree: string | null = null;
|
|
let sourceCommit: string | null = null;
|
|
let confirm = false;
|
|
let json = false;
|
|
let full = false;
|
|
for (let index = 1; index < args.length; index += 1) {
|
|
const arg = args[index];
|
|
if (arg === "--confirm") {
|
|
confirm = true;
|
|
continue;
|
|
}
|
|
if (arg === "--json") {
|
|
json = true;
|
|
continue;
|
|
}
|
|
if (arg === "--full") {
|
|
full = true;
|
|
continue;
|
|
}
|
|
if (arg === "--target" || arg === "--consumer" || arg === "--source-worktree" || arg === "--source-commit") {
|
|
const value = args[index + 1];
|
|
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
|
if (arg === "--target") targetId = value;
|
|
else if (arg === "--consumer") consumerId = value;
|
|
else if (arg === "--source-worktree") sourceWorktree = value;
|
|
else sourceCommit = value.toLowerCase();
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(`unsupported source-artifact option: ${arg}`);
|
|
}
|
|
if (targetId === null) throw new Error("source-artifact requires explicit --target");
|
|
if (consumerId === null) throw new Error("source-artifact requires explicit --consumer");
|
|
if (sourceWorktree === null) throw new Error("source-artifact requires explicit --source-worktree");
|
|
if (!isAbsolute(sourceWorktree)) throw new Error("--source-worktree must be an absolute path");
|
|
if (sourceCommit !== null && !/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full 40-character git SHA");
|
|
if (sourceCommit !== null && action !== "status" && action !== "verify-runtime") throw new Error("--source-commit is accepted only by source-artifact status or verify-runtime");
|
|
if (action === "verify-runtime" && sourceCommit === null) throw new PacSourceArtifactError("source-commit-required");
|
|
if (action === "write" && !confirm) throw new Error("source-artifact write requires --confirm");
|
|
if (action !== "write" && confirm) throw new Error("--confirm is accepted only by source-artifact write");
|
|
return { action, targetId, consumerId, sourceWorktree, sourceCommit, confirm, json, full };
|
|
}
|
|
|
|
export async function runPacSourceArtifact(
|
|
binding: PacSourceArtifactBinding,
|
|
options: PacSourceArtifactOptions,
|
|
observeRuntime?: PacSourceArtifactRuntimeObserver,
|
|
): Promise<Record<string, unknown>> {
|
|
if (binding.target.id.toLowerCase() !== options.targetId.toLowerCase()) {
|
|
throw new Error(`source-artifact target ${options.targetId} does not match resolved target ${binding.target.id}`);
|
|
}
|
|
if (binding.consumer.id.toLowerCase() !== options.consumerId.toLowerCase()) {
|
|
throw new Error(`source-artifact consumer ${options.consumerId} does not match resolved consumer ${binding.consumer.id}`);
|
|
}
|
|
const sourceWorktree = validateSourceWorktree(options.sourceWorktree, binding);
|
|
const sourceHead = git(sourceWorktree, ["rev-parse", "HEAD"]).toLowerCase();
|
|
const sourceWorktreeState: SourceWorktreeState = {
|
|
head: sourceHead,
|
|
clean: git(sourceWorktree, ["status", "--porcelain=v1", "--untracked-files=all"]).length === 0,
|
|
matchesSourceCommit: options.sourceCommit === null ? null : sourceHead === options.sourceCommit,
|
|
};
|
|
assertPacSourceArtifactVerifyWorktree(options.action, options.sourceCommit, sourceWorktreeState);
|
|
const desired = renderDesiredArtifact(binding, sourceWorktree);
|
|
let source = inspectSourceArtifact(sourceWorktree, binding, desired);
|
|
const written: string[] = [];
|
|
if (options.action === "write") {
|
|
written.push(...writeArtifactFilesAtomically(sourceWorktree, desired.files));
|
|
source = inspectSourceArtifact(sourceWorktree, binding, desired);
|
|
}
|
|
const needsRuntime = options.action === "status" || options.action === "verify-runtime";
|
|
const runtime = needsRuntime
|
|
? observeRuntime === undefined
|
|
? unavailableRuntimeObservation()
|
|
: await observeRuntime({
|
|
binding,
|
|
desiredSpec: desired.desiredSpec,
|
|
desiredWrapper: options.sourceCommit === null ? null : desiredRuntimePipelineRunWrapper(binding, desired.pipelineRun, options.sourceCommit),
|
|
provenance: desired.provenance,
|
|
sourceCommit: options.sourceCommit,
|
|
})
|
|
: null;
|
|
const sourceOk = source.aligned && source.provenanceAligned;
|
|
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit);
|
|
const runtimeOk = runtimeAlignment === "aligned";
|
|
const ok = options.action === "check"
|
|
? sourceOk
|
|
: options.action === "verify-runtime"
|
|
? sourceOk && runtimeOk
|
|
: options.action === "write"
|
|
? sourceOk
|
|
: true;
|
|
const desiredSpecSha256 = canonicalSha256(desired.desiredSpec);
|
|
return {
|
|
ok,
|
|
action: `platform-infra-pipelines-as-code-source-artifact-${options.action}`,
|
|
mutation: options.action === "write" && written.length > 0,
|
|
target: binding.target.id,
|
|
consumer: binding.consumer.id,
|
|
node: binding.consumer.node,
|
|
lane: binding.consumer.lane,
|
|
mode: binding.consumer.sourceArtifact.mode,
|
|
renderer: binding.consumer.sourceArtifact.renderer,
|
|
sourceWorktree,
|
|
files: {
|
|
pipelineRun: binding.consumer.sourceArtifact.pipelineRunPath,
|
|
pipeline: binding.consumer.sourceArtifact.pipelinePath,
|
|
written,
|
|
},
|
|
provenance: desired.provenance,
|
|
desired: {
|
|
canonicalSha256: desiredSpecSha256,
|
|
pipeline: binding.consumer.pipeline,
|
|
namespace: binding.consumer.namespace,
|
|
},
|
|
source,
|
|
sourceWorktreeState,
|
|
runtime,
|
|
runtimeTarget: { sourceCommit: options.sourceCommit },
|
|
runtimeAlignment,
|
|
boundary: {
|
|
desiredAuthority: "owning YAML plus domain renderer",
|
|
liveExportAllowed: false,
|
|
sourceWorktreeExplicit: true,
|
|
canonicalAdmissionDefaultsRemoved: [
|
|
"tasks[].taskSpec.metadata={}",
|
|
"tasks[].taskSpec.spec=null",
|
|
"tasks[].taskSpec.params[].type=string",
|
|
"tasks[].taskSpec.results[].type=string",
|
|
"tasks[].taskSpec.steps[].computeResources={}",
|
|
"tasks[].taskSpec.sidecars[].computeResources={}",
|
|
],
|
|
valuesPrinted: false,
|
|
},
|
|
next: sourceArtifactNext(binding, options, sourceOk, sourceWorktreeState, runtime, runtimeAlignment),
|
|
};
|
|
}
|
|
|
|
export function assertPacSourceArtifactVerifyWorktree(
|
|
action: PacSourceArtifactAction,
|
|
sourceCommit: string | null,
|
|
worktree: SourceWorktreeState,
|
|
): void {
|
|
if (action !== "verify-runtime") return;
|
|
if (sourceCommit === null) throw new PacSourceArtifactError("source-commit-required");
|
|
if (!worktree.clean) throw new PacSourceArtifactError("source-worktree-not-clean", { head: worktree.head });
|
|
if (worktree.matchesSourceCommit !== true) {
|
|
throw new PacSourceArtifactError("source-worktree-commit-mismatch", {
|
|
head: worktree.head,
|
|
requestedSourceCommit: sourceCommit,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function sourceArtifactRuntimeAlignment(
|
|
runtime: PacSourceArtifactRuntimeObservation | null,
|
|
worktree: SourceWorktreeState,
|
|
sourceCommit: string | null,
|
|
): PacSourceArtifactRuntimeAlignment {
|
|
if (runtime === null) return "not-requested";
|
|
if (sourceCommit !== null && (!worktree.clean || worktree.matchesSourceCommit !== true)) return "unavailable";
|
|
if (runtime.live.status === "unavailable" || runtime.embedded.status === "unavailable") return "unavailable";
|
|
if (runtime.live.status === "missing" || runtime.embedded.status === "missing") return "missing";
|
|
if (runtime.live.status === "drifted" || runtime.embedded.status === "drifted") return "drifted";
|
|
if (!runtime.live.provenanceAligned || !runtime.embedded.provenanceAligned) return "drifted";
|
|
return "aligned";
|
|
}
|
|
|
|
function sourceArtifactNext(
|
|
binding: PacSourceArtifactBinding,
|
|
options: PacSourceArtifactOptions,
|
|
sourceOk: boolean,
|
|
worktree: SourceWorktreeState,
|
|
runtime: PacSourceArtifactRuntimeObservation | null,
|
|
runtimeAlignment: PacSourceArtifactRuntimeAlignment,
|
|
): string {
|
|
if (!sourceOk) {
|
|
return options.action === "write"
|
|
? "Generated files remain inconsistent; inspect the safe firstDrift evidence and do not publish."
|
|
: `Run source-artifact write --confirm for ${binding.consumer.id}, then rerun check.`;
|
|
}
|
|
if (runtime === null) return `After the consumer PR rolls out, use a clean exact-commit worktree and run verify-runtime for ${binding.consumer.id} with --source-commit <full-sha>.`;
|
|
if (!worktree.clean || (options.sourceCommit !== null && worktree.matchesSourceCommit !== true)) {
|
|
return "Create a clean worktree at the exact GitHub merge commit, then rerun status or verify-runtime with that full source commit.";
|
|
}
|
|
if (runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
|
|
if (runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
|
|
if (runtime.live.status === "drifted" || !runtime.live.provenanceAligned) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
|
|
if (runtime.embedded.status === "unavailable") return "Inspect the exact-commit PaC PipelineRun identity and observer access, then rerun verify-runtime.";
|
|
if (runtime.embedded.status === "missing") return "Wait for or repair the exact-commit PaC PipelineRun; do not validate a prefix-selected run.";
|
|
if (runtime.embedded.status === "drifted" || !runtime.embedded.provenanceAligned) return "Regenerate and merge the consumer source artifact, then verify the new exact-commit PipelineRun.";
|
|
return runtimeAlignment === "aligned"
|
|
? "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
|
|
: "Resolve the reported runtime layer and rerun verify-runtime.";
|
|
}
|
|
|
|
export function renderPacSourceArtifactResult(result: Record<string, unknown>): RenderedCliResult {
|
|
const files = record(result.files);
|
|
const desired = record(result.desired);
|
|
const source = record(result.source);
|
|
const sourceWorktreeState = record(result.sourceWorktreeState);
|
|
const runtime = result.runtime === null ? null : record(result.runtime);
|
|
const live = runtime === null ? null : record(runtime.live);
|
|
const embedded = runtime === null ? null : record(runtime.embedded);
|
|
const firstDrift = source.firstDrift === null ? null : record(source.firstDrift);
|
|
const liveDrift = live?.firstDrift === null ? null : record(live?.firstDrift);
|
|
const embeddedDrift = embedded?.firstDrift === null ? null : record(embedded?.firstDrift);
|
|
const lines = [
|
|
"PAC SOURCE ARTIFACT",
|
|
`target=${text(result.target)} consumer=${text(result.consumer)} node=${text(result.node)} lane=${text(result.lane)}`,
|
|
`mode=${text(result.mode)} renderer=${text(result.renderer)} mutation=${text(result.mutation)}`,
|
|
`pipelineRun=${text(files.pipelineRun)} pipeline=${text(files.pipeline ?? "-")}`,
|
|
`desired=${shortSha(desired.canonicalSha256)} source=${shortSha(source.pipelineCanonicalSha256)} aligned=${text(source.aligned)} provenance=${text(source.provenanceAligned)}`,
|
|
`sourceWorktree=head:${shortSha(sourceWorktreeState.head)} clean:${text(sourceWorktreeState.clean)} commitMatch:${text(sourceWorktreeState.matchesSourceCommit)}`,
|
|
`sourceDrift=${formatDrift(firstDrift)}`,
|
|
`runtime=${text(result.runtimeAlignment)} commit=${text(embedded?.sourceCommit ?? record(result.runtimeTarget).sourceCommit)} candidates=${text(embedded?.candidateCount)}`,
|
|
`live=status:${text(live?.status)} name:${text(live?.name)} hash:${shortSha(live?.canonicalSha256)} provenance:${text(live?.provenanceAligned)} drift:${formatDrift(liveDrift)}`,
|
|
`embedded=status:${text(embedded?.status)} name:${text(embedded?.name)} hash:${shortSha(embedded?.canonicalSha256)} provenance:${text(embedded?.provenanceAligned)} drift:${formatDrift(embeddedDrift)}`,
|
|
`runtimeReason=live:${text(live?.reason)} embedded:${text(embedded?.reason)}`,
|
|
`written=${Array.isArray(files.written) && files.written.length > 0 ? files.written.join(",") : "-"}`,
|
|
];
|
|
if (typeof result.next === "string") lines.push(`next=${result.next}`);
|
|
return renderedCliResult(result.ok !== false, String(result.action), `${lines.join("\n")}\n`);
|
|
}
|
|
|
|
export function canonicalizePacPipelineSpec(value: unknown, path: readonly (string | number)[] = []): unknown {
|
|
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
|
|
if (!isRecord(value)) return value;
|
|
const output: Record<string, unknown> = {};
|
|
for (const key of Object.keys(value).sort()) {
|
|
const child = value[key];
|
|
const childPath = [...path, key];
|
|
if (isProvenTektonAdmissionDefault(childPath, child)) continue;
|
|
output[key] = canonicalizePacPipelineSpec(child, childPath);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function canonicalSha256(value: unknown): string {
|
|
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
|
|
}
|
|
|
|
export function firstPacSourceArtifactDrift(expected: unknown, actual: unknown, path = "$"): PacSourceArtifactDrift | null {
|
|
const left = canonicalizePacPipelineSpec(expected);
|
|
const right = canonicalizePacPipelineSpec(actual);
|
|
return firstCanonicalDrift(left, right, path);
|
|
}
|
|
|
|
function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree: string): DesiredArtifact {
|
|
const sourceArtifact = binding.consumer.sourceArtifact;
|
|
const rendered = sourceArtifact.renderer === "agentrun-control-plane"
|
|
? renderAgentRunDesiredPipeline(binding)
|
|
: renderHwlabDesiredPipeline(binding, sourceWorktree);
|
|
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
|
|
? rendered.pipeline
|
|
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
|
|
if (!provenanceEquals(provenanceFromManifest(pipeline), rendered.provenance)) {
|
|
throw new Error(`${sourceArtifact.renderer} did not render the declared Pipeline provenance contract`);
|
|
}
|
|
const desiredSpec = requiredRecord(pipeline.spec, "rendered Pipeline.spec");
|
|
assertPipelineIdentity(pipeline, binding);
|
|
const pipelineRun = sourceArtifact.mode === "embedded-pipeline-spec"
|
|
? embeddedPipelineRun(binding, desiredSpec, rendered.provenance)
|
|
: remotePipelineRun(binding, pipeline, rendered.provenance);
|
|
const files = sourceArtifact.mode === "embedded-pipeline-spec"
|
|
? [{ path: sourceArtifact.pipelineRunPath, content: pacSourceArtifactYaml(pipelineRun) }]
|
|
: [
|
|
{ path: requiredString(sourceArtifact.pipelinePath, "sourceArtifact.pipelinePath"), content: pacSourceArtifactYaml(pipeline) },
|
|
{ path: sourceArtifact.pipelineRunPath, content: pacSourceArtifactYaml(pipelineRun) },
|
|
];
|
|
return { pipeline, pipelineRun, provenance: rendered.provenance, desiredSpec, files };
|
|
}
|
|
|
|
function renderAgentRunDesiredPipeline(binding: PacSourceArtifactBinding): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
|
const { spec } = resolveAgentRunLaneTarget({ node: binding.consumer.node, lane: binding.consumer.lane });
|
|
const expectedConfigRef = `${AGENTRUN_CONFIG_PATH}#controlPlane.lanes.${spec.lane}`;
|
|
assertConfigRef(binding.consumer.sourceArtifact.configRef, expectedConfigRef);
|
|
assertBindingIdentity(binding, {
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
namespace: spec.ci.namespace,
|
|
pipeline: spec.ci.pipeline,
|
|
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
|
|
serviceAccount: spec.ci.serviceAccountName,
|
|
sourceBranch: spec.source.branch,
|
|
gitopsBranch: spec.gitops.branch,
|
|
});
|
|
return {
|
|
pipeline: renderAgentRunPipelineManifest(spec),
|
|
provenance: {
|
|
configRef: expectedConfigRef,
|
|
effectiveConfigSha256: stableJsonSha256(spec),
|
|
renderer: "agentrun-control-plane",
|
|
mode: binding.consumer.sourceArtifact.mode,
|
|
},
|
|
};
|
|
}
|
|
|
|
function renderHwlabDesiredPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
|
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new Error(`HWLAB source artifact lane ${binding.consumer.lane} is not declared`);
|
|
const spec = hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node);
|
|
const provenance = hwlabRuntimePipelineProvenance(spec);
|
|
if (binding.consumer.sourceArtifact.renderer !== provenance.renderer || binding.consumer.sourceArtifact.mode !== provenance.mode) {
|
|
throw new Error(`sourceArtifact renderer/mode must match ${provenance.configRef}.pipelineProvenance`);
|
|
}
|
|
assertConfigRef(binding.consumer.sourceArtifact.configRef, provenance.configRef);
|
|
assertBindingIdentity(binding, {
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
namespace: "hwlab-ci",
|
|
pipeline: spec.pipeline,
|
|
pipelineRunPrefix: spec.pipelineRunPrefix,
|
|
serviceAccount: spec.serviceAccountName,
|
|
sourceBranch: spec.sourceBranch,
|
|
gitopsBranch: spec.gitopsBranch,
|
|
});
|
|
const pipeline = renderHwlabPipelineFromOwningYaml(spec, sourceWorktree);
|
|
return {
|
|
pipeline,
|
|
provenance,
|
|
};
|
|
}
|
|
|
|
function renderHwlabPipelineFromOwningYaml(spec: HwlabRuntimeLaneSpec, sourceWorktree: string): Record<string, unknown> {
|
|
const temporaryRoot = mkdtempSync(resolve(tmpdir(), "unidesk-pac-source-artifact-"));
|
|
const temporarySource = resolve(temporaryRoot, "source");
|
|
const output = resolve(temporaryRoot, "rendered");
|
|
const sourceCommit = git(sourceWorktree, ["rev-parse", "HEAD"]);
|
|
try {
|
|
copySourceWorktreeForRender(sourceWorktree, temporarySource);
|
|
const overlayRecord = nodeRuntimeRenderOverlay(spec);
|
|
const deployPath = resolve(temporarySource, "deploy", "deploy.yaml");
|
|
const deploy = parseYamlRecord(readFileSync(deployPath, "utf8"), deployPath);
|
|
writeFileSync(deployPath, `${Bun.YAML.stringify(applyNodeRuntimeDeployYamlOverlay(deploy, overlayRecord)).trim()}\n`);
|
|
const renderArgs = [
|
|
"scripts/run-bun.mjs",
|
|
"scripts/gitops-render.mjs",
|
|
"--lane", spec.lane,
|
|
"--node", spec.nodeId,
|
|
"--gitops-root", nodeRuntimeGitopsRoot(spec),
|
|
"--catalog-path", spec.catalogPath,
|
|
"--image-tag-mode", "full",
|
|
"--source-revision", sourceCommit,
|
|
"--source-repo", spec.gitUrl,
|
|
"--source-branch", spec.sourceBranch,
|
|
"--gitops-branch", spec.gitopsBranch,
|
|
"--git-read-url", spec.gitReadUrl,
|
|
"--git-write-url", spec.gitWriteUrl,
|
|
"--registry-prefix", spec.registryPrefix,
|
|
"--runtime-endpoint", spec.publicApiUrl,
|
|
"--web-endpoint", spec.publicWebUrl,
|
|
"--out", output,
|
|
];
|
|
runChecked("node", renderArgs, temporarySource, 120_000, "HWLAB owning YAML renderer");
|
|
const overlay = Buffer.from(JSON.stringify(overlayRecord), "utf8").toString("base64");
|
|
const postprocess = [
|
|
"set -eu",
|
|
`render_dir=${shellQuote(output)}`,
|
|
`overlay_b64=${shellQuote(overlay)}`,
|
|
...nodeRuntimePipelinePostprocessScript(),
|
|
].join("\n");
|
|
runChecked("sh", [], temporarySource, 120_000, "HWLAB UniDesk domain postprocess/verify renderer", postprocess);
|
|
const path = resolve(output, spec.tektonDir, "pipeline.yaml");
|
|
if (!existsSync(path)) throw new Error(`HWLAB domain renderer did not produce ${path}`);
|
|
return parseYamlRecord(readFileSync(path, "utf8"), path);
|
|
} finally {
|
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function copySourceWorktreeForRender(sourceWorktree: string, destination: string): void {
|
|
const excluded = new Set([".git", ".worktree", ".state", "node_modules", "coverage", "tmp", ".tmp"]);
|
|
cpSync(sourceWorktree, destination, {
|
|
recursive: true,
|
|
filter: (source) => {
|
|
const rel = relative(sourceWorktree, source);
|
|
if (rel === "") return true;
|
|
return !excluded.has(rel.split(sep)[0] ?? "");
|
|
},
|
|
});
|
|
const nodeModules = resolve(sourceWorktree, "node_modules");
|
|
if (existsSync(nodeModules)) symlinkSync(nodeModules, resolve(destination, "node_modules"), "dir");
|
|
}
|
|
|
|
function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>, provenance: PacSourceArtifactProvenance): Record<string, unknown> {
|
|
return {
|
|
apiVersion: "tekton.dev/v1",
|
|
kind: "PipelineRun",
|
|
metadata: {
|
|
name: `${binding.consumer.pipelineRunPrefix}-{{ revision }}`,
|
|
namespace: binding.consumer.namespace,
|
|
annotations: pipelineRunAnnotations(binding, provenance),
|
|
labels: pipelineRunLabels(binding, "agentrun"),
|
|
},
|
|
spec: {
|
|
pipelineSpec: desiredSpec,
|
|
taskRunTemplate: taskRunTemplate(binding),
|
|
params: pipelineRunParams(binding, desiredSpec),
|
|
workspaces: pipelineRunWorkspaces(binding, desiredSpec),
|
|
},
|
|
};
|
|
}
|
|
|
|
function remotePipelineRun(binding: PacSourceArtifactBinding, pipeline: Record<string, unknown>, provenance: PacSourceArtifactProvenance): Record<string, unknown> {
|
|
const desiredSpec = requiredRecord(pipeline.spec, "rendered Pipeline.spec");
|
|
const pipelinePath = requiredString(binding.consumer.sourceArtifact.pipelinePath, "sourceArtifact.pipelinePath");
|
|
const annotations = {
|
|
...pipelineRunAnnotations(binding, provenance),
|
|
"pipelinesascode.tekton.dev/pipeline": pipelinePath,
|
|
};
|
|
if (provenance.renderer === "hwlab-runtime-lane") {
|
|
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new Error(`HWLAB source artifact lane ${binding.consumer.lane} is not declared`);
|
|
const spec = hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node);
|
|
Object.assign(annotations, {
|
|
"hwlab.pikastech.local/ci-contract": "tekton-native-primitive-tasks",
|
|
"hwlab.pikastech.local/download-profile": spec.downloadProfileId,
|
|
"hwlab.pikastech.local/gitops-branch": spec.gitopsBranch,
|
|
"hwlab.pikastech.local/network-profile": spec.networkProfileId,
|
|
"hwlab.pikastech.local/node": spec.nodeId,
|
|
"hwlab.pikastech.local/policy": "native-per-service-taskrun-image-publish",
|
|
"hwlab.pikastech.local/runtime-path": spec.runtimePath,
|
|
"hwlab.pikastech.local/source-branch": spec.sourceBranch,
|
|
"hwlab.pikastech.local/source-config": binding.consumer.sourceArtifact.pipelineRunPath,
|
|
});
|
|
}
|
|
return {
|
|
apiVersion: "tekton.dev/v1",
|
|
kind: "PipelineRun",
|
|
metadata: {
|
|
generateName: `${binding.consumer.pipelineRunPrefix}-`,
|
|
namespace: binding.consumer.namespace,
|
|
annotations,
|
|
labels: pipelineRunLabels(binding, "hwlab"),
|
|
},
|
|
spec: {
|
|
timeouts: { pipeline: binding.repository.params.pipeline_timeout },
|
|
taskRunTemplate: taskRunTemplate(binding),
|
|
pipelineRef: { name: binding.consumer.pipeline },
|
|
workspaces: pipelineRunWorkspaces(binding, desiredSpec),
|
|
params: pipelineRunParams(binding, desiredSpec),
|
|
},
|
|
};
|
|
}
|
|
|
|
function pipelineRunAnnotations(binding: PacSourceArtifactBinding, provenance: PacSourceArtifactProvenance): Record<string, string> {
|
|
const branch = requiredParam(binding, "source_branch");
|
|
return {
|
|
"pipelinesascode.tekton.dev/on-event": "[push]",
|
|
"pipelinesascode.tekton.dev/on-target-branch": `[${branch}]`,
|
|
"pipelinesascode.tekton.dev/on-cel-expression": `event == 'push' && target_branch == '${branch}' && node == '${binding.consumer.node}'`,
|
|
"pipelinesascode.tekton.dev/max-keep-runs": String(binding.consumer.sourceArtifact.maxKeepRuns),
|
|
...(binding.consumer.deliveryProvenance == null ? {} : {
|
|
[binding.consumer.deliveryProvenance.markerAnnotation]: binding.consumer.deliveryProvenance.markerValue,
|
|
}),
|
|
...pipelineProvenanceAnnotations(provenance),
|
|
};
|
|
}
|
|
|
|
function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun" | "hwlab"): Record<string, string> {
|
|
return partOf === "agentrun"
|
|
? {
|
|
"app.kubernetes.io/part-of": "agentrun",
|
|
"agentrun.pikastech.local/lane": "v0.2",
|
|
"agentrun.pikastech.local/node": binding.consumer.node,
|
|
"agentrun.pikastech.local/source-commit": "{{ revision }}",
|
|
"agentrun.pikastech.local/trigger": "pipelines-as-code",
|
|
}
|
|
: {
|
|
"app.kubernetes.io/name": `${binding.consumer.id}-pac`,
|
|
"app.kubernetes.io/part-of": "hwlab",
|
|
"unidesk.ai/source-commit": "{{ revision }}",
|
|
"hwlab.pikastech.local/gitops-target": binding.consumer.lane,
|
|
"hwlab.pikastech.local/source-commit": "{{ revision }}",
|
|
"hwlab.pikastech.local/trigger": "pipelines-as-code",
|
|
};
|
|
}
|
|
|
|
function taskRunTemplate(binding: PacSourceArtifactBinding): Record<string, unknown> {
|
|
const declared = binding.consumer.sourceArtifact.taskRunTemplate;
|
|
return {
|
|
serviceAccountName: binding.consumer.deliveryProvenance?.executionServiceAccountName ?? `{{ service_account }}`,
|
|
podTemplate: {
|
|
hostNetwork: declared.hostNetwork,
|
|
dnsPolicy: declared.dnsPolicy,
|
|
securityContext: { fsGroup: declared.fsGroup },
|
|
},
|
|
};
|
|
}
|
|
|
|
function desiredRuntimePipelineRunWrapper(
|
|
binding: PacSourceArtifactBinding,
|
|
pipelineRun: Record<string, unknown>,
|
|
sourceCommit: string,
|
|
): Record<string, unknown> {
|
|
const sourceSpec = structuredClone(requiredRecord(pipelineRun.spec, "generated PipelineRun.spec"));
|
|
delete sourceSpec.pipelineSpec;
|
|
delete sourceSpec.pipelineRef;
|
|
return {
|
|
mode: binding.consumer.sourceArtifact.mode,
|
|
executionMode: "pipeline-spec",
|
|
spec: resolvePacRuntimeTemplates(sourceSpec, binding, sourceCommit),
|
|
};
|
|
}
|
|
|
|
function resolvePacRuntimeTemplates(value: unknown, binding: PacSourceArtifactBinding, sourceCommit: string): unknown {
|
|
if (Array.isArray(value)) return value.map((item) => resolvePacRuntimeTemplates(item, binding, sourceCommit));
|
|
if (isRecord(value)) {
|
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolvePacRuntimeTemplates(item, binding, sourceCommit)]));
|
|
}
|
|
if (typeof value !== "string") return value;
|
|
const templateValues: Readonly<Record<string, string>> = {
|
|
...binding.repository.params,
|
|
revision: sourceCommit,
|
|
repo_url: binding.repository.url,
|
|
};
|
|
const resolved = value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/gu, (_match, key: string) => {
|
|
const replacement = templateValues[key];
|
|
if (replacement === undefined) throw new PacSourceArtifactError("source-artifact-runtime-template-unresolved", { template: pacValueEvidence(key) });
|
|
return replacement;
|
|
});
|
|
if (/\{\{|\}\}/u.test(resolved)) throw new PacSourceArtifactError("source-artifact-runtime-template-invalid");
|
|
return resolved;
|
|
}
|
|
|
|
function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
|
|
const params = arrayRecords(desiredSpec.params, "Pipeline.spec.params");
|
|
return params.map((param) => {
|
|
const name = requiredString(param.name, "Pipeline.spec.params[].name");
|
|
if (name === "revision" || name === "source-commit") return { name, value: "{{ revision }}" };
|
|
if (name === "source-stage-ref") return { name, value: "{{ source_snapshot_prefix }}/{{ revision }}" };
|
|
if (name === "git-url") return { name, value: "{{ repo_url }}" };
|
|
const repositoryParam = name.replaceAll("-", "_");
|
|
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
|
|
return { name, value: `{{ ${repositoryParam} }}` };
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(param, "default")) return { name, value: param.default };
|
|
throw new Error(`Pipeline param ${name} has no default or PaC repository parameter`);
|
|
});
|
|
}
|
|
|
|
function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
|
|
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
|
|
const name = requiredString(workspace.name, "Pipeline.spec.workspaces[].name");
|
|
if (name === "source") {
|
|
return {
|
|
name,
|
|
volumeClaimTemplate: {
|
|
spec: {
|
|
accessModes: ["ReadWriteOnce"],
|
|
resources: { requests: { storage: requiredParam(binding, "workspace_pvc_size") } },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (name === "git-ssh") return { name, secret: { secretName: requiredParam(binding, "git_ssh_secret") } };
|
|
throw new Error(`unsupported Pipeline workspace ${name}; declare a renderer mapping before generating the source artifact`);
|
|
});
|
|
}
|
|
|
|
function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifactBinding, desired: DesiredArtifact): SourceInspection {
|
|
const sourceArtifact = binding.consumer.sourceArtifact;
|
|
const pipelineRunPath = safeArtifactPath(sourceWorktree, sourceArtifact.pipelineRunPath);
|
|
const pipelinePath = sourceArtifact.pipelinePath === null ? null : safeArtifactPath(sourceWorktree, sourceArtifact.pipelinePath);
|
|
if (!existsSync(pipelineRunPath) || (pipelinePath !== null && !existsSync(pipelinePath))) {
|
|
return { status: "missing", aligned: false, pipelineCanonicalSha256: null, pipelineRunCanonicalSha256: null, firstDrift: drift("$", { artifact: "generated" }, undefined), provenanceAligned: false };
|
|
}
|
|
const actualPipelineRun = parseYamlRecord(readFileSync(pipelineRunPath, "utf8"), sourceArtifact.pipelineRunPath);
|
|
const actualPipeline = sourceArtifact.mode === "remote-pipeline-annotation"
|
|
? parseYamlRecord(readFileSync(requiredString(pipelinePath, "pipelinePath"), "utf8"), requiredString(sourceArtifact.pipelinePath, "pipelinePath"))
|
|
: {
|
|
apiVersion: "tekton.dev/v1",
|
|
kind: "Pipeline",
|
|
metadata: desired.pipeline.metadata,
|
|
spec: requiredRecord(requiredRecord(actualPipelineRun.spec, "PipelineRun.spec").pipelineSpec, "PipelineRun.spec.pipelineSpec"),
|
|
};
|
|
const pipelineDrift = firstPacSourceArtifactDrift(desired.pipeline, actualPipeline);
|
|
const pipelineRunDrift = firstPacSourceArtifactDrift(desired.pipelineRun, actualPipelineRun);
|
|
const actualProvenance = sourceArtifact.mode === "remote-pipeline-annotation"
|
|
? provenanceFromManifest(actualPipeline)
|
|
: provenanceFromManifest(actualPipelineRun);
|
|
const wrapperProvenance = provenanceFromManifest(actualPipelineRun);
|
|
const provenanceAligned = provenanceEquals(actualProvenance, desired.provenance) && provenanceEquals(wrapperProvenance, desired.provenance);
|
|
const aligned = pipelineDrift === null && pipelineRunDrift === null && provenanceAligned;
|
|
return {
|
|
status: aligned ? "aligned" : "drifted",
|
|
aligned,
|
|
pipelineCanonicalSha256: canonicalSha256(requiredRecord(actualPipeline.spec, "source Pipeline.spec")),
|
|
pipelineRunCanonicalSha256: canonicalSha256(actualPipelineRun),
|
|
firstDrift: pipelineDrift ?? pipelineRunDrift ?? (provenanceAligned ? null : drift("$.metadata.annotations.provenance", desired.provenance, actualProvenance)),
|
|
provenanceAligned,
|
|
};
|
|
}
|
|
|
|
function validateSourceWorktree(input: string, binding: PacSourceArtifactBinding): string {
|
|
if (!existsSync(input) || !statSync(input).isDirectory()) throw new Error(`source worktree does not exist or is not a directory: ${input}`);
|
|
const worktree = realpathSync(input);
|
|
const top = realpathSync(git(worktree, ["rev-parse", "--show-toplevel"]));
|
|
if (top !== worktree) throw new Error(`--source-worktree must be the git worktree root; resolved ${top}`);
|
|
const remote = git(worktree, ["remote", "get-url", "origin"]);
|
|
const expectedRemote = owningSourceRemote(binding);
|
|
const expectedIdentity = remoteRepositoryIdentity(expectedRemote);
|
|
const observedIdentity = remoteRepositoryIdentity(remote);
|
|
if (expectedIdentity === null || observedIdentity === null
|
|
|| expectedIdentity.host !== observedIdentity.host
|
|
|| expectedIdentity.ownerRepo !== observedIdentity.ownerRepo) {
|
|
throw new PacSourceArtifactError("source-worktree-origin-mismatch", {
|
|
expectedUrlFingerprint: fingerprint(expectedRemote),
|
|
observedUrlFingerprint: fingerprint(remote),
|
|
});
|
|
}
|
|
safeArtifactPath(worktree, binding.consumer.sourceArtifact.pipelineRunPath);
|
|
if (binding.consumer.sourceArtifact.pipelinePath !== null) safeArtifactPath(worktree, binding.consumer.sourceArtifact.pipelinePath);
|
|
return worktree;
|
|
}
|
|
|
|
function owningSourceRemote(binding: PacSourceArtifactBinding): string {
|
|
if (binding.consumer.sourceArtifact.renderer === "agentrun-control-plane") {
|
|
return resolveAgentRunLaneTarget({ node: binding.consumer.node, lane: binding.consumer.lane }).spec.source.worktreeRemote;
|
|
}
|
|
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
|
|
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
|
|
}
|
|
|
|
function remoteRepositoryIdentity(remote: string): { readonly host: string; readonly ownerRepo: string } | null {
|
|
const trimmed = remote.trim();
|
|
const scp = /^[^/@\s]+@([^:\s]+):(.+)$/u.exec(trimmed);
|
|
let host = scp?.[1] ?? "";
|
|
let path = scp?.[2] ?? "";
|
|
if (host.length === 0 || path.length === 0) {
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
if (parsed.username.length > 0 || parsed.password.length > 0 || parsed.search.length > 0 || parsed.hash.length > 0) return null;
|
|
host = parsed.hostname;
|
|
path = parsed.pathname;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
const segments = path.replace(/^\/+|\/+$/gu, "").split("/").filter(Boolean);
|
|
if (segments.length !== 2) return null;
|
|
const owner = segments[0]?.toLowerCase();
|
|
const repo = segments[1]?.replace(/\.git$/iu, "").toLowerCase();
|
|
if (!host || !owner || !repo || !/^[a-z0-9_.-]+$/u.test(owner) || !/^[a-z0-9_.-]+$/u.test(repo)) return null;
|
|
return { host: host.toLowerCase(), ownerRepo: `${owner}/${repo}` };
|
|
}
|
|
|
|
function writeArtifactFilesAtomically(sourceWorktree: string, files: readonly { readonly path: string; readonly content: string }[]): string[] {
|
|
const pending = files.map((file) => ({ ...file, output: safeArtifactPath(sourceWorktree, file.path) }))
|
|
.filter((file) => !existsSync(file.output) || readFileSync(file.output, "utf8") !== file.content);
|
|
if (pending.length === 0) return [];
|
|
for (const file of pending) {
|
|
mkdirSync(dirname(file.output), { recursive: true });
|
|
safeArtifactPath(sourceWorktree, file.path);
|
|
}
|
|
const token = `${process.pid}-${Date.now()}`;
|
|
const staged = pending.map((file, index) => ({ ...file, temporary: `${file.output}.unidesk-${token}-${index}.tmp` }));
|
|
try {
|
|
for (const file of staged) writeFileSync(file.temporary, file.content, { flag: "wx" });
|
|
for (const file of staged) renameSync(file.temporary, file.output);
|
|
} finally {
|
|
for (const file of staged) rmSync(file.temporary, { force: true });
|
|
}
|
|
return pending.map((file) => file.path);
|
|
}
|
|
|
|
function safeArtifactPath(worktree: string, relativePath: string): string {
|
|
if (relativePath.length === 0 || isAbsolute(relativePath) || relativePath.split(/[\\/]/u).includes("..")) {
|
|
throw new Error(`source artifact path must be a non-empty worktree-relative path without ..: ${relativePath}`);
|
|
}
|
|
const root = realpathSync(worktree);
|
|
const candidate = resolve(root, relativePath);
|
|
if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) throw new Error(`source artifact path escapes worktree: ${relativePath}`);
|
|
let current = root;
|
|
for (const segment of relativePath.split(/[\\/]/u).filter(Boolean)) {
|
|
current = resolve(current, segment);
|
|
if (!existsSync(current)) continue;
|
|
if (lstatSync(current).isSymbolicLink()) throw new Error(`source artifact path crosses symlink: ${relativePath}`);
|
|
const resolved = realpathSync(current);
|
|
if (resolved !== root && !resolved.startsWith(`${root}${sep}`)) throw new Error(`source artifact path resolves outside worktree: ${relativePath}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function assertConfigRef(actual: string, expected: string): void {
|
|
if (actual !== expected) throw new Error(`sourceArtifact.configRef must equal resolved owning selector ${expected}; observed ${actual}`);
|
|
const [file, selector] = actual.split("#", 2);
|
|
if (file === undefined || selector === undefined || selector.length === 0) throw new Error(`invalid sourceArtifact.configRef: ${actual}`);
|
|
const parsed = Bun.YAML.parse(readFileSync(rootPath(...file.split("/")), "utf8")) as unknown;
|
|
let value = parsed;
|
|
for (const segment of selector.split(".")) {
|
|
if (!isRecord(value) || !Object.prototype.hasOwnProperty.call(value, segment)) throw new Error(`sourceArtifact.configRef selector does not exist: ${actual}`);
|
|
value = value[segment];
|
|
}
|
|
if (!isRecord(value)) throw new Error(`sourceArtifact.configRef must resolve to an owning YAML object: ${actual}`);
|
|
}
|
|
|
|
function assertBindingIdentity(binding: PacSourceArtifactBinding, expected: {
|
|
node: string;
|
|
lane: string;
|
|
namespace: string;
|
|
pipeline: string;
|
|
pipelineRunPrefix: string;
|
|
serviceAccount: string;
|
|
sourceBranch: string;
|
|
gitopsBranch: string;
|
|
}): void {
|
|
const observed = {
|
|
node: binding.consumer.node,
|
|
lane: binding.consumer.lane,
|
|
namespace: binding.consumer.namespace,
|
|
pipeline: binding.consumer.pipeline,
|
|
pipelineRunPrefix: binding.consumer.pipelineRunPrefix,
|
|
serviceAccount: requiredParam(binding, "service_account"),
|
|
sourceBranch: requiredParam(binding, "source_branch"),
|
|
gitopsBranch: requiredParam(binding, "gitops_branch"),
|
|
};
|
|
for (const key of Object.keys(expected) as Array<keyof typeof expected>) {
|
|
if (observed[key] !== expected[key]) throw new Error(`PaC ${binding.consumer.id} ${key}=${observed[key]} does not match owning renderer ${expected[key]}`);
|
|
}
|
|
if (requiredParam(binding, "pipeline_name") !== expected.pipeline) throw new Error(`PaC ${binding.consumer.id} repository pipeline_name does not match ${expected.pipeline}`);
|
|
if (requiredParam(binding, "pipeline_run_prefix") !== expected.pipelineRunPrefix) throw new Error(`PaC ${binding.consumer.id} repository pipeline_run_prefix does not match ${expected.pipelineRunPrefix}`);
|
|
if (requiredParam(binding, "node") !== expected.node) throw new Error(`PaC ${binding.consumer.id} repository node does not match ${expected.node}`);
|
|
}
|
|
|
|
function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacSourceArtifactBinding): void {
|
|
if (pipeline.apiVersion !== "tekton.dev/v1" || pipeline.kind !== "Pipeline") throw new Error("domain renderer must return tekton.dev/v1 Pipeline");
|
|
const metadata = requiredRecord(pipeline.metadata, "rendered Pipeline.metadata");
|
|
if (metadata.name !== binding.consumer.pipeline) throw new Error(`rendered Pipeline name ${String(metadata.name)} does not match consumer ${binding.consumer.pipeline}`);
|
|
if (metadata.namespace !== binding.consumer.namespace) throw new Error(`rendered Pipeline namespace ${String(metadata.namespace)} does not match consumer ${binding.consumer.namespace}`);
|
|
}
|
|
|
|
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
|
|
const provenance = pipelineProvenanceFromManifest(manifest);
|
|
if (provenance === null) return null;
|
|
if (provenance.renderer !== "agentrun-control-plane" && provenance.renderer !== "hwlab-runtime-lane") return null;
|
|
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
|
|
return provenance as PacSourceArtifactProvenance;
|
|
}
|
|
|
|
function provenanceEquals(actual: PacSourceArtifactProvenance | null, expected: PacSourceArtifactProvenance): boolean {
|
|
return actual !== null
|
|
&& actual.configRef === expected.configRef
|
|
&& actual.effectiveConfigSha256 === expected.effectiveConfigSha256
|
|
&& actual.renderer === expected.renderer
|
|
&& actual.mode === expected.mode;
|
|
}
|
|
|
|
function firstCanonicalDrift(expected: unknown, actual: unknown, path: string): PacSourceArtifactDrift | null {
|
|
if (Object.is(expected, actual)) return null;
|
|
if (Array.isArray(expected) || Array.isArray(actual)) {
|
|
if (!Array.isArray(expected) || !Array.isArray(actual)) return drift(path, expected, actual);
|
|
if (expected.length !== actual.length) return drift(`${path}.length`, expected.length, actual.length);
|
|
for (let index = 0; index < expected.length; index += 1) {
|
|
const child = firstCanonicalDrift(expected[index], actual[index], `${path}[${index}]`);
|
|
if (child !== null) return child;
|
|
}
|
|
return null;
|
|
}
|
|
if (isRecord(expected) || isRecord(actual)) {
|
|
if (!isRecord(expected) || !isRecord(actual)) return drift(path, expected, actual);
|
|
const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
|
|
for (const key of keys) {
|
|
const expectedOwnsKey = Object.prototype.hasOwnProperty.call(expected, key);
|
|
const actualOwnsKey = Object.prototype.hasOwnProperty.call(actual, key);
|
|
const childPath = knownDriftPathKeys.has(key) ? `${path}.${key}` : `${path}.[key:${pacValueEvidence(key)}]`;
|
|
if (!expectedOwnsKey || !actualOwnsKey) return drift(childPath, expected[key], actual[key]);
|
|
const child = firstCanonicalDrift(expected[key], actual[key], childPath);
|
|
if (child !== null) return child;
|
|
}
|
|
return null;
|
|
}
|
|
return drift(path, expected, actual);
|
|
}
|
|
|
|
function isProvenTektonAdmissionDefault(path: readonly (string | number)[], value: unknown): boolean {
|
|
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
|
|
const emptyRecord = isRecord(value) && Object.keys(value).length === 0;
|
|
const atTaskSpec = (suffix: string): boolean => [
|
|
`tasks.*.taskSpec.${suffix}`,
|
|
`spec.tasks.*.taskSpec.${suffix}`,
|
|
`spec.pipelineSpec.tasks.*.taskSpec.${suffix}`,
|
|
].includes(normalized);
|
|
if (atTaskSpec("metadata")) return emptyRecord;
|
|
if (atTaskSpec("spec")) return value === null;
|
|
if (atTaskSpec("params.*.type")) return value === "string";
|
|
if (atTaskSpec("results.*.type")) return value === "string";
|
|
if (atTaskSpec("steps.*.computeResources")) return emptyRecord;
|
|
if (atTaskSpec("sidecars.*.computeResources")) return emptyRecord;
|
|
return false;
|
|
}
|
|
|
|
function drift(path: string, expected: unknown, actual: unknown): PacSourceArtifactDrift {
|
|
return { path, expected: pacValueEvidence(expected), actual: pacValueEvidence(actual) };
|
|
}
|
|
|
|
export function pacValueEvidence(value: unknown): string {
|
|
const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
const stable = stableJsonValue(value);
|
|
const serialized = JSON.stringify(stable) ?? String(stable);
|
|
const length = typeof value === "string"
|
|
? Buffer.byteLength(value, "utf8")
|
|
: Array.isArray(value)
|
|
? value.length
|
|
: isRecord(value)
|
|
? Object.keys(value).length
|
|
: Buffer.byteLength(serialized, "utf8");
|
|
return `type=${type},length=${length},sha256=${fingerprint(serialized).replace(/^sha256:/u, "")}`;
|
|
}
|
|
|
|
function fingerprint(value: string): string {
|
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
}
|
|
|
|
function formatDrift(value: Record<string, unknown> | null): string {
|
|
return value === null ? "-" : `${text(value.path)} expected:${text(value.expected)} actual:${text(value.actual)}`;
|
|
}
|
|
|
|
function unavailableRuntimeObservation(): PacSourceArtifactRuntimeObservation {
|
|
const item: PacSourceArtifactRuntimeItem = { status: "unavailable", name: null, canonicalSha256: null, firstDrift: null, provenanceAligned: false, configRef: null, effectiveConfigSha256: null, sourceCommit: null, reason: "runtime-observer-not-configured", candidateCount: 0 };
|
|
return { live: item, embedded: item };
|
|
}
|
|
|
|
function parseYamlRecord(text: string, label: string): Record<string, unknown> {
|
|
const parsed = Bun.YAML.parse(text) as unknown;
|
|
if (!isRecord(parsed)) throw new Error(`${label} must contain one YAML object`);
|
|
return parsed;
|
|
}
|
|
|
|
export function pacSourceArtifactYaml(value: Record<string, unknown>): string {
|
|
const lines = Bun.YAML.stringify(value, null, 2).split("\n");
|
|
let blockScalarHeaderIndent: number | null = null;
|
|
const normalized = lines.map((line) => {
|
|
const content = line.trim();
|
|
const indent = line.length - line.trimStart().length;
|
|
if (blockScalarHeaderIndent !== null) {
|
|
if (content.length === 0 || indent > blockScalarHeaderIndent) return line;
|
|
blockScalarHeaderIndent = null;
|
|
}
|
|
|
|
const structuralLine = line.replace(/:[\t ]+$/u, ":");
|
|
if (/(?:^|:[\t ]+|-[\t ]+)[|>](?:[1-9][+-]?|[+-][1-9]?|[+-])?[\t ]*(?:#.*)?$/u.test(structuralLine)) {
|
|
blockScalarHeaderIndent = indent;
|
|
}
|
|
return structuralLine;
|
|
}).join("\n");
|
|
const withSingleEofNewline = `${normalized.replace(/\n+$/u, "")}\n`;
|
|
const roundTrip = parseYamlRecord(withSingleEofNewline, "serialized source artifact");
|
|
const roundTripDrift = firstCanonicalDrift(value, roundTrip, "$");
|
|
if (roundTripDrift !== null) {
|
|
throw new PacSourceArtifactError("source-artifact-yaml-round-trip-failed", {
|
|
driftPath: roundTripDrift.path,
|
|
expectedEvidence: roundTripDrift.expected,
|
|
actualEvidence: roundTripDrift.actual,
|
|
});
|
|
}
|
|
return withSingleEofNewline;
|
|
}
|
|
|
|
function requiredParam(binding: PacSourceArtifactBinding, key: string): string {
|
|
return requiredString(binding.repository.params[key], `PaC repository ${binding.repository.id} params.${key}`);
|
|
}
|
|
|
|
function requiredString(value: unknown, label: string): string {
|
|
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
|
|
return value;
|
|
}
|
|
|
|
function requiredRecord(value: unknown, label: string): Record<string, unknown> {
|
|
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
return value;
|
|
}
|
|
|
|
function arrayRecords(value: unknown, label: string): Record<string, unknown>[] {
|
|
if (!Array.isArray(value) || value.some((item) => !isRecord(item))) throw new Error(`${label} must be an array of objects`);
|
|
return value as Record<string, unknown>[];
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return isRecord(value) ? value : {};
|
|
}
|
|
|
|
function isSourceArtifactAction(value: string | undefined): value is PacSourceArtifactAction {
|
|
return value === "plan" || value === "check" || value === "write" || value === "status" || value === "verify-runtime";
|
|
}
|
|
|
|
function git(worktree: string, args: readonly string[]): string {
|
|
const result = spawnSync("git", ["-C", worktree, ...args], { encoding: "utf8", timeout: 15_000, maxBuffer: 1024 * 1024 });
|
|
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed for ${worktree}: ${(result.stderr || result.stdout).trim().slice(0, 1000)}`);
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function runChecked(command: string, args: readonly string[], cwd: string, timeout: number, label: string, input?: string): void {
|
|
const result = spawnSync(command, [...args], { cwd, encoding: "utf8", timeout, maxBuffer: 32 * 1024 * 1024, input });
|
|
if (result.status !== 0) {
|
|
const output = [result.stderr, result.stdout].find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
|
const reason = output ?? result.error?.message ?? `exit=${String(result.status)} signal=${String(result.signal)}`;
|
|
throw new Error(`${label} failed: ${reason.slice(-4000)}`);
|
|
}
|
|
}
|
|
|
|
function shellQuote(value: string): string {
|
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
}
|
|
|
|
function text(value: unknown): string {
|
|
if (value === null || value === undefined || value === "") return "-";
|
|
return String(value);
|
|
}
|
|
|
|
function shortSha(value: unknown): string {
|
|
return typeof value === "string" && value.length > 0 ? value.replace(/^sha256:/u, "").slice(0, 12) : "-";
|
|
}
|