@kungfu-tech/buildchain 2.14.18-alpha.0 → 2.14.18-alpha.10

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 (44) 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/artifact-relay-s3.mjs +11 -1
  35. package/scripts/audit-github-governance.mjs +474 -0
  36. package/scripts/audit-publication-control-plane.mjs +41 -5
  37. package/scripts/check-inventory.mjs +8 -0
  38. package/scripts/generate-channel-promotion-workflow.mjs +3 -0
  39. package/scripts/generate-site-bundle.mjs +5 -0
  40. package/scripts/installer-publication.mjs +351 -0
  41. package/scripts/publication-commit-evidence.mjs +158 -0
  42. package/scripts/reconcile-github-governance.mjs +549 -0
  43. package/scripts/release-candidate-resolver.mjs +53 -0
  44. package/scripts/web-surface-core.mjs +127 -14
@@ -0,0 +1,549 @@
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
+ GITHUB_GOVERNANCE_RULESET_ROLLOUT_CONTRACT,
10
+ GITHUB_GOVERNANCE_ROLLOUT_CONTRACT,
11
+ createGithubGovernanceRolloutPlan,
12
+ createGithubRulesetBypassRolloutPlan,
13
+ createGithubRulesetGovernanceRolloutPlan,
14
+ githubGovernanceDigest,
15
+ normalizeGithubBranchProtectionSnapshot,
16
+ normalizeGithubRulesetSnapshot,
17
+ resolveGithubGovernanceTargetPolicy,
18
+ } from "../packages/core/github-governance-authority.js";
19
+
20
+ function flag(args, name, fallback = "") {
21
+ const index = args.indexOf(`--${name}`);
22
+ return index === -1 ? fallback : String(args[index + 1] || "");
23
+ }
24
+
25
+ function repeatedFlag(args, name) {
26
+ const values = [];
27
+ for (let index = 0; index < args.length; index += 1) {
28
+ if (args[index] === `--${name}`) values.push(String(args[index + 1] || ""));
29
+ }
30
+ return values.filter(Boolean);
31
+ }
32
+
33
+ export function resolveRequiredCheckBindings(checks, appBindings, observedChecks) {
34
+ const explicit = new Map(appBindings.map((value) => {
35
+ const separator = value.lastIndexOf("=");
36
+ const context = value.slice(0, separator).trim();
37
+ const appId = Number(value.slice(separator + 1));
38
+ if (separator <= 0 || !context || !Number.isInteger(appId) || appId <= 0) {
39
+ throw new Error("--required-check-app-id must use <context>=<positive-app-id>");
40
+ }
41
+ return [context, appId];
42
+ }));
43
+ for (const context of explicit.keys()) {
44
+ if (!checks.includes(context)) {
45
+ throw new Error(`required check app binding has no matching --required-check: ${context}`);
46
+ }
47
+ }
48
+ return checks.map((context) => {
49
+ const observed = observedChecks.find((entry) => entry.context === context);
50
+ const appId = explicit.get(context) ?? observed?.app_id;
51
+ if (!Number.isInteger(appId) || appId <= 0) {
52
+ throw new Error(
53
+ `required check must preserve an observed app_id or declare --required-check-app-id: ${context}`,
54
+ );
55
+ }
56
+ return { context, app_id: appId };
57
+ });
58
+ }
59
+
60
+ export function resolveGithubProtectionTargetPolicy({ repository, targetRef } = {}) {
61
+ const policy = resolveGithubGovernanceTargetPolicy({
62
+ repository,
63
+ targetRef,
64
+ });
65
+ return {
66
+ strictRequiredChecks: policy.strictRequiredChecks,
67
+ requiredCheckBindings: policy.requiredCheckBindings.map((entry) => ({
68
+ context: entry.context,
69
+ app_id: entry.appId ?? null,
70
+ })),
71
+ requiredApprovals: policy.requiredApprovals,
72
+ };
73
+ }
74
+
75
+ function hasFlag(args, name) {
76
+ return args.includes(`--${name}`);
77
+ }
78
+
79
+ function required(value, label) {
80
+ const normalized = String(value || "").trim();
81
+ if (!normalized) throw new Error(`${label} is required`);
82
+ return normalized;
83
+ }
84
+
85
+ function readJson(filePath, label) {
86
+ try {
87
+ return JSON.parse(fs.readFileSync(path.resolve(filePath), "utf8"));
88
+ } catch {
89
+ throw new Error(`${label} must be a readable JSON file`);
90
+ }
91
+ }
92
+
93
+ function githubApi(route, { method = "GET", body } = {}) {
94
+ const args = [
95
+ "api",
96
+ route,
97
+ "--method",
98
+ method,
99
+ "-H",
100
+ "Accept: application/vnd.github+json",
101
+ "-H",
102
+ "X-GitHub-Api-Version: 2022-11-28",
103
+ ];
104
+ if (body !== undefined && body !== null) args.push("--input", "-");
105
+ const result = spawnSync("gh", args, {
106
+ encoding: "utf8",
107
+ input: body !== undefined && body !== null ? `${JSON.stringify(body)}\n` : undefined,
108
+ timeout: 60_000,
109
+ });
110
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`;
111
+ if (result.status !== 0) {
112
+ if (/404|not found/i.test(output)) return { exists: false, data: null };
113
+ throw new Error(`GitHub API ${method} ${route} failed closed`);
114
+ }
115
+ return {
116
+ exists: method !== "DELETE",
117
+ data: result.stdout.trim() ? JSON.parse(result.stdout) : null,
118
+ };
119
+ }
120
+
121
+ function protectionEndpoint(repository, branch) {
122
+ return `repos/${repository}/branches/${encodeURIComponent(branch)}/protection`;
123
+ }
124
+
125
+ function readProtection(repository, branch) {
126
+ const response = githubApi(protectionEndpoint(repository, branch));
127
+ return {
128
+ exists: response.exists,
129
+ body: response.exists
130
+ ? normalizeGithubBranchProtectionSnapshot(response.data)
131
+ : null,
132
+ };
133
+ }
134
+
135
+ function rulesetEndpoint(repository, rulesetId) {
136
+ return `repos/${repository}/rulesets/${rulesetId}`;
137
+ }
138
+
139
+ function readRuleset(repository, rulesetId) {
140
+ const response = githubApi(rulesetEndpoint(repository, rulesetId));
141
+ if (!response.exists) throw new Error("GitHub ruleset is absent");
142
+ return normalizeGithubRulesetSnapshot(response.data);
143
+ }
144
+
145
+ function snapshotCore(repository, branch, protection) {
146
+ return {
147
+ schemaVersion: 1,
148
+ contract: "kungfu-buildchain-github-governance-rollback-snapshot",
149
+ repository,
150
+ targetRef: branch,
151
+ protectionExists: protection.exists,
152
+ protection: protection.body,
153
+ };
154
+ }
155
+
156
+ function verifyPlan(plan, expectedContract = GITHUB_GOVERNANCE_ROLLOUT_CONTRACT) {
157
+ if (plan?.contract !== expectedContract) {
158
+ throw new Error("rollout plan contract mismatch");
159
+ }
160
+ const { planRoot, ...core } = plan;
161
+ if (planRoot !== githubGovernanceDigest(core)) {
162
+ throw new Error("rollout plan root mismatch");
163
+ }
164
+ return plan;
165
+ }
166
+
167
+ function rulesetPlan(args) {
168
+ const repository = required(flag(args, "repository"), "--repository");
169
+ const rulesetId = Number(required(flag(args, "ruleset-id"), "--ruleset-id"));
170
+ if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
171
+ throw new Error("--ruleset-id must be a positive integer");
172
+ }
173
+ const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
174
+ const planOutput = required(flag(args, "plan-output"), "--plan-output");
175
+ const before = readRuleset(repository, rulesetId);
176
+ const snapshotCore = {
177
+ schemaVersion: 1,
178
+ contract: "kungfu-buildchain-github-governance-ruleset-rollback-snapshot",
179
+ repository,
180
+ rulesetId,
181
+ ruleset: before,
182
+ };
183
+ const snapshot = {
184
+ ...snapshotCore,
185
+ snapshotRoot: githubGovernanceDigest(snapshotCore),
186
+ };
187
+ const rollout = createGithubRulesetBypassRolloutPlan({
188
+ repository,
189
+ rulesetId,
190
+ inventory: before,
191
+ rollbackSnapshot: before,
192
+ });
193
+ const boundCore = {
194
+ ...rollout,
195
+ snapshotRoot: snapshot.snapshotRoot,
196
+ snapshotPath: path.resolve(snapshotOutput),
197
+ };
198
+ const { planRoot: ignored, ...planCore } = boundCore;
199
+ const finalPlan = {
200
+ ...planCore,
201
+ planRoot: githubGovernanceDigest(planCore),
202
+ };
203
+ fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshot, null, 2)}\n`);
204
+ fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
205
+ return finalPlan;
206
+ }
207
+
208
+ function rulesetPolicyPlan(args) {
209
+ const repository = required(flag(args, "repository"), "--repository");
210
+ const branch = required(flag(args, "branch"), "--branch")
211
+ .replace(/^refs\/heads\//, "");
212
+ const rulesetId = Number(required(flag(args, "ruleset-id"), "--ruleset-id"));
213
+ if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
214
+ throw new Error("--ruleset-id must be a positive integer");
215
+ }
216
+ const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
217
+ const planOutput = required(flag(args, "plan-output"), "--plan-output");
218
+ const before = readRuleset(repository, rulesetId);
219
+ const targetPolicy = resolveGithubGovernanceTargetPolicy({
220
+ repository,
221
+ targetRef: branch,
222
+ });
223
+ const snapshotCore = {
224
+ schemaVersion: 1,
225
+ contract: "kungfu-buildchain-github-governance-ruleset-rollback-snapshot",
226
+ repository,
227
+ targetRef: branch,
228
+ rulesetId,
229
+ ruleset: before,
230
+ };
231
+ const snapshot = {
232
+ ...snapshotCore,
233
+ snapshotRoot: githubGovernanceDigest(snapshotCore),
234
+ };
235
+ const rollout = createGithubRulesetGovernanceRolloutPlan({
236
+ repository,
237
+ targetRef: branch,
238
+ rulesetId,
239
+ inventory: before,
240
+ rollbackSnapshot: before,
241
+ desiredProtection: {
242
+ strictRequiredChecks: targetPolicy.strictRequiredChecks,
243
+ requiredCheckBindings: targetPolicy.requiredCheckBindings,
244
+ requiredApprovals: targetPolicy.requiredApprovals,
245
+ rulesetBypassActors: [],
246
+ },
247
+ });
248
+ const boundCore = {
249
+ ...rollout,
250
+ snapshotRoot: snapshot.snapshotRoot,
251
+ snapshotPath: path.resolve(snapshotOutput),
252
+ };
253
+ const { planRoot: ignored, ...planCore } = boundCore;
254
+ const finalPlan = {
255
+ ...planCore,
256
+ planRoot: githubGovernanceDigest(planCore),
257
+ };
258
+ fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshot, null, 2)}\n`);
259
+ fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
260
+ return finalPlan;
261
+ }
262
+
263
+ function rulesetApply(args) {
264
+ const planPath = required(flag(args, "plan-json"), "--plan-json");
265
+ const confirmed = required(flag(args, "confirm-plan-root"), "--confirm-plan-root");
266
+ const rollout = verifyPlan(
267
+ readJson(planPath, "ruleset rollout plan"),
268
+ GITHUB_GOVERNANCE_RULESET_ROLLOUT_CONTRACT,
269
+ );
270
+ if (rollout.planRoot !== confirmed) {
271
+ throw new Error("--confirm-plan-root does not match the frozen ruleset rollout plan");
272
+ }
273
+ const snapshot = readJson(rollout.snapshotPath, "ruleset rollback snapshot");
274
+ const { snapshotRoot, ...snapshotCore } = snapshot;
275
+ if (snapshotRoot !== rollout.snapshotRoot ||
276
+ snapshotRoot !== githubGovernanceDigest(snapshotCore)) {
277
+ throw new Error("ruleset rollback snapshot root mismatch");
278
+ }
279
+ const current = readRuleset(rollout.repository, rollout.rulesetId);
280
+ if (githubGovernanceDigest(current) !== rollout.inventoryRoot) {
281
+ throw new Error("live GitHub ruleset drifted after planning; apply stopped");
282
+ }
283
+ githubApi(rollout.operations[0].endpoint, {
284
+ method: rollout.operations[0].method,
285
+ body: rollout.operations[0].body,
286
+ });
287
+ const after = readRuleset(rollout.repository, rollout.rulesetId);
288
+ const afterRoot = githubGovernanceDigest(after);
289
+ if (afterRoot !== rollout.expectedObservation.rulesetRoot) {
290
+ throw new Error("post-change GitHub ruleset read-back does not match the rollout plan");
291
+ }
292
+ return {
293
+ schemaVersion: 1,
294
+ contract: "kungfu-buildchain-github-governance-ruleset-rollout-receipt",
295
+ status: "applied",
296
+ planRoot: rollout.planRoot,
297
+ snapshotRoot: rollout.snapshotRoot,
298
+ afterRoot,
299
+ repository: rollout.repository,
300
+ rulesetId: rollout.rulesetId,
301
+ };
302
+ }
303
+
304
+ function rulesetRollback(args) {
305
+ const planPath = required(flag(args, "plan-json"), "--plan-json");
306
+ const confirmed = required(flag(args, "confirm-rollback-root"), "--confirm-rollback-root");
307
+ const rollout = verifyPlan(
308
+ readJson(planPath, "ruleset rollout plan"),
309
+ GITHUB_GOVERNANCE_RULESET_ROLLOUT_CONTRACT,
310
+ );
311
+ if (rollout.snapshotRoot !== confirmed) {
312
+ throw new Error("--confirm-rollback-root does not match the frozen ruleset snapshot");
313
+ }
314
+ const operation = rollout.rollback[0];
315
+ githubApi(operation.endpoint, {
316
+ method: operation.method,
317
+ body: operation.body,
318
+ });
319
+ const restored = readRuleset(rollout.repository, rollout.rulesetId);
320
+ if (githubGovernanceDigest(restored) !== operation.preconditionRoot) {
321
+ throw new Error("ruleset rollback read-back does not match the frozen snapshot");
322
+ }
323
+ return {
324
+ schemaVersion: 1,
325
+ contract: "kungfu-buildchain-github-governance-ruleset-rollback-receipt",
326
+ status: "restored",
327
+ planRoot: rollout.planRoot,
328
+ snapshotRoot: rollout.snapshotRoot,
329
+ repository: rollout.repository,
330
+ rulesetId: rollout.rulesetId,
331
+ };
332
+ }
333
+
334
+ function protectionPolicyPlan(args) {
335
+ const repository = required(flag(args, "repository"), "--repository");
336
+ const branch = required(flag(args, "branch"), "--branch")
337
+ .replace(/^refs\/heads\//, "");
338
+ const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
339
+ const planOutput = required(flag(args, "plan-output"), "--plan-output");
340
+ const protection = readProtection(repository, branch);
341
+ const desiredProtection = resolveGithubProtectionTargetPolicy({
342
+ repository,
343
+ targetRef: branch,
344
+ });
345
+ const snapshot = snapshotCore(repository, branch, protection);
346
+ const snapshotWithRoot = {
347
+ ...snapshot,
348
+ snapshotRoot: githubGovernanceDigest(snapshot),
349
+ };
350
+ const rollout = createGithubGovernanceRolloutPlan({
351
+ repository,
352
+ targetRef: branch,
353
+ inventory: {
354
+ protectionExists: protection.exists,
355
+ protection: protection.body,
356
+ },
357
+ rollbackSnapshot: protection.body || {},
358
+ rollbackProtectionExists: protection.exists,
359
+ desiredProtection,
360
+ });
361
+ const bound = {
362
+ ...rollout,
363
+ snapshotRoot: snapshotWithRoot.snapshotRoot,
364
+ snapshotPath: path.resolve(snapshotOutput),
365
+ };
366
+ const { planRoot: ignored, ...boundCore } = bound;
367
+ const finalPlan = {
368
+ ...boundCore,
369
+ planRoot: githubGovernanceDigest(boundCore),
370
+ };
371
+ fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshotWithRoot, null, 2)}\n`);
372
+ fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
373
+ return finalPlan;
374
+ }
375
+
376
+ function plan(args) {
377
+ const repository = required(flag(args, "repository"), "--repository");
378
+ const branch = required(flag(args, "branch"), "--branch").replace(/^refs\/heads\//, "");
379
+ const checks = repeatedFlag(args, "required-check");
380
+ const checkAppBindings = repeatedFlag(args, "required-check-app-id");
381
+ if (checks.length === 0) {
382
+ throw new Error("at least one --required-check is required");
383
+ }
384
+ const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
385
+ const planOutput = required(flag(args, "plan-output"), "--plan-output");
386
+ const protection = readProtection(repository, branch);
387
+ const checkBindings = resolveRequiredCheckBindings(
388
+ checks,
389
+ checkAppBindings,
390
+ protection.body?.required_status_checks?.checks || [],
391
+ );
392
+ const snapshot = snapshotCore(repository, branch, protection);
393
+ const snapshotWithRoot = {
394
+ ...snapshot,
395
+ snapshotRoot: githubGovernanceDigest(snapshot),
396
+ };
397
+ const rollout = createGithubGovernanceRolloutPlan({
398
+ repository,
399
+ targetRef: branch,
400
+ inventory: {
401
+ protectionExists: protection.exists,
402
+ protection: protection.body,
403
+ },
404
+ rollbackSnapshot: protection.body || {},
405
+ rollbackProtectionExists: protection.exists,
406
+ desiredProtection: {
407
+ strictRequiredChecks: hasFlag(args, "strict-required-checks"),
408
+ requiredCheckBindings: checkBindings,
409
+ requiredApprovals: Number(flag(args, "required-approvals", "1")),
410
+ },
411
+ });
412
+ const bound = {
413
+ ...rollout,
414
+ snapshotRoot: snapshotWithRoot.snapshotRoot,
415
+ snapshotPath: path.resolve(snapshotOutput),
416
+ };
417
+ const { planRoot: ignored, ...boundCore } = bound;
418
+ const finalPlan = {
419
+ ...boundCore,
420
+ planRoot: githubGovernanceDigest(boundCore),
421
+ };
422
+ fs.writeFileSync(path.resolve(snapshotOutput), `${JSON.stringify(snapshotWithRoot, null, 2)}\n`);
423
+ fs.writeFileSync(path.resolve(planOutput), `${JSON.stringify(finalPlan, null, 2)}\n`);
424
+ return finalPlan;
425
+ }
426
+
427
+ function apply(args) {
428
+ const planPath = required(flag(args, "plan-json"), "--plan-json");
429
+ const confirmed = required(flag(args, "confirm-plan-root"), "--confirm-plan-root");
430
+ const rollout = verifyPlan(readJson(planPath, "rollout plan"));
431
+ if (rollout.planRoot !== confirmed) {
432
+ throw new Error("--confirm-plan-root does not match the frozen rollout plan");
433
+ }
434
+ const snapshot = readJson(rollout.snapshotPath, "rollback snapshot");
435
+ const { snapshotRoot, ...snapshotBody } = snapshot;
436
+ if (snapshotRoot !== rollout.snapshotRoot ||
437
+ snapshotRoot !== githubGovernanceDigest(snapshotBody)) {
438
+ throw new Error("rollback snapshot root mismatch");
439
+ }
440
+ const current = readProtection(rollout.repository, rollout.targetRef);
441
+ const currentInventoryRoot = githubGovernanceDigest({
442
+ protectionExists: current.exists,
443
+ protection: current.body,
444
+ });
445
+ if (currentInventoryRoot !== rollout.inventoryRoot) {
446
+ throw new Error("live GitHub protection drifted after planning; apply stopped");
447
+ }
448
+ for (const operation of rollout.operations) {
449
+ githubApi(operation.endpoint, {
450
+ method: operation.method,
451
+ body: operation.body,
452
+ });
453
+ }
454
+ const after = readProtection(rollout.repository, rollout.targetRef);
455
+ if (!after.exists) throw new Error("post-change branch protection is absent");
456
+ const expected = rollout.expectedObservation;
457
+ const actualChecks = [...(after.body.required_status_checks?.checks || [])]
458
+ .sort((left, right) => `${left.context}:${left.app_id}`.localeCompare(
459
+ `${right.context}:${right.app_id}`,
460
+ ));
461
+ const expectedChecks = [...expected.requiredCheckBindings]
462
+ .sort((left, right) => `${left.context}:${left.app_id}`.localeCompare(
463
+ `${right.context}:${right.app_id}`,
464
+ ));
465
+ if (
466
+ after.body.enforce_admins !== true ||
467
+ after.body.required_pull_request_reviews?.require_code_owner_reviews !== true ||
468
+ after.body.required_pull_request_reviews?.dismiss_stale_reviews !== true ||
469
+ after.body.required_pull_request_reviews?.require_last_push_approval !== true ||
470
+ ["users", "teams", "apps"].some((kind) =>
471
+ (after.body.required_pull_request_reviews?.bypass_pull_request_allowances?.[kind] || [])
472
+ .length !== 0) ||
473
+ after.body.required_conversation_resolution !== true ||
474
+ after.body.allow_force_pushes !== false ||
475
+ after.body.allow_deletions !== false ||
476
+ JSON.stringify(actualChecks) !== JSON.stringify(expectedChecks)
477
+ ) {
478
+ throw new Error("post-change GitHub read-back does not match the rollout plan");
479
+ }
480
+ return {
481
+ schemaVersion: 1,
482
+ contract: "kungfu-buildchain-github-governance-rollout-receipt",
483
+ status: "applied",
484
+ planRoot: rollout.planRoot,
485
+ snapshotRoot: rollout.snapshotRoot,
486
+ afterRoot: githubGovernanceDigest(after.body),
487
+ repository: rollout.repository,
488
+ targetRef: rollout.targetRef,
489
+ };
490
+ }
491
+
492
+ function rollback(args) {
493
+ const planPath = required(flag(args, "plan-json"), "--plan-json");
494
+ const confirmed = required(flag(args, "confirm-rollback-root"), "--confirm-rollback-root");
495
+ const rollout = verifyPlan(readJson(planPath, "rollout plan"));
496
+ if (rollout.snapshotRoot !== confirmed) {
497
+ throw new Error("--confirm-rollback-root does not match the frozen rollback snapshot");
498
+ }
499
+ const operation = rollout.rollback[0];
500
+ githubApi(operation.endpoint, {
501
+ method: operation.method,
502
+ body: operation.body,
503
+ });
504
+ const after = readProtection(rollout.repository, rollout.targetRef);
505
+ const restored = after.exists ? after.body : {};
506
+ if (githubGovernanceDigest(restored) !== operation.preconditionRoot) {
507
+ throw new Error("rollback read-back does not match the frozen snapshot");
508
+ }
509
+ return {
510
+ schemaVersion: 1,
511
+ contract: "kungfu-buildchain-github-governance-rollback-receipt",
512
+ status: "restored",
513
+ planRoot: rollout.planRoot,
514
+ snapshotRoot: rollout.snapshotRoot,
515
+ repository: rollout.repository,
516
+ targetRef: rollout.targetRef,
517
+ };
518
+ }
519
+
520
+ function main(args = process.argv.slice(2)) {
521
+ const mode = args[0] || "plan";
522
+ const handlers = new Map([
523
+ ["plan", plan],
524
+ ["apply", apply],
525
+ ["rollback", rollback],
526
+ ["ruleset-plan", rulesetPlan],
527
+ ["ruleset-apply", rulesetApply],
528
+ ["ruleset-rollback", rulesetRollback],
529
+ ["ruleset-policy-plan", rulesetPolicyPlan],
530
+ ["ruleset-policy-apply", rulesetApply],
531
+ ["ruleset-policy-rollback", rulesetRollback],
532
+ ["protection-policy-plan", protectionPolicyPlan],
533
+ ["protection-policy-apply", apply],
534
+ ["protection-policy-rollback", rollback],
535
+ ]);
536
+ const handler = handlers.get(mode) || plan;
537
+ const rest = handlers.has(mode) ? args.slice(1) : args;
538
+ const result = handler(rest);
539
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
540
+ }
541
+
542
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
543
+ try {
544
+ main();
545
+ } catch (error) {
546
+ process.stderr.write(`${error.message}\n`);
547
+ process.exitCode = 1;
548
+ }
549
+ }
@@ -204,6 +204,47 @@ function findDownloadedFilesByExtension(root, extensions = []) {
204
204
  return matches.sort();
205
205
  }
206
206
 
207
+ export function selectReleaseAssetPaths({
208
+ payloadRoot,
209
+ patterns = [],
210
+ } = {}) {
211
+ const matchers = splitPatterns(patterns).map(artifactPatternToRegExp);
212
+ if (matchers.length === 0) return [];
213
+ const files = [];
214
+ const stack = fs.existsSync(payloadRoot) ? [payloadRoot] : [];
215
+ while (stack.length) {
216
+ const current = stack.pop();
217
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
218
+ const fullPath = path.join(current, entry.name);
219
+ if (entry.isDirectory()) {
220
+ stack.push(fullPath);
221
+ } else if (
222
+ entry.isFile() &&
223
+ matchers.some((matcher) => matcher.test(entry.name))
224
+ ) {
225
+ files.push(fullPath);
226
+ }
227
+ }
228
+ }
229
+ files.sort();
230
+ if (files.length === 0) {
231
+ throw new Error(
232
+ `release candidate payload patterns matched no files: ${splitPatterns(patterns).join(", ")}`,
233
+ );
234
+ }
235
+ const basenames = new Set();
236
+ for (const filePath of files) {
237
+ const basename = path.basename(filePath);
238
+ if (basenames.has(basename)) {
239
+ throw new Error(
240
+ `release candidate public asset basename is not unique: ${basename}`,
241
+ );
242
+ }
243
+ basenames.add(basename);
244
+ }
245
+ return files;
246
+ }
247
+
207
248
  function packageNameFromArtifactPath(filePath) {
208
249
  const basename = path.basename(String(filePath || ""));
209
250
  return basename
@@ -347,6 +388,7 @@ export async function resolveReleaseCandidateArtifacts({
347
388
  workflowName = "Build Surface Fixture",
348
389
  artifactName = "",
349
390
  artifactPatterns = "",
391
+ githubReleasePayloadPatterns = "",
350
392
  requiredArtifactCount = 0,
351
393
  publishArtifactKind = "npm",
352
394
  publishPackageMain = "",
@@ -522,6 +564,10 @@ export async function resolveReleaseCandidateArtifacts({
522
564
  const npmTarballPaths = publishArtifactKind === "npm"
523
565
  ? findDownloadedFilesByExtension(payloadDir, [".tgz"])
524
566
  : [];
567
+ const releaseAssetPaths = selectReleaseAssetPaths({
568
+ payloadRoot: payloadDir,
569
+ patterns: githubReleasePayloadPatterns,
570
+ });
525
571
  const downloadedRequiredArtifactCount = publishArtifactKind === "npm"
526
572
  ? npmTarballPaths.length
527
573
  : platformManifestPaths.length;
@@ -547,6 +593,7 @@ export async function resolveReleaseCandidateArtifacts({
547
593
  payloads: outputPath(payloadDir),
548
594
  platformManifests: platformManifestPaths.map(outputPath),
549
595
  npmTarballs: npmTarballPaths.map(outputPath),
596
+ releaseAssets: releaseAssetPaths.map(outputPath),
550
597
  publishRequiredArtifacts: outputPath(requiredArtifactsPath),
551
598
  },
552
599
  version: passport.target?.version || "",
@@ -567,6 +614,9 @@ export async function resolveReleaseCandidateArtifactsCli() {
567
614
  workflowName: env("BUILDCHAIN_RC_WORKFLOW_NAME", ""),
568
615
  artifactName: env("BUILDCHAIN_ARTIFACT_NAME"),
569
616
  artifactPatterns: env("BUILDCHAIN_ARTIFACT_PATTERNS"),
617
+ githubReleasePayloadPatterns: env(
618
+ "BUILDCHAIN_GITHUB_RELEASE_PAYLOAD_PATTERNS",
619
+ ),
570
620
  requiredArtifactCount: env("BUILDCHAIN_REQUIRED_ARTIFACT_COUNT", "0"),
571
621
  publishArtifactKind: env("BUILDCHAIN_PUBLISH_ARTIFACT_KIND", "npm"),
572
622
  publishPackageMain: env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN"),
@@ -589,6 +639,9 @@ export async function resolveReleaseCandidateArtifactsCli() {
589
639
  "release-candidate-platform-manifest-count": String(result.platformManifestCount || 0),
590
640
  "release-candidate-npm-tarball-paths": (result.paths?.npmTarballs || []).join(","),
591
641
  "release-candidate-npm-tarball-count": String(result.npmTarballCount || 0),
642
+ "release-candidate-github-release-artifact-paths": (
643
+ result.paths?.releaseAssets || []
644
+ ).join("\n"),
592
645
  "publish-required-artifacts-json": JSON.stringify(result.publishRequiredArtifacts || []),
593
646
  "publish-required-artifacts-path": result.paths?.publishRequiredArtifacts || "",
594
647
  "release-candidate-run-id": result.run?.id || "",