Merge pull request #1758 from pikasTech/fix/1756-hwlab-pac-live-provenance
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 统一 HWLAB PaC Pipeline provenance 合同
This commit is contained in:
Lyon
2026-07-11 14:20:25 +08:00
committed by GitHub
15 changed files with 378 additions and 65 deletions
+4
View File
@@ -119,6 +119,10 @@ lanes:
pipelineRunPrefix: hwlab-nc01-v03-ci-poll
serviceAccountName: hwlab-nc01-v03-tekton-runner
controlPlaneFieldManager: unidesk-hwlab-nc01-v03-control-plane
pipelineProvenance:
configRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01
renderer: hwlab-runtime-lane
mode: remote-pipeline-annotation
git:
url: git@github.com:pikasTech/HWLAB.git
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
@@ -0,0 +1,6 @@
# R1.1 调查报告
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- 结论:旧实现分别在 `hwlab-node/web-probe.ts``hwlab-node/render.ts` 和 source-artifact renderer 内硬编码 provenancetarget YAML 未声明 owner 合同。
- 阻塞:source-artifact 的正常 postprocess 使用 `sh -c` 传递大脚本,触发 argv 上限后又因空输出执行 `null.trim()`;已纳入同一闭环修复。
- 边界:不放宽 verifier,不从 live runtime 反向导出,不手工修改运行面。
@@ -0,0 +1,6 @@
# R1.2 YAML 与 parser 报告
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- owner`config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01.pipelineProvenance`
- parser:target 必须显式声明合同,并校验精确 `configRef``hwlab-runtime-lane` renderer 与 `remote-pipeline-annotation` mode。
- effective config hash:由共享 helper 对已解析 target spec 做稳定摘要,避免 YAML 静态 hash 的循环依赖。
@@ -0,0 +1,7 @@
# R1.3 共享渲染报告
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- 共享合同:`scripts/src/pipeline-provenance.ts` 统一持有四个 annotation key、annotations 生成与 manifest 读取。
- 正常路径:HWLAB overlay 携带 YAML-derived annotations,正常 control-plane postprocess 使用可复用 native 脚本写入最终 Pipeline。
- source-artifact:复用同一 HWLAB contract,并要求正常 postprocess 已写入一致 provenance;不再二次硬编码或补写 Pipeline。
- 工具修复:大 postprocess 改走 stdin,失败状态不再对空输出调用 `trim()`
@@ -0,0 +1,9 @@
# R1.4 测试与 PR 报告
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- PR[UniDesk #1758](https://github.com/pikasTech/unidesk/pull/1758),非 draft;未合并、未部署。
- 定向测试:29 pass、0 fail、314 assertions。
- 正常路径测试:实际执行 control-plane provenance postprocess,最终 Pipeline 精确包含共享四字段,并保留非 provenance annotation。
- fail-closed:覆盖 target YAML 缺失、错误 owner、错误 renderer、错误 mode 与任意四键伪造。
- 只读原入口:source-artifact `plan` 成功,desired hash 为 `sha256:3fb6e00ba833...`,旧 source artifact 仍正确判为 drifted。
- 自动交付边界:未手工触发 CI/CD;合并后的交付只允许走 GitHub webhook → Gitea mirror → PaC → CI/CD。
@@ -0,0 +1,7 @@
# R1 总结报告
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- 交付:[UniDesk PR #1758](https://github.com/pikasTech/unidesk/pull/1758)。
- YAML owner、parser、正常 control-plane renderer 与 source-artifact renderer 已统一到一份 provenance 合同。
- verifier 保持 fail-closed;没有运行面手工 annotation、source write、部署或 CI/CD 手工触发。
- 剩余工作由主代理 review/preflight/merge,并在合并后观察自动交付链和运行面 alignment。
+25
View File
@@ -0,0 +1,25 @@
# HWLAB PaC 实时 Pipeline 来源合同
- 来源:[UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756)
- 范围:只修 YAML-owned provenance 合同、共享渲染与定向验证;不部署、不手工触发 CI/CD。
## R1 [completed]
修复 [UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756) 的 HWLAB PaC live Pipeline provenance 永久漂移,完成任务后将详细报告写入[任务报告](./details/hwlab-pac-live-provenance/R1_Task_Report.md)。
### R1.1 [completed]
调查 [UniDesk #1756](https://github.com/pikasTech/unidesk/issues/1756) 的 YAML owner、parser、正常 renderer 与 source-artifact 复用边界,完成任务后将详细报告写入[任务报告](./details/hwlab-pac-live-provenance/R1.1_Task_Report.md)。
### R1.2 [completed]
在 owning YAML 与 parser 定义四字段 provenance 合同,禁止代码隐藏默认值,完成任务后将详细报告写入[任务报告](./details/hwlab-pac-live-provenance/R1.2_Task_Report.md)。
### R1.3 [completed]
让正常 GitOps/control-plane renderer 与 source-artifact renderer复用共享 provenance overlay/postprocess,完成任务后将详细报告写入[任务报告](./details/hwlab-pac-live-provenance/R1.3_Task_Report.md)。
### R1.4 [completed]
补齐正常 render 精确四字段及 source-artifact/live/embedded aligned 验证,提交非 draft PR 并 preflight,完成任务后将详细报告写入[任务报告](./details/hwlab-pac-live-provenance/R1.4_Task_Report.md)。
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Responsibility: apply the YAML-owned HWLAB Pipeline provenance contract during the normal control-plane postprocess.
import { readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
const require = createRequire(`${process.cwd()}/package.json`);
const YAML = require("yaml");
export function applyHwlabPipelineProvenance(manifest, annotations, expectedKeys) {
if (!isRecord(manifest) || manifest.kind !== "Pipeline") return false;
const entries = isRecord(annotations) ? Object.entries(annotations) : [];
const actualKeys = entries.map(([key]) => key).sort();
const sortedExpectedKeys = Array.isArray(expectedKeys) ? [...expectedKeys].sort() : [];
if (JSON.stringify(actualKeys) !== JSON.stringify(sortedExpectedKeys) || entries.some(([, value]) => typeof value !== "string" || value.length === 0)) {
throw new Error("overlay.pipelineProvenanceAnnotations must match the shared four-key contract");
}
manifest.metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
manifest.metadata.annotations = { ...(isRecord(manifest.metadata.annotations) ? manifest.metadata.annotations : {}), ...annotations };
return true;
}
export function postprocessHwlabPipelineProvenance({ pipelinePath, overlay, expectedKeys }) {
const documents = YAML.parseAllDocuments(readFileSync(pipelinePath, "utf8"));
const manifests = documents.map((document) => document.toJS()).filter((document) => document !== null);
const changed = manifests.filter((manifest) => applyHwlabPipelineProvenance(manifest, overlay.pipelineProvenanceAnnotations, expectedKeys));
if (changed.length !== 1) throw new Error(`expected exactly one Pipeline in ${pipelinePath}; observed ${changed.length}`);
writeFileSync(pipelinePath, `${manifests.map((manifest) => YAML.stringify(manifest).trimEnd()).join("\n---\n")}\n`);
return { pipelinePath, pipelineName: changed[0]?.metadata?.name ?? null, annotationCount: expectedKeys.length };
}
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function requiredArgument(name) {
const index = process.argv.indexOf(name);
const value = index === -1 ? undefined : process.argv[index + 1];
if (typeof value !== "string" || value.length === 0 || value.startsWith("--")) throw new Error(`${name} is required`);
return value;
}
function decodedJsonEnv(name) {
const encoded = process.env[name];
if (typeof encoded !== "string" || encoded.length === 0) throw new Error(`${name} is required`);
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
}
if (process.argv.includes("--pipeline")) {
const result = postprocessHwlabPipelineProvenance({
pipelinePath: requiredArgument("--pipeline"),
overlay: decodedJsonEnv("UNIDESK_RUNTIME_GITOPS_OVERLAY_B64"),
expectedKeys: decodedJsonEnv("UNIDESK_PIPELINE_PROVENANCE_KEYS_B64"),
});
console.error(JSON.stringify({ event: "unidesk-runtime-pipeline-provenance", ok: true, ...result }));
}
+40 -1
View File
@@ -559,6 +559,12 @@ export interface HwlabRuntimeSourceWorkspaceHostDependenciesSpec {
};
}
export interface HwlabRuntimePipelineProvenanceSpec {
readonly configRef: string;
readonly renderer: "hwlab-runtime-lane";
readonly mode: "remote-pipeline-annotation";
}
export interface HwlabRuntimeLaneSpec {
readonly lane: HwlabRuntimeLane;
readonly nodeId: string;
@@ -578,6 +584,7 @@ export interface HwlabRuntimeLaneSpec {
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly controlPlaneFieldManager: string;
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
readonly gitUrl: string;
readonly gitReadUrl: string;
readonly gitWriteUrl: string;
@@ -647,6 +654,7 @@ interface HwlabLaneConfig {
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly controlPlaneFieldManager: string;
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
readonly git: { readonly url: string; readonly readUrl: string; readonly writeUrl: string };
readonly argo: { readonly repoURL?: string };
readonly gitopsBranch: string;
@@ -886,6 +894,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
pipelineRunPrefix: stringField(raw, "pipelineRunPrefix", `lanes.${id}`),
serviceAccountName: stringField(raw, "serviceAccountName", `lanes.${id}`),
controlPlaneFieldManager: stringField(raw, "controlPlaneFieldManager", `lanes.${id}`),
pipelineProvenance: pipelineProvenanceConfig(raw.pipelineProvenance, `lanes.${id}.pipelineProvenance`),
git: {
url: stringField(git, "url", `lanes.${id}.git`),
readUrl: stringField(git, "readUrl", `lanes.${id}.git`),
@@ -928,6 +937,8 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
}
function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<string, unknown>, targetRaw: Record<string, unknown>): HwlabLaneConfig {
const expectedConfigRef = `${HWLAB_NODE_LANE_CONFIG_PATH}#lanes.${id}.targets.${nodeId}`;
parseHwlabRuntimePipelineProvenance(targetRaw.pipelineProvenance, `lanes.${id}.targets.${nodeId}.pipelineProvenance`, expectedConfigRef);
const merged: Record<string, unknown> = {
...baseRaw,
...targetRaw,
@@ -952,7 +963,34 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
};
delete merged.targets;
delete merged.activeTarget;
return laneConfig(id, merged);
const config = laneConfig(id, merged);
return config;
}
function pipelineProvenanceConfig(value: unknown, path: string): HwlabRuntimePipelineProvenanceSpec | undefined {
if (value === undefined) return undefined;
return parseHwlabRuntimePipelineProvenance(value, path);
}
export function parseHwlabRuntimePipelineProvenance(
value: unknown,
path: string,
expectedConfigRef?: string,
): HwlabRuntimePipelineProvenanceSpec {
if (value === undefined) throw new Error(`${path} must be declared by the target owner`);
const raw = asRecord(value, path);
const configRef = stringField(raw, "configRef", path);
if (!/^config\/hwlab-node-lanes\.yaml#lanes\.[A-Za-z0-9._-]+\.targets\.[A-Za-z0-9._-]+$/u.test(configRef)) {
throw new Error(`${path}.configRef must select a target owner in ${HWLAB_NODE_LANE_CONFIG_PATH}`);
}
if (expectedConfigRef !== undefined && configRef !== expectedConfigRef) {
throw new Error(`${path}.configRef must equal ${expectedConfigRef}`);
}
return {
configRef,
renderer: enumStringField(raw, "renderer", path, ["hwlab-runtime-lane"]),
mode: enumStringField(raw, "mode", path, ["remote-pipeline-annotation"]),
};
}
function sourceAuthorityConfig(value: unknown, path: string): HwlabRuntimeSourceAuthoritySpec | undefined {
@@ -2094,6 +2132,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
pipelineRunPrefix: config.pipelineRunPrefix,
serviceAccountName: config.serviceAccountName,
controlPlaneFieldManager: config.controlPlaneFieldManager,
...(config.pipelineProvenance === undefined ? {} : { pipelineProvenance: config.pipelineProvenance }),
gitUrl: config.git.url,
gitReadUrl: config.git.readUrl,
gitWriteUrl: config.git.writeUrl,
@@ -0,0 +1,29 @@
import { type HwlabRuntimeLaneSpec, hwlabRuntimeLaneConfigPath } from "../hwlab-node-lanes";
import { pipelineProvenanceAnnotations, type PipelineProvenanceContract } from "../pipeline-provenance";
import { stableJsonSha256 } from "../stable-json";
export interface HwlabRuntimePipelineProvenance extends PipelineProvenanceContract {
readonly renderer: "hwlab-runtime-lane";
readonly mode: "remote-pipeline-annotation";
}
export function hwlabRuntimePipelineProvenance(spec: HwlabRuntimeLaneSpec): HwlabRuntimePipelineProvenance {
const declared = spec.pipelineProvenance;
if (declared === undefined) {
throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}.pipelineProvenance is required`);
}
const expectedConfigRef = `${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}`;
if (declared.configRef !== expectedConfigRef) {
throw new Error(`pipelineProvenance.configRef must equal selected target owner ${expectedConfigRef}`);
}
return {
configRef: declared.configRef,
effectiveConfigSha256: stableJsonSha256(spec),
renderer: declared.renderer,
mode: declared.mode,
};
}
export function hwlabRuntimePipelineProvenanceAnnotations(spec: HwlabRuntimeLaneSpec): Record<string, string> {
return pipelineProvenanceAnnotations(hwlabRuntimePipelineProvenance(spec));
}
+18 -5
View File
@@ -41,11 +41,13 @@ import { webObserveShort, webObserveText } from "./web-probe-observe";
import { readNodeRuntimeStatusPolicy, runNodeRuntimeStatusProbe, skippedNodeRuntimeStatusCommand, type NodeRuntimeStatusPolicy, type NodeRuntimeStatusWorkerEvent } from "./control-plane-status-observed";
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
import { nodeRuntimeDeployYamlOverlayShellScript } from "./deploy-overlay";
import { pipelineProvenanceAnnotationKeys } from "../pipeline-provenance";
const runtimeGitopsObservabilityNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-observability.mjs"), "utf8").trimEnd();
const runtimeGitopsPipelineGuardNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-pipeline-guard.mjs"), "utf8").trimEnd();
const runtimeGitopsPostprocessNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-postprocess.mjs"), "utf8").trimEnd();
const runtimeGitopsVerifyNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-verify.mjs"), "utf8").trimEnd();
const runtimePipelineProvenanceNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-pipeline-provenance.mjs"), "utf8").trimEnd();
export function nodeRuntimeGitMirrorJobName(mirror: NodeRuntimeGitMirrorTargetSpec, action: "sync" | "flush"): string {
const prefix = action === "sync" ? mirror.syncJobPrefix : mirror.flushJobPrefix;
@@ -2521,11 +2523,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" doc.metadata = doc.metadata || {};",
" if (typeof overlay.pipelineName === 'string' && overlay.pipelineName.length > 0) doc.metadata.name = overlay.pipelineName;",
" doc.metadata.annotations = doc.metadata.annotations || {};",
" const provenance = overlay.sourceArtifactProvenance || {};",
" if (provenance.configRef) doc.metadata.annotations['unidesk.ai/owning-config-ref'] = provenance.configRef;",
" if (provenance.effectiveConfigSha256) doc.metadata.annotations['unidesk.ai/effective-config-sha256'] = provenance.effectiveConfigSha256;",
" if (provenance.renderer) doc.metadata.annotations['unidesk.ai/source-artifact-renderer'] = provenance.renderer;",
" if (provenance.mode) doc.metadata.annotations['unidesk.ai/source-artifact-mode'] = provenance.mode;",
" doc.metadata.annotations['hwlab.pikastech.local/download-profile'] = overlay.downloadProfileId;",
" doc.metadata.annotations['hwlab.pikastech.local/network-profile'] = overlay.networkProfileId;",
" for (const task of doc.spec?.tasks || []) {",
@@ -2836,10 +2833,26 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"patchArgoYaml(path.join(renderDir, 'argocd', 'project.yaml'));",
"patchArgoYaml(path.join(renderDir, 'argocd', overlay.argoApplicationFile));",
"NODE",
...nodeRuntimePipelineProvenancePostprocessScript(),
...runtimeGitopsPipelineGuardScript(),
];
}
export function nodeRuntimePipelineProvenancePostprocessScript(): string[] {
const expectedKeys = Buffer.from(JSON.stringify(Object.values(pipelineProvenanceAnnotationKeys).sort()), "utf8").toString("base64");
return [
"runtime_gitops_guard_dir=\"$render_dir/.unidesk-runtime-gitops\"",
"mkdir -p \"$runtime_gitops_guard_dir\"",
...writeRuntimeGitopsNativeScript("runtime-pipeline-provenance.mjs", runtimePipelineProvenanceNativeScript, "UNIDESK_RUNTIME_PIPELINE_PROVENANCE_MJS"),
[
"UNIDESK_RUNTIME_GITOPS_OVERLAY_B64=\"$overlay_b64\"",
`UNIDESK_PIPELINE_PROVENANCE_KEYS_B64=${shellQuote(expectedKeys)}`,
"node \"$runtime_gitops_guard_dir/runtime-pipeline-provenance.mjs\"",
"--pipeline \"$render_dir/$(node -e 'const o=JSON.parse(Buffer.from(process.argv[1],\"base64\").toString(\"utf8\")); process.stdout.write(o.tektonDir)' \"$overlay_b64\")/pipeline.yaml\"",
].join(" "),
];
}
function runtimeGitopsPipelineGuardScript(): string[] {
return [
"runtime_gitops_guard_dir=\"$render_dir/.unidesk-runtime-gitops\"",
+3 -8
View File
@@ -13,7 +13,7 @@ import { runCommand, type CommandResult } from "../command";
import { startJob } from "../jobs";
import { classifySshTcpPoolFailure } from "../ssh";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneCiGitWorkspaceSecret, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
import { hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
@@ -40,7 +40,7 @@ import { assertKnownOptions, nodeWebProbeAutoCommandTimeoutSeconds, normalizeNod
import { resolveNodeWebProbeCliOrigin } from "./web-probe-origin";
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
import { resolveEgressProxySourceRef } from "../egress-proxy-sources";
import { stableJsonSha256 } from "../stable-json";
import { hwlabRuntimePipelineProvenanceAnnotations } from "./pipeline-provenance";
export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
const gitSshProxy = httpProxyEndpoint(spec.networkProfile.proxy.http);
@@ -75,12 +75,7 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
},
};
return {
sourceArtifactProvenance: {
configRef: `${hwlabRuntimeLaneConfigPath}#lanes.${spec.lane}.targets.${spec.nodeId}`,
effectiveConfigSha256: stableJsonSha256(spec),
renderer: "hwlab-runtime-lane",
mode: "remote-pipeline-annotation",
},
pipelineProvenanceAnnotations: hwlabRuntimePipelineProvenanceAnnotations(spec),
nodeId: spec.nodeId,
lane: spec.lane,
sourceBranch: spec.sourceBranch,
+53
View File
@@ -0,0 +1,53 @@
export const pipelineProvenanceAnnotationKeys = {
configRef: "unidesk.ai/owning-config-ref",
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
renderer: "unidesk.ai/source-artifact-renderer",
mode: "unidesk.ai/source-artifact-mode",
} as const;
export interface PipelineProvenanceContract {
readonly configRef: string;
readonly effectiveConfigSha256: string;
readonly renderer: string;
readonly mode: string;
}
export function pipelineProvenanceAnnotations(provenance: PipelineProvenanceContract): Record<string, string> {
return {
[pipelineProvenanceAnnotationKeys.configRef]: provenance.configRef,
[pipelineProvenanceAnnotationKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
[pipelineProvenanceAnnotationKeys.renderer]: provenance.renderer,
[pipelineProvenanceAnnotationKeys.mode]: provenance.mode,
};
}
export function withPipelineProvenanceAnnotations(
manifest: Record<string, unknown>,
provenance: PipelineProvenanceContract,
): Record<string, unknown> {
const next = structuredClone(manifest);
const metadata = asRecord(next.metadata, "manifest.metadata");
const annotations = metadata.annotations === undefined ? {} : asRecord(metadata.annotations, "manifest.metadata.annotations");
metadata.annotations = { ...annotations, ...pipelineProvenanceAnnotations(provenance) };
return next;
}
export function pipelineProvenanceFromManifest(manifest: Record<string, unknown>): PipelineProvenanceContract | null {
const metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
const configRef = annotations[pipelineProvenanceAnnotationKeys.configRef];
const effectiveConfigSha256 = annotations[pipelineProvenanceAnnotationKeys.effectiveConfigSha256];
const renderer = annotations[pipelineProvenanceAnnotationKeys.renderer];
const mode = annotations[pipelineProvenanceAnnotationKeys.mode];
if ([configRef, effectiveConfigSha256, renderer, mode].some((value) => typeof value !== "string" || value.length === 0)) return null;
return { configRef, effectiveConfigSha256, renderer, mode } as PipelineProvenanceContract;
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (!isRecord(value)) throw new Error(`${path} must be an object`);
return value;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -6,6 +6,11 @@ import { resolve } from "node:path";
import { rootPath } from "./config";
import { applyNodeRuntimeDeployYamlOverlay, nodeRuntimeDeployYamlOverlayShellScript } from "./hwlab-node/deploy-overlay";
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
import { nodeRuntimePipelineProvenancePostprocessScript } from "./hwlab-node/render";
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
import { hwlabRuntimeLaneSpecForNode, parseHwlabRuntimePipelineProvenance } from "./hwlab-node-lanes";
import { pipelineProvenanceAnnotationKeys, pipelineProvenanceAnnotations } from "./pipeline-provenance";
import {
assertPacSourceArtifactVerifyWorktree,
canonicalSha256,
@@ -206,6 +211,74 @@ describe("PaC source artifact CLI contract", () => {
});
});
describe("HWLAB YAML-owned Pipeline provenance", () => {
const ownerPath = "lanes.v03.targets.NC01.pipelineProvenance";
const configRef = "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01";
test("target owner parser fails closed for missing or inconsistent contracts", () => {
expect(() => parseHwlabRuntimePipelineProvenance(undefined, ownerPath, configRef)).toThrow("must be declared by the target owner");
expect(() => parseHwlabRuntimePipelineProvenance({
configRef: "config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01",
renderer: "hwlab-runtime-lane",
mode: "remote-pipeline-annotation",
}, ownerPath, configRef)).toThrow(`must equal ${configRef}`);
expect(() => parseHwlabRuntimePipelineProvenance({
configRef,
renderer: "another-renderer",
mode: "remote-pipeline-annotation",
}, ownerPath, configRef)).toThrow("renderer must be one of hwlab-runtime-lane");
expect(() => parseHwlabRuntimePipelineProvenance({
configRef,
renderer: "hwlab-runtime-lane",
mode: "embedded-pipeline-spec",
}, ownerPath, configRef)).toThrow("mode must be one of remote-pipeline-annotation");
});
test("normal control-plane postprocess writes exactly the shared four provenance fields", () => {
const temporary = mkdtempSync(resolve(tmpdir(), "unidesk-hwlab-pipeline-provenance-"));
try {
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
const overlay = nodeRuntimeRenderOverlay(spec);
const pipelineDir = resolve(temporary, spec.tektonDir);
const pipelinePath = resolve(pipelineDir, "pipeline.yaml");
mkdirSync(pipelineDir, { recursive: true });
writeFileSync(pipelinePath, Bun.YAML.stringify({
apiVersion: "tekton.dev/v1",
kind: "Pipeline",
metadata: { name: spec.pipeline, namespace: "hwlab-ci", annotations: { "fixture.example/keep": "true" } },
spec: { params: [], tasks: [] },
}));
const overlayBase64 = Buffer.from(JSON.stringify(overlay), "utf8").toString("base64");
const shell = [
`render_dir=${JSON.stringify(temporary)}`,
`overlay_b64=${JSON.stringify(overlayBase64)}`,
...nodeRuntimePipelineProvenancePostprocessScript(),
].join("\n");
const result = spawnSync("sh", [], { cwd: rootPath(), input: shell, encoding: "utf8", timeout: 30_000 });
expect(result.status).toBe(0);
const rendered = Bun.YAML.parse(readFileSync(pipelinePath, "utf8")) as Record<string, any>;
const annotations = rendered.metadata.annotations as Record<string, string>;
const provenanceKeySet = new Set<string>(Object.values(pipelineProvenanceAnnotationKeys));
const renderedProvenance = Object.fromEntries(Object.entries(annotations).filter(([key]) => provenanceKeySet.has(key)));
expect(renderedProvenance).toEqual(overlay.pipelineProvenanceAnnotations);
expect(Object.keys(renderedProvenance).sort()).toEqual(Object.values(pipelineProvenanceAnnotationKeys).sort());
expect(annotations["fixture.example/keep"]).toBe("true");
const invalidOverlay = { ...overlay, pipelineProvenanceAnnotations: { a: "1", b: "2", c: "3", d: "4" } };
const invalidShell = [
`render_dir=${JSON.stringify(temporary)}`,
`overlay_b64=${JSON.stringify(Buffer.from(JSON.stringify(invalidOverlay), "utf8").toString("base64"))}`,
...nodeRuntimePipelineProvenancePostprocessScript(),
].join("\n");
const invalid = spawnSync("sh", [], { cwd: rootPath(), input: invalidShell, encoding: "utf8", timeout: 30_000 });
expect(invalid.status).not.toBe(0);
expect(`${invalid.stdout}\n${invalid.stderr}`).toContain("shared four-key contract");
} finally {
rmSync(temporary, { recursive: true, force: true });
}
});
});
describe("Tekton admission canonicalization", () => {
const admitted = {
tasks: [{
@@ -381,11 +454,19 @@ describe("runtime source-commit selection", () => {
});
test("remote mode validates Pipeline spec live and wrapper on the exact-commit Run", () => {
const remoteProvenance = { ...provenance, mode: "remote-pipeline-annotation" };
const remoteAnnotations = {
...manifestAnnotations(),
"unidesk.ai/source-artifact-mode": "remote-pipeline-annotation",
};
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
const remoteProvenance = hwlabRuntimePipelineProvenance(spec);
const overlay = nodeRuntimeRenderOverlay(spec);
const remoteAnnotations = overlay.pipelineProvenanceAnnotations as Record<string, string>;
expect(spec.pipelineProvenance).toEqual({
configRef: "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01",
renderer: "hwlab-runtime-lane",
mode: "remote-pipeline-annotation",
});
expect(remoteAnnotations).toEqual(pipelineProvenanceAnnotations(remoteProvenance));
expect(Object.keys(remoteAnnotations).sort()).toEqual(Object.values(pipelineProvenanceAnnotationKeys).sort());
expect(readFileSync(rootPath("scripts", "src", "hwlab-node", "render.ts"), "utf8")).not.toContain("overlay.sourceArtifactProvenance");
expect(readFileSync(rootPath("scripts", "src", "platform-infra-pipelines-as-code-source-artifact.ts"), "utf8")).not.toContain('"unidesk.ai/owning-config-ref"');
const wrapper = {
mode: "remote-pipeline-annotation",
executionMode: "pipeline-spec",
@@ -411,7 +492,9 @@ describe("runtime source-commit selection", () => {
? { kind: "ok", value: { metadata: { name: input.pipeline, annotations: remoteAnnotations }, spec: desiredSpec } }
: { kind: "ok", value: { items: [run] } });
expect(observed.live.status).toBe("aligned");
expect(observed.live.provenanceAligned).toBe(true);
expect(observed.embedded.status).toBe("aligned");
expect(observed.embedded.provenanceAligned).toBe(true);
expect(observed.embedded.firstDrift).toBeNull();
});
@@ -11,10 +11,12 @@ import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneS
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";
@@ -167,13 +169,6 @@ export function pacSourceArtifactSafeError(error: unknown): PacSourceArtifactSaf
};
}
const provenanceKeys = {
configRef: "unidesk.ai/owning-config-ref",
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
renderer: "unidesk.ai/source-artifact-renderer",
mode: "unidesk.ai/source-artifact-mode",
} as const;
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",
@@ -442,7 +437,12 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
const rendered = sourceArtifact.renderer === "agentrun-control-plane"
? renderAgentRunDesiredPipeline(binding)
: renderHwlabDesiredPipeline(binding, sourceWorktree);
const pipeline = withPipelineProvenance(rendered.pipeline, rendered.provenance);
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"
@@ -485,8 +485,11 @@ function renderAgentRunDesiredPipeline(binding: PacSourceArtifactBinding): { pip
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 expectedConfigRef = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}`;
assertConfigRef(binding.consumer.sourceArtifact.configRef, expectedConfigRef);
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,
@@ -500,12 +503,7 @@ function renderHwlabDesiredPipeline(binding: PacSourceArtifactBinding, sourceWor
const pipeline = renderHwlabPipelineFromOwningYaml(spec, sourceWorktree);
return {
pipeline,
provenance: {
configRef: expectedConfigRef,
effectiveConfigSha256: stableJsonSha256(spec),
renderer: "hwlab-runtime-lane",
mode: binding.consumer.sourceArtifact.mode,
},
provenance,
};
}
@@ -547,7 +545,7 @@ function renderHwlabPipelineFromOwningYaml(spec: HwlabRuntimeLaneSpec, sourceWor
`overlay_b64=${shellQuote(overlay)}`,
...nodeRuntimePipelinePostprocessScript(),
].join("\n");
runChecked("sh", ["-c", postprocess], temporarySource, 120_000, "HWLAB UniDesk domain postprocess/verify renderer");
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);
@@ -637,10 +635,7 @@ function pipelineRunAnnotations(binding: PacSourceArtifactBinding, provenance: P
"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),
[provenanceKeys.configRef]: provenance.configRef,
[provenanceKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
[provenanceKeys.renderer]: provenance.renderer,
[provenanceKeys.mode]: binding.consumer.sourceArtifact.mode,
...pipelineProvenanceAnnotations(provenance),
};
}
@@ -745,20 +740,6 @@ function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: R
});
}
function withPipelineProvenance(pipeline: Record<string, unknown>, provenance: PacSourceArtifactProvenance): Record<string, unknown> {
const next = structuredClone(pipeline);
const metadata = requiredRecord(next.metadata, "rendered Pipeline.metadata");
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
metadata.annotations = {
...annotations,
[provenanceKeys.configRef]: provenance.configRef,
[provenanceKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
[provenanceKeys.renderer]: provenance.renderer,
[provenanceKeys.mode]: provenance.mode,
};
return next;
}
function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifactBinding, desired: DesiredArtifact): SourceInspection {
const sourceArtifact = binding.consumer.sourceArtifact;
const pipelineRunPath = safeArtifactPath(sourceWorktree, sourceArtifact.pipelineRunPath);
@@ -932,14 +913,11 @@ function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacS
}
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
const metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
const configRef = annotations[provenanceKeys.configRef];
const effectiveConfigSha256 = annotations[provenanceKeys.effectiveConfigSha256];
const renderer = annotations[provenanceKeys.renderer];
const mode = annotations[provenanceKeys.mode];
if (typeof configRef !== "string" || typeof effectiveConfigSha256 !== "string" || (renderer !== "agentrun-control-plane" && renderer !== "hwlab-runtime-lane") || (mode !== "embedded-pipeline-spec" && mode !== "remote-pipeline-annotation")) return null;
return { configRef, effectiveConfigSha256, renderer, mode };
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 {
@@ -1098,9 +1076,13 @@ function git(worktree: string, args: readonly string[]): string {
return result.stdout.trim();
}
function runChecked(command: string, args: readonly string[], cwd: string, timeout: number, label: string): void {
const result = spawnSync(command, [...args], { cwd, encoding: "utf8", timeout, maxBuffer: 32 * 1024 * 1024 });
if (result.status !== 0) throw new Error(`${label} failed: ${(result.stderr || result.stdout).trim().slice(-4000)}`);
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 {