@kungfu-tech/buildchain 3.0.7-alpha.0 → 3.0.7
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/actions/promote-buildchain-ref/README.md +10 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/dist/site/buildchain-contract.json +24 -24
- package/dist/site/buildchain-site.json +91 -30
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +122 -11
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +8 -7
- package/dist/site/node-api-registry.json +683 -105
- package/dist/site/page-registry.json +80 -19
- package/dist/site/public-surface-audit.json +98 -7
- package/dist/site/publication-authority-registry.json +81 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +119 -2
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +2 -2
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +277 -212
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-provider-plane.md +1 -1
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +4 -1
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-publication-authority.js +4 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +3 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/publication-rehearsal-runtime.js +13 -1
- package/packages/core/release-passport.js +130 -20
- package/scripts/assemble-self-publication-admission.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/check-inventory.mjs +3 -1
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-site-bundle.mjs +23 -4
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +6 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ENGINEERING_HOUSEKEEPER_CONTRACT =
|
|
4
|
+
"kungfu-buildchain-engineering-housekeeper/v1";
|
|
5
|
+
export const ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export const HOUSEKEEPER_REASON_CODES = Object.freeze({
|
|
8
|
+
ELIGIBLE_MERGED_BRANCH: "eligible.merged-branch",
|
|
9
|
+
PROTECTED_BRANCH: "branch.protected",
|
|
10
|
+
RETAINED_BRANCH: "branch.retained",
|
|
11
|
+
DEFAULT_BRANCH: "branch.default",
|
|
12
|
+
TARGET_BRANCH: "branch.target",
|
|
13
|
+
OPEN_PR_HEAD: "branch.open-pr-head",
|
|
14
|
+
CROSS_REPOSITORY: "branch.cross-repository",
|
|
15
|
+
NOT_MERGED: "branch.not-merged",
|
|
16
|
+
HEAD_ADVANCED: "branch.head-advanced",
|
|
17
|
+
RENAMED: "branch.renamed",
|
|
18
|
+
TARGET_ADVANCED: "branch.target-advanced",
|
|
19
|
+
PERMISSION_DENIED: "branch.permission-denied",
|
|
20
|
+
PR_ACTIVE: "pull-request.active",
|
|
21
|
+
PR_STALE_REPORT: "pull-request.stale-report",
|
|
22
|
+
PR_LABEL_ELIGIBLE: "pull-request.label-eligible",
|
|
23
|
+
PR_AUTO_CLOSE_DISABLED: "pull-request.auto-close-disabled",
|
|
24
|
+
PR_FORKED: "pull-request.forked",
|
|
25
|
+
REPEATED_NO_OP: "replay.already-applied",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_HOUSEKEEPER_POLICY = Object.freeze({
|
|
29
|
+
protectedPatterns: ["dev/**", "alpha/**", "release/**", "publish-gate/**"],
|
|
30
|
+
retainedPatterns: ["train/**", "authority/**"],
|
|
31
|
+
pullRequests: Object.freeze({
|
|
32
|
+
reportStale: true,
|
|
33
|
+
label: "",
|
|
34
|
+
autoClose: false,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function stableJson(value) {
|
|
39
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
return `{${Object.keys(value)
|
|
42
|
+
.sort()
|
|
43
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
44
|
+
.join(",")}}`;
|
|
45
|
+
}
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function engineeringHousekeeperRoot(value) {
|
|
50
|
+
return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function globMatches(pattern, value) {
|
|
54
|
+
const expression = String(pattern)
|
|
55
|
+
.split("**")
|
|
56
|
+
.map((chunk) =>
|
|
57
|
+
chunk
|
|
58
|
+
.split("*")
|
|
59
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
60
|
+
.join("[^/]*"),
|
|
61
|
+
)
|
|
62
|
+
.join(".*");
|
|
63
|
+
return new RegExp(`^${expression}$`).test(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function matchesAny(patterns, value) {
|
|
67
|
+
return [...(patterns || [])].some((pattern) => globMatches(pattern, value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizePolicy(policy = {}) {
|
|
71
|
+
return {
|
|
72
|
+
protectedPatterns: [
|
|
73
|
+
...(policy.protectedPatterns ||
|
|
74
|
+
DEFAULT_HOUSEKEEPER_POLICY.protectedPatterns),
|
|
75
|
+
].sort(),
|
|
76
|
+
retainedPatterns: [
|
|
77
|
+
...(policy.retainedPatterns ||
|
|
78
|
+
DEFAULT_HOUSEKEEPER_POLICY.retainedPatterns),
|
|
79
|
+
].sort(),
|
|
80
|
+
pullRequests: {
|
|
81
|
+
reportStale: policy.pullRequests?.reportStale !== false,
|
|
82
|
+
label: String(policy.pullRequests?.label || ""),
|
|
83
|
+
autoClose: false,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sortedUnique(values) {
|
|
89
|
+
return [...new Set(values)].sort();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function classifyHousekeeperBranch(branch, policyInput = {}) {
|
|
93
|
+
const policy = normalizePolicy(policyInput);
|
|
94
|
+
const reasons = [];
|
|
95
|
+
if (branch.isDefault) reasons.push(HOUSEKEEPER_REASON_CODES.DEFAULT_BRANCH);
|
|
96
|
+
if (branch.name === branch.target?.name)
|
|
97
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_BRANCH);
|
|
98
|
+
if (
|
|
99
|
+
branch.isProtected === true ||
|
|
100
|
+
matchesAny(policy.protectedPatterns, branch.name)
|
|
101
|
+
)
|
|
102
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.PROTECTED_BRANCH);
|
|
103
|
+
if (matchesAny(policy.retainedPatterns, branch.name))
|
|
104
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.RETAINED_BRANCH);
|
|
105
|
+
if (branch.sourceRepository && branch.sourceRepository !== branch.repository)
|
|
106
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.CROSS_REPOSITORY);
|
|
107
|
+
if ((branch.openPullRequestNumbers || []).length > 0)
|
|
108
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.OPEN_PR_HEAD);
|
|
109
|
+
if (branch.ancestry !== "ancestor")
|
|
110
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.NOT_MERGED);
|
|
111
|
+
const eligible =
|
|
112
|
+
reasons.length === 0 &&
|
|
113
|
+
Boolean(branch.headOid) &&
|
|
114
|
+
Boolean(branch.target?.headOid);
|
|
115
|
+
return {
|
|
116
|
+
kind: "branch",
|
|
117
|
+
repository: branch.repository,
|
|
118
|
+
name: branch.name,
|
|
119
|
+
headOid: branch.headOid,
|
|
120
|
+
target: { name: branch.target?.name, headOid: branch.target?.headOid },
|
|
121
|
+
eligible,
|
|
122
|
+
decision: eligible ? "delete" : "retain",
|
|
123
|
+
reasonCodes: eligible
|
|
124
|
+
? [HOUSEKEEPER_REASON_CODES.ELIGIBLE_MERGED_BRANCH]
|
|
125
|
+
: sortedUnique(reasons),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function classifyHousekeeperPullRequest(pullRequest, policyInput = {}) {
|
|
130
|
+
const policy = normalizePolicy(policyInput);
|
|
131
|
+
const active = ["open", "draft"].includes(pullRequest.state);
|
|
132
|
+
const forked = pullRequest.headRepository !== pullRequest.repository;
|
|
133
|
+
const stale = active && pullRequest.stale === true;
|
|
134
|
+
const actions = stale && policy.pullRequests.reportStale ? ["report"] : [];
|
|
135
|
+
if (stale && policy.pullRequests.label) actions.push("label");
|
|
136
|
+
const reasons = [HOUSEKEEPER_REASON_CODES.PR_AUTO_CLOSE_DISABLED];
|
|
137
|
+
if (active) reasons.push(HOUSEKEEPER_REASON_CODES.PR_ACTIVE);
|
|
138
|
+
if (stale) reasons.push(HOUSEKEEPER_REASON_CODES.PR_STALE_REPORT);
|
|
139
|
+
if (stale && policy.pullRequests.label)
|
|
140
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.PR_LABEL_ELIGIBLE);
|
|
141
|
+
if (forked) reasons.push(HOUSEKEEPER_REASON_CODES.PR_FORKED);
|
|
142
|
+
return {
|
|
143
|
+
kind: "pull-request",
|
|
144
|
+
repository: pullRequest.repository,
|
|
145
|
+
number: pullRequest.number,
|
|
146
|
+
headRepository: pullRequest.headRepository,
|
|
147
|
+
headRef: pullRequest.headRef,
|
|
148
|
+
headOid: pullRequest.headOid,
|
|
149
|
+
state: pullRequest.state,
|
|
150
|
+
actions: actions.sort(),
|
|
151
|
+
reasonCodes: sortedUnique(reasons),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createEngineeringHousekeeperPlan({
|
|
156
|
+
repository,
|
|
157
|
+
target,
|
|
158
|
+
branches = [],
|
|
159
|
+
pullRequests = [],
|
|
160
|
+
policy = {},
|
|
161
|
+
observedAt,
|
|
162
|
+
}) {
|
|
163
|
+
const normalizedPolicy = normalizePolicy(policy);
|
|
164
|
+
const inventory = [
|
|
165
|
+
...branches.map((branch) =>
|
|
166
|
+
classifyHousekeeperBranch(branch, normalizedPolicy),
|
|
167
|
+
),
|
|
168
|
+
...pullRequests.map((pullRequest) =>
|
|
169
|
+
classifyHousekeeperPullRequest(pullRequest, normalizedPolicy),
|
|
170
|
+
),
|
|
171
|
+
].sort((left, right) =>
|
|
172
|
+
`${left.kind}:${left.name || String(left.number).padStart(12, "0")}`.localeCompare(
|
|
173
|
+
`${right.kind}:${right.name || String(right.number).padStart(12, "0")}`,
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
const body = {
|
|
177
|
+
contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
|
|
178
|
+
schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
|
|
179
|
+
mode: "plan",
|
|
180
|
+
repository,
|
|
181
|
+
target,
|
|
182
|
+
observedAt,
|
|
183
|
+
policy: normalizedPolicy,
|
|
184
|
+
inventory,
|
|
185
|
+
actions: inventory.flatMap((entry) =>
|
|
186
|
+
entry.kind === "branch" && entry.eligible
|
|
187
|
+
? [
|
|
188
|
+
{
|
|
189
|
+
kind: "delete-branch",
|
|
190
|
+
name: entry.name,
|
|
191
|
+
expectedHeadOid: entry.headOid,
|
|
192
|
+
targetName: entry.target.name,
|
|
193
|
+
expectedTargetHeadOid: entry.target.headOid,
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
: entry.kind === "pull-request"
|
|
197
|
+
? entry.actions.map((action) => ({
|
|
198
|
+
kind: `${action}-pull-request`,
|
|
199
|
+
number: entry.number,
|
|
200
|
+
expectedHeadOid: entry.headOid,
|
|
201
|
+
}))
|
|
202
|
+
: [],
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
return { ...body, planRoot: engineeringHousekeeperRoot(body) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function revalidateHousekeeperBranchAction(
|
|
209
|
+
action,
|
|
210
|
+
current,
|
|
211
|
+
policy = {},
|
|
212
|
+
) {
|
|
213
|
+
const reasons = [];
|
|
214
|
+
if (current.name !== action.name)
|
|
215
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.RENAMED);
|
|
216
|
+
if (current.headOid !== action.expectedHeadOid)
|
|
217
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.HEAD_ADVANCED);
|
|
218
|
+
if (current.target?.headOid !== action.expectedTargetHeadOid)
|
|
219
|
+
reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_ADVANCED);
|
|
220
|
+
const classification = classifyHousekeeperBranch(current, policy);
|
|
221
|
+
if (!classification.eligible) reasons.push(...classification.reasonCodes);
|
|
222
|
+
return {
|
|
223
|
+
ok: reasons.length === 0,
|
|
224
|
+
action,
|
|
225
|
+
currentHeadOid: current.headOid,
|
|
226
|
+
currentTargetHeadOid: current.target?.headOid,
|
|
227
|
+
reasonCodes: sortedUnique(reasons),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function createEngineeringHousekeeperReceipt({
|
|
232
|
+
plan,
|
|
233
|
+
outcomes,
|
|
234
|
+
appliedAt,
|
|
235
|
+
}) {
|
|
236
|
+
const orderedOutcomes = [...outcomes].sort((a, b) =>
|
|
237
|
+
stableJson(a).localeCompare(stableJson(b)),
|
|
238
|
+
);
|
|
239
|
+
const body = {
|
|
240
|
+
contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
|
|
241
|
+
schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
|
|
242
|
+
mode: "receipt",
|
|
243
|
+
planRoot: plan.planRoot,
|
|
244
|
+
appliedAt,
|
|
245
|
+
outcomes: orderedOutcomes,
|
|
246
|
+
};
|
|
247
|
+
return { ...body, receiptRoot: engineeringHousekeeperRoot(body) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function classifyHousekeeperReplay(plan, priorReceipt) {
|
|
251
|
+
const alreadyApplied = priorReceipt?.planRoot === plan.planRoot;
|
|
252
|
+
return {
|
|
253
|
+
alreadyApplied,
|
|
254
|
+
action: alreadyApplied ? "no-op" : "apply",
|
|
255
|
+
reasonCodes: alreadyApplied
|
|
256
|
+
? [HOUSEKEEPER_REASON_CODES.REPEATED_NO_OP]
|
|
257
|
+
: [],
|
|
258
|
+
};
|
|
259
|
+
}
|
package/packages/core/index.js
CHANGED
|
@@ -287,6 +287,9 @@ export {
|
|
|
287
287
|
verifyGithubGovernanceReceipt,
|
|
288
288
|
} from "./github-governance-authority.js";
|
|
289
289
|
|
|
290
|
+
export * from "./engineering-housekeeper.js";
|
|
291
|
+
export * from "./engineering-housekeeper-github.js";
|
|
292
|
+
|
|
290
293
|
export {
|
|
291
294
|
ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
|
|
292
295
|
ARTIFACT_PASSPORT_POINTER_CONTRACT,
|
|
@@ -944,26 +944,49 @@ export function normalizeKfd1ContractWorldWitness(witness, { metadata = resolveK
|
|
|
944
944
|
};
|
|
945
945
|
}
|
|
946
946
|
|
|
947
|
-
function
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
947
|
+
function indexArtifactSearchRoots({ roots = [], artifactPaths = [] } = {}) {
|
|
948
|
+
const wanted = [...new Set(artifactPaths.filter(Boolean))];
|
|
949
|
+
const indexed = new Map();
|
|
950
|
+
const stack = roots.filter((root) => root && fs.existsSync(root) && fs.statSync(root).isDirectory());
|
|
951
|
+
while (stack.length > 0) {
|
|
952
|
+
const current = stack.pop();
|
|
953
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
954
|
+
const candidate = path.join(current, entry.name);
|
|
955
|
+
if (entry.isDirectory()) { stack.push(candidate); continue; }
|
|
956
|
+
if (!entry.isFile()) continue;
|
|
957
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
958
|
+
for (const artifactPath of wanted) {
|
|
959
|
+
if (normalized === artifactPath || normalized.endsWith(`/${artifactPath}`)) {
|
|
960
|
+
const candidates = indexed.get(artifactPath) || [];
|
|
961
|
+
candidates.push(candidate);
|
|
962
|
+
indexed.set(artifactPath, candidates);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return indexed;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function resolveArtifactFile({ cwd, artifactRoot, artifactIndex = new Map(), artifacts = [], artifactPath, expectedSha256 = "" }) {
|
|
971
|
+
const candidates = artifactIndex.size > 0
|
|
972
|
+
? [...(artifactIndex.get(artifactPath) || []), artifactRoot ? path.resolve(artifactRoot, artifactPath) : "", path.resolve(cwd, artifactPath)]
|
|
973
|
+
: [path.resolve(cwd, artifactPath), artifactRoot ? path.resolve(artifactRoot, artifactPath) : ""];
|
|
952
974
|
for (const artifact of artifacts) {
|
|
953
975
|
const sourcePath = artifact.sourcePath || artifact.path || "";
|
|
954
|
-
if (!sourcePath)
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
976
|
+
if (!sourcePath) continue;
|
|
957
977
|
const normalizedSource = sourcePath.replace(/\\/g, "/");
|
|
958
|
-
if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`))
|
|
959
|
-
candidates.push(path.resolve(sourcePath));
|
|
960
|
-
}
|
|
978
|
+
if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) candidates.push(path.resolve(sourcePath));
|
|
961
979
|
const name = artifact.name || artifact.filename || "";
|
|
962
|
-
if (name && name === artifactPath)
|
|
963
|
-
candidates.push(path.resolve(sourcePath));
|
|
964
|
-
}
|
|
980
|
+
if (name && name === artifactPath) candidates.push(path.resolve(sourcePath));
|
|
965
981
|
}
|
|
966
|
-
|
|
982
|
+
const existing = [...new Set(candidates)].filter(
|
|
983
|
+
(candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile(),
|
|
984
|
+
);
|
|
985
|
+
if (expectedSha256) {
|
|
986
|
+
const exact = existing.find((candidate) => sha256File(candidate) === expectedSha256);
|
|
987
|
+
if (exact) return exact;
|
|
988
|
+
}
|
|
989
|
+
return existing[0] || "";
|
|
967
990
|
}
|
|
968
991
|
|
|
969
992
|
function resolveSourceFile({ cwd, sourcePath }) {
|
|
@@ -977,6 +1000,7 @@ function resolveSourceFile({ cwd, sourcePath }) {
|
|
|
977
1000
|
export function createKfd1ReleaseGateEvidence({
|
|
978
1001
|
cwd = process.cwd(),
|
|
979
1002
|
artifactRoot = "",
|
|
1003
|
+
artifactSearchRoots = [],
|
|
980
1004
|
artifacts = [],
|
|
981
1005
|
witnesses = [],
|
|
982
1006
|
verifiedAt = new Date().toISOString(),
|
|
@@ -986,6 +1010,10 @@ export function createKfd1ReleaseGateEvidence({
|
|
|
986
1010
|
if (normalizedWitnesses.length === 0) {
|
|
987
1011
|
return undefined;
|
|
988
1012
|
}
|
|
1013
|
+
const artifactIndex = indexArtifactSearchRoots({
|
|
1014
|
+
roots: artifactSearchRoots,
|
|
1015
|
+
artifactPaths: normalizedWitnesses.flatMap((witness) => witness.surfaces.map((surface) => surface.artifactPath)),
|
|
1016
|
+
});
|
|
989
1017
|
const worlds = normalizedWitnesses.map((witness) => {
|
|
990
1018
|
const preBuildWitnessSha256 = sha256Json(witness);
|
|
991
1019
|
const sourceResults = witness.surfaces.map((surface) => {
|
|
@@ -1025,8 +1053,10 @@ export function createKfd1ReleaseGateEvidence({
|
|
|
1025
1053
|
const filePath = resolveArtifactFile({
|
|
1026
1054
|
cwd,
|
|
1027
1055
|
artifactRoot,
|
|
1056
|
+
artifactIndex,
|
|
1028
1057
|
artifacts,
|
|
1029
1058
|
artifactPath: surface.artifactPath,
|
|
1059
|
+
expectedSha256: surface.expectedSha256,
|
|
1030
1060
|
});
|
|
1031
1061
|
if (!filePath) {
|
|
1032
1062
|
return {
|
|
@@ -102,7 +102,19 @@ function safeRelativePath(value, label) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
function fileDigest(filePath) {
|
|
105
|
-
|
|
105
|
+
const hash = crypto.createHash("sha256");
|
|
106
|
+
const descriptor = fs.openSync(filePath, "r");
|
|
107
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
108
|
+
try {
|
|
109
|
+
for (;;) {
|
|
110
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
111
|
+
if (count === 0) break;
|
|
112
|
+
hash.update(buffer.subarray(0, count));
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
fs.closeSync(descriptor);
|
|
116
|
+
}
|
|
117
|
+
return `sha256:${hash.digest("hex")}`;
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
function rootedPayload(value) {
|
|
@@ -146,6 +146,15 @@ function invariantSemanticPreimage(passport) {
|
|
|
146
146
|
return value;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function hasCompleteInvariantCoverage(value) {
|
|
150
|
+
const coverage = value?.coverage;
|
|
151
|
+
return Number.isInteger(coverage?.required)
|
|
152
|
+
&& coverage.required > 0
|
|
153
|
+
&& coverage.verified === coverage.required
|
|
154
|
+
&& [coverage.missing, coverage.falsified, coverage.unqualified]
|
|
155
|
+
.every((items) => Array.isArray(items) && items.length === 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
function normalizeInvariantPassport(meta, index = 0) {
|
|
150
159
|
const value = meta?.value;
|
|
151
160
|
const label = `invariantPassportJsons[${index}]`;
|
|
@@ -165,11 +174,18 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
165
174
|
if (value.passportRoot !== expectedRoot) {
|
|
166
175
|
throw new Error(`${label}.passportRoot mismatch: expected ${expectedRoot}, got ${value.passportRoot}`);
|
|
167
176
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
const verifiedPassport = value.verdict === "verified" && value.coverage?.complete === true;
|
|
178
|
+
const scopedUnqualifiedClaim = value.verdict === "unqualified"
|
|
179
|
+
&& value.coverage?.complete === false
|
|
180
|
+
&& hasCompleteInvariantCoverage(value)
|
|
181
|
+
&& value.releaseClaims?.verdict === "unqualified"
|
|
182
|
+
&& /^sha256:[0-9a-f]{64}$/.test(optionalString(value.releaseClaims?.claimRoot))
|
|
183
|
+
&& Array.isArray(value.releaseClaims?.diagnostics)
|
|
184
|
+
&& value.releaseClaims.diagnostics.length > 0
|
|
185
|
+
&& Array.isArray(value.diagnostics)
|
|
186
|
+
&& value.diagnostics.length === 0;
|
|
187
|
+
if (!verifiedPassport && !scopedUnqualifiedClaim) {
|
|
188
|
+
throw new Error(`${label}.verdict must be verified, or unqualified only for complete invariant coverage with one explicit unqualified releaseClaims section`);
|
|
173
189
|
}
|
|
174
190
|
if (value.source?.dirty !== false) {
|
|
175
191
|
throw new Error(`${label}.source.dirty must be false`);
|
|
@@ -182,6 +198,9 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
182
198
|
: [];
|
|
183
199
|
if (platforms.length === 0) throw new Error(`${label}.coverage.platforms must be non-empty`);
|
|
184
200
|
if (!Array.isArray(value.residualRisk)) throw new Error(`${label}.residualRisk must be an array`);
|
|
201
|
+
const releaseClaimRisks = scopedUnqualifiedClaim
|
|
202
|
+
? value.releaseClaims.diagnostics.map((entry) => `Exit/provider claim ${optionalString(entry?.code) || "unqualified"}: ${optionalString(entry?.message) || "consumer claim remains unqualified"}`)
|
|
203
|
+
: [];
|
|
185
204
|
return {
|
|
186
205
|
path: optionalString(meta.path),
|
|
187
206
|
sha256: optionalString(meta.sha256),
|
|
@@ -194,7 +213,15 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
194
213
|
source: structuredClone(value.source),
|
|
195
214
|
coverage: structuredClone(value.coverage),
|
|
196
215
|
platforms,
|
|
197
|
-
|
|
216
|
+
admission: {
|
|
217
|
+
scope: "consumer-invariant-coverage",
|
|
218
|
+
result: "passed",
|
|
219
|
+
consumerVerdict: value.verdict,
|
|
220
|
+
releaseClaimsVerdict: optionalString(value.releaseClaims?.verdict),
|
|
221
|
+
},
|
|
222
|
+
releaseClaims: value.releaseClaims ? structuredClone(value.releaseClaims) : undefined,
|
|
223
|
+
diagnostics: Array.isArray(value.diagnostics) ? structuredClone(value.diagnostics) : [],
|
|
224
|
+
residualRisk: [...new Set([...value.residualRisk, ...releaseClaimRisks])].sort(),
|
|
198
225
|
};
|
|
199
226
|
}
|
|
200
227
|
|
|
@@ -533,7 +560,12 @@ function mergeAuthoritativeImpactBase(impact, basePassport = undefined) {
|
|
|
533
560
|
return merged;
|
|
534
561
|
}
|
|
535
562
|
|
|
536
|
-
function parseJsonCommandOutput({
|
|
563
|
+
function parseJsonCommandOutput({
|
|
564
|
+
command = "",
|
|
565
|
+
cwd = process.cwd(),
|
|
566
|
+
label = "command",
|
|
567
|
+
acceptNonzeroJson = undefined,
|
|
568
|
+
} = {}) {
|
|
537
569
|
const normalized = String(command || "").trim();
|
|
538
570
|
if (!normalized) {
|
|
539
571
|
return { value: undefined, path: "", sha256: "" };
|
|
@@ -547,7 +579,7 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
|
|
|
547
579
|
if (result.error) {
|
|
548
580
|
throw new Error(`${label} failed to start: ${result.error.message}`);
|
|
549
581
|
}
|
|
550
|
-
if (result.status !== 0) {
|
|
582
|
+
if (result.status !== 0 && typeof acceptNonzeroJson !== "function") {
|
|
551
583
|
const stderr = String(result.stderr || "").trim();
|
|
552
584
|
throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
|
|
553
585
|
}
|
|
@@ -561,10 +593,15 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
|
|
|
561
593
|
} catch (error) {
|
|
562
594
|
throw new Error(`${label} output must be valid JSON: ${error.message}`, { cause: error });
|
|
563
595
|
}
|
|
596
|
+
if (result.status !== 0 && !acceptNonzeroJson({ status: result.status, value: parsed })) {
|
|
597
|
+
const stderr = String(result.stderr || "").trim();
|
|
598
|
+
throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
|
|
599
|
+
}
|
|
564
600
|
return {
|
|
565
601
|
value: parsed,
|
|
566
602
|
path: "",
|
|
567
603
|
sha256: sha256Text(stableJson(parsed)),
|
|
604
|
+
status: result.status,
|
|
568
605
|
};
|
|
569
606
|
}
|
|
570
607
|
|
|
@@ -879,6 +916,41 @@ function arrayOrSingleton(value) {
|
|
|
879
916
|
return value && typeof value === "object" ? [value] : [];
|
|
880
917
|
}
|
|
881
918
|
|
|
919
|
+
const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
|
|
920
|
+
|
|
921
|
+
function normalizeKfd2ResidualRisk(entry, { claim = {}, claimIndex = 0, riskIndex = 0 } = {}) {
|
|
922
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
923
|
+
return entry;
|
|
924
|
+
}
|
|
925
|
+
const reason = optionalString(entry).trim();
|
|
926
|
+
if (!reason) {
|
|
927
|
+
return entry;
|
|
928
|
+
}
|
|
929
|
+
const claimId = optionalString(claim.id || `claim-${claimIndex + 1}`)
|
|
930
|
+
.toLowerCase()
|
|
931
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
932
|
+
.replace(/^-+|-+$/g, "") || `claim-${claimIndex + 1}`;
|
|
933
|
+
const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility)
|
|
934
|
+
? claim.responsibility
|
|
935
|
+
: {};
|
|
936
|
+
return {
|
|
937
|
+
id: `${claimId}-residual-risk-${riskIndex + 1}`,
|
|
938
|
+
definedBy: KFD2_RESIDUAL_RISK_SCHEMA,
|
|
939
|
+
riskType: "manual-review-risk",
|
|
940
|
+
trustImpact: "downgrade-warning",
|
|
941
|
+
machineProvability: "not-machine-verifiable",
|
|
942
|
+
agentAction: "request-maintainer-review",
|
|
943
|
+
reason,
|
|
944
|
+
owner: optionalString(
|
|
945
|
+
responsibility.releaseDecisionOwner
|
|
946
|
+
|| responsibility.owner
|
|
947
|
+
|| responsibility.sourceOwner
|
|
948
|
+
|| responsibility.sourceContractOwner
|
|
949
|
+
|| "release maintainer",
|
|
950
|
+
),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
882
954
|
function normalizeKfd2Claim(raw = {}, index = 0) {
|
|
883
955
|
const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
884
956
|
const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
|
|
@@ -892,10 +964,13 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
|
|
|
892
964
|
? claim.audit_boundary
|
|
893
965
|
: {};
|
|
894
966
|
const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
|
|
895
|
-
const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
967
|
+
const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
|
|
968
|
+
normalizeKfd2ResidualRisk(entry, { claim, claimIndex: index, riskIndex }),
|
|
969
|
+
{
|
|
970
|
+
kind: "residualRisk",
|
|
971
|
+
label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
|
|
972
|
+
},
|
|
973
|
+
));
|
|
899
974
|
const downgradeReasons = arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason)
|
|
900
975
|
.map((entry, reasonIndex) => validateKfd2TrustTaxonomyEntry(entry, {
|
|
901
976
|
kind: "downgradeReason",
|
|
@@ -1494,6 +1569,7 @@ export function collectGitHubReleasePassport({
|
|
|
1494
1569
|
outputDir = ".buildchain/release-passport",
|
|
1495
1570
|
assetsJson = "",
|
|
1496
1571
|
assetsDir = "",
|
|
1572
|
+
kfdArtifactSearchRoots = [],
|
|
1497
1573
|
releaseJson = "",
|
|
1498
1574
|
productName = "Buildchain",
|
|
1499
1575
|
packageName = "@kungfu-tech/buildchain",
|
|
@@ -1568,6 +1644,11 @@ export function collectGitHubReleasePassport({
|
|
|
1568
1644
|
.filter(Boolean)
|
|
1569
1645
|
.map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined, { cwd, label: "kfd3ArtifactWitnessJsons entry" }))
|
|
1570
1646
|
.filter((meta) => meta.value);
|
|
1647
|
+
const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
|
|
1648
|
+
command: kfd3ArtifactVerifyCommand,
|
|
1649
|
+
cwd,
|
|
1650
|
+
label: "KFD-3 artifact verify command",
|
|
1651
|
+
});
|
|
1571
1652
|
const kfdSupportMatrixMeta = parseJsonInputWithMeta(
|
|
1572
1653
|
kfdSupportMatrixJson,
|
|
1573
1654
|
undefined,
|
|
@@ -1578,11 +1659,6 @@ export function collectGitHubReleasePassport({
|
|
|
1578
1659
|
.map((gateJson) => parseJsonInputWithMeta(gateJson, undefined, { cwd, label: "kfdProductGateJsons entry" }))
|
|
1579
1660
|
.filter((meta) => meta.value);
|
|
1580
1661
|
const basePassportMeta = parseJsonInputWithMeta(basePassportJson, undefined, { cwd, label: "basePassportJson" });
|
|
1581
|
-
const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
|
|
1582
|
-
command: kfd3ArtifactVerifyCommand,
|
|
1583
|
-
cwd,
|
|
1584
|
-
label: "KFD-3 artifact verify command",
|
|
1585
|
-
});
|
|
1586
1662
|
const invariantPassportMetas = (invariantPassportJsons || [])
|
|
1587
1663
|
.filter(Boolean)
|
|
1588
1664
|
.map((passportJson) => parseJsonInputWithMeta(passportJson, undefined, { cwd, label: "invariantPassportJsons entry" }))
|
|
@@ -1591,6 +1667,22 @@ export function collectGitHubReleasePassport({
|
|
|
1591
1667
|
command: invariantPassportCommand,
|
|
1592
1668
|
cwd,
|
|
1593
1669
|
label: "invariant passport command",
|
|
1670
|
+
acceptNonzeroJson: ({ status, value }) => {
|
|
1671
|
+
if (status !== 2) return false;
|
|
1672
|
+
try {
|
|
1673
|
+
const normalized = normalizeInvariantPassport(
|
|
1674
|
+
{ value, path: "", sha256: sha256Text(stableJson(value)) },
|
|
1675
|
+
invariantPassportMetas.length,
|
|
1676
|
+
);
|
|
1677
|
+
return normalized.verdict === "unqualified"
|
|
1678
|
+
&& normalized.coverage?.complete === false
|
|
1679
|
+
&& normalized.releaseClaims?.verdict === "unqualified"
|
|
1680
|
+
&& normalized.admission?.scope === "consumer-invariant-coverage"
|
|
1681
|
+
&& normalized.admission?.result === "passed";
|
|
1682
|
+
} catch {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1594
1686
|
});
|
|
1595
1687
|
if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
|
|
1596
1688
|
const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
|
|
@@ -1664,6 +1756,7 @@ export function collectGitHubReleasePassport({
|
|
|
1664
1756
|
const kfd1 = createKfd1ReleaseGateEvidence({
|
|
1665
1757
|
cwd,
|
|
1666
1758
|
artifactRoot: assetsDir ? path.resolve(cwd, assetsDir) : "",
|
|
1759
|
+
artifactSearchRoots: kfdArtifactSearchRoots.map((root) => path.resolve(cwd, root)),
|
|
1667
1760
|
artifacts: assets,
|
|
1668
1761
|
witnesses: kfd1WitnessMetas.map((meta) => meta.value),
|
|
1669
1762
|
});
|
|
@@ -2092,7 +2185,17 @@ function validateReleaseEvidenceContracts({
|
|
|
2092
2185
|
for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
|
|
2093
2186
|
try {
|
|
2094
2187
|
const policy = normalizeGitHubArtifactAttestationPolicy(value);
|
|
2095
|
-
|
|
2188
|
+
const release = passport?.release || {};
|
|
2189
|
+
const acceptedSourceShas = new Set([String(release.sourceSha || "").toLowerCase()]);
|
|
2190
|
+
if (
|
|
2191
|
+
release.treeEquivalent === true
|
|
2192
|
+
&& release.builtSourceTreeSha
|
|
2193
|
+
&& release.builtSourceTreeSha === release.promotionChannelTreeSha
|
|
2194
|
+
&& release.builtSourceSha
|
|
2195
|
+
) {
|
|
2196
|
+
acceptedSourceShas.add(String(release.builtSourceSha).toLowerCase());
|
|
2197
|
+
}
|
|
2198
|
+
if (!acceptedSourceShas.has(policy.caller.sourceSha)) {
|
|
2096
2199
|
issues.push(issue(
|
|
2097
2200
|
"error",
|
|
2098
2201
|
`githubArtifactAttestations[${index}].caller.sourceSha`,
|
|
@@ -2490,8 +2593,15 @@ function validatePassportTrustSections({ passport, issues }) {
|
|
|
2490
2593
|
].filter(Boolean));
|
|
2491
2594
|
for (const [index, entry] of (section.passports || []).entries()) {
|
|
2492
2595
|
const prefix = `invariantPassports.passports[${index}]`;
|
|
2493
|
-
if (entry.
|
|
2494
|
-
|
|
2596
|
+
if (entry.admission?.scope !== "consumer-invariant-coverage" || entry.admission?.result !== "passed") {
|
|
2597
|
+
issues.push(issue("error", `${prefix}.admission`, `${prefix}.admission must record passed consumer invariant coverage`));
|
|
2598
|
+
}
|
|
2599
|
+
if (entry.verdict !== "verified" && !(entry.verdict === "unqualified" && entry.releaseClaims?.verdict === "unqualified")) {
|
|
2600
|
+
issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified or preserve one explicit unqualified releaseClaims verdict`));
|
|
2601
|
+
}
|
|
2602
|
+
if (entry.coverage?.complete !== true && !hasCompleteInvariantCoverage(entry)) {
|
|
2603
|
+
issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage must be complete or prove every required invariant coordinate verified`));
|
|
2604
|
+
}
|
|
2495
2605
|
if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
|
|
2496
2606
|
if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
|
|
2497
2607
|
issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
|
|
@@ -173,7 +173,7 @@ async function main() {
|
|
|
173
173
|
artifactDigest: artifactSet.manifestSetDigest,
|
|
174
174
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
|
|
175
175
|
issuedAt: issuedAt.toISOString(),
|
|
176
|
-
expiresAt: new Date(issuedAt.getTime() +
|
|
176
|
+
expiresAt: new Date(issuedAt.getTime() + 15 * 60 * 1000).toISOString(),
|
|
177
177
|
qualification: {
|
|
178
178
|
required: qualificationRequired,
|
|
179
179
|
predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
|
|
@@ -453,7 +453,7 @@ function main() {
|
|
|
453
453
|
const reviewRules = (environmentState.protection_rules || []).filter((rule) => rule.type === "required_reviewers");
|
|
454
454
|
const runsOn = (block.match(/^\s{4}runs-on:\s*([^\n#]+)/m)?.[1] || "").trim().replace(/["']/g, "");
|
|
455
455
|
const observedAt = new Date();
|
|
456
|
-
const expiresAt = new Date(observedAt.getTime() +
|
|
456
|
+
const expiresAt = new Date(observedAt.getTime() + 15 * 60 * 1000);
|
|
457
457
|
const receipt = evaluatePublicationControlPlaneSnapshot({
|
|
458
458
|
repository,
|
|
459
459
|
workflowPath,
|