@kungfu-tech/buildchain 2.12.4 → 2.12.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/buildchain.mjs +3 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +2008 -12
- package/dist/site/buildchain-site.json +74 -11
- package/dist/site/capability-registry.json +4 -3
- package/dist/site/controller-registry.json +1626 -0
- package/dist/site/kfd-claims.json +88 -74
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +18 -2
- package/dist/site/node-api-registry.json +20 -7
- package/dist/site/page-registry.json +58 -5
- package/dist/site/public-surface-audit.json +572 -102
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +15 -6
- package/dist/site/workflow-registry.json +534 -68
- package/docs/MAP.md +2 -1
- package/docs/controller-evidence.md +113 -0
- package/docs/release-passport.md +6 -0
- package/package.json +3 -1
- package/packages/core/buildchain-contract.js +62 -1
- package/packages/core/buildchain-kfd-claims.js +3 -0
- package/packages/core/controller-evidence.js +505 -0
- package/packages/core/index.js +17 -0
- package/packages/core/public-surface-audit.js +40 -3
- package/packages/core/release-candidate.js +26 -0
- package/packages/core/release-passport.js +19 -0
- package/scripts/check-inventory.mjs +4 -1
- package/scripts/controller-evidence.mjs +170 -0
- package/scripts/generate-channel-build-workflow.mjs +141 -2
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +11 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./kfd-gate.js";
|
|
17
17
|
import { createSurfaceTimestampPolicy } from "./surface-manifest.js";
|
|
18
18
|
import { validatePublishEvidence as validateTransactionPublishEvidence } from "./publish-transaction.js";
|
|
19
|
+
import { normalizeControllerReceiptReferences } from "./controller-evidence.js";
|
|
19
20
|
|
|
20
21
|
export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
|
|
21
22
|
export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
|
|
@@ -989,6 +990,8 @@ export function createReleasePassport({
|
|
|
989
990
|
kfd1 = undefined,
|
|
990
991
|
kfd2Claims = [],
|
|
991
992
|
kfd3 = undefined,
|
|
993
|
+
controllerReceipts = [],
|
|
994
|
+
controllerReceiptReferences = [],
|
|
992
995
|
} = {}) {
|
|
993
996
|
const normalizedTag = nonEmptyString(tag, "tag");
|
|
994
997
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
|
|
@@ -1016,6 +1019,18 @@ export function createReleasePassport({
|
|
|
1016
1019
|
kfd1Section: normalizedKfd1?.passportSection,
|
|
1017
1020
|
kfd3Section: normalizedKfd3?.passportSection,
|
|
1018
1021
|
});
|
|
1022
|
+
const builtSourceSha = optionalString(release.builtSourceSha || release.built_source_sha);
|
|
1023
|
+
const promotionChannelSha = optionalString(release.promotionChannelSha || release.promotion_channel_sha);
|
|
1024
|
+
const treeEquivalent = release.treeEquivalent === true;
|
|
1025
|
+
const normalizedControllerReceipts = normalizeControllerReceiptReferences({
|
|
1026
|
+
receipts: controllerReceipts,
|
|
1027
|
+
references: controllerReceiptReferences,
|
|
1028
|
+
expectedSourceSha: sourceSha,
|
|
1029
|
+
acceptedSourceShas: treeEquivalent && promotionChannelSha === sourceSha && builtSourceSha
|
|
1030
|
+
? [builtSourceSha]
|
|
1031
|
+
: [],
|
|
1032
|
+
requirePassed: true,
|
|
1033
|
+
});
|
|
1019
1034
|
const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
|
|
1020
1035
|
const normalizedPublishSummary = normalizePublishSummary({
|
|
1021
1036
|
packageSet: normalizedPackageSet,
|
|
@@ -1069,6 +1084,7 @@ export function createReleasePassport({
|
|
|
1069
1084
|
"artifact-evidence.json",
|
|
1070
1085
|
"publish evidence",
|
|
1071
1086
|
"release-state transaction",
|
|
1087
|
+
"controller receipt references",
|
|
1072
1088
|
],
|
|
1073
1089
|
timestampFields: ["generatedAt", "publishedAt", "surfaceTimestampPolicy.generatedAt", "surfaceTimestampPolicy.publishedAt"],
|
|
1074
1090
|
timestampFieldsParticipateInArtifactDigest: true,
|
|
@@ -1138,6 +1154,7 @@ export function createReleasePassport({
|
|
|
1138
1154
|
...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
|
|
1139
1155
|
...(normalizedKfd2 ? { "kfd-2": normalizedKfd2 } : {}),
|
|
1140
1156
|
...(normalizedKfd3 ? { [normalizedKfd3.key || "kfd-3"]: normalizedKfd3.passportSection } : {}),
|
|
1157
|
+
...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
|
|
1141
1158
|
versionImpact: normalizedImpact.versionImpact,
|
|
1142
1159
|
surfaceImpacts: normalizedImpact.surfaceImpacts,
|
|
1143
1160
|
artifacts: [
|
|
@@ -1218,6 +1235,7 @@ export function collectGitHubReleasePassport({
|
|
|
1218
1235
|
kfd3PrebuildWitnessJsons = [],
|
|
1219
1236
|
kfd3ArtifactWitnessJsons = [],
|
|
1220
1237
|
kfd3ArtifactVerifyCommand = "",
|
|
1238
|
+
controllerReceiptReferences = [],
|
|
1221
1239
|
basePassportJson = "",
|
|
1222
1240
|
requireBaseKfd = false,
|
|
1223
1241
|
releaseJsonExtra = "",
|
|
@@ -1340,6 +1358,7 @@ export function collectGitHubReleasePassport({
|
|
|
1340
1358
|
kfd1,
|
|
1341
1359
|
kfd2Claims: kfd2ClaimMetas.map((meta) => meta.value),
|
|
1342
1360
|
kfd3,
|
|
1361
|
+
controllerReceiptReferences,
|
|
1343
1362
|
publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
|
|
1344
1363
|
transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
|
|
1345
1364
|
workflow,
|
|
@@ -234,6 +234,9 @@ if (rootPackage.exports?.["./homebrew"] !== "./packages/core/homebrew.js") {
|
|
|
234
234
|
if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildchain-contract.js") {
|
|
235
235
|
throw new Error("root package must export @kungfu-tech/buildchain/buildchain-contract");
|
|
236
236
|
}
|
|
237
|
+
if (rootPackage.exports?.["./controller-evidence"] !== "./packages/core/controller-evidence.js") {
|
|
238
|
+
throw new Error("root package must export @kungfu-tech/buildchain/controller-evidence");
|
|
239
|
+
}
|
|
237
240
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
238
241
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
239
242
|
}
|
|
@@ -1040,7 +1043,7 @@ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-rele
|
|
|
1040
1043
|
throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
|
|
1041
1044
|
}
|
|
1042
1045
|
|
|
1043
|
-
for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
|
|
1046
|
+
for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
|
|
1044
1047
|
if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
|
|
1045
1048
|
throw new Error(`site bundle missing ${siteFile}`);
|
|
1046
1049
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
createControllerPlan,
|
|
8
|
+
createControllerReceipt,
|
|
9
|
+
validateControllerPlan,
|
|
10
|
+
validateControllerReceipt,
|
|
11
|
+
} from "../packages/core/controller-evidence.js";
|
|
12
|
+
|
|
13
|
+
function argument(name, fallback = "") {
|
|
14
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
15
|
+
return index === -1 ? fallback : process.argv[index + 1] || "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function env(name, fallback = "") {
|
|
19
|
+
return String(process.env[name] ?? fallback).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(filePath, label = filePath) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`could not read ${label}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseJson(value, label, fallback = undefined) {
|
|
31
|
+
if (!String(value || "").trim() && fallback !== undefined) return fallback;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(value);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`${label} must be valid JSON: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeJson(filePath, value) {
|
|
40
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
41
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeOutputs(outputs) {
|
|
45
|
+
const outputPath = env("GITHUB_OUTPUT");
|
|
46
|
+
if (!outputPath) return;
|
|
47
|
+
fs.appendFileSync(outputPath, Object.entries(outputs).map(([name, value]) => `${name}=${String(value ?? "")}\n`).join(""));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sha256File(filePath) {
|
|
51
|
+
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function descriptorFromRegistry(registry, controllerId) {
|
|
55
|
+
const descriptor = registry.controllers?.find((entry) => entry.id === controllerId);
|
|
56
|
+
if (!descriptor) throw new Error(`controller registry does not declare ${controllerId}`);
|
|
57
|
+
return descriptor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeStageStatus(value) {
|
|
61
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
62
|
+
return {
|
|
63
|
+
success: "passed",
|
|
64
|
+
failure: "failed",
|
|
65
|
+
cancelled: "cancelled",
|
|
66
|
+
skipped: "skipped",
|
|
67
|
+
}[normalized] || normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectEvidence() {
|
|
71
|
+
const inline = parseJson(env("BUILDCHAIN_CONTROLLER_EVIDENCE_JSON", "[]"), "controller evidence JSON", []);
|
|
72
|
+
const files = parseJson(env("BUILDCHAIN_CONTROLLER_EVIDENCE_FILES_JSON", "[]"), "controller evidence files JSON", []);
|
|
73
|
+
if (!Array.isArray(inline) || !Array.isArray(files)) throw new Error("controller evidence inputs must be arrays");
|
|
74
|
+
return [
|
|
75
|
+
...inline,
|
|
76
|
+
...files.map((entry, index) => {
|
|
77
|
+
const filePath = path.resolve(String(entry.path || ""));
|
|
78
|
+
if (!entry.kind || !entry.path) throw new Error(`controller evidence file ${index} requires kind and path`);
|
|
79
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) throw new Error(`controller evidence file is missing: ${entry.path}`);
|
|
80
|
+
return {
|
|
81
|
+
kind: String(entry.kind),
|
|
82
|
+
digest: sha256File(filePath),
|
|
83
|
+
...(entry.artifact ? { artifact: String(entry.artifact) } : {}),
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function planMode() {
|
|
90
|
+
const registryPath = path.resolve(env("BUILDCHAIN_CONTROLLER_REGISTRY", ".buildchain/runtime/dist/site/controller-registry.json"));
|
|
91
|
+
const registry = readJson(registryPath, "controller registry");
|
|
92
|
+
const descriptor = descriptorFromRegistry(registry, env("BUILDCHAIN_CONTROLLER_ID"));
|
|
93
|
+
const plan = createControllerPlan({
|
|
94
|
+
descriptor,
|
|
95
|
+
source: {
|
|
96
|
+
repository: env("BUILDCHAIN_CONTROLLER_SOURCE_REPOSITORY"),
|
|
97
|
+
sha: env("BUILDCHAIN_CONTROLLER_SOURCE_SHA"),
|
|
98
|
+
},
|
|
99
|
+
runtime: {
|
|
100
|
+
ref: env("BUILDCHAIN_CONTROLLER_RUNTIME_REF"),
|
|
101
|
+
sha: env("BUILDCHAIN_CONTROLLER_RUNTIME_SHA"),
|
|
102
|
+
contractDigest: env("BUILDCHAIN_CONTROLLER_CONTRACT_DIGEST"),
|
|
103
|
+
},
|
|
104
|
+
inputs: parseJson(env("BUILDCHAIN_CONTROLLER_INPUTS_JSON", "{}"), "controller inputs JSON", {}),
|
|
105
|
+
});
|
|
106
|
+
const outputPath = path.resolve(env("BUILDCHAIN_CONTROLLER_PLAN_PATH", ".buildchain/controller/plan.json"));
|
|
107
|
+
writeJson(outputPath, plan);
|
|
108
|
+
writeOutputs({
|
|
109
|
+
"controller-plan-path": outputPath,
|
|
110
|
+
"controller-plan-json": JSON.stringify(plan),
|
|
111
|
+
"controller-plan-digest": plan.digest,
|
|
112
|
+
});
|
|
113
|
+
return plan;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function receiptMode() {
|
|
117
|
+
const planPath = path.resolve(env("BUILDCHAIN_CONTROLLER_PLAN_PATH", ".buildchain/controller/plan.json"));
|
|
118
|
+
const stages = parseJson(env("BUILDCHAIN_CONTROLLER_STAGES_JSON", "[]"), "controller stages JSON", []);
|
|
119
|
+
if (!Array.isArray(stages)) throw new Error("controller stages JSON must be an array");
|
|
120
|
+
const receipt = createControllerReceipt({
|
|
121
|
+
plan: readJson(planPath, "controller plan"),
|
|
122
|
+
stages: stages.map((stage) => ({ ...stage, status: normalizeStageStatus(stage.status) })),
|
|
123
|
+
evidence: collectEvidence(),
|
|
124
|
+
reason: env("BUILDCHAIN_CONTROLLER_REASON_CODE")
|
|
125
|
+
? {
|
|
126
|
+
code: env("BUILDCHAIN_CONTROLLER_REASON_CODE"),
|
|
127
|
+
summary: env("BUILDCHAIN_CONTROLLER_REASON_SUMMARY", "controller did not pass"),
|
|
128
|
+
}
|
|
129
|
+
: undefined,
|
|
130
|
+
artifact: env("BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT"),
|
|
131
|
+
});
|
|
132
|
+
const outputPath = path.resolve(env("BUILDCHAIN_CONTROLLER_RECEIPT_PATH", ".buildchain/controller/receipt.json"));
|
|
133
|
+
writeJson(outputPath, receipt);
|
|
134
|
+
writeOutputs({
|
|
135
|
+
"controller-receipt-path": outputPath,
|
|
136
|
+
"controller-receipt-json": JSON.stringify(receipt),
|
|
137
|
+
"controller-receipt-digest": receipt.digest,
|
|
138
|
+
"controller-receipt-status": receipt.status,
|
|
139
|
+
"controller-receipt-qualifying": String(receipt.qualifying),
|
|
140
|
+
});
|
|
141
|
+
return receipt;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateMode() {
|
|
145
|
+
const kind = env("BUILDCHAIN_CONTROLLER_EVIDENCE_KIND", argument("kind", "receipt"));
|
|
146
|
+
const filePath = path.resolve(env("BUILDCHAIN_CONTROLLER_EVIDENCE_PATH", argument("file")));
|
|
147
|
+
const value = readJson(filePath, `controller ${kind}`);
|
|
148
|
+
const validation = kind === "plan" ? validateControllerPlan(value) : validateControllerReceipt(value);
|
|
149
|
+
if (!validation.ok || (process.argv.includes("--require-qualifying") && !validation.qualifying)) {
|
|
150
|
+
throw new Error(`controller ${kind} validation failed: ${validation.issues.join("; ") || "not qualifying"}`);
|
|
151
|
+
}
|
|
152
|
+
return validation;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function controllerEvidenceCli() {
|
|
156
|
+
const mode = argument("mode", env("BUILDCHAIN_CONTROLLER_MODE", "plan"));
|
|
157
|
+
if (mode === "plan") return planMode();
|
|
158
|
+
if (mode === "receipt") return receiptMode();
|
|
159
|
+
if (mode === "validate") return validateMode();
|
|
160
|
+
throw new Error(`unsupported controller evidence mode: ${mode}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
164
|
+
try {
|
|
165
|
+
controllerEvidenceCli();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -45,7 +45,16 @@ function routerInputs(inputBlock) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function routerOutputs(outputBlock) {
|
|
48
|
-
const forwarded = outputBlock
|
|
48
|
+
const forwarded = outputBlock
|
|
49
|
+
.replace(/^ value: \$\{\{ jobs\.[^.]+\.outputs\.([^ }]+) \}\}$/gm, " value: ${{ jobs.build.outputs.$1 }}")
|
|
50
|
+
.replaceAll("build-lifecycle", "build-channel-router")
|
|
51
|
+
.replace("value: ${{ jobs.build.outputs.controller-plan-artifact }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-artifact }}")
|
|
52
|
+
.replace("value: ${{ jobs.build.outputs.controller-plan-json }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-json }}")
|
|
53
|
+
.replace("value: ${{ jobs.build.outputs.controller-plan-digest }}", "value: ${{ jobs.controller-plan.outputs.controller-plan-digest }}")
|
|
54
|
+
.replace("value: ${{ jobs.build.outputs.controller-receipt-artifact }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-artifact }}")
|
|
55
|
+
.replace("value: ${{ jobs.build.outputs.controller-receipt-json }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-json }}")
|
|
56
|
+
.replace("value: ${{ jobs.build.outputs.controller-receipt-digest }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-digest }}")
|
|
57
|
+
.replace("value: ${{ jobs.build.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}");
|
|
49
58
|
return [
|
|
50
59
|
" buildchain-channel:",
|
|
51
60
|
' description: "Resolved Buildchain channel: alpha, stable, or override"',
|
|
@@ -72,7 +81,127 @@ function forwardedInputs(names) {
|
|
|
72
81
|
.join("\n");
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
|
|
84
|
+
function routerControllerPlanJob() {
|
|
85
|
+
return ` controller-plan:
|
|
86
|
+
name: Plan channel router controller evidence
|
|
87
|
+
needs: resolve-channel
|
|
88
|
+
runs-on: ubuntu-24.04
|
|
89
|
+
outputs:
|
|
90
|
+
controller-plan-artifact: \${{ steps.names.outputs.controller-plan-artifact }}
|
|
91
|
+
controller-plan-json: \${{ steps.plan.outputs.controller-plan-json }}
|
|
92
|
+
controller-plan-digest: \${{ steps.plan.outputs.controller-plan-digest }}
|
|
93
|
+
runtime-sha: \${{ steps.identities.outputs.runtime-sha }}
|
|
94
|
+
steps:
|
|
95
|
+
- name: Checkout selected Buildchain runtime
|
|
96
|
+
uses: actions/checkout@v7.0.0
|
|
97
|
+
with:
|
|
98
|
+
repository: \${{ inputs.buildchain-repository }}
|
|
99
|
+
ref: \${{ needs.resolve-channel.outputs.buildchain-ref }}
|
|
100
|
+
path: .buildchain/runtime
|
|
101
|
+
persist-credentials: false
|
|
102
|
+
|
|
103
|
+
- name: Resolve selected runtime identity
|
|
104
|
+
id: identities
|
|
105
|
+
shell: bash
|
|
106
|
+
run: |
|
|
107
|
+
runtime_sha="$(git -C .buildchain/runtime rev-parse HEAD)"
|
|
108
|
+
contract_digest="$(node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(".buildchain/runtime/dist/site/buildchain-contract.json","utf8")); process.stdout.write(value.contractDigest)')"
|
|
109
|
+
{
|
|
110
|
+
echo "runtime-sha=\${runtime_sha}"
|
|
111
|
+
echo "contract-digest=\${contract_digest}"
|
|
112
|
+
} >> "$GITHUB_OUTPUT"
|
|
113
|
+
|
|
114
|
+
- name: Create channel router controller plan
|
|
115
|
+
id: plan
|
|
116
|
+
run: node .buildchain/runtime/scripts/controller-evidence.mjs --mode plan
|
|
117
|
+
env:
|
|
118
|
+
BUILDCHAIN_CONTROLLER_ID: build-channel-router
|
|
119
|
+
BUILDCHAIN_CONTROLLER_SOURCE_REPOSITORY: \${{ github.repository }}
|
|
120
|
+
BUILDCHAIN_CONTROLLER_SOURCE_SHA: \${{ github.sha }}
|
|
121
|
+
BUILDCHAIN_CONTROLLER_RUNTIME_REF: \${{ needs.resolve-channel.outputs.buildchain-ref }}
|
|
122
|
+
BUILDCHAIN_CONTROLLER_RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
|
|
123
|
+
BUILDCHAIN_CONTROLLER_CONTRACT_DIGEST: \${{ steps.identities.outputs.contract-digest }}
|
|
124
|
+
BUILDCHAIN_CONTROLLER_INPUTS_JSON: \${{ toJSON(inputs) }}
|
|
125
|
+
BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
|
|
126
|
+
|
|
127
|
+
- name: Resolve channel router controller plan artifact
|
|
128
|
+
id: names
|
|
129
|
+
run: echo "controller-plan-artifact=buildchain-channel-controller-plan-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
|
|
130
|
+
env:
|
|
131
|
+
SOURCE_SHA: \${{ github.sha }}
|
|
132
|
+
|
|
133
|
+
- name: Upload channel router controller plan
|
|
134
|
+
uses: actions/upload-artifact@v7.0.1
|
|
135
|
+
with:
|
|
136
|
+
name: \${{ steps.names.outputs.controller-plan-artifact }}
|
|
137
|
+
path: .buildchain/controller/plan.json
|
|
138
|
+
if-no-files-found: error`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function routerControllerReceiptJob() {
|
|
142
|
+
return ` controller-receipt:
|
|
143
|
+
name: Finalize channel router controller evidence
|
|
144
|
+
needs:
|
|
145
|
+
- resolve-channel
|
|
146
|
+
- controller-plan
|
|
147
|
+
- build
|
|
148
|
+
if: \${{ always() && needs.controller-plan.result == 'success' }}
|
|
149
|
+
runs-on: ubuntu-24.04
|
|
150
|
+
outputs:
|
|
151
|
+
controller-receipt-artifact: \${{ steps.names.outputs.controller-receipt-artifact }}
|
|
152
|
+
controller-receipt-json: \${{ steps.receipt.outputs.controller-receipt-json }}
|
|
153
|
+
controller-receipt-digest: \${{ steps.receipt.outputs.controller-receipt-digest }}
|
|
154
|
+
controller-receipt-status: \${{ steps.receipt.outputs.controller-receipt-status }}
|
|
155
|
+
steps:
|
|
156
|
+
- name: Checkout selected Buildchain runtime
|
|
157
|
+
uses: actions/checkout@v7.0.0
|
|
158
|
+
with:
|
|
159
|
+
repository: \${{ inputs.buildchain-repository }}
|
|
160
|
+
ref: \${{ needs.controller-plan.outputs.runtime-sha }}
|
|
161
|
+
path: .buildchain/runtime
|
|
162
|
+
persist-credentials: false
|
|
163
|
+
|
|
164
|
+
- name: Download channel router controller plan
|
|
165
|
+
uses: actions/download-artifact@v7.0.0
|
|
166
|
+
with:
|
|
167
|
+
name: \${{ needs.controller-plan.outputs.controller-plan-artifact }}
|
|
168
|
+
path: .buildchain/controller
|
|
169
|
+
|
|
170
|
+
- name: Create channel router controller receipt
|
|
171
|
+
id: receipt
|
|
172
|
+
run: node .buildchain/runtime/scripts/controller-evidence.mjs --mode receipt
|
|
173
|
+
env:
|
|
174
|
+
BUILDCHAIN_CONTROLLER_PLAN_PATH: .buildchain/controller/plan.json
|
|
175
|
+
BUILDCHAIN_CONTROLLER_STAGES_JSON: >-
|
|
176
|
+
[{"id":"resolve-channel","status":"\${{ needs.resolve-channel.result }}"},{"id":"build","status":"\${{ needs.build.result }}"},{"id":"aggregate","status":"\${{ needs.build.result }}"}]
|
|
177
|
+
BUILDCHAIN_CONTROLLER_EVIDENCE_JSON: \${{ needs.build.outputs.controller-receipt-digest != '' && format('[{{"kind":"nested-controller-receipt","digest":"{0}"}}]', needs.build.outputs.controller-receipt-digest) || '[]' }}
|
|
178
|
+
BUILDCHAIN_CONTROLLER_REASON_CODE: \${{ needs.build.result == 'success' && '' || 'nested-build-incomplete' }}
|
|
179
|
+
BUILDCHAIN_CONTROLLER_REASON_SUMMARY: Nested build controller did not complete successfully
|
|
180
|
+
BUILDCHAIN_CONTROLLER_RECEIPT_ARTIFACT: buildchain-channel-controller-receipt-\${{ github.sha }}
|
|
181
|
+
BUILDCHAIN_CONTROLLER_RECEIPT_PATH: .buildchain/controller/receipt.json
|
|
182
|
+
|
|
183
|
+
- name: Resolve channel router controller receipt artifact
|
|
184
|
+
id: names
|
|
185
|
+
run: echo "controller-receipt-artifact=buildchain-channel-controller-receipt-\${SOURCE_SHA}" >> "$GITHUB_OUTPUT"
|
|
186
|
+
env:
|
|
187
|
+
SOURCE_SHA: \${{ github.sha }}
|
|
188
|
+
|
|
189
|
+
- name: Upload channel router controller receipt
|
|
190
|
+
if: \${{ always() && steps.receipt.outcome == 'success' }}
|
|
191
|
+
uses: actions/upload-artifact@v7.0.1
|
|
192
|
+
with:
|
|
193
|
+
name: \${{ steps.names.outputs.controller-receipt-artifact }}
|
|
194
|
+
path: .buildchain/controller/receipt.json
|
|
195
|
+
if-no-files-found: error
|
|
196
|
+
|
|
197
|
+
- name: Enforce qualifying channel router controller receipt
|
|
198
|
+
if: \${{ steps.receipt.outputs.controller-receipt-qualifying != 'true' }}
|
|
199
|
+
run: |
|
|
200
|
+
echo "::error::channel router controller receipt is not qualifying: \${{ steps.receipt.outputs.controller-receipt-status }}"
|
|
201
|
+
exit 1`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function generateChannelBuildWorkflowBase(source) {
|
|
76
205
|
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
77
206
|
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
78
207
|
const outputs = blockBetween(source, " outputs:\n", "\npermissions:\n");
|
|
@@ -83,6 +212,16 @@ export function generateChannelBuildWorkflow(source) {
|
|
|
83
212
|
return `# Generated by scripts/generate-channel-build-workflow.mjs. Do not edit directly.\nname: Buildchain Channel Build\n\non:\n workflow_call:\n inputs:\n${routerInputs(inputs)}\n secrets:\n${secrets.trimEnd()}\n outputs:\n${routerOutputs(outputs)}\n\npermissions:\n contents: read\n issues: write\n id-token: write\n\njobs:\n resolve-channel:\n name: Resolve Buildchain channel\n runs-on: ubuntu-24.04\n outputs:\n channel: \${{ steps.channel.outputs.channel }}\n buildchain-ref: \${{ steps.channel.outputs.buildchain-ref }}\n contract-lock-path: \${{ steps.lock.outputs.path }}\n selection-source: \${{ steps.channel.outputs.selection-source }}\n reason: \${{ steps.channel.outputs.reason }}\n steps:\n - name: Resolve router source\n id: router\n shell: bash\n env:\n BUILDCHAIN_ROUTER_WORKFLOW_REF: \${{ job.workflow_ref }}\n run: |\n set -euo pipefail\n workflow_ref=\"\${BUILDCHAIN_ROUTER_WORKFLOW_REF}\"\n repository=\"\${workflow_ref%%/.github/workflows/*}\"\n ref=\"\${workflow_ref##*@}\"\n ref=\"\${ref#refs/heads/}\"\n ref=\"\${ref#refs/tags/}\"\n if [[ -z \"\${repository}\" || \"\${repository}\" = \"\${workflow_ref}\" || -z \"\${ref}\" ]]; then\n echo \"Unable to resolve Buildchain router source from job.workflow_ref=\${workflow_ref}\" >&2\n exit 1\n fi\n {\n echo \"repository=\${repository}\"\n echo \"ref=\${ref}\"\n } >> \"\${GITHUB_OUTPUT}\"\n\n - name: Checkout Buildchain router\n uses: actions/checkout@v7.0.0\n with:\n repository: \${{ steps.router.outputs.repository }}\n ref: \${{ steps.router.outputs.ref }}\n path: .buildchain/router\n persist-credentials: false\n\n - name: Resolve channel\n id: channel\n shell: bash\n env:\n BUILDCHAIN_CHANNEL: \${{ inputs.buildchain-channel }}\n BUILDCHAIN_REQUESTED_REF: \${{ inputs.buildchain-ref }}\n BUILDCHAIN_PUBLISH_CHANNEL: \${{ inputs.publish-channel }}\n BUILDCHAIN_EVENT_NAME: \${{ github.event_name }}\n BUILDCHAIN_GIT_REF: \${{ github.ref }}\n BUILDCHAIN_RELEASE_PRERELEASE: \${{ github.event.release.prerelease }}\n BUILDCHAIN_ROUTER_REF: \${{ steps.router.outputs.ref }}\n run: |\n node .buildchain/router/scripts/buildchain-channel-router.mjs \\\n --cwd .buildchain/router \\\n --channel \"\${BUILDCHAIN_CHANNEL}\" \\\n --buildchain-ref \"\${BUILDCHAIN_REQUESTED_REF}\" \\\n --publish-channel \"\${BUILDCHAIN_PUBLISH_CHANNEL}\" \\\n --event-name \"\${BUILDCHAIN_EVENT_NAME}\" \\\n --ref \"\${BUILDCHAIN_GIT_REF}\" \\\n --release-prerelease \"\${BUILDCHAIN_RELEASE_PRERELEASE}\" \\\n --router-ref \"\${BUILDCHAIN_ROUTER_REF}\"\n\n - name: Select consumer contract lock\n id: lock\n shell: bash\n env:\n EXPLICIT_PATH: \${{ inputs.buildchain-contract-lock-path }}\n ALPHA_PATH: \${{ inputs.buildchain-alpha-contract-lock-path }}\n STABLE_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}\n CHANNEL: \${{ steps.channel.outputs.channel }}\n run: |\n set -euo pipefail\n path=\"\${EXPLICIT_PATH}\"\n if [[ -z \"\${path}\" ]]; then\n if [[ \"\${CHANNEL}\" = \"alpha\" ]]; then\n path=\"\${ALPHA_PATH}\"\n else\n path=\"\${STABLE_PATH}\"\n fi\n fi\n echo \"path=\${path}\" >> \"\${GITHUB_OUTPUT}\"\n\n build:\n name: Build with resolved channel\n needs: resolve-channel\n uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n issues: write\n id-token: write\n with:\n${forwardedInputs(names)}\n secrets: inherit\n`;
|
|
84
213
|
}
|
|
85
214
|
|
|
215
|
+
export function generateChannelBuildWorkflow(source) {
|
|
216
|
+
const generated = generateChannelBuildWorkflowBase(source)
|
|
217
|
+
.replace("\n build:\n", `\n${routerControllerPlanJob()}\n\n build:\n`)
|
|
218
|
+
.replace(
|
|
219
|
+
" needs: resolve-channel\n uses: ./.github/workflows/.build.yml",
|
|
220
|
+
" needs:\n - resolve-channel\n - controller-plan\n uses: ./.github/workflows/.build.yml",
|
|
221
|
+
);
|
|
222
|
+
return `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n`;
|
|
223
|
+
}
|
|
224
|
+
|
|
86
225
|
function main() {
|
|
87
226
|
const source = fs.readFileSync(sourcePath, "utf8");
|
|
88
227
|
const generated = generateChannelBuildWorkflow(source);
|
|
@@ -27,6 +27,8 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
27
27
|
);
|
|
28
28
|
const gateAggregateJson = env("BUILDCHAIN_GATE_PROFILE_AGGREGATE_JSON");
|
|
29
29
|
const gateAggregate = gateAggregateJson ? JSON.parse(gateAggregateJson) : undefined;
|
|
30
|
+
const controllerReceiptJson = env("BUILDCHAIN_CONTROLLER_RECEIPT_JSON");
|
|
31
|
+
const controllerReceipts = controllerReceiptJson ? [JSON.parse(controllerReceiptJson)] : [];
|
|
30
32
|
const passport = createReleaseCandidatePassport({
|
|
31
33
|
repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
|
|
32
34
|
pullRequest: {
|
|
@@ -49,6 +51,7 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
49
51
|
workflowShellRef: env("BUILDCHAIN_WORKFLOW_SHELL_REF", buildSummary.runtime?.workflowShellRef || ""),
|
|
50
52
|
},
|
|
51
53
|
gateAggregate,
|
|
54
|
+
controllerReceipts,
|
|
52
55
|
workflow: {
|
|
53
56
|
name: env("GITHUB_WORKFLOW"),
|
|
54
57
|
runId: env("GITHUB_RUN_ID", buildSummary.git?.runId || ""),
|
|
@@ -77,6 +80,7 @@ export function generateReleaseCandidatePassportCli() {
|
|
|
77
80
|
candidateHash: passport.candidateHash,
|
|
78
81
|
platformCount: passport.platformMatrix.length,
|
|
79
82
|
gateProfileEvidence: passport.gateProfileEvidence,
|
|
83
|
+
controllerReceipts: passport.controllerReceipts || [],
|
|
80
84
|
}),
|
|
81
85
|
});
|
|
82
86
|
return passport;
|
|
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { createBuildchainContractWorld } from "../packages/core/buildchain-contract.js";
|
|
8
|
+
import { createControllerRegistry } from "../packages/core/controller-evidence.js";
|
|
8
9
|
import {
|
|
9
10
|
BUILDCHAIN_AGENT_MANUALS,
|
|
10
11
|
createBuildchainKfdClaimRegistry,
|
|
@@ -316,6 +317,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
316
317
|
"product-mechanism": { capabilityGroup: "getting-started", audience: ["agent", "maintainer"], maturity: "stable", order: 30 },
|
|
317
318
|
cli: { capabilityGroup: "api-cli-reference", audience: ["agent", "developer"], maturity: "stable", order: 40 },
|
|
318
319
|
"release-passport": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 100 },
|
|
320
|
+
"controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
|
|
319
321
|
"binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
|
|
320
322
|
"publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
|
|
321
323
|
"release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
|
|
@@ -478,6 +480,7 @@ function nodeApiMeta(exportName) {
|
|
|
478
480
|
"./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
|
|
479
481
|
"./release-line-bootstrap": { group: "governance-versioning", summary: "Semver release-line bootstrap planning and version-state APIs." },
|
|
480
482
|
"./buildchain-contract": { group: "governance-versioning", summary: "Runtime contract world and compatibility digest APIs for floating-ref drift checks." },
|
|
483
|
+
"./controller-evidence": { group: "reusable-build", summary: "Project-independent controller descriptors, source/runtime-bound plans, receipts, aggregates, and validation APIs." },
|
|
481
484
|
"./surface-manifest": { group: "site-and-propagation", summary: "Surface manifest timestamp and reproducibility policy APIs." },
|
|
482
485
|
"./issue-reporting": { group: "observability-diagnostics", summary: "Buildchain-owned issue reporting API for workflow friction feedback." },
|
|
483
486
|
"./buildchain-layout": { group: "kfd-trust", summary: "Versioned Buildchain repository-layout discovery contract plus canonical .buildchain path resolution and migration APIs." },
|
|
@@ -753,6 +756,7 @@ function buildSiteBundle() {
|
|
|
753
756
|
"docs/stable-candidate-patrol.md",
|
|
754
757
|
"docs/release-governance.md",
|
|
755
758
|
"docs/release-passport.md",
|
|
759
|
+
"docs/controller-evidence.md",
|
|
756
760
|
"docs/publish-transaction.md",
|
|
757
761
|
"docs/homebrew.md",
|
|
758
762
|
"docs/site-bundle-contract.md",
|
|
@@ -862,6 +866,7 @@ function buildSiteBundle() {
|
|
|
862
866
|
status: "active",
|
|
863
867
|
})),
|
|
864
868
|
};
|
|
869
|
+
const controllerRegistry = createControllerRegistry({ workflows: workflowRegistry.workflows });
|
|
865
870
|
const publicSurfaceAudit = collectPublicSurfaceReverseAudit({
|
|
866
871
|
root,
|
|
867
872
|
cliRegistry,
|
|
@@ -949,6 +954,7 @@ function buildSiteBundle() {
|
|
|
949
954
|
"manual-registry.json",
|
|
950
955
|
"node-api-registry.json",
|
|
951
956
|
"workflow-registry.json",
|
|
957
|
+
"controller-registry.json",
|
|
952
958
|
"public-surface-audit.json",
|
|
953
959
|
"release-model.json",
|
|
954
960
|
"artifact-schemas.json",
|
|
@@ -1029,6 +1035,7 @@ function buildSiteBundle() {
|
|
|
1029
1035
|
"manual-registry.json",
|
|
1030
1036
|
"node-api-registry.json",
|
|
1031
1037
|
"workflow-registry.json",
|
|
1038
|
+
"controller-registry.json",
|
|
1032
1039
|
"public-surface-audit.json",
|
|
1033
1040
|
"release-model.json",
|
|
1034
1041
|
"artifact-schemas.json",
|
|
@@ -1055,6 +1062,7 @@ function buildSiteBundle() {
|
|
|
1055
1062
|
"manual-registry.json",
|
|
1056
1063
|
"node-api-registry.json",
|
|
1057
1064
|
"workflow-registry.json",
|
|
1065
|
+
"controller-registry.json",
|
|
1058
1066
|
"public-surface-audit.json",
|
|
1059
1067
|
"artifact-schemas.json",
|
|
1060
1068
|
"buildchain-contract.json",
|
|
@@ -1191,6 +1199,7 @@ function buildSiteBundle() {
|
|
|
1191
1199
|
"capability-grouped navigation registry for docs, CLI, Node API, workflows, actions, and KFD claims",
|
|
1192
1200
|
"release model facts",
|
|
1193
1201
|
"workflow and action registries",
|
|
1202
|
+
"controller evidence descriptors and input classification",
|
|
1194
1203
|
"CLI command registry",
|
|
1195
1204
|
"public surface reverse audit",
|
|
1196
1205
|
"manual and Node API registries",
|
|
@@ -1221,12 +1230,13 @@ function buildSiteBundle() {
|
|
|
1221
1230
|
"manual-registry.json": manualRegistry,
|
|
1222
1231
|
"node-api-registry.json": nodeApiRegistry,
|
|
1223
1232
|
"workflow-registry.json": workflowRegistry,
|
|
1233
|
+
"controller-registry.json": controllerRegistry,
|
|
1224
1234
|
"public-surface-audit.json": publicSurfaceAudit,
|
|
1225
1235
|
"release-model.json": releaseModel,
|
|
1226
1236
|
"artifact-schemas.json": artifactSchemas,
|
|
1227
1237
|
"badge-endpoint-registry.json": badgeEndpointRegistry,
|
|
1228
1238
|
"publication-registry.json": publicationRegistry,
|
|
1229
|
-
"buildchain-contract.json": createBuildchainContractWorld({ root }),
|
|
1239
|
+
"buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
|
|
1230
1240
|
"kfd-upstream-aggregate.json": collectKfdUpstreamFacts({ cwd: root, includeOwn: false }),
|
|
1231
1241
|
"kfd-claims.json": createBuildchainKfdClaimRegistry({ root }),
|
|
1232
1242
|
"product-mechanism.json": productMechanism,
|