@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.
Files changed (43) hide show
  1. package/README.md +1 -0
  2. package/actions/promote-buildchain-ref/README.md +33 -30
  3. package/bin/buildchain.mjs +16 -4
  4. package/dist/site/buildchain-contract.json +86 -31
  5. package/dist/site/buildchain-site.json +88 -27
  6. package/dist/site/capability-registry.json +6 -5
  7. package/dist/site/cli-registry.json +12 -0
  8. package/dist/site/controller-registry.json +50 -6
  9. package/dist/site/kfd-claims.json +110 -21
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +21 -6
  12. package/dist/site/node-api-registry.json +19 -6
  13. package/dist/site/page-registry.json +70 -17
  14. package/dist/site/public-surface-audit.json +130 -23
  15. package/dist/site/publication-authority-registry.json +21 -1
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +1 -0
  18. package/dist/site/site-manifest.json +18 -10
  19. package/dist/site/workflow-registry.json +62 -16
  20. package/docs/MAP.md +3 -1
  21. package/docs/github-governance-authority.md +267 -0
  22. package/docs/lifecycle-protocol.md +10 -10
  23. package/docs/publish-transaction.md +73 -4
  24. package/docs/release-governance.md +38 -24
  25. package/docs/reusable-build-surface.md +29 -11
  26. package/docs/web-surface-deployments.md +46 -0
  27. package/package.json +2 -1
  28. package/packages/core/buildchain-kfd-claims.js +2 -0
  29. package/packages/core/buildchain-publication-authority.js +1 -0
  30. package/packages/core/controller-evidence.js +1 -1
  31. package/packages/core/github-governance-authority.js +1312 -0
  32. package/packages/core/index.js +22 -0
  33. package/packages/core/publication-control-plane-audit.js +4 -1
  34. package/scripts/audit-github-governance.mjs +474 -0
  35. package/scripts/audit-publication-control-plane.mjs +41 -5
  36. package/scripts/check-inventory.mjs +8 -0
  37. package/scripts/generate-channel-promotion-workflow.mjs +3 -0
  38. package/scripts/generate-site-bundle.mjs +5 -0
  39. package/scripts/installer-publication.mjs +351 -0
  40. package/scripts/publication-commit-evidence.mjs +158 -0
  41. package/scripts/reconcile-github-governance.mjs +549 -0
  42. package/scripts/release-candidate-resolver.mjs +53 -0
  43. package/scripts/web-surface-core.mjs +127 -14
@@ -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
- branchPolicy.sourceSha === branchPolicy.headSha &&
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
- entry.merge_commit_sha === authorizationSha &&
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" && review.user?.login !== mergedPullRequest?.user?.login
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: String(branchState.commit?.sha || "").toLowerCase(),
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