@kungfu-tech/buildchain 3.0.1 → 3.0.2-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/README.md +5 -3
- package/actions/promote-buildchain-ref/README.md +7 -0
- package/bin/buildchain.mjs +173 -513
- package/bin/internal/cli-options.mjs +79 -0
- package/bin/internal/trust-release-cli.mjs +469 -0
- package/dist/site/agent-index.json +2 -0
- package/dist/site/artifact-schemas.json +5 -0
- package/dist/site/badge-endpoint-registry.json +18 -18
- package/dist/site/badges/v1/kfd-12/aligned.json +1 -1
- package/dist/site/badges/v1/kfd-12/declared.json +1 -1
- package/dist/site/badges/v1/kfd-12/downgraded.json +1 -1
- package/dist/site/badges/v1/kfd-12/draft.json +1 -1
- package/dist/site/badges/v1/kfd-12/failed.json +1 -1
- package/dist/site/badges/v1/kfd-12/missing.json +1 -1
- package/dist/site/badges/v1/kfd-12/passed.json +1 -1
- package/dist/site/badges/v1/kfd-12/planned.json +1 -1
- package/dist/site/badges/v1/kfd-13/aligned.json +1 -1
- package/dist/site/badges/v1/kfd-13/declared.json +1 -1
- package/dist/site/badges/v1/kfd-13/downgraded.json +1 -1
- package/dist/site/badges/v1/kfd-13/draft.json +1 -1
- package/dist/site/badges/v1/kfd-13/failed.json +1 -1
- package/dist/site/badges/v1/kfd-13/missing.json +1 -1
- package/dist/site/badges/v1/kfd-13/passed.json +1 -1
- package/dist/site/badges/v1/kfd-13/planned.json +1 -1
- package/dist/site/buildchain-contract.json +68 -28
- package/dist/site/buildchain-site.json +28 -18
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +108 -0
- package/dist/site/controller-registry.json +36 -4
- package/dist/site/kfd-claims.json +156 -12
- package/dist/site/kfd-upstream-aggregate.json +20 -11
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +36 -10
- package/dist/site/page-registry.json +20 -10
- package/dist/site/public-surface-audit.json +122 -10
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-passport-check-manifest.json +11 -0
- package/dist/site/release-provenance.json +4 -0
- package/dist/site/schemas/kfd-product-gate-input-v1.schema.json +164 -0
- package/dist/site/schemas/kfd-support-projection-v1.schema.json +106 -0
- package/dist/site/schemas/release-passport-v1.schema.json +3 -0
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +17 -5
- package/docs/cli.md +9 -2
- package/docs/kfd-support.md +43 -7
- package/docs/release-passport.md +15 -0
- package/package.json +7 -3
- package/packages/core/cache-evidence.js +288 -0
- package/packages/core/diagnostics.js +276 -10
- package/packages/core/github-governance-authority.js +71 -16
- package/packages/core/index.js +27 -0
- package/packages/core/kfd-product-gates.js +865 -0
- package/packages/core/kfd.js +16 -7
- package/packages/core/kfd3-surface-register.js +105 -1
- package/packages/core/public-surface-audit.js +6 -1
- package/packages/core/release-passport-contract.js +13 -0
- package/packages/core/release-passport.js +87 -3
- package/scripts/check-internal-architecture.mjs +171 -0
- package/scripts/check-inventory.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +24 -0
- package/scripts/locked-source-checkout.mjs +48 -0
- package/scripts/promotion-routing-evidence.mjs +73 -0
- package/scripts/reconcile-github-governance.mjs +158 -25
- package/scripts/shifu-gate-profile.mjs +26 -20
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
collectKfdUpstreamFacts,
|
|
16
16
|
} from "../packages/core/kfd.js";
|
|
17
17
|
import { KFD_AGENT_HUB_ADOPTION_SCHEMA } from "../packages/core/kfd-agent-hub.js";
|
|
18
|
+
import {
|
|
19
|
+
KFD_PRODUCT_GATE_INPUT_SCHEMA,
|
|
20
|
+
KFD_SUPPORT_PROJECTION_SCHEMA,
|
|
21
|
+
} from "../packages/core/kfd-product-gates.js";
|
|
18
22
|
import {
|
|
19
23
|
createReadmeBadgeEndpointRegistry,
|
|
20
24
|
} from "../packages/core/readme-badges.js";
|
|
@@ -445,6 +449,15 @@ function cliCommandMeta(id) {
|
|
|
445
449
|
"kfd-3-register": { group: "kfd-trust", purpose: "Declare detected KFD-3 public surfaces into a product-owned surface registry." },
|
|
446
450
|
"kfd-3-witness": { group: "kfd-trust", purpose: "Generate release-passport-compatible KFD-3 surface witnesses from a product registry." },
|
|
447
451
|
"kfd-4-schema": { group: "kfd-trust", purpose: "Print the default KFD-4 schema exposed by the KFD package standards metadata." },
|
|
452
|
+
"kfd-4-gate": { group: "kfd-trust", purpose: "Evaluate source-bound KFD-4 observer-perspective and contrastive-replay product evidence." },
|
|
453
|
+
"kfd-4-verify": { group: "kfd-trust", purpose: "Verify a retained KFD-4 product-gate result without widening product support." },
|
|
454
|
+
"kfd-5-schema": { group: "kfd-trust", purpose: "Print the default KFD-5 schema exposed by the KFD package standards metadata." },
|
|
455
|
+
"kfd-5-gate": { group: "kfd-trust", purpose: "Evaluate source-bound KFD-5 Primitive discovery qualification evidence." },
|
|
456
|
+
"kfd-5-verify": { group: "kfd-trust", purpose: "Verify a retained KFD-5 product-gate result without widening product support." },
|
|
457
|
+
"kfd-7-schema": { group: "kfd-trust", purpose: "Print the default KFD-7 schema exposed by the KFD package standards metadata." },
|
|
458
|
+
"kfd-7-gate": { group: "kfd-trust", purpose: "Evaluate a source-bound KFD-7 Domain Profile, evidence obligations, and independent review." },
|
|
459
|
+
"kfd-7-verify": { group: "kfd-trust", purpose: "Verify a retained KFD-7 product-gate result without widening product support." },
|
|
460
|
+
"kfd-support": { group: "kfd-trust", purpose: "Project and verify a product-owned KFD-1 through KFD-13 support matrix against KFD-4/5/7 gates." },
|
|
448
461
|
"kfd-aggregate": { group: "kfd-trust", purpose: "Return a product KFD view that combines own KFD status with upstream KFD aggregate facts." },
|
|
449
462
|
"kfd-upstream": { group: "kfd-trust", purpose: "Inspect KFD upstream aggregate command families." },
|
|
450
463
|
"kfd-upstream-check": { group: "kfd-trust", purpose: "Validate a KFD upstream aggregate document and fail closed on missing evidence." },
|
|
@@ -503,6 +516,7 @@ function nodeApiMeta(exportName) {
|
|
|
503
516
|
"./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
|
|
504
517
|
"./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
|
|
505
518
|
"./candidate-timeline": { group: "observability-diagnostics", summary: "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs." },
|
|
519
|
+
"./cache-evidence": { group: "observability-diagnostics", summary: "Content-addressed cache operation receipts and source/platform-bound evidence-set verification APIs." },
|
|
506
520
|
"./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
|
|
507
521
|
"./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
|
|
508
522
|
"./portable-dev-cache": { group: "observability-diagnostics", summary: "Portable dependency/compiler cache plan, exact-root verification, and provider receipt APIs." },
|
|
@@ -529,6 +543,7 @@ function nodeApiMeta(exportName) {
|
|
|
529
543
|
"./issue-reporting": { group: "observability-diagnostics", summary: "Buildchain-owned issue reporting API for workflow friction feedback." },
|
|
530
544
|
"./buildchain-layout": { group: "kfd-trust", summary: "Versioned Buildchain repository-layout discovery contract plus canonical .buildchain path resolution and migration APIs." },
|
|
531
545
|
"./kfd": { group: "kfd-trust", summary: "Unified KFD standards, schema discovery, KFD-1/KFD-2/KFD-3 grouped APIs, KFD-4 schema discovery, upstream KFD aggregate facts, and Buildchain KFD claim helpers." },
|
|
546
|
+
"./kfd-product-gates": { group: "kfd-trust", summary: "Fail-closed KFD-4, KFD-5, and KFD-7 product evidence gates plus non-widening KFD support-matrix release projections." },
|
|
532
547
|
"./public-surface-audit": { group: "kfd-trust", summary: "Reverse audit APIs for CLI, workflow, action, site page, and documentation command surfaces." },
|
|
533
548
|
"./kfd-gate": { group: "kfd-trust", summary: "KFD-1/KFD-2/KFD-3 release gate evidence and validation APIs." },
|
|
534
549
|
"./buildchain-kfd-claims": { group: "kfd-trust", summary: "Buildchain self KFD claim registry, witnesses, and public claim APIs." },
|
|
@@ -993,6 +1008,9 @@ function buildSiteBundle() {
|
|
|
993
1008
|
"release-passport-check-manifest.json",
|
|
994
1009
|
"schemas/release-passport-v1.schema.json",
|
|
995
1010
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
1011
|
+
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
1012
|
+
"schemas/kfd-support-projection-v1.schema.json",
|
|
1013
|
+
"kfd-support.json",
|
|
996
1014
|
"artifact-evidence.json",
|
|
997
1015
|
"product-mechanism.json",
|
|
998
1016
|
"impact.json",
|
|
@@ -1021,6 +1039,8 @@ function buildSiteBundle() {
|
|
|
1021
1039
|
"release-passport-check-manifest.json",
|
|
1022
1040
|
"schemas/release-passport-v1.schema.json",
|
|
1023
1041
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
1042
|
+
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
1043
|
+
"schemas/kfd-support-projection-v1.schema.json",
|
|
1024
1044
|
"buildchain-contract.json",
|
|
1025
1045
|
"kfd-claims.json",
|
|
1026
1046
|
"product-mechanism.json",
|
|
@@ -1150,6 +1170,8 @@ function buildSiteBundle() {
|
|
|
1150
1170
|
"release-passport-check-manifest.json",
|
|
1151
1171
|
"schemas/release-passport-v1.schema.json",
|
|
1152
1172
|
"schemas/kfd-agent-hub-adoption.schema.json",
|
|
1173
|
+
"schemas/kfd-product-gate-input-v1.schema.json",
|
|
1174
|
+
"schemas/kfd-support-projection-v1.schema.json",
|
|
1153
1175
|
"buildchain-contract.json",
|
|
1154
1176
|
"kfd-upstream-aggregate.json",
|
|
1155
1177
|
"kfd-claims.json",
|
|
@@ -1323,6 +1345,8 @@ function buildSiteBundle() {
|
|
|
1323
1345
|
"release-passport-check-manifest.json": createReleasePassportCheckManifest(),
|
|
1324
1346
|
"schemas/release-passport-v1.schema.json": RELEASE_PASSPORT_SCHEMA,
|
|
1325
1347
|
"schemas/kfd-agent-hub-adoption.schema.json": KFD_AGENT_HUB_ADOPTION_SCHEMA,
|
|
1348
|
+
"schemas/kfd-product-gate-input-v1.schema.json": KFD_PRODUCT_GATE_INPUT_SCHEMA,
|
|
1349
|
+
"schemas/kfd-support-projection-v1.schema.json": KFD_SUPPORT_PROJECTION_SCHEMA,
|
|
1326
1350
|
"badge-endpoint-registry.json": badgeEndpointRegistry,
|
|
1327
1351
|
"publication-registry.json": publicationRegistry,
|
|
1328
1352
|
"buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
|
|
@@ -177,6 +177,21 @@ function gitObjectDirectory(referencePath) {
|
|
|
177
177
|
return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) || "";
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function directoryBytes(directory) {
|
|
181
|
+
if (!fs.existsSync(directory)) return 0;
|
|
182
|
+
let bytes = 0;
|
|
183
|
+
const pending = [directory];
|
|
184
|
+
while (pending.length) {
|
|
185
|
+
const current = pending.pop();
|
|
186
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
187
|
+
const child = path.join(current, entry.name);
|
|
188
|
+
if (entry.isDirectory()) pending.push(child);
|
|
189
|
+
else if (entry.isFile()) bytes += fs.statSync(child).size;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return bytes;
|
|
193
|
+
}
|
|
194
|
+
|
|
180
195
|
function writeAlternates(targetPath, referencePath) {
|
|
181
196
|
const objectDirectory = gitObjectDirectory(referencePath);
|
|
182
197
|
if (!objectDirectory) {
|
|
@@ -413,6 +428,13 @@ export function lockedSourceCheckout({
|
|
|
413
428
|
fallbackUsed: normalizedMode === "off",
|
|
414
429
|
fallbackReason: normalizedMode === "off" ? "cache disabled" : "",
|
|
415
430
|
githubFetchAttempts: 0,
|
|
431
|
+
lookupDurationMs: 0,
|
|
432
|
+
restoreDurationMs: 0,
|
|
433
|
+
githubFetchDurationMs: 0,
|
|
434
|
+
restoredBytes: null,
|
|
435
|
+
restoredBytesStatus:
|
|
436
|
+
normalizedMode === "off" ? "not-applicable" : "unavailable",
|
|
437
|
+
restoredBytesMethod: "",
|
|
416
438
|
},
|
|
417
439
|
verification: {
|
|
418
440
|
head: "",
|
|
@@ -425,8 +447,13 @@ export function lockedSourceCheckout({
|
|
|
425
447
|
let checkoutError;
|
|
426
448
|
let checkoutSha = sha;
|
|
427
449
|
if (normalizedMode !== "off") {
|
|
450
|
+
const cacheAttemptStartedAt = Date.now();
|
|
451
|
+
const objectBytesBefore = directoryBytes(
|
|
452
|
+
path.join(targetPath, ".git", "objects"),
|
|
453
|
+
);
|
|
428
454
|
try {
|
|
429
455
|
if (renderedReferenceRepository) {
|
|
456
|
+
const lookupStartedAt = Date.now();
|
|
430
457
|
const alternates = writeAlternates(targetPath, renderedReferenceRepository);
|
|
431
458
|
evidence.cache.transport = "reference-repository";
|
|
432
459
|
evidence.cache.referenceAvailable = alternates;
|
|
@@ -436,9 +463,13 @@ export function lockedSourceCheckout({
|
|
|
436
463
|
if (!hasCommit(targetPath, sha, timeoutMs)) {
|
|
437
464
|
throw new Error("reference repository does not contain source commit");
|
|
438
465
|
}
|
|
466
|
+
evidence.cache.lookupDurationMs = Date.now() - lookupStartedAt;
|
|
467
|
+
const restoreStartedAt = Date.now();
|
|
439
468
|
checkoutFetchedCommit(targetPath, sha, timeoutMs);
|
|
469
|
+
evidence.cache.restoreDurationMs = Date.now() - restoreStartedAt;
|
|
440
470
|
} else if (renderedMirrorUrl) {
|
|
441
471
|
evidence.cache.transport = "mirror-url";
|
|
472
|
+
const restoreStartedAt = Date.now();
|
|
442
473
|
const fetchResult = fetchSourceCommit({
|
|
443
474
|
targetPath,
|
|
444
475
|
remoteName: "buildchain-cache",
|
|
@@ -452,15 +483,29 @@ export function lockedSourceCheckout({
|
|
|
452
483
|
evidence.cache.fetchMode = fetchResult.fetchMode;
|
|
453
484
|
checkoutSha = fetchResult.checkoutSha || sha;
|
|
454
485
|
checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
|
|
486
|
+
evidence.cache.restoreDurationMs = Date.now() - restoreStartedAt;
|
|
455
487
|
} else {
|
|
456
488
|
throw new Error("checkout cache is enabled but no mirror URL or reference repository template was provided");
|
|
457
489
|
}
|
|
458
490
|
evidence.cache.hit = true;
|
|
459
491
|
evidence.cache.fallbackUsed = false;
|
|
492
|
+
evidence.cache.restoredBytes = Math.max(
|
|
493
|
+
0,
|
|
494
|
+
directoryBytes(path.join(targetPath, ".git", "objects")) -
|
|
495
|
+
objectBytesBefore,
|
|
496
|
+
);
|
|
497
|
+
evidence.cache.restoredBytesStatus = "observed";
|
|
498
|
+
evidence.cache.restoredBytesMethod = "git-object-store-delta";
|
|
460
499
|
} catch (error) {
|
|
461
500
|
checkoutError = error;
|
|
462
501
|
evidence.cache.hit = false;
|
|
463
502
|
evidence.cache.fallbackReason = error.message;
|
|
503
|
+
if (
|
|
504
|
+
evidence.cache.lookupDurationMs === 0 &&
|
|
505
|
+
evidence.cache.restoreDurationMs === 0
|
|
506
|
+
) {
|
|
507
|
+
evidence.cache.lookupDurationMs = Date.now() - cacheAttemptStartedAt;
|
|
508
|
+
}
|
|
464
509
|
if (normalizedMode === "require" || normalizedFallback === "fail") {
|
|
465
510
|
evidence.durationMs = Date.now() - startedAt;
|
|
466
511
|
writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
|
|
@@ -477,6 +522,7 @@ export function lockedSourceCheckout({
|
|
|
477
522
|
evidence.cache.fallbackReason = checkoutError.message;
|
|
478
523
|
}
|
|
479
524
|
try {
|
|
525
|
+
const githubFetchStartedAt = Date.now();
|
|
480
526
|
const fetchResult = runBoundedFetch({
|
|
481
527
|
attempts: normalizedFetchAttempts,
|
|
482
528
|
fetch: () => fetchSourceCommit({
|
|
@@ -496,6 +542,8 @@ export function lockedSourceCheckout({
|
|
|
496
542
|
evidence.cache.githubFetchAttempts = fetchResult.attempts;
|
|
497
543
|
evidence.cache.fetchMode = fetchResult.value.fetchMode;
|
|
498
544
|
checkoutSha = fetchResult.value.checkoutSha || sha;
|
|
545
|
+
evidence.cache.githubFetchDurationMs =
|
|
546
|
+
Date.now() - githubFetchStartedAt;
|
|
499
547
|
} catch (error) {
|
|
500
548
|
evidence.durationMs = Date.now() - startedAt;
|
|
501
549
|
writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
function createPromotionRoutingEvidence({
|
|
8
|
+
env = process.env,
|
|
9
|
+
cwd = process.cwd(),
|
|
10
|
+
} = {}) {
|
|
11
|
+
const routing = {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
contract: "buildchain.promotion-routing/v1",
|
|
14
|
+
router: { ref: env.ROUTER_REF, sha: env.ROUTER_SHA },
|
|
15
|
+
shell: {
|
|
16
|
+
channel: env.SHELL_CHANNEL,
|
|
17
|
+
ref: env.SHELL_REF,
|
|
18
|
+
sha: env.SHELL_SHA,
|
|
19
|
+
},
|
|
20
|
+
runtime: {
|
|
21
|
+
requestedRef: env.RUNTIME_REF,
|
|
22
|
+
resolvedSha: env.RUNTIME_SHA,
|
|
23
|
+
},
|
|
24
|
+
contractLock: { path: env.LOCK_PATH, digest: env.LOCK_DIGEST },
|
|
25
|
+
publication: {
|
|
26
|
+
channel: env.PUBLICATION_CHANNEL,
|
|
27
|
+
targetRef: env.TARGET_REF,
|
|
28
|
+
},
|
|
29
|
+
trustedOverrideUsed: env.OVERRIDE_USED === "true",
|
|
30
|
+
};
|
|
31
|
+
const controllerDir = path.join(cwd, ".buildchain", "controller");
|
|
32
|
+
const routingPath = path.join(controllerDir, "promotion-routing.json");
|
|
33
|
+
const candidatePath = path.join(
|
|
34
|
+
controllerDir,
|
|
35
|
+
"release-candidate-promotion-passport.json",
|
|
36
|
+
);
|
|
37
|
+
const candidate = JSON.parse(
|
|
38
|
+
fs.readFileSync(path.resolve(cwd, env.RELEASE_CANDIDATE_PASSPORT), "utf8"),
|
|
39
|
+
);
|
|
40
|
+
fs.mkdirSync(controllerDir, { recursive: true });
|
|
41
|
+
fs.writeFileSync(routingPath, `${JSON.stringify(routing, null, 2)}\n`);
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
candidatePath,
|
|
44
|
+
`${JSON.stringify({ ...candidate, promotionRouting: routing }, null, 2)}\n`,
|
|
45
|
+
);
|
|
46
|
+
if (env.GITHUB_OUTPUT) {
|
|
47
|
+
fs.appendFileSync(
|
|
48
|
+
env.GITHUB_OUTPUT,
|
|
49
|
+
[
|
|
50
|
+
"routing-path=.buildchain/controller/promotion-routing.json",
|
|
51
|
+
"candidate-passport-path=.buildchain/controller/release-candidate-promotion-passport.json",
|
|
52
|
+
"",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { routing, routingPath, candidatePath };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
process.argv[1] &&
|
|
61
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
62
|
+
) {
|
|
63
|
+
try {
|
|
64
|
+
createPromotionRoutingEvidence();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(
|
|
67
|
+
`promotion routing evidence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
68
|
+
);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { createPromotionRoutingEvidence };
|
|
@@ -90,6 +90,12 @@ function readJson(filePath, label) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function writeJson(filePath, value) {
|
|
94
|
+
const target = path.resolve(filePath);
|
|
95
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
96
|
+
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
function githubApi(route, { method = "GET", body } = {}) {
|
|
94
100
|
const args = [
|
|
95
101
|
"api",
|
|
@@ -142,6 +148,42 @@ function readRuleset(repository, rulesetId) {
|
|
|
142
148
|
return normalizeGithubRulesetSnapshot(response.data);
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
function readMatchingRulesetEntries(repository, branch) {
|
|
152
|
+
const response = githubApi(
|
|
153
|
+
`repos/${repository}/rulesets?includes_parents=false&per_page=100`,
|
|
154
|
+
);
|
|
155
|
+
const include = `refs/heads/${branch}`;
|
|
156
|
+
return (response.data || [])
|
|
157
|
+
.map((entry) => ({
|
|
158
|
+
id: Number(entry.id),
|
|
159
|
+
ruleset: readRuleset(repository, entry.id),
|
|
160
|
+
}))
|
|
161
|
+
.filter(({ ruleset }) => {
|
|
162
|
+
const refName = ruleset.conditions?.ref_name || {};
|
|
163
|
+
return (
|
|
164
|
+
ruleset.target === "branch" &&
|
|
165
|
+
(refName.include || []).length === 1 &&
|
|
166
|
+
refName.include[0] === include &&
|
|
167
|
+
(refName.exclude || []).length === 0
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
.sort((left, right) => left.ruleset.name.localeCompare(right.ruleset.name));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readMatchingRulesets(repository, branch) {
|
|
174
|
+
return readMatchingRulesetEntries(repository, branch).map(
|
|
175
|
+
({ ruleset }) => ruleset,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function rulesetInventory(repository, branch) {
|
|
180
|
+
return {
|
|
181
|
+
repository,
|
|
182
|
+
targetRef: branch,
|
|
183
|
+
matchingRulesets: readMatchingRulesets(repository, branch),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
function snapshotCore(repository, branch, protection) {
|
|
146
188
|
return {
|
|
147
189
|
schemaVersion: 1,
|
|
@@ -200,8 +242,8 @@ function rulesetPlan(args) {
|
|
|
200
242
|
...planCore,
|
|
201
243
|
planRoot: githubGovernanceDigest(planCore),
|
|
202
244
|
};
|
|
203
|
-
|
|
204
|
-
|
|
245
|
+
writeJson(snapshotOutput, snapshot);
|
|
246
|
+
writeJson(planOutput, finalPlan);
|
|
205
247
|
return finalPlan;
|
|
206
248
|
}
|
|
207
249
|
|
|
@@ -209,13 +251,45 @@ function rulesetPolicyPlan(args) {
|
|
|
209
251
|
const repository = required(flag(args, "repository"), "--repository");
|
|
210
252
|
const branch = required(flag(args, "branch"), "--branch")
|
|
211
253
|
.replace(/^refs\/heads\//, "");
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
throw new Error("--ruleset-id must be a positive integer");
|
|
215
|
-
}
|
|
254
|
+
const requestedRulesetId = flag(args, "ruleset-id");
|
|
255
|
+
const rulesetName = required(flag(args, "ruleset-name"), "--ruleset-name");
|
|
216
256
|
const snapshotOutput = required(flag(args, "snapshot-output"), "--snapshot-output");
|
|
217
257
|
const planOutput = required(flag(args, "plan-output"), "--plan-output");
|
|
218
|
-
const
|
|
258
|
+
const matchingEntries = readMatchingRulesetEntries(repository, branch);
|
|
259
|
+
const inventory = {
|
|
260
|
+
repository,
|
|
261
|
+
targetRef: branch,
|
|
262
|
+
matchingRulesets: matchingEntries.map(({ ruleset }) => ruleset),
|
|
263
|
+
};
|
|
264
|
+
let rulesetId = null;
|
|
265
|
+
let before = null;
|
|
266
|
+
if (requestedRulesetId) {
|
|
267
|
+
rulesetId = Number(requestedRulesetId);
|
|
268
|
+
if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
|
|
269
|
+
throw new Error("--ruleset-id must be a positive integer");
|
|
270
|
+
}
|
|
271
|
+
before = readRuleset(repository, rulesetId);
|
|
272
|
+
if (
|
|
273
|
+
!inventory.matchingRulesets.some(
|
|
274
|
+
(ruleset) =>
|
|
275
|
+
githubGovernanceDigest(ruleset) === githubGovernanceDigest(before),
|
|
276
|
+
)
|
|
277
|
+
) {
|
|
278
|
+
throw new Error("--ruleset-id does not target the exact requested branch");
|
|
279
|
+
}
|
|
280
|
+
} else if (matchingEntries.length === 1) {
|
|
281
|
+
before = matchingEntries[0].ruleset;
|
|
282
|
+
rulesetId = matchingEntries[0].id;
|
|
283
|
+
if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
|
|
284
|
+
throw new Error("matching ruleset id could not be resolved");
|
|
285
|
+
}
|
|
286
|
+
} else if (inventory.matchingRulesets.length > 1) {
|
|
287
|
+
throw new Error("multiple exact-target rulesets are ambiguous");
|
|
288
|
+
} else if (!hasFlag(args, "create-if-missing")) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"exact-target ruleset is absent; pass --create-if-missing to plan creation",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
219
293
|
const targetPolicy = resolveGithubGovernanceTargetPolicy({
|
|
220
294
|
repository,
|
|
221
295
|
targetRef: branch,
|
|
@@ -225,6 +299,7 @@ function rulesetPolicyPlan(args) {
|
|
|
225
299
|
contract: "kungfu-buildchain-github-governance-ruleset-rollback-snapshot",
|
|
226
300
|
repository,
|
|
227
301
|
targetRef: branch,
|
|
302
|
+
rulesetExists: Boolean(before),
|
|
228
303
|
rulesetId,
|
|
229
304
|
ruleset: before,
|
|
230
305
|
};
|
|
@@ -236,13 +311,16 @@ function rulesetPolicyPlan(args) {
|
|
|
236
311
|
repository,
|
|
237
312
|
targetRef: branch,
|
|
238
313
|
rulesetId,
|
|
239
|
-
|
|
314
|
+
rulesetName,
|
|
315
|
+
inventory,
|
|
240
316
|
rollbackSnapshot: before,
|
|
241
317
|
desiredProtection: {
|
|
242
318
|
strictRequiredChecks: targetPolicy.strictRequiredChecks,
|
|
243
319
|
requiredCheckBindings: targetPolicy.requiredCheckBindings,
|
|
244
320
|
requiredApprovals: targetPolicy.requiredApprovals,
|
|
245
321
|
rulesetBypassActors: [],
|
|
322
|
+
blockDeletions: hasFlag(args, "block-deletions"),
|
|
323
|
+
blockNonFastForward: hasFlag(args, "block-non-fast-forward"),
|
|
246
324
|
},
|
|
247
325
|
});
|
|
248
326
|
const boundCore = {
|
|
@@ -255,8 +333,8 @@ function rulesetPolicyPlan(args) {
|
|
|
255
333
|
...planCore,
|
|
256
334
|
planRoot: githubGovernanceDigest(planCore),
|
|
257
335
|
};
|
|
258
|
-
|
|
259
|
-
|
|
336
|
+
writeJson(snapshotOutput, snapshot);
|
|
337
|
+
writeJson(planOutput, finalPlan);
|
|
260
338
|
return finalPlan;
|
|
261
339
|
}
|
|
262
340
|
|
|
@@ -276,20 +354,29 @@ function rulesetApply(args) {
|
|
|
276
354
|
snapshotRoot !== githubGovernanceDigest(snapshotCore)) {
|
|
277
355
|
throw new Error("ruleset rollback snapshot root mismatch");
|
|
278
356
|
}
|
|
279
|
-
const
|
|
280
|
-
|
|
357
|
+
const currentInventory = rulesetInventory(
|
|
358
|
+
rollout.repository,
|
|
359
|
+
rollout.targetRef,
|
|
360
|
+
);
|
|
361
|
+
if (githubGovernanceDigest(currentInventory) !== rollout.inventoryRoot) {
|
|
281
362
|
throw new Error("live GitHub ruleset drifted after planning; apply stopped");
|
|
282
363
|
}
|
|
283
|
-
githubApi(rollout.operations[0].endpoint, {
|
|
364
|
+
const result = githubApi(rollout.operations[0].endpoint, {
|
|
284
365
|
method: rollout.operations[0].method,
|
|
285
366
|
body: rollout.operations[0].body,
|
|
286
367
|
});
|
|
287
|
-
const
|
|
368
|
+
const rulesetId =
|
|
369
|
+
rollout.rulesetId ||
|
|
370
|
+
Number(result.data?.id || 0);
|
|
371
|
+
if (!Number.isInteger(rulesetId) || rulesetId <= 0) {
|
|
372
|
+
throw new Error("GitHub did not return the created ruleset id");
|
|
373
|
+
}
|
|
374
|
+
const after = readRuleset(rollout.repository, rulesetId);
|
|
288
375
|
const afterRoot = githubGovernanceDigest(after);
|
|
289
376
|
if (afterRoot !== rollout.expectedObservation.rulesetRoot) {
|
|
290
377
|
throw new Error("post-change GitHub ruleset read-back does not match the rollout plan");
|
|
291
378
|
}
|
|
292
|
-
|
|
379
|
+
const receiptCore = {
|
|
293
380
|
schemaVersion: 1,
|
|
294
381
|
contract: "kungfu-buildchain-github-governance-ruleset-rollout-receipt",
|
|
295
382
|
status: "applied",
|
|
@@ -297,7 +384,21 @@ function rulesetApply(args) {
|
|
|
297
384
|
snapshotRoot: rollout.snapshotRoot,
|
|
298
385
|
afterRoot,
|
|
299
386
|
repository: rollout.repository,
|
|
300
|
-
|
|
387
|
+
targetRef: rollout.targetRef,
|
|
388
|
+
rulesetId,
|
|
389
|
+
rollback:
|
|
390
|
+
rollout.action === "create"
|
|
391
|
+
? {
|
|
392
|
+
method: "DELETE",
|
|
393
|
+
endpoint: rulesetEndpoint(rollout.repository, rulesetId),
|
|
394
|
+
body: null,
|
|
395
|
+
preconditionRoot: afterRoot,
|
|
396
|
+
}
|
|
397
|
+
: rollout.rollback[0],
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
...receiptCore,
|
|
401
|
+
receiptRoot: githubGovernanceDigest(receiptCore),
|
|
301
402
|
};
|
|
302
403
|
}
|
|
303
404
|
|
|
@@ -311,14 +412,46 @@ function rulesetRollback(args) {
|
|
|
311
412
|
if (rollout.snapshotRoot !== confirmed) {
|
|
312
413
|
throw new Error("--confirm-rollback-root does not match the frozen ruleset snapshot");
|
|
313
414
|
}
|
|
314
|
-
|
|
415
|
+
let rulesetId = rollout.rulesetId;
|
|
416
|
+
let operation = rollout.rollback[0];
|
|
417
|
+
if (operation.requiresApplyReceipt) {
|
|
418
|
+
const receipt = readJson(
|
|
419
|
+
required(flag(args, "apply-receipt"), "--apply-receipt"),
|
|
420
|
+
"ruleset apply receipt",
|
|
421
|
+
);
|
|
422
|
+
const { receiptRoot, ...receiptCore } = receipt;
|
|
423
|
+
if (
|
|
424
|
+
receiptRoot !== githubGovernanceDigest(receiptCore) ||
|
|
425
|
+
receipt.planRoot !== rollout.planRoot ||
|
|
426
|
+
receipt.repository !== rollout.repository ||
|
|
427
|
+
receipt.targetRef !== rollout.targetRef
|
|
428
|
+
) {
|
|
429
|
+
throw new Error("ruleset apply receipt does not match the rollout plan");
|
|
430
|
+
}
|
|
431
|
+
rulesetId = Number(receipt.rulesetId);
|
|
432
|
+
operation = receipt.rollback;
|
|
433
|
+
const current = readRuleset(rollout.repository, rulesetId);
|
|
434
|
+
if (githubGovernanceDigest(current) !== operation.preconditionRoot) {
|
|
435
|
+
throw new Error("created ruleset drifted after apply; rollback stopped");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
315
438
|
githubApi(operation.endpoint, {
|
|
316
439
|
method: operation.method,
|
|
317
440
|
body: operation.body,
|
|
318
441
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
442
|
+
if (operation.method === "DELETE") {
|
|
443
|
+
const remaining = readMatchingRulesets(
|
|
444
|
+
rollout.repository,
|
|
445
|
+
rollout.targetRef,
|
|
446
|
+
);
|
|
447
|
+
if (remaining.length !== 0) {
|
|
448
|
+
throw new Error("created ruleset remains after rollback");
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
const restored = readRuleset(rollout.repository, rulesetId);
|
|
452
|
+
if (githubGovernanceDigest(restored) !== operation.preconditionRoot) {
|
|
453
|
+
throw new Error("ruleset rollback read-back does not match the frozen snapshot");
|
|
454
|
+
}
|
|
322
455
|
}
|
|
323
456
|
return {
|
|
324
457
|
schemaVersion: 1,
|
|
@@ -327,7 +460,7 @@ function rulesetRollback(args) {
|
|
|
327
460
|
planRoot: rollout.planRoot,
|
|
328
461
|
snapshotRoot: rollout.snapshotRoot,
|
|
329
462
|
repository: rollout.repository,
|
|
330
|
-
rulesetId
|
|
463
|
+
rulesetId,
|
|
331
464
|
};
|
|
332
465
|
}
|
|
333
466
|
|
|
@@ -368,8 +501,8 @@ function protectionPolicyPlan(args) {
|
|
|
368
501
|
...boundCore,
|
|
369
502
|
planRoot: githubGovernanceDigest(boundCore),
|
|
370
503
|
};
|
|
371
|
-
|
|
372
|
-
|
|
504
|
+
writeJson(snapshotOutput, snapshotWithRoot);
|
|
505
|
+
writeJson(planOutput, finalPlan);
|
|
373
506
|
return finalPlan;
|
|
374
507
|
}
|
|
375
508
|
|
|
@@ -419,8 +552,8 @@ function plan(args) {
|
|
|
419
552
|
...boundCore,
|
|
420
553
|
planRoot: githubGovernanceDigest(boundCore),
|
|
421
554
|
};
|
|
422
|
-
|
|
423
|
-
|
|
555
|
+
writeJson(snapshotOutput, snapshotWithRoot);
|
|
556
|
+
writeJson(planOutput, finalPlan);
|
|
424
557
|
return finalPlan;
|
|
425
558
|
}
|
|
426
559
|
|
|
@@ -114,10 +114,24 @@ export function prepareGateExecutionFiles(files) {
|
|
|
114
114
|
for (const file of files) fs.rmSync(file, { force: true });
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export function commandSpawnOptions({
|
|
118
|
+
cwd,
|
|
119
|
+
env = process.env,
|
|
120
|
+
streamOutput = false,
|
|
121
|
+
} = {}) {
|
|
122
|
+
if (streamOutput) return { cwd, env, stdio: "inherit" };
|
|
123
|
+
return {
|
|
124
|
+
cwd,
|
|
125
|
+
env,
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
function runArgv(
|
|
118
132
|
argv,
|
|
119
133
|
args,
|
|
120
|
-
{ cwd, env = process.env, allowFailure = false } = {},
|
|
134
|
+
{ cwd, env = process.env, allowFailure = false, streamOutput = false } = {},
|
|
121
135
|
) {
|
|
122
136
|
const command = argv[0];
|
|
123
137
|
const commandArgs = [...argv.slice(1), ...args];
|
|
@@ -126,25 +140,12 @@ function runArgv(
|
|
|
126
140
|
const batchInvocation = windowsBatch
|
|
127
141
|
? windowsBatchInvocation(command, commandArgs, { cwd })
|
|
128
142
|
: null;
|
|
143
|
+
const spawnOptions = commandSpawnOptions({ cwd, env, streamOutput });
|
|
129
144
|
const result = windowsBatch
|
|
130
|
-
? spawnSync(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
cwd,
|
|
135
|
-
env,
|
|
136
|
-
encoding: "utf8",
|
|
137
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
138
|
-
},
|
|
139
|
-
)
|
|
140
|
-
: spawnSync(command, commandArgs, {
|
|
141
|
-
cwd,
|
|
142
|
-
env,
|
|
143
|
-
encoding: "utf8",
|
|
144
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
145
|
-
});
|
|
146
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
147
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
145
|
+
? spawnSync(batchInvocation.command, batchInvocation.args, spawnOptions)
|
|
146
|
+
: spawnSync(command, commandArgs, spawnOptions);
|
|
147
|
+
if (!streamOutput && result.stdout) process.stdout.write(result.stdout);
|
|
148
|
+
if (!streamOutput && result.stderr) process.stderr.write(result.stderr);
|
|
148
149
|
if (result.error) throw result.error;
|
|
149
150
|
const status = result.status ?? 1;
|
|
150
151
|
if (!allowFailure && status !== 0)
|
|
@@ -257,7 +258,12 @@ function runMode() {
|
|
|
257
258
|
],
|
|
258
259
|
registry,
|
|
259
260
|
);
|
|
260
|
-
const runResult = runArgv(argv, runArgs, {
|
|
261
|
+
const runResult = runArgv(argv, runArgs, {
|
|
262
|
+
cwd,
|
|
263
|
+
env,
|
|
264
|
+
allowFailure: true,
|
|
265
|
+
streamOutput: true,
|
|
266
|
+
});
|
|
261
267
|
let receipt = fs.existsSync(receiptPath)
|
|
262
268
|
? readJson(receiptPath, "Shifu gate receipt")
|
|
263
269
|
: null;
|