@kungfu-tech/buildchain 2.14.18-alpha.1 → 2.14.18-alpha.11
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/README.md +1 -0
- package/actions/promote-buildchain-ref/README.md +33 -30
- package/bin/buildchain.mjs +16 -4
- package/dist/site/buildchain-contract.json +86 -31
- package/dist/site/buildchain-site.json +88 -27
- package/dist/site/capability-registry.json +6 -5
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/controller-registry.json +50 -6
- package/dist/site/kfd-claims.json +110 -21
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +21 -6
- package/dist/site/node-api-registry.json +19 -6
- package/dist/site/page-registry.json +70 -17
- package/dist/site/public-surface-audit.json +130 -23
- package/dist/site/publication-authority-registry.json +21 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +18 -10
- package/dist/site/workflow-registry.json +62 -16
- package/docs/MAP.md +3 -1
- package/docs/github-governance-authority.md +267 -0
- package/docs/lifecycle-protocol.md +10 -10
- package/docs/publish-transaction.md +73 -4
- package/docs/release-governance.md +38 -24
- package/docs/reusable-build-surface.md +29 -11
- package/docs/web-surface-deployments.md +46 -0
- package/package.json +2 -1
- package/packages/core/buildchain-kfd-claims.js +2 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/controller-evidence.js +1 -1
- package/packages/core/github-governance-authority.js +1312 -0
- package/packages/core/index.js +22 -0
- package/packages/core/publication-control-plane-audit.js +4 -1
- package/scripts/audit-github-governance.mjs +474 -0
- package/scripts/audit-publication-control-plane.mjs +41 -5
- package/scripts/check-inventory.mjs +8 -0
- package/scripts/generate-channel-promotion-workflow.mjs +3 -0
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/installer-publication.mjs +351 -0
- package/scripts/publication-commit-evidence.mjs +158 -0
- package/scripts/reconcile-github-governance.mjs +549 -0
- package/scripts/release-candidate-resolver.mjs +53 -0
- package/scripts/web-surface-core.mjs +127 -14
package/packages/core/index.js
CHANGED
|
@@ -201,6 +201,28 @@ export {
|
|
|
201
201
|
} from "./buildchain-publication-authority.js";
|
|
202
202
|
|
|
203
203
|
export { evaluatePublicationControlPlaneSnapshot } from "./publication-control-plane-audit.js";
|
|
204
|
+
export {
|
|
205
|
+
BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY,
|
|
206
|
+
BUILDCHAIN_GITHUB_GOVERNANCE_PROTECTED_PATHS,
|
|
207
|
+
GITHUB_GOVERNANCE_AUTHORITY_CONTRACT,
|
|
208
|
+
GITHUB_GOVERNANCE_RECEIPT_CONTRACT,
|
|
209
|
+
GITHUB_GOVERNANCE_ROLLOUT_CONTRACT,
|
|
210
|
+
GITHUB_GOVERNANCE_RULESET_ROLLOUT_CONTRACT,
|
|
211
|
+
codeownersForPath,
|
|
212
|
+
compileEffectiveGithubGovernancePolicy,
|
|
213
|
+
createBuildchainGithubGovernanceAuthority,
|
|
214
|
+
createGithubGovernanceRolloutPlan,
|
|
215
|
+
createGithubRulesetBypassRolloutPlan,
|
|
216
|
+
createGithubRulesetGovernanceRolloutPlan,
|
|
217
|
+
evaluateCodeownersAuthority,
|
|
218
|
+
evaluateGithubGovernanceSnapshot,
|
|
219
|
+
githubGovernanceDigest,
|
|
220
|
+
normalizeGithubBranchProtectionSnapshot,
|
|
221
|
+
normalizeGithubRulesetSnapshot,
|
|
222
|
+
parseCodeowners,
|
|
223
|
+
resolveGithubGovernanceTargetPolicy,
|
|
224
|
+
verifyGithubGovernanceReceipt,
|
|
225
|
+
} from "./github-governance-authority.js";
|
|
204
226
|
|
|
205
227
|
export {
|
|
206
228
|
ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
|
|
@@ -122,6 +122,8 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
122
122
|
Array.isArray(branchPolicy.releaseReconciliation.changedPaths) &&
|
|
123
123
|
branchPolicy.releaseReconciliation.changedPaths.length > 0
|
|
124
124
|
);
|
|
125
|
+
const sourceBranchBindingPass = branchPolicy.sourceSha === branchPolicy.headSha ||
|
|
126
|
+
branchPolicy.sourceContainedInBranch === true;
|
|
125
127
|
const providerTransactionBranchPass = branchPolicy.ref === branch &&
|
|
126
128
|
branchPolicy.policyMode === "provider-enforced-transaction" &&
|
|
127
129
|
branchPolicy.protected === true &&
|
|
@@ -132,7 +134,7 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
132
134
|
branchPolicy.requiredCheckPassed === true &&
|
|
133
135
|
/^[0-9a-f]{40}$/i.test(String(branchPolicy.requiredCheckSha || "")) &&
|
|
134
136
|
branchPolicy.requiredCheckSha === branchPolicy.pullRequestHeadSha &&
|
|
135
|
-
|
|
137
|
+
sourceBranchBindingPass &&
|
|
136
138
|
sourceAuthorizationPass &&
|
|
137
139
|
branchPolicy.mergedPullRequest === true &&
|
|
138
140
|
branchPolicy.baseRef === branch &&
|
|
@@ -158,6 +160,7 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
158
160
|
environmentPolicy.declared === true &&
|
|
159
161
|
environmentPolicy.exists === true &&
|
|
160
162
|
environmentPolicy.protected === true &&
|
|
163
|
+
environmentPolicy.branchAuthorized === true &&
|
|
161
164
|
(environmentPolicy.reviewRequired !== true || environmentPolicy.preventSelfReview === true),
|
|
162
165
|
environmentPolicy,
|
|
163
166
|
),
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY,
|
|
10
|
+
compileEffectiveGithubGovernancePolicy,
|
|
11
|
+
evaluateCodeownersAuthority,
|
|
12
|
+
evaluateGithubGovernanceSnapshot,
|
|
13
|
+
githubGovernanceDigest,
|
|
14
|
+
githubRepositoryIdentityRoot,
|
|
15
|
+
resolveGithubGovernanceTargetRefs,
|
|
16
|
+
} from "../packages/core/github-governance-authority.js";
|
|
17
|
+
|
|
18
|
+
const CODEOWNERS_PATHS = [
|
|
19
|
+
".github/CODEOWNERS",
|
|
20
|
+
"CODEOWNERS",
|
|
21
|
+
"docs/CODEOWNERS",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function flag(args, name, fallback = "") {
|
|
25
|
+
const index = args.indexOf(`--${name}`);
|
|
26
|
+
return index === -1 ? fallback : String(args[index + 1] || "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasFlag(args, name) {
|
|
30
|
+
return args.includes(`--${name}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function command(commandName, commandArgs, label) {
|
|
34
|
+
const result = spawnSync(commandName, commandArgs, {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
timeout: 60_000,
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
GH_TOKEN: process.env.GH_TOKEN || "",
|
|
40
|
+
GITHUB_TOKEN: process.env.GITHUB_TOKEN || "",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
44
|
+
const statusMatch = output.match(/\b(?:HTTP|status:?)\s*(\d{3})\b/i);
|
|
45
|
+
const status = Number(statusMatch?.[1] || (result.status === 0 ? 200 : 0));
|
|
46
|
+
if (result.status !== 0) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
status,
|
|
50
|
+
reason: /401|unauthorized/i.test(output)
|
|
51
|
+
? "unauthorized"
|
|
52
|
+
: /403|forbidden/i.test(output)
|
|
53
|
+
? "forbidden"
|
|
54
|
+
: /404|not found/i.test(output)
|
|
55
|
+
? "not-found"
|
|
56
|
+
: "unavailable",
|
|
57
|
+
label,
|
|
58
|
+
data: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
status,
|
|
65
|
+
reason: "read",
|
|
66
|
+
label,
|
|
67
|
+
data: JSON.parse(result.stdout),
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
status,
|
|
73
|
+
reason: "invalid-json",
|
|
74
|
+
label,
|
|
75
|
+
data: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function githubApi(route, label) {
|
|
81
|
+
return command(
|
|
82
|
+
"gh",
|
|
83
|
+
[
|
|
84
|
+
"api",
|
|
85
|
+
route,
|
|
86
|
+
"-H",
|
|
87
|
+
"Accept: application/vnd.github+json",
|
|
88
|
+
"-H",
|
|
89
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
90
|
+
],
|
|
91
|
+
label,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolvedAbsence(result) {
|
|
96
|
+
return result.ok || result.reason === "not-found";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function decodeContent(result) {
|
|
100
|
+
if (!result.ok) return "";
|
|
101
|
+
return Buffer.from(String(result.data?.content || ""), "base64").toString("utf8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readCodeowners(repository, ref) {
|
|
105
|
+
const attempts = CODEOWNERS_PATHS.map((candidatePath) => {
|
|
106
|
+
const encoded = candidatePath.split("/").map(encodeURIComponent).join("/");
|
|
107
|
+
return {
|
|
108
|
+
path: candidatePath,
|
|
109
|
+
result: githubApi(
|
|
110
|
+
`repos/${repository}/contents/${encoded}?ref=${encodeURIComponent(ref)}`,
|
|
111
|
+
`${repository} ${candidatePath}`,
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const found = attempts.find((attempt) => attempt.result.ok);
|
|
116
|
+
return {
|
|
117
|
+
path: found?.path || "",
|
|
118
|
+
source: found ? decodeContent(found.result) : "",
|
|
119
|
+
readable: attempts.every((attempt) => resolvedAbsence(attempt.result)),
|
|
120
|
+
attempts: attempts.map((attempt) => ({
|
|
121
|
+
path: attempt.path,
|
|
122
|
+
status: attempt.result.ok ? "present" : attempt.result.reason,
|
|
123
|
+
})),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readRulesets(repository) {
|
|
128
|
+
const listing = githubApi(
|
|
129
|
+
`repos/${repository}/rulesets?includes_parents=true&per_page=100`,
|
|
130
|
+
`${repository} rulesets`,
|
|
131
|
+
);
|
|
132
|
+
if (!listing.ok) {
|
|
133
|
+
return {
|
|
134
|
+
readable: listing.reason === "not-found",
|
|
135
|
+
listing,
|
|
136
|
+
rulesets: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const rulesets = [];
|
|
140
|
+
let readable = true;
|
|
141
|
+
for (const entry of Array.isArray(listing.data) ? listing.data : []) {
|
|
142
|
+
if (!entry?.id) continue;
|
|
143
|
+
const detail = githubApi(
|
|
144
|
+
`repos/${repository}/rulesets/${entry.id}`,
|
|
145
|
+
`${repository} ruleset ${entry.id}`,
|
|
146
|
+
);
|
|
147
|
+
readable = readable && detail.ok;
|
|
148
|
+
if (detail.ok) rulesets.push(detail.data);
|
|
149
|
+
}
|
|
150
|
+
return { readable, listing, rulesets };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readBranchNames(repository) {
|
|
154
|
+
const names = [];
|
|
155
|
+
for (let page = 1; page <= 20; page += 1) {
|
|
156
|
+
const response = githubApi(
|
|
157
|
+
`repos/${repository}/branches?per_page=100&page=${page}`,
|
|
158
|
+
`${repository} branches page ${page}`,
|
|
159
|
+
);
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
return { readable: false, result: response, names: [] };
|
|
162
|
+
}
|
|
163
|
+
const entries = Array.isArray(response.data) ? response.data : [];
|
|
164
|
+
names.push(...entries.map((entry) => String(entry?.name || "")).filter(Boolean));
|
|
165
|
+
if (entries.length < 100) {
|
|
166
|
+
return {
|
|
167
|
+
readable: true,
|
|
168
|
+
result: response,
|
|
169
|
+
names: [...new Set(names)].sort(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
readable: false,
|
|
175
|
+
result: {
|
|
176
|
+
ok: false,
|
|
177
|
+
reason: "pagination-limit",
|
|
178
|
+
},
|
|
179
|
+
names: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readOrganizationRepositories(organization) {
|
|
184
|
+
const repositories = [];
|
|
185
|
+
for (let page = 1; page <= 20; page += 1) {
|
|
186
|
+
const response = githubApi(
|
|
187
|
+
`orgs/${organization}/repos?per_page=100&type=all&page=${page}`,
|
|
188
|
+
`managed repositories page ${page}`,
|
|
189
|
+
);
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
return { readable: false, result: response, repositories: [] };
|
|
192
|
+
}
|
|
193
|
+
const entries = Array.isArray(response.data) ? response.data : [];
|
|
194
|
+
repositories.push(...entries);
|
|
195
|
+
if (entries.length < 100) {
|
|
196
|
+
return {
|
|
197
|
+
readable: true,
|
|
198
|
+
result: response,
|
|
199
|
+
repositories,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
readable: false,
|
|
205
|
+
result: {
|
|
206
|
+
ok: false,
|
|
207
|
+
reason: "pagination-limit",
|
|
208
|
+
},
|
|
209
|
+
repositories: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeMembership(result) {
|
|
214
|
+
return result.ok
|
|
215
|
+
? {
|
|
216
|
+
state: String(result.data?.state || ""),
|
|
217
|
+
role: String(result.data?.role || ""),
|
|
218
|
+
}
|
|
219
|
+
: {
|
|
220
|
+
state: "unreadable",
|
|
221
|
+
role: "unreadable",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function resolveVerifierSourceRevision(
|
|
226
|
+
root,
|
|
227
|
+
requested = process.env.GITHUB_SHA || "",
|
|
228
|
+
run = spawnSync,
|
|
229
|
+
) {
|
|
230
|
+
const result = run("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
231
|
+
encoding: "utf8",
|
|
232
|
+
timeout: 10_000,
|
|
233
|
+
});
|
|
234
|
+
const revision = String(result.stdout || "").trim();
|
|
235
|
+
if (result.status !== 0 || !/^[0-9a-f]{40}$/i.test(revision)) {
|
|
236
|
+
throw new Error("verifier source revision is unavailable");
|
|
237
|
+
}
|
|
238
|
+
const cleanliness = run("git", ["-C", root, "diff", "--quiet", "HEAD", "--"], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
timeout: 10_000,
|
|
241
|
+
});
|
|
242
|
+
if (cleanliness.status !== 0) {
|
|
243
|
+
throw new Error("verifier checkout contains tracked drift");
|
|
244
|
+
}
|
|
245
|
+
const normalized = revision.toLowerCase();
|
|
246
|
+
if (requested && (!/^[0-9a-f]{40}$/i.test(requested) ||
|
|
247
|
+
requested.toLowerCase() !== normalized)) {
|
|
248
|
+
throw new Error("requested verifier source revision does not match the current checkout");
|
|
249
|
+
}
|
|
250
|
+
return normalized;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function addMinutes(iso, minutes) {
|
|
254
|
+
return new Date(Date.parse(iso) + minutes * 60_000).toISOString();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function repositorySelector(repositories, requested) {
|
|
258
|
+
if (!requested) return repositories;
|
|
259
|
+
const exact = repositories.filter((repository) =>
|
|
260
|
+
repository.full_name === requested ||
|
|
261
|
+
repository.name === requested);
|
|
262
|
+
if (exact.length !== 1) {
|
|
263
|
+
throw new Error(`repository selector must resolve exactly once: ${requested}`);
|
|
264
|
+
}
|
|
265
|
+
return exact;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function collectGithubGovernanceAudit({
|
|
269
|
+
organization = BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.organization,
|
|
270
|
+
repository = "",
|
|
271
|
+
targetRef = "",
|
|
272
|
+
observedAt = new Date().toISOString(),
|
|
273
|
+
ttlMinutes = 15,
|
|
274
|
+
verifierSourceRevision = "",
|
|
275
|
+
root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
|
|
276
|
+
api = githubApi,
|
|
277
|
+
} = {}) {
|
|
278
|
+
if (api !== githubApi) {
|
|
279
|
+
throw new Error("injected API clients must use collectGithubGovernanceAuditFromSnapshot");
|
|
280
|
+
}
|
|
281
|
+
const organizationState = githubApi(`orgs/${organization}`, "organization");
|
|
282
|
+
const repositoriesState = readOrganizationRepositories(organization);
|
|
283
|
+
const developmentMembership = githubApi(
|
|
284
|
+
`orgs/${organization}/memberships/${BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.authority.developmentIdentity}`,
|
|
285
|
+
"development membership",
|
|
286
|
+
);
|
|
287
|
+
const reviewMembership = githubApi(
|
|
288
|
+
`orgs/${organization}/memberships/${BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.authority.reviewIdentity}`,
|
|
289
|
+
"review membership",
|
|
290
|
+
);
|
|
291
|
+
if (!organizationState.ok || !repositoriesState.readable) {
|
|
292
|
+
throw new Error("organization or managed repository inventory is unreadable; governance audit fails closed");
|
|
293
|
+
}
|
|
294
|
+
const selected = repositorySelector(repositoriesState.repositories, repository);
|
|
295
|
+
if (targetRef && selected.length !== 1) {
|
|
296
|
+
throw new Error("--target-ref requires exactly one selected repository");
|
|
297
|
+
}
|
|
298
|
+
const revision = resolveVerifierSourceRevision(root, verifierSourceRevision);
|
|
299
|
+
const verifier = {
|
|
300
|
+
runtime: `node-${process.version}`,
|
|
301
|
+
sourceRevision: revision,
|
|
302
|
+
identityRoot: githubGovernanceDigest({
|
|
303
|
+
contract: BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.contract,
|
|
304
|
+
policyRoot: BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.policyRoot,
|
|
305
|
+
sourceRevision: revision,
|
|
306
|
+
runtime: process.version,
|
|
307
|
+
}),
|
|
308
|
+
};
|
|
309
|
+
const memberships = {
|
|
310
|
+
[BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.authority.developmentIdentity]:
|
|
311
|
+
normalizeMembership(developmentMembership),
|
|
312
|
+
[BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.authority.reviewIdentity]:
|
|
313
|
+
normalizeMembership(reviewMembership),
|
|
314
|
+
};
|
|
315
|
+
const receipts = [];
|
|
316
|
+
const diagnostics = [];
|
|
317
|
+
for (const metadata of selected) {
|
|
318
|
+
const fullName = String(metadata.full_name || "");
|
|
319
|
+
const visibilityClass = String(
|
|
320
|
+
metadata.visibility || (metadata.private ? "private" : "public"),
|
|
321
|
+
);
|
|
322
|
+
const repositoryIdentityRoot = githubRepositoryIdentityRoot({
|
|
323
|
+
provider: "github",
|
|
324
|
+
providerRepositoryId: String(metadata.node_id || metadata.id || ""),
|
|
325
|
+
});
|
|
326
|
+
const defaultBranch = String(metadata.default_branch || "").replace(/^refs\/heads\//, "");
|
|
327
|
+
const repositoryState = {
|
|
328
|
+
fullName,
|
|
329
|
+
visibility: visibilityClass,
|
|
330
|
+
identityRoot: repositoryIdentityRoot,
|
|
331
|
+
defaultBranch,
|
|
332
|
+
};
|
|
333
|
+
const branchesState = targetRef
|
|
334
|
+
? {
|
|
335
|
+
readable: true,
|
|
336
|
+
result: { ok: true, reason: "targeted-read" },
|
|
337
|
+
names: [String(targetRef).replace(/^refs\/heads\//, "")],
|
|
338
|
+
}
|
|
339
|
+
: readBranchNames(fullName);
|
|
340
|
+
const branches = resolveGithubGovernanceTargetRefs({
|
|
341
|
+
repository: repositoryState,
|
|
342
|
+
availableRefs: branchesState.names,
|
|
343
|
+
requestedTargetRef: targetRef,
|
|
344
|
+
});
|
|
345
|
+
const rulesetState = readRulesets(fullName);
|
|
346
|
+
for (const branch of branches) {
|
|
347
|
+
const branchState = githubApi(
|
|
348
|
+
`repos/${fullName}/branches/${encodeURIComponent(branch)}`,
|
|
349
|
+
`${fullName} branch`,
|
|
350
|
+
);
|
|
351
|
+
const protectionState = githubApi(
|
|
352
|
+
`repos/${fullName}/branches/${encodeURIComponent(branch)}/protection`,
|
|
353
|
+
`${fullName} branch protection`,
|
|
354
|
+
);
|
|
355
|
+
const codeownersState = readCodeowners(fullName, branch);
|
|
356
|
+
const codeowners = evaluateCodeownersAuthority({
|
|
357
|
+
source: codeownersState.source,
|
|
358
|
+
sourcePath: codeownersState.path,
|
|
359
|
+
reviewAuthority: BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.authority.reviewIdentity,
|
|
360
|
+
});
|
|
361
|
+
const effectivePolicy = compileEffectiveGithubGovernancePolicy({
|
|
362
|
+
branch,
|
|
363
|
+
defaultBranch,
|
|
364
|
+
protectedBranch: branchState.data?.protected === true,
|
|
365
|
+
protection: protectionState.ok ? protectionState.data : null,
|
|
366
|
+
rulesets: rulesetState.rulesets,
|
|
367
|
+
});
|
|
368
|
+
const apiEvidence = {
|
|
369
|
+
complete: branchesState.readable &&
|
|
370
|
+
branchState.ok &&
|
|
371
|
+
resolvedAbsence(protectionState) &&
|
|
372
|
+
rulesetState.readable &&
|
|
373
|
+
codeownersState.readable &&
|
|
374
|
+
developmentMembership.ok &&
|
|
375
|
+
reviewMembership.ok,
|
|
376
|
+
readable: branchesState.readable &&
|
|
377
|
+
branchState.ok &&
|
|
378
|
+
rulesetState.readable &&
|
|
379
|
+
codeownersState.readable,
|
|
380
|
+
ambiguous: false,
|
|
381
|
+
provider: "github",
|
|
382
|
+
endpointClasses: {
|
|
383
|
+
organization: organizationState.ok ? "read" : organizationState.reason,
|
|
384
|
+
repositories: repositoriesState.readable
|
|
385
|
+
? "read"
|
|
386
|
+
: repositoriesState.result.reason,
|
|
387
|
+
branches: branchesState.readable ? "read" : branchesState.result.reason,
|
|
388
|
+
branch: branchState.ok ? "read" : branchState.reason,
|
|
389
|
+
protection: protectionState.ok ? "read" : protectionState.reason,
|
|
390
|
+
rulesets: rulesetState.readable ? "read" : rulesetState.listing.reason,
|
|
391
|
+
codeowners: codeowners.exists
|
|
392
|
+
? "present"
|
|
393
|
+
: codeownersState.readable
|
|
394
|
+
? "absent"
|
|
395
|
+
: "unreadable",
|
|
396
|
+
memberships: developmentMembership.ok && reviewMembership.ok
|
|
397
|
+
? "read"
|
|
398
|
+
: "unreadable",
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
receipts.push(evaluateGithubGovernanceSnapshot({
|
|
402
|
+
repository: repositoryState,
|
|
403
|
+
targetRef: branch,
|
|
404
|
+
organizationPlan: String(organizationState.data?.plan?.name || ""),
|
|
405
|
+
codeowners,
|
|
406
|
+
effectivePolicy,
|
|
407
|
+
memberships,
|
|
408
|
+
apiEvidence,
|
|
409
|
+
observedAt,
|
|
410
|
+
expiresAt: addMinutes(observedAt, ttlMinutes),
|
|
411
|
+
verifier,
|
|
412
|
+
}));
|
|
413
|
+
diagnostics.push({
|
|
414
|
+
repositoryIdentityRoot,
|
|
415
|
+
visibility: visibilityClass,
|
|
416
|
+
targetRef: branch,
|
|
417
|
+
endpointClasses: apiEvidence.endpointClasses,
|
|
418
|
+
codeownersAttempts: codeownersState.attempts,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const visibility = selected.reduce((counts, item) => {
|
|
423
|
+
const key = String(item.visibility || (item.private ? "private" : "public"));
|
|
424
|
+
counts[key] = Number(counts[key] || 0) + 1;
|
|
425
|
+
return counts;
|
|
426
|
+
}, {});
|
|
427
|
+
const core = {
|
|
428
|
+
schemaVersion: 1,
|
|
429
|
+
contract: "kungfu-buildchain-github-governance-audit",
|
|
430
|
+
organization,
|
|
431
|
+
policyRoot: BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.policyRoot,
|
|
432
|
+
organizationPlan: String(organizationState.data?.plan?.name || "unknown"),
|
|
433
|
+
observedAt,
|
|
434
|
+
expiresAt: addMinutes(observedAt, ttlMinutes),
|
|
435
|
+
inventory: {
|
|
436
|
+
repositoryCount: selected.length,
|
|
437
|
+
targetCount: receipts.length,
|
|
438
|
+
visibility,
|
|
439
|
+
qualifyingCount: receipts.filter((receipt) => receipt.qualifying).length,
|
|
440
|
+
nonQualifyingCount: receipts.filter((receipt) => !receipt.qualifying).length,
|
|
441
|
+
},
|
|
442
|
+
receipts,
|
|
443
|
+
diagnostics,
|
|
444
|
+
verifier,
|
|
445
|
+
};
|
|
446
|
+
return { ...core, auditRoot: githubGovernanceDigest(core) };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function main(args = process.argv.slice(2)) {
|
|
450
|
+
const result = collectGithubGovernanceAudit({
|
|
451
|
+
organization: flag(args, "organization", BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY.organization),
|
|
452
|
+
repository: flag(args, "repository"),
|
|
453
|
+
targetRef: flag(args, "target-ref"),
|
|
454
|
+
observedAt: flag(args, "observed-at", new Date().toISOString()),
|
|
455
|
+
ttlMinutes: Number(flag(args, "ttl-minutes", "15")),
|
|
456
|
+
verifierSourceRevision: flag(args, "source-revision"),
|
|
457
|
+
});
|
|
458
|
+
const output = flag(args, "output");
|
|
459
|
+
const serialized = `${JSON.stringify(result, null, 2)}\n`;
|
|
460
|
+
if (output) fs.writeFileSync(path.resolve(output), serialized);
|
|
461
|
+
if (!output || hasFlag(args, "json")) process.stdout.write(serialized);
|
|
462
|
+
if (hasFlag(args, "require-qualifying") && result.inventory.nonQualifyingCount > 0) {
|
|
463
|
+
process.exitCode = 2;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
468
|
+
try {
|
|
469
|
+
main();
|
|
470
|
+
} catch (error) {
|
|
471
|
+
process.stderr.write(`${error.message}\n`);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
@@ -239,6 +239,16 @@ function main() {
|
|
|
239
239
|
const deploymentBranches = environment !== "none" && environmentState.deployment_branch_policy?.custom_branch_policies === true
|
|
240
240
|
? githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}/deployment-branch-policies?per_page=100`, "Environment deployment branch policy")
|
|
241
241
|
: { branch_policies: [] };
|
|
242
|
+
const exactEnvironmentBranchPolicy = (deploymentBranches.branch_policies || []).find((entry) =>
|
|
243
|
+
entry?.type === "branch" && entry?.name === branch
|
|
244
|
+
);
|
|
245
|
+
const environmentBranchAuthorized = environment !== "none" && (
|
|
246
|
+
(
|
|
247
|
+
environmentState.deployment_branch_policy?.protected_branches === true &&
|
|
248
|
+
branchState.protected === true
|
|
249
|
+
) ||
|
|
250
|
+
Boolean(exactEnvironmentBranchPolicy)
|
|
251
|
+
);
|
|
242
252
|
const oidc = githubJson(`repos/${repository}/actions/oidc/customization/sub`, "OIDC subject policy");
|
|
243
253
|
if (!["npm-trusted-publisher", "github-token", "oidc-role"].includes(publisherMode)) {
|
|
244
254
|
throw new Error(`unsupported --publisher-mode: ${publisherMode}`);
|
|
@@ -259,7 +269,7 @@ function main() {
|
|
|
259
269
|
enforceAdmins: protection.enforce_admins?.enabled === true,
|
|
260
270
|
observedRulesetCount: rulesets.length,
|
|
261
271
|
};
|
|
262
|
-
} else if (rulesetBranchPolicy.rulesetCount > 0) {
|
|
272
|
+
} else if (rulesetBranchPolicy.rulesetCount > 0 && rulesetBranchPolicy.strict) {
|
|
263
273
|
branchPolicy = rulesetBranchPolicy;
|
|
264
274
|
} else {
|
|
265
275
|
if (!/^[0-9a-f]{40}$/.test(sourceSha)) {
|
|
@@ -277,10 +287,24 @@ function main() {
|
|
|
277
287
|
message: sourceMessage,
|
|
278
288
|
changedPaths: sourceChangedPaths,
|
|
279
289
|
};
|
|
290
|
+
const branchHeadSha = String(branchState.commit?.sha || "").toLowerCase();
|
|
291
|
+
const sourceComparison = sourceSha === branchHeadSha
|
|
292
|
+
? null
|
|
293
|
+
: githubJson(
|
|
294
|
+
`repos/${repository}/compare/${sourceSha}...${branchHeadSha}`,
|
|
295
|
+
"source protected-branch lineage",
|
|
296
|
+
);
|
|
297
|
+
const sourceContainedInBranch = sourceSha === branchHeadSha || (
|
|
298
|
+
sourceComparison?.status === "ahead" &&
|
|
299
|
+
String(sourceComparison?.merge_base_commit?.sha || "").toLowerCase() === sourceSha
|
|
300
|
+
);
|
|
280
301
|
let pullRequests = githubJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "source pull-request lineage");
|
|
281
302
|
let mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
|
|
282
303
|
entry?.merged_at &&
|
|
283
|
-
|
|
304
|
+
(
|
|
305
|
+
String(entry.merge_commit_sha || "").toLowerCase() === authorizationSha ||
|
|
306
|
+
String(entry.head?.sha || "").toLowerCase() === authorizationSha
|
|
307
|
+
) &&
|
|
284
308
|
entry.base?.ref === branch &&
|
|
285
309
|
entry.head?.repo?.full_name === repository
|
|
286
310
|
);
|
|
@@ -318,10 +342,12 @@ function main() {
|
|
|
318
342
|
const login = String(review?.user?.login || "");
|
|
319
343
|
if (login) latestReviews.set(login, review);
|
|
320
344
|
}
|
|
345
|
+
const pullRequestHeadSha = String(mergedPullRequest?.head?.sha || "").toLowerCase();
|
|
321
346
|
const independentApprovals = [...latestReviews.values()].filter((review) =>
|
|
322
|
-
review.state === "APPROVED" &&
|
|
347
|
+
review.state === "APPROVED" &&
|
|
348
|
+
review.user?.login !== mergedPullRequest?.user?.login &&
|
|
349
|
+
String(review.commit_id || "").toLowerCase() === pullRequestHeadSha
|
|
323
350
|
);
|
|
324
|
-
const pullRequestHeadSha = String(mergedPullRequest?.head?.sha || "").toLowerCase();
|
|
325
351
|
const checkRuns = /^[0-9a-f]{40}$/.test(pullRequestHeadSha)
|
|
326
352
|
? githubJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?per_page=100`, "merged pull-request head check runs")
|
|
327
353
|
: { check_runs: [] };
|
|
@@ -360,7 +386,8 @@ function main() {
|
|
|
360
386
|
requiredCheckSha: pullRequestHeadSha,
|
|
361
387
|
sourceSha,
|
|
362
388
|
authorizationSha,
|
|
363
|
-
headSha:
|
|
389
|
+
headSha: branchHeadSha,
|
|
390
|
+
sourceContainedInBranch,
|
|
364
391
|
releaseReconciliation,
|
|
365
392
|
mergedPullRequest: Boolean(mergedPullRequest),
|
|
366
393
|
pullRequestNumber: mergedPullRequest?.number || 0,
|
|
@@ -371,6 +398,8 @@ function main() {
|
|
|
371
398
|
independentApproval: independentApprovals.length > 0,
|
|
372
399
|
configurationRead: false,
|
|
373
400
|
evidenceSource: "public-provider-transaction",
|
|
401
|
+
observedRulesetCount: rulesetBranchPolicy.rulesetCount,
|
|
402
|
+
configuredPolicyMode: rulesetBranchPolicy.rulesetCount > 0 ? "ruleset" : "unreadable",
|
|
374
403
|
};
|
|
375
404
|
}
|
|
376
405
|
let publisher;
|
|
@@ -435,6 +464,13 @@ function main() {
|
|
|
435
464
|
protected: (environmentState.protection_rules || []).length > 0 ||
|
|
436
465
|
environmentState.deployment_branch_policy?.protected_branches === true ||
|
|
437
466
|
(deploymentBranches.branch_policies || []).length > 0,
|
|
467
|
+
branchAuthorized: environmentBranchAuthorized,
|
|
468
|
+
branchPolicyMode: environmentState.deployment_branch_policy?.protected_branches === true
|
|
469
|
+
? "protected-branches"
|
|
470
|
+
: exactEnvironmentBranchPolicy
|
|
471
|
+
? "exact-custom-branch"
|
|
472
|
+
: "unqualified",
|
|
473
|
+
authorizedBranch: exactEnvironmentBranchPolicy?.name || "",
|
|
438
474
|
reviewRequired: reviewRules.length > 0,
|
|
439
475
|
preventSelfReview: reviewRules.some((rule) => rule.prevent_self_review === true),
|
|
440
476
|
},
|
|
@@ -66,6 +66,7 @@ const requiredPaths = [
|
|
|
66
66
|
"scripts/npm-publish-dry-run.mjs",
|
|
67
67
|
"scripts/npm-publish-transaction.mjs",
|
|
68
68
|
"scripts/publication-package.mjs",
|
|
69
|
+
"scripts/publication-commit-evidence.mjs",
|
|
69
70
|
"scripts/release-candidate-resolver.mjs",
|
|
70
71
|
"scripts/buildchain-patrol.mjs",
|
|
71
72
|
"scripts/observed-evidence.mjs",
|
|
@@ -897,8 +898,15 @@ for (const requiredSnippet of [
|
|
|
897
898
|
"github-release:",
|
|
898
899
|
"default: true",
|
|
899
900
|
"github-release: ${{ inputs.github-release }}",
|
|
901
|
+
"github-release-payload-patterns:",
|
|
902
|
+
"github-release-artifact-paths: ${{ steps.rc.outputs.release-candidate-github-release-artifact-paths }}",
|
|
900
903
|
"github-release-title: ${{ inputs.github-release-title }}",
|
|
901
904
|
"github-release-notes: ${{ inputs.github-release-notes }}",
|
|
905
|
+
"publication-commit-command:",
|
|
906
|
+
"BUILDCHAIN_PUBLICATION_COMMIT_SIGNING_KEY:",
|
|
907
|
+
"KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY:",
|
|
908
|
+
"Commit consumer publication authority last",
|
|
909
|
+
"node .buildchain/runtime/scripts/publication-commit-evidence.mjs",
|
|
902
910
|
"require-publish-source-lock: \"true\"",
|
|
903
911
|
"publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
|
|
904
912
|
"publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
|
|
@@ -209,6 +209,7 @@ permissions:
|
|
|
209
209
|
contents: write
|
|
210
210
|
id-token: write
|
|
211
211
|
issues: write
|
|
212
|
+
pull-requests: write
|
|
212
213
|
|
|
213
214
|
jobs:
|
|
214
215
|
resolve-promotion:
|
|
@@ -409,6 +410,7 @@ jobs:
|
|
|
409
410
|
contents: write
|
|
410
411
|
id-token: write
|
|
411
412
|
issues: write
|
|
413
|
+
pull-requests: write
|
|
412
414
|
with:
|
|
413
415
|
${alphaForwarded}
|
|
414
416
|
secrets: inherit
|
|
@@ -424,6 +426,7 @@ ${alphaForwarded}
|
|
|
424
426
|
contents: write
|
|
425
427
|
id-token: write
|
|
426
428
|
issues: write
|
|
429
|
+
pull-requests: write
|
|
427
430
|
with:
|
|
428
431
|
${stableForwarded}
|
|
429
432
|
secrets: inherit
|