@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-alpha.2
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/dist/site/buildchain-contract.json +49 -19
- package/dist/site/buildchain-site.json +4 -4
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +23 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/node-api-registry.json +17 -4
- package/dist/site/public-surface-audit.json +9 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +4 -4
- package/dist/site/workflow-registry.json +8 -2
- package/package.json +2 -1
- 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 +10 -0
- package/scripts/auditable-demo.mjs +29 -6
- package/scripts/generate-site-bundle.mjs +1 -0
- package/scripts/locked-source-checkout.mjs +48 -0
- package/scripts/reconcile-github-governance.mjs +158 -25
- package/scripts/shifu-gate-profile.mjs +26 -20
|
@@ -1191,20 +1191,38 @@ export function createGithubRulesetGovernanceRolloutPlan({
|
|
|
1191
1191
|
repository,
|
|
1192
1192
|
targetRef,
|
|
1193
1193
|
rulesetId,
|
|
1194
|
+
rulesetName,
|
|
1194
1195
|
inventory,
|
|
1195
1196
|
rollbackSnapshot,
|
|
1196
1197
|
desiredProtection,
|
|
1197
1198
|
} = {}) {
|
|
1198
1199
|
const fullName = requiredString(repository, "repository");
|
|
1199
1200
|
const branch = normalizedRef(targetRef);
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1201
|
+
const creating = rulesetId === null || rulesetId === undefined || rulesetId === "";
|
|
1202
|
+
const id = creating ? null : Number(rulesetId);
|
|
1203
|
+
if (!creating && (!Number.isInteger(id) || id <= 0)) {
|
|
1204
|
+
throw new Error("ruleset id must be a positive integer when supplied");
|
|
1203
1205
|
}
|
|
1204
|
-
if (!inventory || !rollbackSnapshot) {
|
|
1205
|
-
throw new Error(
|
|
1206
|
+
if (!inventory || (!creating && !rollbackSnapshot)) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
"read-only inventory and any existing frozen rollback snapshot are required",
|
|
1209
|
+
);
|
|
1206
1210
|
}
|
|
1207
|
-
const before =
|
|
1211
|
+
const before = creating
|
|
1212
|
+
? {
|
|
1213
|
+
name: requiredString(rulesetName, "ruleset name"),
|
|
1214
|
+
target: "branch",
|
|
1215
|
+
enforcement: "active",
|
|
1216
|
+
bypass_actors: [],
|
|
1217
|
+
conditions: {
|
|
1218
|
+
ref_name: {
|
|
1219
|
+
include: [`refs/heads/${branch}`],
|
|
1220
|
+
exclude: [],
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1223
|
+
rules: [],
|
|
1224
|
+
}
|
|
1225
|
+
: normalizeGithubRulesetSnapshot(rollbackSnapshot);
|
|
1208
1226
|
const exactInclude = before.conditions?.ref_name?.include || [];
|
|
1209
1227
|
const exactExclude = before.conditions?.ref_name?.exclude || [];
|
|
1210
1228
|
if (
|
|
@@ -1265,28 +1283,53 @@ export function createGithubRulesetGovernanceRolloutPlan({
|
|
|
1265
1283
|
});
|
|
1266
1284
|
if (pullRequestRules.length === 0) desiredRules.push(desiredPullRequestRule);
|
|
1267
1285
|
if (statusCheckRules.length === 0) desiredRules.push(desiredStatusCheckRule);
|
|
1286
|
+
if (
|
|
1287
|
+
desiredProtection?.blockDeletions === true &&
|
|
1288
|
+
!desiredRules.some((rule) => rule.type === "deletion")
|
|
1289
|
+
) {
|
|
1290
|
+
desiredRules.push({ type: "deletion" });
|
|
1291
|
+
}
|
|
1292
|
+
if (
|
|
1293
|
+
desiredProtection?.blockNonFastForward === true &&
|
|
1294
|
+
!desiredRules.some((rule) => rule.type === "non_fast_forward")
|
|
1295
|
+
) {
|
|
1296
|
+
desiredRules.push({ type: "non_fast_forward" });
|
|
1297
|
+
}
|
|
1268
1298
|
const desired = {
|
|
1269
1299
|
...before,
|
|
1300
|
+
name: rulesetName
|
|
1301
|
+
? requiredString(rulesetName, "ruleset name")
|
|
1302
|
+
: before.name,
|
|
1270
1303
|
bypass_actors: providerRulesetBypassActors(
|
|
1271
1304
|
desiredProtection?.rulesetBypassActors,
|
|
1272
1305
|
),
|
|
1273
1306
|
rules: desiredRules,
|
|
1274
1307
|
};
|
|
1275
|
-
const endpoint =
|
|
1308
|
+
const endpoint = creating
|
|
1309
|
+
? `repos/${fullName}/rulesets`
|
|
1310
|
+
: `repos/${fullName}/rulesets/${id}`;
|
|
1276
1311
|
const core = {
|
|
1277
1312
|
schemaVersion: 1,
|
|
1278
1313
|
contract: GITHUB_GOVERNANCE_RULESET_ROLLOUT_CONTRACT,
|
|
1279
1314
|
repository: fullName,
|
|
1280
1315
|
targetRef: branch,
|
|
1281
1316
|
rulesetId: id,
|
|
1317
|
+
rulesetName: desired.name,
|
|
1318
|
+
action: creating ? "create" : "update",
|
|
1282
1319
|
inventoryRoot: githubGovernanceDigest(inventory),
|
|
1283
|
-
rollbackSnapshotRoot:
|
|
1284
|
-
|
|
1320
|
+
rollbackSnapshotRoot: creating
|
|
1321
|
+
? githubGovernanceDigest({ rulesetExists: false, targetRef: branch })
|
|
1322
|
+
: githubGovernanceDigest(before),
|
|
1323
|
+
operations: [
|
|
1324
|
+
{ method: creating ? "POST" : "PUT", endpoint, body: desired },
|
|
1325
|
+
],
|
|
1285
1326
|
impact: [
|
|
1286
1327
|
"replace ruleset bypass actors with the exact provider-admitted desired set",
|
|
1287
1328
|
"require fresh Code Owner review and resolved review threads",
|
|
1288
1329
|
"bind required status checks and strictness to the authoritative target descriptor",
|
|
1289
|
-
|
|
1330
|
+
creating
|
|
1331
|
+
? "create one exact-branch active ruleset because no matching ruleset exists"
|
|
1332
|
+
: "preserve unrelated ruleset rules and exact target conditions",
|
|
1290
1333
|
],
|
|
1291
1334
|
expectedObservation: {
|
|
1292
1335
|
rulesetRoot: githubGovernanceDigest(desired),
|
|
@@ -1296,12 +1339,24 @@ export function createGithubRulesetGovernanceRolloutPlan({
|
|
|
1296
1339
|
desiredProtection?.strictRequiredChecks === true,
|
|
1297
1340
|
requiredApprovals,
|
|
1298
1341
|
},
|
|
1299
|
-
rollback:
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1342
|
+
rollback: creating
|
|
1343
|
+
? [
|
|
1344
|
+
{
|
|
1345
|
+
method: "DELETE",
|
|
1346
|
+
endpoint: `repos/${fullName}/rulesets/{ruleset_id}`,
|
|
1347
|
+
body: null,
|
|
1348
|
+
preconditionRoot: githubGovernanceDigest(desired),
|
|
1349
|
+
requiresApplyReceipt: true,
|
|
1350
|
+
},
|
|
1351
|
+
]
|
|
1352
|
+
: [
|
|
1353
|
+
{
|
|
1354
|
+
method: "PUT",
|
|
1355
|
+
endpoint,
|
|
1356
|
+
body: before,
|
|
1357
|
+
preconditionRoot: githubGovernanceDigest(before),
|
|
1358
|
+
},
|
|
1359
|
+
],
|
|
1305
1360
|
};
|
|
1306
1361
|
return { ...core, planRoot: githubGovernanceDigest(core) };
|
|
1307
1362
|
}
|
package/packages/core/index.js
CHANGED
|
@@ -64,6 +64,16 @@ export {
|
|
|
64
64
|
verifyPortableDevCachePlan,
|
|
65
65
|
} from "./portable-dev-cache.js";
|
|
66
66
|
|
|
67
|
+
export {
|
|
68
|
+
BUILDCHAIN_CACHE_EVIDENCE_SET_CONTRACT,
|
|
69
|
+
BUILDCHAIN_CACHE_OPERATION_RECEIPT_CONTRACT,
|
|
70
|
+
cacheEvidenceDigest,
|
|
71
|
+
createCacheEvidenceSet,
|
|
72
|
+
createCacheOperationReceipt,
|
|
73
|
+
verifyCacheEvidenceSet,
|
|
74
|
+
verifyCacheOperationReceipt,
|
|
75
|
+
} from "./cache-evidence.js";
|
|
76
|
+
|
|
67
77
|
export {
|
|
68
78
|
explainReleaseLineDryRun,
|
|
69
79
|
formatReleaseLineDryRun,
|
|
@@ -164,7 +164,18 @@ function validateScene(value) {
|
|
|
164
164
|
for (const key of ["background", "accent"]) {
|
|
165
165
|
if (value[key] !== undefined) invariant(/^#[0-9a-fA-F]{6}$/.test(value[key]), `scene.${key} is invalid`);
|
|
166
166
|
}
|
|
167
|
-
return
|
|
167
|
+
return {
|
|
168
|
+
schema: value.schema,
|
|
169
|
+
id: value.id,
|
|
170
|
+
width: value.width,
|
|
171
|
+
height: value.height,
|
|
172
|
+
fps: value.fps,
|
|
173
|
+
durationMs: value.durationMs,
|
|
174
|
+
title: value.title,
|
|
175
|
+
commandLabel: value.commandLabel ?? "",
|
|
176
|
+
background: (value.background ?? "#10151f").toLowerCase(),
|
|
177
|
+
accent: (value.accent ?? "#67e8a5").toLowerCase(),
|
|
178
|
+
};
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
function validateProjection(value, scene, transcriptLineCount) {
|
|
@@ -190,7 +201,17 @@ function validateProjection(value, scene, transcriptLineCount) {
|
|
|
190
201
|
}
|
|
191
202
|
if (cue.annotation !== undefined) text(cue.annotation, 0, 200, `projection.cues[${index}].annotation`);
|
|
192
203
|
}
|
|
193
|
-
return
|
|
204
|
+
return {
|
|
205
|
+
schema: value.schema,
|
|
206
|
+
evidenceClass: value.evidenceClass,
|
|
207
|
+
claimBoundary: value.claimBoundary,
|
|
208
|
+
cues: value.cues.map((cue) => ({
|
|
209
|
+
startMs: cue.startMs,
|
|
210
|
+
endMs: cue.endMs,
|
|
211
|
+
transcriptLines: cue.transcriptLines,
|
|
212
|
+
annotation: cue.annotation ?? "",
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
function validateSourceCoordinate(value) {
|
|
@@ -427,7 +448,9 @@ function finalizeGate(values) {
|
|
|
427
448
|
const sourceCoordinate = validateSourceCoordinate(readJson(sourceCoordinatePath, "source artifact coordinate"));
|
|
428
449
|
invariant(sourceCoordinate.sourceSha === sourceSha, "source artifact coordinate SHA mismatch");
|
|
429
450
|
ensureEmptyDirectory(output, "gate bundle");
|
|
430
|
-
|
|
451
|
+
fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
|
|
452
|
+
writeJson(path.join(output, "scene.json"), normalized.scene);
|
|
453
|
+
writeJson(path.join(output, "public-projection.json"), normalized.projection);
|
|
431
454
|
copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
|
|
432
455
|
copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
|
|
433
456
|
for (const name of listFiles(smokeOutput)) {
|
|
@@ -454,9 +477,9 @@ function finalizeGate(values) {
|
|
|
454
477
|
smokeManifestRoot: sha256(readRegular(path.join(smokeOutput, "manifest.json"), "smoke manifest")),
|
|
455
478
|
},
|
|
456
479
|
qualifiedInputs: {
|
|
457
|
-
transcript: sha256(readRegular(path.join(
|
|
458
|
-
projection: sha256(readRegular(path.join(
|
|
459
|
-
scene: sha256(readRegular(path.join(
|
|
480
|
+
transcript: sha256(readRegular(path.join(output, "complete-transcript.txt"), "transcript")),
|
|
481
|
+
projection: sha256(readRegular(path.join(output, "public-projection.json"), "projection")),
|
|
482
|
+
scene: sha256(readRegular(path.join(output, "scene.json"), "scene")),
|
|
460
483
|
evidenceClass: normalized.projection.evidenceClass,
|
|
461
484
|
claimBoundary: normalized.projection.claimBoundary,
|
|
462
485
|
},
|
|
@@ -516,6 +516,7 @@ function nodeApiMeta(exportName) {
|
|
|
516
516
|
"./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
|
|
517
517
|
"./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
|
|
518
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." },
|
|
519
520
|
"./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
|
|
520
521
|
"./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
|
|
521
522
|
"./portable-dev-cache": { group: "observability-diagnostics", summary: "Portable dependency/compiler cache plan, exact-root verification, and provider receipt APIs." },
|
|
@@ -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);
|
|
@@ -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
|
|