@kungfu-tech/buildchain 4.0.0 → 4.0.1-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/AGENTS.md +13 -5
- package/actions/promote-buildchain-ref/README.md +11 -6
- package/architecture/internal-capabilities.json +11 -2
- package/architecture/maintainability-policy.json +149 -17
- package/architecture/v4-floating-consumer-policy.json +75 -0
- package/architecture/v4-floating-consumer-policy.md +32 -0
- package/architecture/v4-runtime-ref-resume-authority.json +64 -0
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +53 -0
- package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +105 -0
- package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
- package/dist/site/buildchain-contract.json +101 -31
- package/dist/site/buildchain-site.json +73 -27
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +62 -6
- package/dist/site/kfd-claims.json +68 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +799 -127
- package/dist/site/page-registry.json +63 -17
- package/dist/site/public-surface-audit.json +49 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +46 -18
- package/docs/MAP.md +2 -0
- package/docs/dev-delivery-warrant.md +19 -1
- package/docs/lifecycle-protocol.md +41 -0
- package/docs/node-api-reference.md +207 -134
- package/docs/reusable-build-surface.md +41 -0
- package/docs/runtime-train-validation.md +10 -0
- package/docs/v4-runtime-ref-resume-authority.md +69 -0
- package/docs/versioning.md +1 -0
- package/package.json +6 -4
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -1
- package/packages/core/dev-delivery-proof.js +37 -3
- package/packages/core/dev-delivery-warrant.js +3 -3
- package/packages/core/index.js +2 -0
- package/packages/core/publication-authority.js +5 -5
- package/packages/core/release-candidate.js +79 -19
- package/packages/core/release-passport.js +239 -33
- package/packages/core/v4-floating-consumer-evidence.js +324 -0
- package/packages/core/v4-floating-consumer-policy.js +446 -0
- package/packages/core/v4-floating-consumer-release-passport.js +126 -0
- package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
- package/packages/core/v4-runtime-selector-persistence.js +228 -0
- package/packages/core/workflow-yaml-contract.js +48 -0
- package/scripts/audit-publication-control-plane.mjs +31 -31
- package/scripts/check-inventory.mjs +1 -1
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +198 -0
- package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
- package/scripts/dev-delivery-source-proof-reuse.mjs +631 -0
- package/scripts/dev-pr-auto-merge.mjs +37 -48
- package/scripts/dev-pr-delivery-warrant.mjs +205 -2
- package/scripts/dev-pr-prequeue-guard.mjs +399 -0
- package/scripts/ensure-github-release.mjs +3 -3
- package/scripts/generate-channel-build-workflow.mjs +3 -0
- package/scripts/generate-channel-promotion-workflow.mjs +112 -12
- package/scripts/generate-release-candidate-passport.mjs +41 -33
- package/scripts/init-repo.mjs +5 -1
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/npm-publish-transaction.mjs +101 -89
- package/scripts/resume-from-candidate-run.mjs +11 -14
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/v4-consumer-policy.mjs +231 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const V4_RUNTIME_PERSISTENCE_SCAN_CONTRACT =
|
|
6
|
+
"kungfu-buildchain-v4-runtime-persistence-scan/v1";
|
|
7
|
+
|
|
8
|
+
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
9
|
+
const TRAIN_V4_REF = /^train\/v4\/v4\.\d+\/[A-Za-z0-9._/-]+$/u;
|
|
10
|
+
const AUTHORITY_V4_REF = /^authority\/v4\/v4\.\d+\/[A-Za-z0-9._/-]+$/u;
|
|
11
|
+
const RUNTIME_SELECTOR =
|
|
12
|
+
/(?:buildchain[-_. ]?(?:ref|runtime)|runtime[-_. ]?sha|resume[-_. ]?buildchain)/iu;
|
|
13
|
+
const EXACT_SHA_LITERAL = /\b[0-9a-f]{40}\b/giu;
|
|
14
|
+
const EXTERNAL_AUTHORITY = Object.freeze([
|
|
15
|
+
["oidc", /(?:id-token\s*:\s*write|oidc)/iu],
|
|
16
|
+
["iam", /(?:role-to-assume|aws[-_. ]?(?:role|iam)|\biam\b)/iu],
|
|
17
|
+
["repository-variable", /\$\{\{\s*vars\./u],
|
|
18
|
+
["secret", /\$\{\{\s*secrets\./u],
|
|
19
|
+
["environment", /\$\{\{\s*env\./u],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function stableJson(value) {
|
|
23
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
24
|
+
if (value && typeof value === "object") {
|
|
25
|
+
return `{${Object.keys(value)
|
|
26
|
+
.sort()
|
|
27
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
28
|
+
.join(",")}}`;
|
|
29
|
+
}
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function documentRoot(value) {
|
|
34
|
+
return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeRef(value) {
|
|
38
|
+
return String(value || "")
|
|
39
|
+
.trim()
|
|
40
|
+
.replace(/^refs\/(?:heads|tags)\//u, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transientSelector(value) {
|
|
44
|
+
const normalized = normalizeRef(value);
|
|
45
|
+
return (
|
|
46
|
+
EXACT_SHA.test(normalized.toLowerCase()) ||
|
|
47
|
+
TRAIN_V4_REF.test(normalized) ||
|
|
48
|
+
AUTHORITY_V4_REF.test(normalized) ||
|
|
49
|
+
/\$\{\{\s*(?:vars|secrets|env)\./u.test(value)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listFiles(rootPath, relative, output) {
|
|
54
|
+
const directory = path.join(rootPath, relative);
|
|
55
|
+
if (!fs.existsSync(directory)) return;
|
|
56
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
57
|
+
const child = path.join(relative, entry.name);
|
|
58
|
+
if (entry.isDirectory()) listFiles(rootPath, child, output);
|
|
59
|
+
else if (entry.isFile() && /\.(?:json|toml|ya?ml)$/u.test(entry.name)) {
|
|
60
|
+
output.push(child.split(path.sep).join("/"));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inputDefaultFailures(lines, sourcePath) {
|
|
66
|
+
const failures = [];
|
|
67
|
+
let current;
|
|
68
|
+
for (const [index, line] of lines.entries()) {
|
|
69
|
+
const key = line.match(/^(\s*)([a-z0-9-]+):\s*(?:#.*)?$/u);
|
|
70
|
+
if (key) {
|
|
71
|
+
const name = key[2];
|
|
72
|
+
if (RUNTIME_SELECTOR.test(name))
|
|
73
|
+
current = { indent: key[1].length, name };
|
|
74
|
+
else if (current && key[1].length <= current.indent) current = undefined;
|
|
75
|
+
}
|
|
76
|
+
if (!current) continue;
|
|
77
|
+
const defaultMatch = line.match(/^\s*default:\s*(.*?)\s*(?:#.*)?$/u);
|
|
78
|
+
if (!defaultMatch) continue;
|
|
79
|
+
const value = defaultMatch[1]
|
|
80
|
+
.replace(/^(?:["'])(.*)(?:["'])$/u, "$1")
|
|
81
|
+
.trim();
|
|
82
|
+
if (transientSelector(value)) {
|
|
83
|
+
failures.push({
|
|
84
|
+
code: "persistent-runtime-default",
|
|
85
|
+
path: sourcePath,
|
|
86
|
+
line: index + 1,
|
|
87
|
+
selector: current.name,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return failures;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function jsonSelectorFailures(value, sourcePath, trail = []) {
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.flatMap((entry, index) =>
|
|
97
|
+
jsonSelectorFailures(entry, sourcePath, [...trail, index]),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!value || typeof value !== "object") return [];
|
|
101
|
+
return Object.entries(value).flatMap(([key, entry]) => {
|
|
102
|
+
const entryTrail = [...trail, key];
|
|
103
|
+
const selected =
|
|
104
|
+
(RUNTIME_SELECTOR.test(key) ||
|
|
105
|
+
trail.some((part) => RUNTIME_SELECTOR.test(String(part)))) &&
|
|
106
|
+
typeof entry === "string" &&
|
|
107
|
+
transientSelector(entry)
|
|
108
|
+
? [
|
|
109
|
+
{
|
|
110
|
+
code: "persistent-runtime-json-value",
|
|
111
|
+
path: sourcePath,
|
|
112
|
+
jsonPointer: `/${entryTrail.map((part) => String(part).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`,
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
: [];
|
|
116
|
+
return [
|
|
117
|
+
...selected,
|
|
118
|
+
...jsonSelectorFailures(entry, sourcePath, entryTrail),
|
|
119
|
+
];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function inspectSource({
|
|
124
|
+
resolvedRoot,
|
|
125
|
+
sourcePath,
|
|
126
|
+
files,
|
|
127
|
+
failures,
|
|
128
|
+
authorityUsage,
|
|
129
|
+
}) {
|
|
130
|
+
const file = path.resolve(resolvedRoot, sourcePath);
|
|
131
|
+
if (!file.startsWith(`${resolvedRoot}${path.sep}`) || !fs.existsSync(file)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`runtime persistence scan path is missing or escapes root: ${sourcePath}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const text = fs.readFileSync(file, "utf8");
|
|
137
|
+
const lines = text.split(/\r?\n/u);
|
|
138
|
+
files.push({ path: sourcePath, digest: documentRoot(text) });
|
|
139
|
+
failures.push(...inputDefaultFailures(lines, sourcePath));
|
|
140
|
+
if (sourcePath.endsWith(".json")) {
|
|
141
|
+
try {
|
|
142
|
+
failures.push(...jsonSelectorFailures(JSON.parse(text), sourcePath));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
failures.push({
|
|
145
|
+
code: "runtime-selector-json-invalid",
|
|
146
|
+
path: sourcePath,
|
|
147
|
+
errorRoot: documentRoot(String(error?.message || error)),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const [index, line] of lines.entries()) {
|
|
152
|
+
if (RUNTIME_SELECTOR.test(line)) {
|
|
153
|
+
for (const match of line.matchAll(EXACT_SHA_LITERAL)) {
|
|
154
|
+
failures.push({
|
|
155
|
+
code: "persistent-runtime-exact-sha",
|
|
156
|
+
path: sourcePath,
|
|
157
|
+
line: index + 1,
|
|
158
|
+
selectorRoot: documentRoot(match[0].toLowerCase()),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (/\$\{\{\s*(?:vars|secrets|env)\./u.test(line)) {
|
|
162
|
+
failures.push({
|
|
163
|
+
code: "persistent-runtime-external-indirection",
|
|
164
|
+
path: sourcePath,
|
|
165
|
+
line: index + 1,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const [authorityClass, pattern] of EXTERNAL_AUTHORITY) {
|
|
170
|
+
if (pattern.test(line)) {
|
|
171
|
+
authorityUsage.push({
|
|
172
|
+
class: authorityClass,
|
|
173
|
+
path: sourcePath,
|
|
174
|
+
line: index + 1,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function scanV4RuntimeSelectorPersistence({
|
|
182
|
+
root: callerRoot = process.cwd(),
|
|
183
|
+
paths = undefined,
|
|
184
|
+
} = {}) {
|
|
185
|
+
const resolvedRoot = path.resolve(callerRoot);
|
|
186
|
+
const sourcePaths = [];
|
|
187
|
+
if (paths) sourcePaths.push(...paths);
|
|
188
|
+
else {
|
|
189
|
+
for (const relative of [
|
|
190
|
+
".github/workflows",
|
|
191
|
+
".github/actions",
|
|
192
|
+
".buildchain",
|
|
193
|
+
]) {
|
|
194
|
+
listFiles(resolvedRoot, relative, sourcePaths);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const excluded = new Set([
|
|
198
|
+
".buildchain/contract-lock.json",
|
|
199
|
+
".buildchain/alpha-contract-lock.json",
|
|
200
|
+
]);
|
|
201
|
+
const files = [];
|
|
202
|
+
const failures = [];
|
|
203
|
+
const authorityUsage = [];
|
|
204
|
+
for (const sourcePath of [...new Set(sourcePaths)].sort()) {
|
|
205
|
+
if (!excluded.has(sourcePath)) {
|
|
206
|
+
inspectSource({
|
|
207
|
+
resolvedRoot,
|
|
208
|
+
sourcePath,
|
|
209
|
+
files,
|
|
210
|
+
failures,
|
|
211
|
+
authorityUsage,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const payload = {
|
|
216
|
+
schemaVersion: 1,
|
|
217
|
+
contract: V4_RUNTIME_PERSISTENCE_SCAN_CONTRACT,
|
|
218
|
+
status: failures.length === 0 ? "passed" : "rejected",
|
|
219
|
+
files,
|
|
220
|
+
authorityUsage: authorityUsage.sort((left, right) =>
|
|
221
|
+
stableJson(left).localeCompare(stableJson(right)),
|
|
222
|
+
),
|
|
223
|
+
failures: failures.sort((left, right) =>
|
|
224
|
+
stableJson(left).localeCompare(stableJson(right)),
|
|
225
|
+
),
|
|
226
|
+
};
|
|
227
|
+
return { ...payload, root: documentRoot(payload) };
|
|
228
|
+
}
|
|
@@ -263,6 +263,54 @@ export function parseWorkflowCallJobs(text) {
|
|
|
263
263
|
.filter((job) => job.uses);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
function blockScalarIndent(lines, lineIndex, parentIndent) {
|
|
267
|
+
for (let index = lineIndex + 1; index < lines.length; index += 1) {
|
|
268
|
+
if (!lines[index].trim()) continue;
|
|
269
|
+
const indent = indentation(lines[index]);
|
|
270
|
+
return indent > parentIndent ? indent : null;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Enumerate semantic `uses` mapping nodes without mistaking shell/heredoc text
|
|
277
|
+
* inside YAML block scalars for workflow syntax. The returned scalar retains
|
|
278
|
+
* expression-vs-literal classification used by the reusable-call contract.
|
|
279
|
+
*/
|
|
280
|
+
export function parseYamlUses(text) {
|
|
281
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
282
|
+
const uses = [];
|
|
283
|
+
let block = null;
|
|
284
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
285
|
+
const line = lines[index];
|
|
286
|
+
const indent = indentation(line);
|
|
287
|
+
if (block) {
|
|
288
|
+
if (!line.trim()) continue;
|
|
289
|
+
if (indent >= block.contentIndent) continue;
|
|
290
|
+
block = null;
|
|
291
|
+
}
|
|
292
|
+
const mapping = line.match(
|
|
293
|
+
/^(\s*)(?:-\s+)?([A-Za-z0-9_.-]+):(?:\s*(.*))?$/,
|
|
294
|
+
);
|
|
295
|
+
if (!mapping) continue;
|
|
296
|
+
const value = stripComment(mapping[3] || "");
|
|
297
|
+
if (/^[>|][+-]?$/.test(value)) {
|
|
298
|
+
const contentIndent = blockScalarIndent(lines, index, indent);
|
|
299
|
+
if (contentIndent !== null) block = { contentIndent };
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (mapping[2] !== "uses") continue;
|
|
303
|
+
uses.push({
|
|
304
|
+
line: index + 1,
|
|
305
|
+
column: mapping[1].length + 1,
|
|
306
|
+
raw: value,
|
|
307
|
+
scalar: scalar(value),
|
|
308
|
+
value: unquote(value),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return uses;
|
|
312
|
+
}
|
|
313
|
+
|
|
266
314
|
export function parseWorkflowDocument(text) {
|
|
267
315
|
return {
|
|
268
316
|
triggers: parseTriggers(String(text || "").split(/\r?\n/)),
|
|
@@ -16,12 +16,21 @@ function flag(name, fallback = "") {
|
|
|
16
16
|
return index === -1 ? fallback : String(process.argv[index + 1] || "");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function commandJson(command, args, label) {
|
|
20
|
-
const
|
|
19
|
+
function commandJson(command, args, label, { publicReadFallback = false } = {}) {
|
|
20
|
+
const options = {
|
|
21
21
|
encoding: "utf8",
|
|
22
22
|
timeout: 60_000,
|
|
23
23
|
maxBuffer: GITHUB_JSON_MAX_BUFFER,
|
|
24
|
-
}
|
|
24
|
+
};
|
|
25
|
+
let result = spawnSyncCommand(command, args, options);
|
|
26
|
+
const fallbackToken = String(process.env.BUILDCHAIN_GITHUB_PUBLIC_READ_TOKEN || "");
|
|
27
|
+
const primaryToken = String(process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "");
|
|
28
|
+
if (result.status !== 0 && publicReadFallback && fallbackToken && fallbackToken !== primaryToken) {
|
|
29
|
+
result = spawnSyncCommand(command, args, {
|
|
30
|
+
...options,
|
|
31
|
+
env: { ...process.env, GH_TOKEN: fallbackToken, GITHUB_TOKEN: fallbackToken },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
25
34
|
if (result.status !== 0) {
|
|
26
35
|
const category = /401|E401|unauthorized/i.test(result.stderr) ? "unauthorized" : "unavailable";
|
|
27
36
|
throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
|
|
@@ -33,18 +42,22 @@ function commandJson(command, args, label) {
|
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
function githubJson(apiPath, label) {
|
|
37
|
-
return commandJson("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], label);
|
|
45
|
+
function githubJson(apiPath, label, { publicReadFallback = false } = {}) {
|
|
46
|
+
return commandJson("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], label, { publicReadFallback });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function githubPublicJson(apiPath, label) {
|
|
50
|
+
return githubJson(apiPath, label, { publicReadFallback: true });
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
function githubJsonOptional(apiPath, label, fallback) {
|
|
53
|
+
function githubJsonOptional(apiPath, label, fallback, fallbackPattern = /404|not found/i) {
|
|
41
54
|
const result = spawnSyncCommand("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
|
|
42
55
|
encoding: "utf8",
|
|
43
56
|
timeout: 60_000,
|
|
44
57
|
maxBuffer: GITHUB_JSON_MAX_BUFFER,
|
|
45
58
|
});
|
|
46
59
|
if (result.status !== 0) {
|
|
47
|
-
if (
|
|
60
|
+
if (fallbackPattern.test(`${result.stdout}\n${result.stderr}`)) return fallback;
|
|
48
61
|
const category = /401|403|unauthorized|forbidden/i.test(`${result.stdout}\n${result.stderr}`) ? "unauthorized" : "unavailable";
|
|
49
62
|
throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
|
|
50
63
|
}
|
|
@@ -56,20 +69,7 @@ function githubJsonOptional(apiPath, label, fallback) {
|
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
function githubJsonReadLimited(apiPath, label, fallback) {
|
|
59
|
-
|
|
60
|
-
encoding: "utf8",
|
|
61
|
-
timeout: 60_000,
|
|
62
|
-
maxBuffer: GITHUB_JSON_MAX_BUFFER,
|
|
63
|
-
});
|
|
64
|
-
if (result.status !== 0) {
|
|
65
|
-
if (/401|403|404|unauthorized|forbidden|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
|
|
66
|
-
throw new Error(`${label} is unavailable; publication control-plane audit fails closed`);
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(result.stdout);
|
|
70
|
-
} catch {
|
|
71
|
-
throw new Error(`${label} did not return JSON; publication control-plane audit fails closed`);
|
|
72
|
-
}
|
|
72
|
+
return githubJsonOptional(apiPath, label, fallback, /401|403|404|unauthorized|forbidden|not found/i);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function rulesetIncludesBranch(ruleset, branch, defaultBranch) {
|
|
@@ -218,7 +218,7 @@ function main() {
|
|
|
218
218
|
if (!repository || !branch) throw new Error("--repository and --branch are required");
|
|
219
219
|
|
|
220
220
|
const encodedWorkflow = workflowPath.split("/").map(encodeURIComponent).join("/");
|
|
221
|
-
const workflowFile =
|
|
221
|
+
const workflowFile = githubPublicJson(
|
|
222
222
|
`repos/${workflowRepository}/contents/${encodedWorkflow}${workflowRef ? `?ref=${encodeURIComponent(workflowRef)}` : ""}`,
|
|
223
223
|
"publication workflow source",
|
|
224
224
|
);
|
|
@@ -231,8 +231,8 @@ function main() {
|
|
|
231
231
|
!/^\s*[a-z-]+:\s*write\s*$/m.test(workflowHeader) &&
|
|
232
232
|
!/permissions\s*:\s*write-all/i.test(workflowHeader);
|
|
233
233
|
|
|
234
|
-
const repositoryState =
|
|
235
|
-
const branchState =
|
|
234
|
+
const repositoryState = githubPublicJson(`repos/${repository}`, "repository metadata");
|
|
235
|
+
const branchState = githubPublicJson(`repos/${repository}/branches/${encodeURIComponent(branch)}`, "branch summary");
|
|
236
236
|
const exactTransactionSource = /^[0-9a-f]{40}$/.test(sourceSha);
|
|
237
237
|
const protection = exactTransactionSource
|
|
238
238
|
? null
|
|
@@ -290,7 +290,7 @@ function main() {
|
|
|
290
290
|
if (!/^[0-9a-f]{40}$/.test(sourceSha)) {
|
|
291
291
|
throw new Error("--source-sha is required when detailed branch policy is not readable");
|
|
292
292
|
}
|
|
293
|
-
const sourceCommit =
|
|
293
|
+
const sourceCommit = githubPublicJson(`repos/${repository}/commits/${sourceSha}`, "source commit");
|
|
294
294
|
const sourceParents = Array.isArray(sourceCommit.parents) ? sourceCommit.parents.map((entry) => String(entry?.sha || "").toLowerCase()) : [];
|
|
295
295
|
const sourceChangedPaths = Array.isArray(sourceCommit.files) ? sourceCommit.files.map((entry) => String(entry?.filename || "")) : [];
|
|
296
296
|
const sourceMessage = String(sourceCommit.commit?.message || "").split("\n", 1)[0];
|
|
@@ -305,7 +305,7 @@ function main() {
|
|
|
305
305
|
const branchHeadSha = String(branchState.commit?.sha || "").toLowerCase();
|
|
306
306
|
const sourceComparison = sourceSha === branchHeadSha
|
|
307
307
|
? null
|
|
308
|
-
:
|
|
308
|
+
: githubPublicJson(
|
|
309
309
|
`repos/${repository}/compare/${sourceSha}...${branchHeadSha}`,
|
|
310
310
|
"source protected-branch lineage",
|
|
311
311
|
);
|
|
@@ -313,7 +313,7 @@ function main() {
|
|
|
313
313
|
sourceComparison?.status === "ahead" &&
|
|
314
314
|
String(sourceComparison?.merge_base_commit?.sha || "").toLowerCase() === sourceSha
|
|
315
315
|
);
|
|
316
|
-
let pullRequests =
|
|
316
|
+
let pullRequests = githubPublicJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "source pull-request lineage");
|
|
317
317
|
let mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
|
|
318
318
|
entry?.merged_at &&
|
|
319
319
|
(
|
|
@@ -324,7 +324,7 @@ function main() {
|
|
|
324
324
|
entry.head?.repo?.full_name === repository
|
|
325
325
|
);
|
|
326
326
|
if (!mergedPullRequest && allowReleaseReconciliation) {
|
|
327
|
-
const packageFile =
|
|
327
|
+
const packageFile = githubPublicJson(
|
|
328
328
|
`repos/${repository}/contents/package.json?ref=${encodeURIComponent(sourceSha)}`,
|
|
329
329
|
"release reconciliation package metadata",
|
|
330
330
|
);
|
|
@@ -340,7 +340,7 @@ function main() {
|
|
|
340
340
|
});
|
|
341
341
|
if (releaseReconciliation.qualifying) {
|
|
342
342
|
authorizationSha = parentSha;
|
|
343
|
-
pullRequests =
|
|
343
|
+
pullRequests = githubPublicJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "release parent pull-request lineage");
|
|
344
344
|
mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
|
|
345
345
|
entry?.merged_at &&
|
|
346
346
|
entry.merge_commit_sha === authorizationSha &&
|
|
@@ -350,7 +350,7 @@ function main() {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
const reviews = mergedPullRequest
|
|
353
|
-
?
|
|
353
|
+
? githubPublicJson(`repos/${repository}/pulls/${mergedPullRequest.number}/reviews?per_page=100`, "source pull-request reviews")
|
|
354
354
|
: [];
|
|
355
355
|
const latestReviews = new Map();
|
|
356
356
|
for (const review of Array.isArray(reviews) ? reviews : []) {
|
|
@@ -378,7 +378,7 @@ function main() {
|
|
|
378
378
|
(prefixedRequiredStatusChecks.length === 1 ? prefixedRequiredStatusChecks[0] : requiredStatusCheck);
|
|
379
379
|
const requiredStatusCheckMatchCount = exactRequiredStatusCheck ? 1 : prefixedRequiredStatusChecks.length;
|
|
380
380
|
const checkRuns = /^[0-9a-f]{40}$/.test(pullRequestHeadSha)
|
|
381
|
-
?
|
|
381
|
+
? githubPublicJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?check_name=${encodeURIComponent(resolvedRequiredStatusCheck)}&filter=latest&per_page=100`, "merged pull-request required check runs")
|
|
382
382
|
: { check_runs: [] };
|
|
383
383
|
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) =>
|
|
384
384
|
entry.context === resolvedRequiredStatusCheck
|
|
@@ -1019,7 +1019,7 @@ for (const requiredSnippet of [
|
|
|
1019
1019
|
"resume-buildchain-runtime-sha:",
|
|
1020
1020
|
"github-release: true",
|
|
1021
1021
|
"release-passport-buildchain-self-kfd: true",
|
|
1022
|
-
|
|
1022
|
+
"artifact-patterns: ${{ startsWith(github.event.workflow_run.head_branch || inputs['target-ref'], 'alpha/') && 'buildchain-package-*' || '' }}",
|
|
1023
1023
|
"release-passport-impact-json: .buildchain/release-impact.json",
|
|
1024
1024
|
]) {
|
|
1025
1025
|
if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseYamlUses } from "../packages/core/workflow-yaml-contract.js";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
|
|
9
|
+
|
|
10
|
+
function fail(message) {
|
|
11
|
+
throw new Error(`v4-floating-consumer-policy-contract: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function workflowJobBlock(source, job) {
|
|
15
|
+
return (
|
|
16
|
+
source.match(
|
|
17
|
+
new RegExp(
|
|
18
|
+
`^ ${job}:\\n([\\s\\S]*?)(?=^ [a-z0-9_-]+:|(?![\\s\\S]))`,
|
|
19
|
+
"mu",
|
|
20
|
+
),
|
|
21
|
+
)?.[1] || ""
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertTrustGatedJobs(source, jobs) {
|
|
26
|
+
for (const job of jobs) {
|
|
27
|
+
const block = workflowJobBlock(source, job);
|
|
28
|
+
if (!block.includes("- trust-gate")) {
|
|
29
|
+
fail(`.build.yml job ${job} is not directly gated by trust-gate`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function jobDependsOn(source, job, dependency) {
|
|
35
|
+
const block = workflowJobBlock(source, job);
|
|
36
|
+
return new RegExp(
|
|
37
|
+
`^ needs:\\s*(?:${dependency}|\\[[^\\]]*\\b${dependency}\\b[^\\]]*\\])\\s*$`,
|
|
38
|
+
"mu",
|
|
39
|
+
).test(block);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertOrdered(relative, markers) {
|
|
43
|
+
const source = read(relative);
|
|
44
|
+
let cursor = -1;
|
|
45
|
+
for (const marker of markers) {
|
|
46
|
+
const next = source.indexOf(marker, cursor + 1);
|
|
47
|
+
if (next < 0) fail(`${relative} is missing ${marker}`);
|
|
48
|
+
if (next <= cursor)
|
|
49
|
+
fail(`${relative} does not enforce ${markers.join(" -> ")}`);
|
|
50
|
+
cursor = next;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertPromotionCertificationWiring(source) {
|
|
55
|
+
for (const marker of [
|
|
56
|
+
".buildchain/runtime/promotion-shell/scripts/v4-consumer-policy.mjs certify",
|
|
57
|
+
'--caller-root "${{ github.workspace }}"',
|
|
58
|
+
'--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
|
|
59
|
+
'--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
|
|
60
|
+
"release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
|
|
61
|
+
]) {
|
|
62
|
+
if (!source.includes(marker)) {
|
|
63
|
+
fail(`promotion certification is missing ${marker}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertPersistedSelectors() {
|
|
69
|
+
const offenders = [];
|
|
70
|
+
const workflows = fs
|
|
71
|
+
.readdirSync(path.join(root, ".github/workflows"))
|
|
72
|
+
.filter((name) => /\.ya?ml$/u.test(name))
|
|
73
|
+
.sort();
|
|
74
|
+
for (const name of workflows) {
|
|
75
|
+
const relative = `.github/workflows/${name}`;
|
|
76
|
+
for (const node of parseYamlUses(read(relative))) {
|
|
77
|
+
const match = node.value.match(
|
|
78
|
+
/^kungfu-systems\/buildchain\/(.+)@(.+)$/u,
|
|
79
|
+
);
|
|
80
|
+
if (!match) continue;
|
|
81
|
+
const selector = match[2];
|
|
82
|
+
const isV4 =
|
|
83
|
+
selector === "v4" ||
|
|
84
|
+
selector === "v4-alpha" ||
|
|
85
|
+
/^v4(?:[./-]|$)/u.test(selector) ||
|
|
86
|
+
/^[0-9a-f]{40}$/iu.test(selector) ||
|
|
87
|
+
selector.includes("${{");
|
|
88
|
+
if (isV4 && !["v4", "v4-alpha"].includes(selector)) {
|
|
89
|
+
offenders.push(`${relative}:${node.line} @${selector}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (offenders.length)
|
|
94
|
+
fail(
|
|
95
|
+
`persisted v4 selectors must be v4 or v4-alpha: ${offenders.join(", ")}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function checkV4FloatingConsumerPolicyContract() {
|
|
100
|
+
const policy = JSON.parse(
|
|
101
|
+
read("architecture/v4-floating-consumer-policy.json"),
|
|
102
|
+
);
|
|
103
|
+
if (policy.contract !== "kungfu-buildchain-v4-floating-consumer-policy/v1") {
|
|
104
|
+
fail("architecture policy contract is missing");
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
policy.contractLocks?.selectedLockMustBindResolvedWorkflowShell !== true
|
|
108
|
+
) {
|
|
109
|
+
fail("contract lock must bind the visible workflow shell");
|
|
110
|
+
}
|
|
111
|
+
assertPersistedSelectors();
|
|
112
|
+
assertOrdered(".github/workflows/.build.yml", [
|
|
113
|
+
"Enforce v4 floating consumer policy",
|
|
114
|
+
"Validate consumer package manager contract",
|
|
115
|
+
]);
|
|
116
|
+
assertTrustGatedJobs(read(".github/workflows/.build.yml"), [
|
|
117
|
+
"resolve-source",
|
|
118
|
+
"resolve-contract",
|
|
119
|
+
"controller-plan",
|
|
120
|
+
"artifact-transfer",
|
|
121
|
+
"build-native",
|
|
122
|
+
"build-linux-container",
|
|
123
|
+
"summarize",
|
|
124
|
+
]);
|
|
125
|
+
assertOrdered(".github/workflows/publication-artifact.yml", [
|
|
126
|
+
"Enforce v4 floating consumer policy",
|
|
127
|
+
"Resolve controller identities",
|
|
128
|
+
"Install Buildchain runtime dependencies",
|
|
129
|
+
]);
|
|
130
|
+
const stageCanary = read(".github/workflows/v4-stage-capsule-canary.yml");
|
|
131
|
+
if (
|
|
132
|
+
!stageCanary.includes("consumer-admission:") ||
|
|
133
|
+
!stageCanary.includes("needs: consumer-admission")
|
|
134
|
+
) {
|
|
135
|
+
fail(
|
|
136
|
+
"Stage Capsule qualification is not transitively gated by consumer admission",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const promotion = read(".github/workflows/release-candidate-promote.yml");
|
|
140
|
+
if (
|
|
141
|
+
!promotion.includes("consumer-admission:") ||
|
|
142
|
+
!jobDependsOn(promotion, "alpha", "consumer-admission") ||
|
|
143
|
+
!jobDependsOn(promotion, "stable", "consumer-admission")
|
|
144
|
+
) {
|
|
145
|
+
fail(
|
|
146
|
+
"release candidate promotion is not transitively gated by consumer admission",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
assertPromotionCertificationWiring(
|
|
150
|
+
read(".github/workflows/.release-candidate-promote.yml"),
|
|
151
|
+
);
|
|
152
|
+
for (const [relative, marker] of [
|
|
153
|
+
["packages/core/release-candidate.js", "consumerPolicy"],
|
|
154
|
+
["packages/core/release-passport.js", "v4ConsumerPolicy"],
|
|
155
|
+
[
|
|
156
|
+
"actions/promote-buildchain-ref/action.yml",
|
|
157
|
+
"release-passport-v4-consumer-policy-certification-json",
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"actions/promote-buildchain-ref/action.yml",
|
|
161
|
+
"release-passport-v4-consumer-policy-certification-root",
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
".github/workflows/.release-candidate-promote.yml",
|
|
165
|
+
"v4-policy-certification",
|
|
166
|
+
],
|
|
167
|
+
]) {
|
|
168
|
+
if (!read(relative).includes(marker)) {
|
|
169
|
+
fail(
|
|
170
|
+
`${relative} does not bind v4 consumer policy evidence marker ${marker}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const agents = read("AGENTS.md");
|
|
175
|
+
for (const invariant of [
|
|
176
|
+
"source-persisted exact commit SHA",
|
|
177
|
+
"matching stable and alpha contract locks",
|
|
178
|
+
"trusted non-persistent runtime input",
|
|
179
|
+
]) {
|
|
180
|
+
if (!agents.includes(invariant))
|
|
181
|
+
fail(`AGENTS.md is missing invariant: ${invariant}`);
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, entrypoints: policy.scope.publicWorkflowEntrypoints };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
process.argv[1] &&
|
|
188
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
189
|
+
) {
|
|
190
|
+
try {
|
|
191
|
+
process.stdout.write(
|
|
192
|
+
`${JSON.stringify(checkV4FloatingConsumerPolicyContract(), null, 2)}\n`,
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(error.message);
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -10,9 +10,7 @@ const DEFAULT_ROOT = path.resolve(
|
|
|
10
10
|
);
|
|
11
11
|
const CALLER_PATH = ".github/workflows/v4-public-consumer-dogfood.yml";
|
|
12
12
|
const REUSABLE_PATH = ".github/workflows/v4-stage-capsule-canary.yml";
|
|
13
|
-
export const
|
|
14
|
-
"train/v4/v4.0/public-consumer-self-dogfood";
|
|
15
|
-
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
13
|
+
export const V4_PUBLIC_DOGFOOD_ALPHA_REF = "v4-alpha";
|
|
16
14
|
const PRIVATE_CONSUMER = ["buildchain", "self", "dogfood"].join("-");
|
|
17
15
|
const PRIVATE_SHADOW = ["kungfu", "shadow"].join("-");
|
|
18
16
|
|
|
@@ -139,13 +137,8 @@ function assertArchitecture(root) {
|
|
|
139
137
|
);
|
|
140
138
|
const dogfood = architecture.publicConsumerDogfood;
|
|
141
139
|
const validationRef = dogfood?.validationRef;
|
|
142
|
-
if (
|
|
143
|
-
validationRef
|
|
144
|
-
!EXACT_SHA.test(String(validationRef))
|
|
145
|
-
)
|
|
146
|
-
fail(
|
|
147
|
-
"architecture validationRef must be the exact public train or a protected commit SHA",
|
|
148
|
-
);
|
|
140
|
+
if (validationRef !== V4_PUBLIC_DOGFOOD_ALPHA_REF)
|
|
141
|
+
fail("architecture validationRef must use the floating v4-alpha channel");
|
|
149
142
|
const caller = read(root, CALLER_PATH);
|
|
150
143
|
if (caller !== expectedV4PublicDogfoodWorkflow(validationRef))
|
|
151
144
|
fail(`${CALLER_PATH} must remain the exact thin public consumer caller`);
|
|
@@ -168,7 +161,7 @@ function assertArchitecture(root) {
|
|
|
168
161
|
dogfood.relativeOrSelfInvocationAllowed !== false ||
|
|
169
162
|
dogfood.directQualificationInvocationAllowed !== false ||
|
|
170
163
|
dogfood.candidateBranchOverrideAllowed !== false ||
|
|
171
|
-
dogfood.recursionRecovery !== "
|
|
164
|
+
dogfood.recursionRecovery !== "floating-selector-with-trusted-runtime-input"
|
|
172
165
|
)
|
|
173
166
|
fail(
|
|
174
167
|
"architecture publicConsumerDogfood contract is incomplete or widened",
|
|
@@ -226,6 +219,8 @@ function assertPolicySources(root) {
|
|
|
226
219
|
"No agent may add or restore a relative/self reusable-workflow call",
|
|
227
220
|
"never solve recursion with an internal exception",
|
|
228
221
|
"scripts/check-v4-public-dogfood-contract.mjs",
|
|
222
|
+
"source-persisted exact commit SHA",
|
|
223
|
+
"v4-alpha",
|
|
229
224
|
])
|
|
230
225
|
if (!agents.includes(invariant))
|
|
231
226
|
fail(`AGENTS.md is missing invariant: ${invariant}`);
|