@kungfu-tech/buildchain 2.12.6 → 2.12.7-alpha.1
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 +7 -3
- package/bin/buildchain.mjs +78 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +137 -37
- package/dist/site/buildchain-site.json +68 -12
- package/dist/site/capability-registry.json +6 -5
- package/dist/site/cli-registry.json +36 -0
- package/dist/site/controller-registry.json +104 -13
- package/dist/site/kfd-claims.json +251 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +15 -1
- package/dist/site/node-api-registry.json +43 -4
- package/dist/site/page-registry.json +54 -7
- package/dist/site/public-surface-audit.json +150 -17
- package/dist/site/publication-authority-registry.json +754 -0
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +4 -0
- package/dist/site/site-manifest.json +14 -5
- package/dist/site/workflow-registry.json +108 -10
- package/docs/MAP.md +2 -1
- package/docs/publication-artifacts.md +12 -10
- package/docs/publication-authority.md +204 -0
- package/package.json +5 -1
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +79 -0
- package/packages/core/controller-evidence.js +7 -7
- package/packages/core/index.js +28 -0
- package/packages/core/publication-authority.js +725 -0
- package/packages/core/publication-control-plane-audit.js +133 -0
- package/scripts/assemble-self-publication-admission.mjs +182 -0
- package/scripts/audit-publication-control-plane.mjs +406 -0
- package/scripts/check-inventory.mjs +20 -2
- package/scripts/generate-site-bundle.mjs +16 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { evaluatePublicationControlPlaneSnapshot } from "../packages/core/publication-control-plane-audit.js";
|
|
7
|
+
|
|
8
|
+
function flag(name, fallback = "") {
|
|
9
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
10
|
+
return index === -1 ? fallback : String(process.argv[index + 1] || "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function commandJson(command, args, label) {
|
|
14
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 60_000 });
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
const category = /401|E401|unauthorized/i.test(result.stderr) ? "unauthorized" : "unavailable";
|
|
17
|
+
throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(result.stdout);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`${label} did not return JSON; publication control-plane audit fails closed`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function githubJson(apiPath, label) {
|
|
27
|
+
return commandJson("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], label);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function githubJsonOptional(apiPath, label, fallback) {
|
|
31
|
+
const result = spawnSync("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: 60_000,
|
|
34
|
+
});
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
if (/404|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
|
|
37
|
+
const category = /401|403|unauthorized|forbidden/i.test(`${result.stdout}\n${result.stderr}`) ? "unauthorized" : "unavailable";
|
|
38
|
+
throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(result.stdout);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(`${label} did not return JSON; publication control-plane audit fails closed`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function githubJsonReadLimited(apiPath, label, fallback) {
|
|
48
|
+
const result = spawnSync("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
timeout: 60_000,
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
if (/401|403|404|unauthorized|forbidden|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
|
|
54
|
+
throw new Error(`${label} is unavailable; publication control-plane audit fails closed`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(result.stdout);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`${label} did not return JSON; publication control-plane audit fails closed`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function rulesetIncludesBranch(ruleset, branch, defaultBranch) {
|
|
64
|
+
const includes = ruleset.conditions?.ref_name?.include || [];
|
|
65
|
+
const excludes = ruleset.conditions?.ref_name?.exclude || [];
|
|
66
|
+
const ref = `refs/heads/${branch}`;
|
|
67
|
+
const matches = (pattern) => (pattern === "~DEFAULT_BRANCH" && branch === defaultBranch) || pattern === branch || pattern === ref ||
|
|
68
|
+
pattern === "refs/heads/*" || (pattern.endsWith("*") && ref.startsWith(pattern.slice(0, -1)));
|
|
69
|
+
return includes.some(matches) && !excludes.some(matches);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeRulesetBranchPolicy(rulesets, branch, defaultBranch) {
|
|
73
|
+
const applicable = rulesets.filter((ruleset) =>
|
|
74
|
+
ruleset.enforcement === "active" && rulesetIncludesBranch(ruleset, branch, defaultBranch)
|
|
75
|
+
);
|
|
76
|
+
const rules = applicable.flatMap((ruleset) => ruleset.rules || []);
|
|
77
|
+
const pullRequest = rules.find((rule) => rule.type === "pull_request")?.parameters || {};
|
|
78
|
+
const requiredChecks = rules.find((rule) => rule.type === "required_status_checks")?.parameters || {};
|
|
79
|
+
const adminBypass = applicable.some((ruleset) => (ruleset.bypass_actors || []).some((actor) =>
|
|
80
|
+
actor.actor_type === "OrganizationAdmin" && actor.bypass_mode !== "pull_request"
|
|
81
|
+
));
|
|
82
|
+
return {
|
|
83
|
+
ref: branch,
|
|
84
|
+
policyMode: "ruleset",
|
|
85
|
+
strict: requiredChecks.strict_required_status_checks_policy === true,
|
|
86
|
+
requiredApprovals: Number(pullRequest.required_approving_review_count || 0),
|
|
87
|
+
requireConversationResolution: pullRequest.required_review_thread_resolution === true,
|
|
88
|
+
enforceAdmins: !adminBypass,
|
|
89
|
+
rulesetCount: applicable.length,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function jobBlock(workflowText, jobId) {
|
|
94
|
+
const lines = String(workflowText).split(/\r?\n/);
|
|
95
|
+
const jobsIndex = lines.findIndex((line) => /^jobs:\s*$/.test(line));
|
|
96
|
+
if (jobsIndex === -1) return "";
|
|
97
|
+
const start = lines.findIndex((line, index) => index > jobsIndex && new RegExp(`^ ${jobId}:\\s*$`).test(line));
|
|
98
|
+
if (start === -1) return "";
|
|
99
|
+
let end = lines.length;
|
|
100
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
101
|
+
if (/^ [A-Za-z0-9_-]+:\s*$/.test(lines[index])) {
|
|
102
|
+
end = index;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return lines.slice(start, end).join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function first(value, keys) {
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const parts = key.split(".");
|
|
112
|
+
let current = value;
|
|
113
|
+
for (const part of parts) current = current && typeof current === "object" ? current[part] : undefined;
|
|
114
|
+
if (current !== undefined && current !== null && current !== "") return current;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function npmTrustEntries(value) {
|
|
120
|
+
if (Array.isArray(value)) return value;
|
|
121
|
+
for (const key of ["relationships", "trustedPublishers", "trusted_publishers", "publishers", "items"]) {
|
|
122
|
+
if (Array.isArray(value?.[key])) return value[key];
|
|
123
|
+
}
|
|
124
|
+
return value && typeof value === "object" ? Object.values(value).filter((entry) => entry && typeof entry === "object") : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readSanitizedProviderAudit(filePath) {
|
|
128
|
+
if (!filePath) throw new Error("--provider-audit-json is required for oidc-role publisher mode");
|
|
129
|
+
const value = JSON.parse(fs.readFileSync(path.resolve(filePath), "utf8"));
|
|
130
|
+
const forbidden = /^(?:policy|policyDocument|token|secret|credentials)$/i;
|
|
131
|
+
const pending = [value];
|
|
132
|
+
while (pending.length) {
|
|
133
|
+
const current = pending.pop();
|
|
134
|
+
if (!current || typeof current !== "object") continue;
|
|
135
|
+
for (const [key, nested] of Object.entries(current)) {
|
|
136
|
+
if (forbidden.test(key)) throw new Error(`provider audit must be sanitized; forbidden field: ${key}`);
|
|
137
|
+
pending.push(nested);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readJsonValue(value, label) {
|
|
144
|
+
if (!value) return null;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(fs.existsSync(value) ? fs.readFileSync(path.resolve(value), "utf8") : value);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error(`${label} must be valid JSON or a path to a JSON file`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeNpmPublisher(value, { packageName, repository, workflowFilename, environment }) {
|
|
153
|
+
const entries = npmTrustEntries(value);
|
|
154
|
+
const normalized = entries.map((entry) => {
|
|
155
|
+
const actions = first(entry, ["allowedActions", "allowed_actions", "permissions", "actions"]);
|
|
156
|
+
const actionList = Array.isArray(actions) ? actions.map(String) : String(actions || "").split(/[\s,]+/).filter(Boolean);
|
|
157
|
+
return {
|
|
158
|
+
packageName: String(first(entry, ["packageName", "package", "package_name"]) || packageName),
|
|
159
|
+
provider: String(first(entry, ["provider", "providerType", "provider.type", "type"]) || "").toLowerCase(),
|
|
160
|
+
repository: String(first(entry, ["repository", "repo", "configuration.repository", "claims.repository"]) || ""),
|
|
161
|
+
workflowFilename: String(first(entry, ["workflowFilename", "workflow_file", "file", "configuration.workflowFilename", "claims.workflow"]) || "").split("/").pop(),
|
|
162
|
+
environment: String(first(entry, ["environment", "env", "configuration.environment", "claims.environment"]) || ""),
|
|
163
|
+
allowPublish: actionList.some((action) => /^(?:npm[ _-]?)?publish$/i.test(action)) || first(entry, ["allowPublish", "allow_publish"]) === true,
|
|
164
|
+
enforcement: "audited-control-plane",
|
|
165
|
+
authorizationDeferred: false,
|
|
166
|
+
configurationRead: true,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
return normalized.find((entry) =>
|
|
170
|
+
entry.packageName === packageName &&
|
|
171
|
+
/github/.test(entry.provider) &&
|
|
172
|
+
entry.repository === repository &&
|
|
173
|
+
entry.workflowFilename === workflowFilename &&
|
|
174
|
+
entry.environment === environment
|
|
175
|
+
) || {
|
|
176
|
+
packageName,
|
|
177
|
+
provider: "",
|
|
178
|
+
repository: "",
|
|
179
|
+
workflowFilename: "",
|
|
180
|
+
environment: "",
|
|
181
|
+
allowPublish: false,
|
|
182
|
+
enforcement: "audited-control-plane",
|
|
183
|
+
authorizationDeferred: false,
|
|
184
|
+
configurationRead: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function main() {
|
|
189
|
+
const repository = flag("repository");
|
|
190
|
+
const workflowRepository = flag("workflow-repository", repository);
|
|
191
|
+
const workflowPath = flag("workflow", ".github/workflows/release-candidate-promote.yml");
|
|
192
|
+
const workflowRef = flag("workflow-ref");
|
|
193
|
+
const publisherWorkflowPath = flag("publisher-workflow", workflowPath);
|
|
194
|
+
const jobId = flag("job", "promote");
|
|
195
|
+
const environment = flag("environment", "none");
|
|
196
|
+
const providerEnvironment = environment === "none" ? "" : environment;
|
|
197
|
+
const branch = flag("branch");
|
|
198
|
+
const sourceSha = flag("source-sha").toLowerCase();
|
|
199
|
+
const packageName = flag("package", "@kungfu-tech/buildchain");
|
|
200
|
+
const publisherMode = flag("publisher-mode", "npm-trusted-publisher");
|
|
201
|
+
if (!repository || !branch) throw new Error("--repository and --branch are required");
|
|
202
|
+
|
|
203
|
+
const encodedWorkflow = workflowPath.split("/").map(encodeURIComponent).join("/");
|
|
204
|
+
const workflowFile = githubJson(
|
|
205
|
+
`repos/${workflowRepository}/contents/${encodedWorkflow}${workflowRef ? `?ref=${encodeURIComponent(workflowRef)}` : ""}`,
|
|
206
|
+
"publication workflow source",
|
|
207
|
+
);
|
|
208
|
+
const workflowText = Buffer.from(String(workflowFile.content || ""), "base64").toString("utf8");
|
|
209
|
+
const block = jobBlock(workflowText, jobId);
|
|
210
|
+
if (!block) throw new Error(`publication workflow job is missing: ${workflowPath}#${jobId}`);
|
|
211
|
+
const jobsOffset = workflowText.search(/^jobs:\s*$/m);
|
|
212
|
+
const workflowHeader = jobsOffset === -1 ? workflowText : workflowText.slice(0, jobsOffset);
|
|
213
|
+
const explicitReadOnlyWorkflowPermissions = /^permissions:\s*\n(?:^[ \t]+[a-z-]+:\s*read\s*$\n?)+/m.test(workflowHeader) &&
|
|
214
|
+
!/^\s*[a-z-]+:\s*write\s*$/m.test(workflowHeader) &&
|
|
215
|
+
!/permissions\s*:\s*write-all/i.test(workflowHeader);
|
|
216
|
+
|
|
217
|
+
const repositoryState = githubJson(`repos/${repository}`, "repository metadata");
|
|
218
|
+
const branchState = githubJson(`repos/${repository}/branches/${encodeURIComponent(branch)}`, "branch summary");
|
|
219
|
+
const exactTransactionSource = /^[0-9a-f]{40}$/.test(sourceSha);
|
|
220
|
+
const protection = exactTransactionSource
|
|
221
|
+
? null
|
|
222
|
+
: githubJsonReadLimited(`repos/${repository}/branches/${encodeURIComponent(branch)}/protection`, "branch protection", null);
|
|
223
|
+
const rulesetList = githubJsonOptional(`repos/${repository}/rulesets?includes_parents=true&per_page=100`, "repository rulesets", []);
|
|
224
|
+
const rulesets = [];
|
|
225
|
+
for (const entry of Array.isArray(rulesetList) ? rulesetList : []) {
|
|
226
|
+
if (!entry?.id) continue;
|
|
227
|
+
rulesets.push(githubJson(`repos/${repository}/rulesets/${entry.id}`, `repository ruleset ${entry.id}`));
|
|
228
|
+
}
|
|
229
|
+
const environmentDeclared = /^ {4}environment\s*:/m.test(block);
|
|
230
|
+
const environmentState = environment === "none"
|
|
231
|
+
? {}
|
|
232
|
+
: githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}`, "publication Environment");
|
|
233
|
+
const deploymentBranches = environment !== "none" && environmentState.deployment_branch_policy?.custom_branch_policies === true
|
|
234
|
+
? githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}/deployment-branch-policies?per_page=100`, "Environment deployment branch policy")
|
|
235
|
+
: { branch_policies: [] };
|
|
236
|
+
const oidc = githubJson(`repos/${repository}/actions/oidc/customization/sub`, "OIDC subject policy");
|
|
237
|
+
if (!["npm-trusted-publisher", "github-token", "oidc-role"].includes(publisherMode)) {
|
|
238
|
+
throw new Error(`unsupported --publisher-mode: ${publisherMode}`);
|
|
239
|
+
}
|
|
240
|
+
const longLivedWorkflowCredentialPresent = (
|
|
241
|
+
/^\s*(?:NODE_AUTH_TOKEN|NPM_TOKEN|npm-token|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\s*:/im.test(block) ||
|
|
242
|
+
/\$\{\{\s*secrets\.(?:NODE_AUTH_TOKEN|NPM_TOKEN|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\b/im.test(block)
|
|
243
|
+
);
|
|
244
|
+
const rulesetBranchPolicy = normalizeRulesetBranchPolicy(rulesets, branch, repositoryState.default_branch);
|
|
245
|
+
let branchPolicy;
|
|
246
|
+
if (protection) {
|
|
247
|
+
branchPolicy = {
|
|
248
|
+
ref: branch,
|
|
249
|
+
policyMode: "branch-protection",
|
|
250
|
+
strict: protection.required_status_checks?.strict === true,
|
|
251
|
+
requiredApprovals: protection.required_pull_request_reviews?.required_approving_review_count || 0,
|
|
252
|
+
requireConversationResolution: protection.required_conversation_resolution?.enabled === true,
|
|
253
|
+
enforceAdmins: protection.enforce_admins?.enabled === true,
|
|
254
|
+
observedRulesetCount: rulesets.length,
|
|
255
|
+
};
|
|
256
|
+
} else if (rulesetBranchPolicy.rulesetCount > 0) {
|
|
257
|
+
branchPolicy = rulesetBranchPolicy;
|
|
258
|
+
} else {
|
|
259
|
+
if (!/^[0-9a-f]{40}$/.test(sourceSha)) {
|
|
260
|
+
throw new Error("--source-sha is required when detailed branch policy is not readable");
|
|
261
|
+
}
|
|
262
|
+
const pullRequests = githubJson(`repos/${repository}/commits/${sourceSha}/pulls`, "source pull-request lineage");
|
|
263
|
+
const mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
|
|
264
|
+
entry?.merged_at &&
|
|
265
|
+
entry.merge_commit_sha === sourceSha &&
|
|
266
|
+
entry.base?.ref === branch &&
|
|
267
|
+
entry.head?.repo?.full_name === repository
|
|
268
|
+
);
|
|
269
|
+
const reviews = mergedPullRequest
|
|
270
|
+
? githubJson(`repos/${repository}/pulls/${mergedPullRequest.number}/reviews?per_page=100`, "source pull-request reviews")
|
|
271
|
+
: [];
|
|
272
|
+
const latestReviews = new Map();
|
|
273
|
+
for (const review of Array.isArray(reviews) ? reviews : []) {
|
|
274
|
+
const login = String(review?.user?.login || "");
|
|
275
|
+
if (login) latestReviews.set(login, review);
|
|
276
|
+
}
|
|
277
|
+
const independentApprovals = [...latestReviews.values()].filter((review) =>
|
|
278
|
+
review.state === "APPROVED" && review.user?.login !== mergedPullRequest?.user?.login
|
|
279
|
+
);
|
|
280
|
+
const checkRuns = githubJson(`repos/${repository}/commits/${sourceSha}/check-runs?per_page=100`, "source check runs");
|
|
281
|
+
const requiredStatusCheckPolicy = branchState.protection?.required_status_checks || {};
|
|
282
|
+
const requiredStatusChecks = requiredStatusCheckPolicy.contexts || [];
|
|
283
|
+
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) => entry.context === "check");
|
|
284
|
+
branchPolicy = {
|
|
285
|
+
ref: branch,
|
|
286
|
+
policyMode: "provider-enforced-transaction",
|
|
287
|
+
protected: branchState.protected === true,
|
|
288
|
+
enforcementLevel: branchState.protection?.required_status_checks?.enforcement_level || "",
|
|
289
|
+
requiredStatusChecks,
|
|
290
|
+
requiredCheckPassed: (checkRuns.check_runs || []).some((entry) =>
|
|
291
|
+
entry.name === "check" &&
|
|
292
|
+
entry.conclusion === "success" &&
|
|
293
|
+
(!requiredCheckSource?.app_id || entry.app?.id === requiredCheckSource.app_id)
|
|
294
|
+
),
|
|
295
|
+
requiredCheckAppId: requiredCheckSource?.app_id || 0,
|
|
296
|
+
sourceSha,
|
|
297
|
+
headSha: String(branchState.commit?.sha || "").toLowerCase(),
|
|
298
|
+
mergedPullRequest: Boolean(mergedPullRequest),
|
|
299
|
+
pullRequestNumber: mergedPullRequest?.number || 0,
|
|
300
|
+
baseRef: mergedPullRequest?.base?.ref || "",
|
|
301
|
+
headRepository: mergedPullRequest?.head?.repo?.full_name || "",
|
|
302
|
+
approvalCount: independentApprovals.length,
|
|
303
|
+
independentApproval: independentApprovals.length > 0,
|
|
304
|
+
configurationRead: false,
|
|
305
|
+
evidenceSource: "public-provider-transaction",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
let publisher;
|
|
309
|
+
if (publisherMode === "npm-trusted-publisher") {
|
|
310
|
+
const trust = readJsonValue(flag("npm-trust-json"), "--npm-trust-json");
|
|
311
|
+
publisher = trust
|
|
312
|
+
? normalizeNpmPublisher(trust, {
|
|
313
|
+
packageName,
|
|
314
|
+
repository,
|
|
315
|
+
workflowFilename: path.basename(publisherWorkflowPath),
|
|
316
|
+
environment: providerEnvironment,
|
|
317
|
+
})
|
|
318
|
+
: {
|
|
319
|
+
packageName,
|
|
320
|
+
provider: "github",
|
|
321
|
+
repository,
|
|
322
|
+
workflowFilename: path.basename(publisherWorkflowPath),
|
|
323
|
+
environment: providerEnvironment,
|
|
324
|
+
allowPublish: false,
|
|
325
|
+
enforcement: "provider-at-transaction",
|
|
326
|
+
authorizationDeferred: true,
|
|
327
|
+
configurationRead: false,
|
|
328
|
+
};
|
|
329
|
+
} else if (publisherMode === "github-token") {
|
|
330
|
+
publisher = {
|
|
331
|
+
provider: "github-token",
|
|
332
|
+
repository,
|
|
333
|
+
workflowPath,
|
|
334
|
+
permissionScoped: /^\s{6}contents:\s*write\s*$/m.test(block) && !/^\s{2}contents:\s*write\s*$/m.test(workflowText),
|
|
335
|
+
};
|
|
336
|
+
} else {
|
|
337
|
+
publisher = readSanitizedProviderAudit(flag("provider-audit-json"));
|
|
338
|
+
}
|
|
339
|
+
publisher.longLivedWorkflowCredentialPresent = longLivedWorkflowCredentialPresent;
|
|
340
|
+
|
|
341
|
+
const reviewRules = (environmentState.protection_rules || []).filter((rule) => rule.type === "required_reviewers");
|
|
342
|
+
const runsOn = (block.match(/^\s{4}runs-on:\s*([^\n#]+)/m)?.[1] || "").trim().replace(/["']/g, "");
|
|
343
|
+
const observedAt = new Date();
|
|
344
|
+
const expiresAt = new Date(observedAt.getTime() + 10 * 60 * 1000);
|
|
345
|
+
const receipt = evaluatePublicationControlPlaneSnapshot({
|
|
346
|
+
repository,
|
|
347
|
+
workflowPath,
|
|
348
|
+
publisherWorkflowPath,
|
|
349
|
+
environment,
|
|
350
|
+
branch,
|
|
351
|
+
packageName,
|
|
352
|
+
publisherMode,
|
|
353
|
+
observedAt: observedAt.toISOString(),
|
|
354
|
+
expiresAt: expiresAt.toISOString(),
|
|
355
|
+
snapshot: {
|
|
356
|
+
actions: {
|
|
357
|
+
defaultWorkflowPermissions: explicitReadOnlyWorkflowPermissions ? "read" : "unqualified",
|
|
358
|
+
canApprovePullRequestReviews: false,
|
|
359
|
+
evidenceSource: "exact-workflow-source",
|
|
360
|
+
},
|
|
361
|
+
branch: branchPolicy,
|
|
362
|
+
environment: {
|
|
363
|
+
name: environment === "none" ? "none" : environmentState.name || environment,
|
|
364
|
+
declared: environmentDeclared,
|
|
365
|
+
exists: Boolean(environmentState.id || environmentState.node_id),
|
|
366
|
+
protected: (environmentState.protection_rules || []).length > 0 ||
|
|
367
|
+
environmentState.deployment_branch_policy?.protected_branches === true ||
|
|
368
|
+
(deploymentBranches.branch_policies || []).length > 0,
|
|
369
|
+
reviewRequired: reviewRules.length > 0,
|
|
370
|
+
preventSelfReview: reviewRules.some((rule) => rule.prevent_self_review === true),
|
|
371
|
+
},
|
|
372
|
+
oidc: {
|
|
373
|
+
workflowPath: publisherWorkflowPath,
|
|
374
|
+
environment: providerEnvironment,
|
|
375
|
+
idTokenJobScoped: /^\s{6}id-token:\s*write\s*$/m.test(block) && !/^\s{2}id-token:\s*write\s*$/m.test(workflowText),
|
|
376
|
+
githubTokenJobScoped: /^\s{6}contents:\s*write\s*$/m.test(block) && !/^\s{2}contents:\s*write\s*$/m.test(workflowText),
|
|
377
|
+
longLivedCredentialPresent: publisher.longLivedWorkflowCredentialPresent,
|
|
378
|
+
useDefaultSubject: oidc.use_default === true,
|
|
379
|
+
includedClaims: oidc.include_claim_keys || [],
|
|
380
|
+
},
|
|
381
|
+
publisher,
|
|
382
|
+
runner: {
|
|
383
|
+
class: runsOn === "ubuntu-24.04" ? "ephemeral" : "unqualified",
|
|
384
|
+
label: runsOn,
|
|
385
|
+
githubHosted: runsOn === "ubuntu-24.04",
|
|
386
|
+
selfHostedAuthorized: /self-hosted/i.test(runsOn),
|
|
387
|
+
evidenceSource: "exact-workflow-job",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
const output = flag("output");
|
|
392
|
+
const serialized = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
393
|
+
if (output) fs.writeFileSync(path.resolve(output), serialized);
|
|
394
|
+
else process.stdout.write(serialized);
|
|
395
|
+
const failed = receipt.facts.filter((entry) => entry.status !== "pass").map((entry) => entry.id);
|
|
396
|
+
if (failed.length && !process.argv.includes("--allow-nonqualifying")) {
|
|
397
|
+
throw new Error(`publication control-plane audit is non-qualifying: ${failed.join(", ")}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
main();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(`publication control-plane audit: ${error.message}`);
|
|
405
|
+
process.exitCode = 1;
|
|
406
|
+
}
|
|
@@ -84,6 +84,8 @@ const requiredPaths = [
|
|
|
84
84
|
".github/workflows/npm-publish.yml",
|
|
85
85
|
".github/workflows/paper-release.yml",
|
|
86
86
|
".github/workflows/binary-distribution.yml",
|
|
87
|
+
".github/workflows/.binary-release-assets.yml",
|
|
88
|
+
".github/workflows/binary-release-assets.yml",
|
|
87
89
|
".github/workflows/verify.yml",
|
|
88
90
|
".github/workflows/.build.yml",
|
|
89
91
|
".github/workflows/.gate-profile.yml",
|
|
@@ -761,6 +763,7 @@ if (commonJsSourcePattern.test(standaloneBinaryScript)) {
|
|
|
761
763
|
const npmPublishWorkflow = fs.readFileSync(path.join(root, ".github/workflows/npm-publish.yml"), "utf8");
|
|
762
764
|
const buildchainRefPromotionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/buildchain-ref-promotion.yml"), "utf8");
|
|
763
765
|
const binaryDistributionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/binary-distribution.yml"), "utf8");
|
|
766
|
+
const binaryReleaseAssetsWorkflow = fs.readFileSync(path.join(root, ".github/workflows/.binary-release-assets.yml"), "utf8");
|
|
764
767
|
const selfHostedRunnerSmokeWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-hosted-runner-smoke.yml"), "utf8");
|
|
765
768
|
const npmDryRunScript = fs.readFileSync(path.join(root, "scripts/npm-publish-dry-run.mjs"), "utf8");
|
|
766
769
|
const npmPublishTransactionScript = fs.readFileSync(path.join(root, "scripts/npm-publish-transaction.mjs"), "utf8");
|
|
@@ -968,14 +971,29 @@ for (const requiredSnippet of [
|
|
|
968
971
|
"verify artifact",
|
|
969
972
|
"scripts/create-release-bundle.mjs",
|
|
970
973
|
"buildchain-release-bundle",
|
|
971
|
-
"scripts/ensure-github-release.mjs",
|
|
972
974
|
"--impact-json .buildchain/release-evidence/authoritative-release-state-impact.json",
|
|
973
|
-
"gh release upload",
|
|
974
975
|
]) {
|
|
975
976
|
if (!binaryDistributionWorkflow.includes(requiredSnippet)) {
|
|
976
977
|
throw new Error(`binary distribution workflow missing required snippet: ${requiredSnippet}`);
|
|
977
978
|
}
|
|
978
979
|
}
|
|
980
|
+
for (const requiredSnippet of [
|
|
981
|
+
"uses: ./.github/workflows/.publication-authority.yml",
|
|
982
|
+
"environment: buildchain-release-assets",
|
|
983
|
+
"needs: publication-authority",
|
|
984
|
+
"scripts/ensure-github-release.mjs",
|
|
985
|
+
"gh release upload",
|
|
986
|
+
"capability.artifactDigest !== actualArtifact",
|
|
987
|
+
]) {
|
|
988
|
+
if (!binaryReleaseAssetsWorkflow.includes(requiredSnippet)) {
|
|
989
|
+
throw new Error(`binary release assets workflow missing required snippet: ${requiredSnippet}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (const forbiddenSnippet of ["contents: write", "id-token: write", "gh release upload"]) {
|
|
993
|
+
if (binaryDistributionWorkflow.includes(forbiddenSnippet)) {
|
|
994
|
+
throw new Error(`binary distribution evidence workflow must not carry product authority: ${forbiddenSnippet}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
979
997
|
for (const forbiddenSnippet of [
|
|
980
998
|
"gh release create",
|
|
981
999
|
]) {
|
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { createBuildchainContractWorld } from "../packages/core/buildchain-contract.js";
|
|
8
8
|
import { createControllerRegistry } from "../packages/core/controller-evidence.js";
|
|
9
|
+
import { createBuildchainPublicationAuthorityRegistry } from "../packages/core/buildchain-publication-authority.js";
|
|
9
10
|
import {
|
|
10
11
|
BUILDCHAIN_AGENT_MANUALS,
|
|
11
12
|
createBuildchainKfdClaimRegistry,
|
|
@@ -317,6 +318,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
317
318
|
"product-mechanism": { capabilityGroup: "getting-started", audience: ["agent", "maintainer"], maturity: "stable", order: 30 },
|
|
318
319
|
cli: { capabilityGroup: "api-cli-reference", audience: ["agent", "developer"], maturity: "stable", order: 40 },
|
|
319
320
|
"release-passport": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 100 },
|
|
321
|
+
"publication-authority": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 105 },
|
|
320
322
|
"controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
|
|
321
323
|
"binary-distribution": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 110 },
|
|
322
324
|
"publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
|
|
@@ -373,6 +375,8 @@ function pageCapabilityMeta(relPath, category) {
|
|
|
373
375
|
|
|
374
376
|
function cliCommandMeta(id) {
|
|
375
377
|
const map = new Map(Object.entries({
|
|
378
|
+
audit: { group: "release-passport-trust", purpose: "Inspect read-only publication authority audit commands." },
|
|
379
|
+
"audit-publication-control-plane": { group: "release-passport-trust", purpose: "Read GitHub publication controls, bind provider-enforced npm identity, and emit a sanitized expiring receipt." },
|
|
376
380
|
badges: { group: "distribution-indexes", purpose: "Inspect README badge command families." },
|
|
377
381
|
"badges-bundle": { group: "distribution-indexes", purpose: "Generate or verify the combined KFD and Release Passport badge bundle." },
|
|
378
382
|
"badges-readme": { group: "distribution-indexes", purpose: "Generate or verify managed README badge blocks." },
|
|
@@ -380,6 +384,9 @@ function cliCommandMeta(id) {
|
|
|
380
384
|
"build-facts": { group: "observability-diagnostics", purpose: "Collect and verify Git source, version, module output, product artifact, and legacy Kungfu buildinfo facts." },
|
|
381
385
|
collect: { group: "release-passport-trust", purpose: "Inspect release evidence collection command families." },
|
|
382
386
|
"collect-github-release": { group: "release-passport-trust", purpose: "Collect GitHub Release assets into a release passport." },
|
|
387
|
+
create: { group: "release-passport-trust", purpose: "Create canonical sealed publication evidence documents." },
|
|
388
|
+
"create-publication-admission": { group: "release-passport-trust", purpose: "Create a canonical short-lived publication admission envelope from exact consumer bindings." },
|
|
389
|
+
"create-runner-provenance": { group: "release-passport-trust", purpose: "Create runner provenance evidence with an explicit qualification floor." },
|
|
383
390
|
diagnostics: { group: "observability-diagnostics", purpose: "Inspect diagnostics command families." },
|
|
384
391
|
"diagnostics-summary": { group: "observability-diagnostics", purpose: "Summarize diagnostics artifacts into JSON and cross-platform lifecycle timing tables." },
|
|
385
392
|
doctor: { group: "getting-started", purpose: "Report local integration readiness." },
|
|
@@ -450,6 +457,7 @@ function cliCommandMeta(id) {
|
|
|
450
457
|
"verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
|
|
451
458
|
"verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
|
|
452
459
|
"verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
|
|
460
|
+
"verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
|
|
453
461
|
"verify-release-passport": { group: "release-passport-trust", purpose: "Fail closed unless a release passport and its evidence are complete." },
|
|
454
462
|
version: { group: "getting-started", purpose: "Print the package or embedded binary version." },
|
|
455
463
|
"web-surface": { group: "site-and-propagation", purpose: "Plan, verify, and apply Buildchain web-surface deployments." },
|
|
@@ -473,6 +481,9 @@ function nodeApiMeta(exportName) {
|
|
|
473
481
|
"./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
|
|
474
482
|
"./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
|
|
475
483
|
"./publication-package": { group: "reusable-build", summary: "Publication npm package synthesis APIs for Buildchain-managed paper release presets." },
|
|
484
|
+
"./publication-authority": { group: "release-passport-trust", summary: "Sealed publication authority registry, runner provenance, control-plane audit, admission, and independent verification APIs." },
|
|
485
|
+
"./publication-control-plane-audit": { group: "release-passport-trust", summary: "Read-only publication control-plane snapshot evaluation APIs." },
|
|
486
|
+
"./buildchain-publication-authority": { group: "release-passport-trust", summary: "Buildchain-owned closed-world publication authority descriptor registry." },
|
|
476
487
|
"./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
|
|
477
488
|
"./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
|
|
478
489
|
"./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
|
|
@@ -867,6 +878,7 @@ function buildSiteBundle() {
|
|
|
867
878
|
})),
|
|
868
879
|
};
|
|
869
880
|
const controllerRegistry = createControllerRegistry({ workflows: workflowRegistry.workflows });
|
|
881
|
+
const publicationAuthorityRegistry = createBuildchainPublicationAuthorityRegistry({ root });
|
|
870
882
|
const publicSurfaceAudit = collectPublicSurfaceReverseAudit({
|
|
871
883
|
root,
|
|
872
884
|
cliRegistry,
|
|
@@ -955,6 +967,7 @@ function buildSiteBundle() {
|
|
|
955
967
|
"node-api-registry.json",
|
|
956
968
|
"workflow-registry.json",
|
|
957
969
|
"controller-registry.json",
|
|
970
|
+
"publication-authority-registry.json",
|
|
958
971
|
"public-surface-audit.json",
|
|
959
972
|
"release-model.json",
|
|
960
973
|
"artifact-schemas.json",
|
|
@@ -1036,6 +1049,7 @@ function buildSiteBundle() {
|
|
|
1036
1049
|
"node-api-registry.json",
|
|
1037
1050
|
"workflow-registry.json",
|
|
1038
1051
|
"controller-registry.json",
|
|
1052
|
+
"publication-authority-registry.json",
|
|
1039
1053
|
"public-surface-audit.json",
|
|
1040
1054
|
"release-model.json",
|
|
1041
1055
|
"artifact-schemas.json",
|
|
@@ -1063,6 +1077,7 @@ function buildSiteBundle() {
|
|
|
1063
1077
|
"node-api-registry.json",
|
|
1064
1078
|
"workflow-registry.json",
|
|
1065
1079
|
"controller-registry.json",
|
|
1080
|
+
"publication-authority-registry.json",
|
|
1066
1081
|
"public-surface-audit.json",
|
|
1067
1082
|
"artifact-schemas.json",
|
|
1068
1083
|
"buildchain-contract.json",
|
|
@@ -1231,6 +1246,7 @@ function buildSiteBundle() {
|
|
|
1231
1246
|
"node-api-registry.json": nodeApiRegistry,
|
|
1232
1247
|
"workflow-registry.json": workflowRegistry,
|
|
1233
1248
|
"controller-registry.json": controllerRegistry,
|
|
1249
|
+
"publication-authority-registry.json": publicationAuthorityRegistry,
|
|
1234
1250
|
"public-surface-audit.json": publicSurfaceAudit,
|
|
1235
1251
|
"release-model.json": releaseModel,
|
|
1236
1252
|
"artifact-schemas.json": artifactSchemas,
|