@kungfu-tech/buildchain 3.0.2 → 3.0.3-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/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +27 -1
- package/actions/promote-buildchain-ref/README.md +90 -19
- package/actions/report-buildchain-issue/README.md +20 -2
- package/actions/run-lifecycle/README.md +5 -0
- package/actions/validate-config/README.md +19 -1
- package/bin/buildchain.mjs +56 -2
- package/bin/internal/command-registry.mjs +82 -0
- package/bin/internal/trust-release-cli.mjs +1 -0
- package/contracts/auditable-demo-media-profiles-v1.json +116 -1
- package/contracts/buildchain-v2-residuals-v1.json +146 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
- package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
- package/dist/site/buildchain-contract.json +188 -46
- package/dist/site/buildchain-site.json +181 -90
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +860 -98
- package/dist/site/controller-registry.json +118 -5
- package/dist/site/kfd-claims.json +233 -19
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -20
- package/dist/site/node-api-registry.json +374 -59
- package/dist/site/page-registry.json +156 -65
- package/dist/site/public-surface-audit.json +231 -22
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +24 -24
- package/dist/site/workflow-registry.json +575 -79
- package/docs/MAP.md +22 -4
- package/docs/auditable-demo.md +54 -13
- package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
- package/docs/cli.md +119 -12
- package/docs/consumer-issue-reporting.md +20 -2
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +24 -31
- package/docs/lifecycle-protocol.md +20 -2
- package/docs/migration-inventory.md +24 -5
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/ownership.md +19 -1
- package/docs/publication-artifacts.md +158 -18
- package/docs/publication-authority.md +4 -4
- package/docs/publish-transaction.md +78 -31
- package/docs/release-candidate.md +24 -0
- package/docs/release-flow.md +50 -32
- package/docs/release-governance.md +39 -32
- package/docs/release-passport.md +4 -0
- package/docs/release-propagation.md +57 -12
- package/docs/reusable-build-surface.md +218 -90
- package/docs/runtime-train-validation.md +20 -2
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/stable-candidate-patrol.md +6 -6
- package/docs/toolkit-observability.md +22 -0
- package/docs/versioning.md +46 -24
- package/docs/web-surface-deployments.md +82 -1
- package/package.json +7 -5
- package/packages/core/README.md +60 -4
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +9 -4
- package/packages/core/cache-evidence.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/diagnostics.js +105 -30
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/index.js +40 -0
- package/packages/core/paper.js +3226 -0
- package/packages/core/public-surface-audit.js +3 -0
- package/packages/core/publication-artifact.js +1 -6
- package/packages/core/publication-authority.js +1 -0
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/publication-reproducibility.js +947 -0
- package/packages/core/publication-sealed-bundle.js +190 -0
- package/packages/core/publish-transaction.js +147 -17
- package/packages/core/release-candidate.js +234 -0
- package/packages/core/release-passport.js +519 -228
- package/packages/core/release-propagation.js +175 -1
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +266 -7
- package/scripts/aws-codebuild-toolchain.mjs +285 -0
- package/scripts/aws-macos-jit-core.mjs +378 -0
- package/scripts/aws-macos-jit.mjs +123 -0
- package/scripts/aws-runner-burst-core.mjs +248 -0
- package/scripts/aws-runner-burst.mjs +79 -0
- package/scripts/aws-windows-jit-core.mjs +374 -0
- package/scripts/aws-windows-jit.mjs +121 -0
- package/scripts/build-contract-core.mjs +69 -6
- package/scripts/build-standalone-binary.mjs +23 -0
- package/scripts/buildchain-contract-lock.mjs +1 -1
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-internal-architecture.mjs +69 -2
- package/scripts/check-inventory.mjs +70 -2
- package/scripts/check-javascript-syntax.mjs +31 -0
- package/scripts/check-maintainability.mjs +371 -0
- package/scripts/compiler-cache-evidence.mjs +221 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/generate-channel-promotion-workflow.mjs +3 -12
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/generate-site-bundle.mjs +84 -18
- package/scripts/import-artifact-signing-results.mjs +64 -5
- package/scripts/inspect-artifact-signing-requests.mjs +52 -14
- package/scripts/maintainability-metrics.mjs +339 -0
- package/scripts/npm-publish-transaction.mjs +57 -6
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/paper.mjs +645 -0
- package/scripts/promotion-channel-router.mjs +16 -4
- package/scripts/publication-reproducibility.mjs +62 -0
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/release-propagation.mjs +39 -0
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/resolve-build-contract.mjs +6 -0
- package/scripts/route-offline-runners.mjs +236 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/run-lifecycle-core.mjs +62 -1
- package/scripts/runtime-ref-core.mjs +36 -4
- package/scripts/seal-artifact-signing-requests.mjs +64 -12
- package/scripts/stable-candidate-qualification.mjs +50 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-core.mjs +272 -86
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
export const RELEASE_PROPAGATION_GRAPH_CONTRACT = "kungfu-buildchain-release-propagation-graph";
|
|
6
6
|
export const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
|
|
7
7
|
export const RELEASE_PROPAGATION_LOCK_CONTRACT = "kungfu-buildchain-release-propagation-lock";
|
|
8
|
+
export const RELEASE_PROPAGATION_RECEIPT_CONTRACT = "kungfu-buildchain-release-propagation-receipt";
|
|
8
9
|
|
|
9
10
|
const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
|
|
10
11
|
const SUPPORTED_CHANNEL_POLICIES = new Set(["preserve", "explicit"]);
|
|
@@ -31,6 +32,49 @@ function sha256Json(value) {
|
|
|
31
32
|
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function releaseVersion(upstreamRelease) {
|
|
36
|
+
return upstreamRelease.package?.version || upstreamRelease.publicationArtifact?.version || "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeBranchPart(value) {
|
|
40
|
+
const normalized = String(value || "")
|
|
41
|
+
.trim()
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replaceAll(/[^a-z0-9._-]+/g, "-")
|
|
44
|
+
.replaceAll(/^-+|-+$/g, "");
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
throw new Error(`release propagation branch component is empty: ${value}`);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function releaseIdentity(upstreamRelease) {
|
|
52
|
+
const version = releaseVersion(upstreamRelease);
|
|
53
|
+
if (!version) {
|
|
54
|
+
throw new Error("upstream release does not expose a package or publication version");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
repository: upstreamRelease.repository,
|
|
58
|
+
version,
|
|
59
|
+
channel: upstreamRelease.channel,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function propagationIdentity({ upstreamRelease, targetNode }) {
|
|
64
|
+
return {
|
|
65
|
+
release: releaseIdentity(upstreamRelease),
|
|
66
|
+
downstreamRepository: targetNode.repository,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function propagationBranch({ upstreamRelease, targetNode }) {
|
|
71
|
+
const identity = propagationIdentity({ upstreamRelease, targetNode });
|
|
72
|
+
const digest = sha256Json(identity).slice(0, 12);
|
|
73
|
+
const repository = safeBranchPart(upstreamRelease.repository.replace("/", "-"));
|
|
74
|
+
const version = safeBranchPart(releaseVersion(upstreamRelease));
|
|
75
|
+
return `buildchain/release-propagation/${repository}/${version}-${upstreamRelease.channel}-${digest}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
function assertPlainObject(value, label) {
|
|
35
79
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
36
80
|
throw new Error(`${label} must be an object`);
|
|
@@ -305,6 +349,8 @@ export function createReleasePropagationLock({
|
|
|
305
349
|
upstreamRelease,
|
|
306
350
|
downstreamChannel,
|
|
307
351
|
} = {}) {
|
|
352
|
+
const identity = propagationIdentity({ upstreamRelease, targetNode });
|
|
353
|
+
const propagationKey = sha256Json(identity);
|
|
308
354
|
const lock = {
|
|
309
355
|
schemaVersion: 1,
|
|
310
356
|
contract: RELEASE_PROPAGATION_LOCK_CONTRACT,
|
|
@@ -331,6 +377,9 @@ export function createReleasePropagationLock({
|
|
|
331
377
|
edge: edge.id,
|
|
332
378
|
channelPolicy: edge.channelPolicy,
|
|
333
379
|
channelMap: edge.channelMap,
|
|
380
|
+
releaseIdentity: identity.release,
|
|
381
|
+
propagationKey,
|
|
382
|
+
branch: propagationBranch({ upstreamRelease, targetNode }),
|
|
334
383
|
exact: true,
|
|
335
384
|
floatingTags: false,
|
|
336
385
|
},
|
|
@@ -374,6 +423,8 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
|
|
|
374
423
|
channel: downstreamChannel,
|
|
375
424
|
baseRef: lock.downstream.baseRef,
|
|
376
425
|
lockPath: lock.downstream.lockPath,
|
|
426
|
+
propagationKey: lock.propagation.propagationKey,
|
|
427
|
+
branch: lock.propagation.branch,
|
|
377
428
|
lock,
|
|
378
429
|
});
|
|
379
430
|
}
|
|
@@ -387,6 +438,7 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
|
|
|
387
438
|
targetCount: targets.length,
|
|
388
439
|
channels: [...new Set(targets.map((target) => target.channel))],
|
|
389
440
|
repositories: targets.map((target) => target.repository),
|
|
441
|
+
releaseIdentity: releaseIdentity(upstreamRelease),
|
|
390
442
|
},
|
|
391
443
|
};
|
|
392
444
|
}
|
|
@@ -411,13 +463,135 @@ export function writeReleasePropagationLock({ plan, target = "", cwd = process.c
|
|
|
411
463
|
}
|
|
412
464
|
const selected = matches[0];
|
|
413
465
|
const outputPath = path.resolve(cwd, output || selected.lockPath);
|
|
466
|
+
const rendered = stableJson(selected.lock);
|
|
467
|
+
const previous = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf8") : "";
|
|
414
468
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
415
|
-
|
|
469
|
+
if (previous !== rendered) {
|
|
470
|
+
fs.writeFileSync(outputPath, rendered);
|
|
471
|
+
}
|
|
416
472
|
return {
|
|
417
473
|
target: selected.target,
|
|
418
474
|
repository: selected.repository,
|
|
419
475
|
channel: selected.channel,
|
|
420
476
|
path: outputPath,
|
|
421
477
|
lockSha256: selected.lock.lockSha256,
|
|
478
|
+
propagationKey: selected.propagationKey || selected.lock.propagation?.propagationKey || "",
|
|
479
|
+
branch: selected.branch || selected.lock.propagation?.branch || "",
|
|
480
|
+
status: previous === rendered ? "reused" : "written",
|
|
481
|
+
changed: previous !== rendered,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const RECEIPT_VISIBILITY_STATES = new Set(["pending", "visible", "failed", "not-requested"]);
|
|
486
|
+
const RECEIPT_PR_STATES = new Set(["planned", "created", "updated", "reused", "no-change", "failed"]);
|
|
487
|
+
|
|
488
|
+
function receiptVisibilityState(value, label) {
|
|
489
|
+
const state = optionalString(value) || "pending";
|
|
490
|
+
if (!RECEIPT_VISIBILITY_STATES.has(state)) {
|
|
491
|
+
throw new Error(`${label} must be pending, visible, failed, or not-requested`);
|
|
492
|
+
}
|
|
493
|
+
return state;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function receiptPrOutcome(value = {}) {
|
|
497
|
+
const outcome = assertPlainObject(value, "release propagation PR outcome");
|
|
498
|
+
const state = optionalString(outcome.state) || "planned";
|
|
499
|
+
if (!RECEIPT_PR_STATES.has(state)) {
|
|
500
|
+
throw new Error("release propagation PR outcome state is unsupported");
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
state,
|
|
504
|
+
number: outcome.number === undefined || outcome.number === null || outcome.number === ""
|
|
505
|
+
? null
|
|
506
|
+
: Number(outcome.number),
|
|
507
|
+
url: optionalString(outcome.url),
|
|
508
|
+
branch: assertString(outcome.branch, "release propagation PR outcome branch"),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function createReleasePropagationReceipt({
|
|
513
|
+
plan,
|
|
514
|
+
target = "",
|
|
515
|
+
lockResult,
|
|
516
|
+
prOutcome,
|
|
517
|
+
stagingState = "pending",
|
|
518
|
+
productionState = "not-requested",
|
|
519
|
+
observedAt = "",
|
|
520
|
+
} = {}) {
|
|
521
|
+
const selectedPlan = assertPlainObject(plan, "plan");
|
|
522
|
+
const matches = selectedPlan.targets.filter((entry) =>
|
|
523
|
+
!target || entry.target === target || entry.repository === target);
|
|
524
|
+
if (matches.length !== 1) {
|
|
525
|
+
throw new Error(`expected exactly one propagation target, found ${matches.length}`);
|
|
526
|
+
}
|
|
527
|
+
const selected = matches[0];
|
|
528
|
+
const result = assertPlainObject(lockResult, "release propagation lock result");
|
|
529
|
+
if (result.lockSha256 !== selected.lock.lockSha256) {
|
|
530
|
+
throw new Error("release propagation receipt lock result does not match the plan");
|
|
531
|
+
}
|
|
532
|
+
const propagationKey = selected.propagationKey || selected.lock.propagation?.propagationKey || "";
|
|
533
|
+
if (result.propagationKey && result.propagationKey !== propagationKey) {
|
|
534
|
+
throw new Error("release propagation receipt key does not match the lock result");
|
|
535
|
+
}
|
|
536
|
+
const branch = selected.branch || selected.lock.propagation?.branch || "";
|
|
537
|
+
const normalizedPr = receiptPrOutcome({ ...prOutcome, branch: prOutcome?.branch || branch });
|
|
538
|
+
if (normalizedPr.branch !== branch) {
|
|
539
|
+
throw new Error("release propagation PR branch does not match the exact plan");
|
|
540
|
+
}
|
|
541
|
+
const upstreamRelease = selectedPlan.upstreamRelease;
|
|
542
|
+
const release = releaseIdentity(upstreamRelease);
|
|
543
|
+
const states = {
|
|
544
|
+
"package-published": {
|
|
545
|
+
state: upstreamRelease.package ? "complete" : "not-applicable",
|
|
546
|
+
evidence: upstreamRelease.package
|
|
547
|
+
? {
|
|
548
|
+
name: upstreamRelease.package.name,
|
|
549
|
+
version: upstreamRelease.package.version,
|
|
550
|
+
integrity: upstreamRelease.package.integrity,
|
|
551
|
+
}
|
|
552
|
+
: null,
|
|
553
|
+
},
|
|
554
|
+
"alpha-complete": {
|
|
555
|
+
state: release.channel === "alpha" ? "complete" : "not-applicable",
|
|
556
|
+
evidence: release.channel === "alpha"
|
|
557
|
+
? {
|
|
558
|
+
releasePassportSha256: upstreamRelease.releasePassport.sha256,
|
|
559
|
+
tag: upstreamRelease.tag,
|
|
560
|
+
}
|
|
561
|
+
: null,
|
|
562
|
+
},
|
|
563
|
+
"staging-visible": {
|
|
564
|
+
state: receiptVisibilityState(stagingState, "stagingState"),
|
|
565
|
+
},
|
|
566
|
+
"production-visible": {
|
|
567
|
+
state: receiptVisibilityState(productionState, "productionState"),
|
|
568
|
+
},
|
|
569
|
+
};
|
|
570
|
+
const body = {
|
|
571
|
+
schemaVersion: 1,
|
|
572
|
+
contract: RELEASE_PROPAGATION_RECEIPT_CONTRACT,
|
|
573
|
+
propagationKey,
|
|
574
|
+
observedAt: optionalString(observedAt),
|
|
575
|
+
release,
|
|
576
|
+
upstream: {
|
|
577
|
+
repository: upstreamRelease.repository,
|
|
578
|
+
tag: upstreamRelease.tag,
|
|
579
|
+
sourceSha: upstreamRelease.sourceSha,
|
|
580
|
+
releasePassport: upstreamRelease.releasePassport,
|
|
581
|
+
},
|
|
582
|
+
downstream: {
|
|
583
|
+
repository: selected.repository,
|
|
584
|
+
channel: selected.channel,
|
|
585
|
+
baseRef: selected.baseRef,
|
|
586
|
+
lockPath: selected.lockPath,
|
|
587
|
+
lockSha256: result.lockSha256,
|
|
588
|
+
lockWriteState: result.status || (result.changed === false ? "reused" : "written"),
|
|
589
|
+
pullRequest: normalizedPr,
|
|
590
|
+
},
|
|
591
|
+
states,
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
...body,
|
|
595
|
+
receiptSha256: sha256Json(body),
|
|
422
596
|
};
|
|
423
597
|
}
|
|
@@ -194,12 +194,14 @@ export function evaluateStableReleaseGate({
|
|
|
194
194
|
: undefined;
|
|
195
195
|
const attestorAllowed = required.allowedAttestors.length === 0 ||
|
|
196
196
|
required.allowedAttestors.includes(string(evidence?.attestor));
|
|
197
|
+
const completionTimingValid = required.source === "release-candidate" ||
|
|
198
|
+
(completedAt && completedAt.milliseconds >= candidatePublished.milliseconds);
|
|
197
199
|
const valid = Boolean(
|
|
198
200
|
evidence &&
|
|
199
201
|
string(evidence.status) === "success" &&
|
|
200
202
|
string(evidence.candidateSha) === candidateSha &&
|
|
201
203
|
completedAt &&
|
|
202
|
-
|
|
204
|
+
completionTimingValid &&
|
|
203
205
|
attestorAllowed,
|
|
204
206
|
);
|
|
205
207
|
if (completedAt) {
|
|
@@ -222,6 +224,7 @@ export function evaluateStableReleaseGate({
|
|
|
222
224
|
evidenceUrl: string(evidence?.evidenceUrl),
|
|
223
225
|
attestor: string(evidence?.attestor),
|
|
224
226
|
attestorAllowed,
|
|
227
|
+
completionTimingValid: Boolean(completionTimingValid),
|
|
225
228
|
},
|
|
226
229
|
));
|
|
227
230
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
7
|
+
|
|
8
|
+
const CONTRACT = "kungfu-buildchain-artifact-signing-delegation/v1";
|
|
9
|
+
|
|
10
|
+
function required(value, label) {
|
|
11
|
+
const normalized = String(value || "").trim();
|
|
12
|
+
if (!normalized) throw new Error(`${label} is required`);
|
|
13
|
+
if (/[\r\n\0]/u.test(normalized))
|
|
14
|
+
throw new Error(`${label} contains control characters`);
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function optional(value, label) {
|
|
19
|
+
const normalized = String(value || "").trim();
|
|
20
|
+
if (/[\r\n\0]/u.test(normalized))
|
|
21
|
+
throw new Error(`${label} contains control characters`);
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exactSha(value, label) {
|
|
26
|
+
const normalized = required(value, label);
|
|
27
|
+
if (!/^[0-9a-f]{40}$/u.test(normalized))
|
|
28
|
+
throw new Error(`${label} must be an exact SHA`);
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function positiveInteger(value, label, { allowZero = false } = {}) {
|
|
33
|
+
const normalized = Number(value);
|
|
34
|
+
if (!Number.isSafeInteger(normalized) || normalized < (allowZero ? 0 : 1)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${label} must be ${allowZero ? "a non-negative" : "a positive"} integer`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function repository(value, label) {
|
|
43
|
+
const normalized = required(value, label);
|
|
44
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(normalized)) {
|
|
45
|
+
throw new Error(`${label} must be owner/repository`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeRelativePath(value, label) {
|
|
51
|
+
const normalized = required(value, label).replaceAll("\\", "/");
|
|
52
|
+
const resolved = path.posix.normalize(normalized);
|
|
53
|
+
if (
|
|
54
|
+
resolved === ".." ||
|
|
55
|
+
resolved.startsWith("../") ||
|
|
56
|
+
path.posix.isAbsolute(resolved)
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`${label} must be a safe relative path`);
|
|
59
|
+
}
|
|
60
|
+
if (!/^[A-Za-z0-9._ /-]+$/u.test(resolved))
|
|
61
|
+
throw new Error(`${label} contains unsafe shell characters`);
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateArtifactSigningDelegation(value) {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
67
|
+
throw new Error("delegation must be an object");
|
|
68
|
+
if (value.schemaVersion !== 1 || value.contract !== CONTRACT)
|
|
69
|
+
throw new Error("artifact signing delegation contract mismatch");
|
|
70
|
+
const requestCount = positiveInteger(value.request?.count, "request.count", {
|
|
71
|
+
allowZero: true,
|
|
72
|
+
});
|
|
73
|
+
const delegation = {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
contract: CONTRACT,
|
|
76
|
+
source: {
|
|
77
|
+
repository: repository(value.source?.repository, "source.repository"),
|
|
78
|
+
runId: required(value.source?.runId, "source.runId"),
|
|
79
|
+
runAttempt: positiveInteger(
|
|
80
|
+
value.source?.runAttempt,
|
|
81
|
+
"source.runAttempt",
|
|
82
|
+
),
|
|
83
|
+
sha: exactSha(value.source?.sha, "source.sha"),
|
|
84
|
+
treeSha: exactSha(value.source?.treeSha, "source.treeSha"),
|
|
85
|
+
},
|
|
86
|
+
runtime: {
|
|
87
|
+
repository: repository(value.runtime?.repository, "runtime.repository"),
|
|
88
|
+
sha: exactSha(value.runtime?.sha, "runtime.sha"),
|
|
89
|
+
},
|
|
90
|
+
platform: {
|
|
91
|
+
id: required(value.platform?.id, "platform.id"),
|
|
92
|
+
name: required(value.platform?.name, "platform.name"),
|
|
93
|
+
},
|
|
94
|
+
request: {
|
|
95
|
+
count: requestCount,
|
|
96
|
+
artifact:
|
|
97
|
+
requestCount > 0
|
|
98
|
+
? required(value.request?.artifact, "request.artifact")
|
|
99
|
+
: optional(value.request?.artifact, "request.artifact"),
|
|
100
|
+
},
|
|
101
|
+
authority: {
|
|
102
|
+
runId:
|
|
103
|
+
requestCount > 0
|
|
104
|
+
? required(value.authority?.runId, "authority.runId")
|
|
105
|
+
: optional(value.authority?.runId, "authority.runId"),
|
|
106
|
+
resultArtifact:
|
|
107
|
+
requestCount > 0
|
|
108
|
+
? required(
|
|
109
|
+
value.authority?.resultArtifact,
|
|
110
|
+
"authority.resultArtifact",
|
|
111
|
+
)
|
|
112
|
+
: optional(
|
|
113
|
+
value.authority?.resultArtifact,
|
|
114
|
+
"authority.resultArtifact",
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
artifact: {
|
|
118
|
+
name: required(value.artifact?.name, "artifact.name"),
|
|
119
|
+
manifestArtifact: required(
|
|
120
|
+
value.artifact?.manifestArtifact,
|
|
121
|
+
"artifact.manifestArtifact",
|
|
122
|
+
),
|
|
123
|
+
diagnosticsArtifact: required(
|
|
124
|
+
value.artifact?.diagnosticsArtifact,
|
|
125
|
+
"artifact.diagnosticsArtifact",
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
workingDirectory: safeRelativePath(
|
|
129
|
+
value.workingDirectory || ".",
|
|
130
|
+
"workingDirectory",
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
if (
|
|
134
|
+
requestCount === 0 &&
|
|
135
|
+
(delegation.authority.runId || delegation.authority.resultArtifact)
|
|
136
|
+
) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"unsigned delegation must not contain authority result coordinates",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return delegation;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createArtifactSigningDelegation({
|
|
145
|
+
sourceRepository = process.env.GITHUB_REPOSITORY,
|
|
146
|
+
sourceRunId = process.env.GITHUB_RUN_ID,
|
|
147
|
+
sourceRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
|
|
148
|
+
sourceSha = process.env.BUILDCHAIN_SOURCE_SHA,
|
|
149
|
+
sourceTreeSha = process.env.BUILDCHAIN_SOURCE_TREE_SHA,
|
|
150
|
+
runtimeRepository = process.env.BUILDCHAIN_RUNTIME_REPOSITORY,
|
|
151
|
+
runtimeSha = process.env.BUILDCHAIN_RUNTIME_SHA,
|
|
152
|
+
platformId = process.env.BUILDCHAIN_PLATFORM_ID,
|
|
153
|
+
platformName = process.env.BUILDCHAIN_PLATFORM_NAME,
|
|
154
|
+
requestCount = process.env.BUILDCHAIN_SIGNING_REQUEST_COUNT || "0",
|
|
155
|
+
requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT || "",
|
|
156
|
+
authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
|
|
157
|
+
resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT || "",
|
|
158
|
+
artifactName = process.env.BUILDCHAIN_ARTIFACT_NAME,
|
|
159
|
+
manifestArtifact = process.env.BUILDCHAIN_MANIFEST_ARTIFACT_NAME,
|
|
160
|
+
diagnosticsArtifact = process.env.BUILDCHAIN_DIAGNOSTICS_ARTIFACT_NAME,
|
|
161
|
+
workingDirectory = process.env.BUILDCHAIN_SIGNING_CWD || ".",
|
|
162
|
+
} = {}) {
|
|
163
|
+
return validateArtifactSigningDelegation({
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
contract: CONTRACT,
|
|
166
|
+
source: {
|
|
167
|
+
repository: sourceRepository,
|
|
168
|
+
runId: sourceRunId,
|
|
169
|
+
runAttempt: Number(sourceRunAttempt),
|
|
170
|
+
sha: sourceSha,
|
|
171
|
+
treeSha: sourceTreeSha,
|
|
172
|
+
},
|
|
173
|
+
runtime: { repository: runtimeRepository, sha: runtimeSha },
|
|
174
|
+
platform: { id: platformId, name: platformName },
|
|
175
|
+
request: { count: Number(requestCount), artifact: requestArtifact },
|
|
176
|
+
authority: { runId: authorityRunId, resultArtifact },
|
|
177
|
+
artifact: { name: artifactName, manifestArtifact, diagnosticsArtifact },
|
|
178
|
+
workingDirectory,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function sealArtifactSigningDelegation({
|
|
183
|
+
outputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
|
|
184
|
+
...values
|
|
185
|
+
} = {}) {
|
|
186
|
+
const delegation = createArtifactSigningDelegation(values);
|
|
187
|
+
const target = path.resolve(required(outputPath, "delegation output path"));
|
|
188
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
189
|
+
fs.writeFileSync(target, `${JSON.stringify(delegation, null, 2)}\n`);
|
|
190
|
+
return delegation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function readArtifactSigningDelegation(
|
|
194
|
+
inputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
|
|
195
|
+
) {
|
|
196
|
+
const target = path.resolve(required(inputPath, "delegation input path"));
|
|
197
|
+
return validateArtifactSigningDelegation(
|
|
198
|
+
JSON.parse(fs.readFileSync(target, "utf8")),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function artifactSigningDelegationOutputs(delegation) {
|
|
203
|
+
const value = validateArtifactSigningDelegation(delegation);
|
|
204
|
+
return {
|
|
205
|
+
"request-count": String(value.request.count),
|
|
206
|
+
"request-artifact": value.request.artifact,
|
|
207
|
+
"authority-run-id": value.authority.runId,
|
|
208
|
+
"result-artifact": value.authority.resultArtifact,
|
|
209
|
+
"artifact-name": value.artifact.name,
|
|
210
|
+
"manifest-artifact-name": value.artifact.manifestArtifact,
|
|
211
|
+
"diagnostics-artifact-name": value.artifact.diagnosticsArtifact,
|
|
212
|
+
"working-directory": value.workingDirectory,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function assertArtifactSigningDelegationContext(
|
|
217
|
+
delegation,
|
|
218
|
+
{
|
|
219
|
+
sourceRepository = process.env.BUILDCHAIN_EXPECTED_SOURCE_REPOSITORY || "",
|
|
220
|
+
sourceRunId = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ID || "",
|
|
221
|
+
sourceRunAttempt = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ATTEMPT || "",
|
|
222
|
+
sourceSha = process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || "",
|
|
223
|
+
runtimeRepository = process.env.BUILDCHAIN_EXPECTED_RUNTIME_REPOSITORY ||
|
|
224
|
+
"",
|
|
225
|
+
runtimeSha = process.env.BUILDCHAIN_EXPECTED_RUNTIME_SHA || "",
|
|
226
|
+
platformId = process.env.BUILDCHAIN_EXPECTED_PLATFORM_ID || "",
|
|
227
|
+
} = {},
|
|
228
|
+
) {
|
|
229
|
+
const value = validateArtifactSigningDelegation(delegation);
|
|
230
|
+
const expectations = [
|
|
231
|
+
[sourceRepository, value.source.repository, "source repository"],
|
|
232
|
+
[sourceRunId, value.source.runId, "source run ID"],
|
|
233
|
+
[sourceRunAttempt, String(value.source.runAttempt), "source run attempt"],
|
|
234
|
+
[sourceSha, value.source.sha, "source SHA"],
|
|
235
|
+
[runtimeRepository, value.runtime.repository, "runtime repository"],
|
|
236
|
+
[runtimeSha, value.runtime.sha, "runtime SHA"],
|
|
237
|
+
[platformId, value.platform.id, "platform ID"],
|
|
238
|
+
];
|
|
239
|
+
for (const [expected, actual, label] of expectations) {
|
|
240
|
+
if (expected && String(expected) !== actual)
|
|
241
|
+
throw new Error(`artifact signing delegation ${label} mismatch`);
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
process.argv[1] &&
|
|
248
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
const mode = process.argv[2] || "seal";
|
|
252
|
+
if (mode === "seal") {
|
|
253
|
+
sealArtifactSigningDelegation();
|
|
254
|
+
} else if (mode === "outputs") {
|
|
255
|
+
const delegation = assertArtifactSigningDelegationContext(
|
|
256
|
+
readArtifactSigningDelegation(),
|
|
257
|
+
);
|
|
258
|
+
writeGitHubOutputs(artifactSigningDelegationOutputs(delegation));
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error(`unsupported artifact signing delegation mode: ${mode}`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error(
|
|
264
|
+
`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
|
|
265
|
+
);
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -254,15 +254,33 @@ function addMinutes(iso, minutes) {
|
|
|
254
254
|
return new Date(Date.parse(iso) + minutes * 60_000).toISOString();
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
257
|
+
function repositoryVisibility(repository) {
|
|
258
|
+
return String(
|
|
259
|
+
repository.visibility || (repository.private ? "private" : "public"),
|
|
260
|
+
).toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function selectGithubGovernanceRepositories(
|
|
264
|
+
repositories,
|
|
265
|
+
requested = "",
|
|
266
|
+
descriptor = BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY,
|
|
267
|
+
) {
|
|
268
|
+
const exact = requested
|
|
269
|
+
? repositories.filter((repository) =>
|
|
270
|
+
repository.full_name === requested || repository.name === requested)
|
|
271
|
+
: repositories;
|
|
272
|
+
if (requested && exact.length !== 1) {
|
|
273
|
+
throw new Error("repository selector must resolve exactly once");
|
|
264
274
|
}
|
|
265
|
-
|
|
275
|
+
const managedVisibilities = new Set(
|
|
276
|
+
descriptor.repositoryAdmission.managedVisibilities || ["public"],
|
|
277
|
+
);
|
|
278
|
+
const selected = exact.filter((repository) =>
|
|
279
|
+
managedVisibilities.has(repositoryVisibility(repository)));
|
|
280
|
+
if (requested && selected.length !== 1) {
|
|
281
|
+
throw new Error("repository selector is outside managed governance scope");
|
|
282
|
+
}
|
|
283
|
+
return selected;
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
export function collectGithubGovernanceAudit({
|
|
@@ -291,7 +309,10 @@ export function collectGithubGovernanceAudit({
|
|
|
291
309
|
if (!organizationState.ok || !repositoriesState.readable) {
|
|
292
310
|
throw new Error("organization or managed repository inventory is unreadable; governance audit fails closed");
|
|
293
311
|
}
|
|
294
|
-
const selected =
|
|
312
|
+
const selected = selectGithubGovernanceRepositories(
|
|
313
|
+
repositoriesState.repositories,
|
|
314
|
+
repository,
|
|
315
|
+
);
|
|
295
316
|
if (targetRef && selected.length !== 1) {
|
|
296
317
|
throw new Error("--target-ref requires exactly one selected repository");
|
|
297
318
|
}
|
|
@@ -316,9 +337,7 @@ export function collectGithubGovernanceAudit({
|
|
|
316
337
|
const diagnostics = [];
|
|
317
338
|
for (const metadata of selected) {
|
|
318
339
|
const fullName = String(metadata.full_name || "");
|
|
319
|
-
const visibilityClass =
|
|
320
|
-
metadata.visibility || (metadata.private ? "private" : "public"),
|
|
321
|
-
);
|
|
340
|
+
const visibilityClass = repositoryVisibility(metadata);
|
|
322
341
|
const repositoryIdentityRoot = githubRepositoryIdentityRoot({
|
|
323
342
|
provider: "github",
|
|
324
343
|
providerRepositoryId: String(metadata.node_id || metadata.id || ""),
|
|
@@ -420,7 +439,7 @@ export function collectGithubGovernanceAudit({
|
|
|
420
439
|
}
|
|
421
440
|
}
|
|
422
441
|
const visibility = selected.reduce((counts, item) => {
|
|
423
|
-
const key =
|
|
442
|
+
const key = repositoryVisibility(item);
|
|
424
443
|
counts[key] = Number(counts[key] || 0) + 1;
|
|
425
444
|
return counts;
|
|
426
445
|
}, {});
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import {
|
|
7
7
|
evaluateBuildchainReleaseReconciliation,
|
|
8
8
|
evaluatePublicationControlPlaneSnapshot,
|
|
9
|
+
matchesGithubDeploymentPolicy,
|
|
9
10
|
} from "../packages/core/publication-control-plane-audit.js";
|
|
10
11
|
|
|
11
12
|
function flag(name, fallback = "") {
|
|
@@ -197,8 +198,10 @@ function main() {
|
|
|
197
198
|
const requiredStatusCheck = flag("required-status-check", "check");
|
|
198
199
|
const jobId = flag("job", "promote");
|
|
199
200
|
const environment = flag("environment", "none");
|
|
200
|
-
const providerEnvironment = environment === "none" ? "" : environment;
|
|
201
201
|
const branch = flag("branch");
|
|
202
|
+
const environmentRef = flag("environment-ref", branch);
|
|
203
|
+
const environmentRefType = flag("environment-ref-type", "branch");
|
|
204
|
+
const providerEnvironment = environment === "none" ? "" : environment;
|
|
202
205
|
const sourceSha = flag("source-sha").toLowerCase();
|
|
203
206
|
const packageName = flag("package", "@kungfu-tech/buildchain");
|
|
204
207
|
const publisherMode = flag("publisher-mode", "npm-trusted-publisher");
|
|
@@ -239,15 +242,19 @@ function main() {
|
|
|
239
242
|
const deploymentBranches = environment !== "none" && environmentState.deployment_branch_policy?.custom_branch_policies === true
|
|
240
243
|
? githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}/deployment-branch-policies?per_page=100`, "Environment deployment branch policy")
|
|
241
244
|
: { branch_policies: [] };
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
if (!["branch", "tag"].includes(environmentRefType)) {
|
|
246
|
+
throw new Error(`unsupported --environment-ref-type: ${environmentRefType}`);
|
|
247
|
+
}
|
|
248
|
+
const matchingEnvironmentPolicy = (deploymentBranches.branch_policies || []).find((entry) =>
|
|
249
|
+
matchesGithubDeploymentPolicy(entry, { ref: environmentRef, refType: environmentRefType })
|
|
244
250
|
);
|
|
245
251
|
const environmentBranchAuthorized = environment !== "none" && (
|
|
246
252
|
(
|
|
253
|
+
environmentRefType === "branch" &&
|
|
247
254
|
environmentState.deployment_branch_policy?.protected_branches === true &&
|
|
248
255
|
branchState.protected === true
|
|
249
256
|
) ||
|
|
250
|
-
Boolean(
|
|
257
|
+
Boolean(matchingEnvironmentPolicy)
|
|
251
258
|
);
|
|
252
259
|
const oidc = githubJson(`repos/${repository}/actions/oidc/customization/sub`, "OIDC subject policy");
|
|
253
260
|
if (!["npm-trusted-publisher", "github-token", "oidc-role"].includes(publisherMode)) {
|
|
@@ -465,12 +472,14 @@ function main() {
|
|
|
465
472
|
environmentState.deployment_branch_policy?.protected_branches === true ||
|
|
466
473
|
(deploymentBranches.branch_policies || []).length > 0,
|
|
467
474
|
branchAuthorized: environmentBranchAuthorized,
|
|
468
|
-
branchPolicyMode: environmentState.deployment_branch_policy?.protected_branches === true
|
|
475
|
+
branchPolicyMode: environmentRefType === "branch" && environmentState.deployment_branch_policy?.protected_branches === true
|
|
469
476
|
? "protected-branches"
|
|
470
|
-
:
|
|
471
|
-
?
|
|
477
|
+
: matchingEnvironmentPolicy
|
|
478
|
+
? `custom-${environmentRefType}-policy`
|
|
472
479
|
: "unqualified",
|
|
473
|
-
authorizedBranch:
|
|
480
|
+
authorizedBranch: matchingEnvironmentPolicy?.name || "",
|
|
481
|
+
authorizedRef: environmentRef,
|
|
482
|
+
authorizedRefType: environmentRefType,
|
|
474
483
|
reviewRequired: reviewRules.length > 0,
|
|
475
484
|
preventSelfReview: reviewRules.some((rule) => rule.prevent_self_review === true),
|
|
476
485
|
},
|