@kungfu-tech/buildchain 4.1.6 → 4.1.7
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 +7 -5
- package/CONTRIBUTING.md +12 -6
- package/README.md +9 -5
- package/architecture/action-taxonomy.json +2 -0
- package/architecture/agent-change-map.md +6 -1
- package/architecture/build-orchestration.json +7 -1
- package/architecture/ci-lane-change-budget.json +1836 -0
- package/architecture/consumer-upgrade.json +6482 -0
- package/architecture/internal-capabilities.json +5 -2
- package/architecture/maintainability-debt.json +18 -10
- package/architecture/maintainability-policy.json +9 -9
- package/architecture/release-topology.json +10 -1
- package/architecture/universal-workflow-bootstrap.json +88 -18
- package/architecture/universal-workflow-capability-policy.json +2 -2
- package/architecture/workflow-taxonomy.json +2 -1
- package/contracts/dev-delivery-authority-v2.schema.json +29 -3
- package/contracts/fixtures/consumer-upgrade/build-v4.0.0.json +764 -0
- package/contracts/fixtures/consumer-upgrade/consumer-calls.json +447 -0
- package/contracts/fixtures/consumer-upgrade/dev-delivery-identity-v4.0.0.json +27 -0
- package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.0.json +256 -0
- package/contracts/fixtures/consumer-upgrade/dev-pr-auto-merge-v4.0.9.json +358 -0
- package/contracts/fixtures/consumer-upgrade/release-v4.0.0.json +815 -0
- package/contracts/promotion-invocation-v1.schema.json +5 -0
- package/dist/site/buildchain-contract.json +40 -23
- package/dist/site/buildchain-site.json +20 -20
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/controller-registry.json +17 -5
- package/dist/site/kfd-claims.json +1376 -143
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +22 -22
- package/dist/site/page-registry.json +14 -14
- package/dist/site/public-surface-audit.json +1733 -101
- package/dist/site/publication-authority-registry.json +651 -32
- package/dist/site/publication-registry.json +1 -1
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +29 -3
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +2501 -149
- package/docs/MAP.md +5 -4
- package/docs/code-organization.md +11 -8
- package/docs/getting-started.md +31 -2
- package/docs/migration-inventory.md +8 -6
- package/docs/node-api-reference.md +36 -36
- package/docs/workflow-path-migration.md +8 -7
- package/package.json +1 -1
- package/packages/core/build/artifact/contracts.js +10 -2
- package/packages/core/build/artifact/transport.js +1 -1
- package/packages/core/build/artifact/upload.js +2 -2
- package/packages/core/build/plan/action.js +2 -0
- package/packages/core/build/plan/admission.js +9 -3
- package/packages/core/build/plan/compatibility-artifacts.js +47 -0
- package/packages/core/build/plan/compatibility-plan.js +121 -0
- package/packages/core/build/plan/compatibility.js +162 -0
- package/packages/core/build/plan/configuration.js +10 -37
- package/packages/core/build/plan/identity.js +42 -0
- package/packages/core/build/plan/lifecycle.js +1 -0
- package/packages/core/build/plan/resolve.js +2 -0
- package/packages/core/build/summary/aggregation.js +4 -2
- package/packages/core/build/summary/compatibility.js +24 -0
- package/packages/core/build/summary/finalization.js +18 -0
- package/packages/core/build/summary/historical-outputs.js +88 -0
- package/packages/core/contracts/runtime-contract-inspection.js +8 -0
- package/packages/core/dev-delivery/candidate/submission.js +2 -1
- package/packages/core/dev-delivery/commands/dev-delivery-authority-command-adapters.mjs +2 -1
- package/packages/core/dev-delivery/commands/dev-delivery-authority.mjs +6 -5
- package/packages/core/dev-delivery/commands/dev-delivery-warrant-options.mjs +22 -3
- package/packages/core/dev-delivery/dev-delivery-candidate-identity.js +13 -4
- package/packages/core/dev-delivery/dev-delivery-warrant.js +4 -1
- package/packages/core/dev-delivery/warrant/handoff.js +2 -1
- package/packages/core/dev-delivery/warrant/transitions.js +2 -2
- package/packages/core/governance/buildchain-publication-authority.js +5 -9
- package/packages/core/release/github-release-metadata.js +58 -0
- package/packages/core/release/promote-candidate/action.js +4 -2
- package/packages/core/release/promote-candidate/preparation.js +6 -0
- package/packages/core/release/promote-candidate/publication.js +36 -0
- package/packages/core/release/promote-candidate/release-documents.js +1 -1
- package/packages/core/release/promotion/candidate.js +4 -2
- package/packages/core/release/promotion/compatibility-context.js +93 -0
- package/packages/core/release/promotion/compatibility-qualification.js +48 -0
- package/packages/core/release/promotion/compatibility.js +141 -0
- package/packages/core/release/promotion/historical-evidence-action.js +17 -0
- package/packages/core/release/promotion/historical-evidence-files.js +112 -0
- package/packages/core/release/promotion/historical-evidence.js +231 -0
- package/packages/core/release/promotion/qualification.js +12 -1
- package/packages/core/release/promotion/routing.js +3 -2
- package/packages/core/release/promotion-request.js +31 -2
- package/packages/core/release/release-tail-provider-adapters.js +18 -0
- package/packages/core/runtime/entry/actions.js +4 -1
- package/packages/core/runtime/entry/compatibility.js +53 -0
- package/packages/core/workflow/engine/conformance.js +7 -3
- package/packages/core/workflow/pipeline/controller.js +26 -26
- package/scripts/check-dev-delivery-actions.mjs +12 -0
- package/scripts/check-inventory.mjs +1 -1
- package/scripts/check-public-dogfood-contract.mjs +2 -0
- package/scripts/check-release-topology.mjs +5 -1
- package/scripts/check-self-consumer-contract.mjs +5 -1
- package/scripts/generate-bootstrap-admission.mjs +20 -0
- package/scripts/generate-site-bundle.mjs +5 -5
- package/scripts/generate-workflow-taxonomy.mjs +17 -0
- package/scripts/maintainability-metrics.mjs +30 -2
- package/scripts/maintainability-public-surface.mjs +5 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readJson } from "../plan/values.js";
|
|
2
|
+
|
|
3
|
+
// Project provider-confirmed evidence; this never grants publication authority.
|
|
4
|
+
export function historicalBuildOutputs(
|
|
5
|
+
plan,
|
|
6
|
+
artifacts,
|
|
7
|
+
controller,
|
|
8
|
+
summary,
|
|
9
|
+
file,
|
|
10
|
+
) {
|
|
11
|
+
const runtime = summary.runtime;
|
|
12
|
+
const source = summary.publishSource;
|
|
13
|
+
const gate = summary.publishGate;
|
|
14
|
+
const json = (value) => JSON.stringify(value);
|
|
15
|
+
const name = (reference) => reference?.name || "";
|
|
16
|
+
const outputs = {
|
|
17
|
+
"buildchain-channel": plan.identity.channel,
|
|
18
|
+
"buildchain-channel-selection-source": "called-workflow",
|
|
19
|
+
"buildchain-channel-reason": `Selected by ${plan.identity.ref}`,
|
|
20
|
+
"runner-preset": plan.environment.runners.preset,
|
|
21
|
+
"platforms-json": json(
|
|
22
|
+
plan.platforms.map((platform) => ({
|
|
23
|
+
...platform,
|
|
24
|
+
runner: json(platform.runner),
|
|
25
|
+
})),
|
|
26
|
+
),
|
|
27
|
+
"runner-routing-json": plan.routing ? json(plan.routing) : "",
|
|
28
|
+
"platform-count": plan.platforms.length,
|
|
29
|
+
"linux-container-enabled": plan.container.enabled,
|
|
30
|
+
"linux-container-image": plan.container.image,
|
|
31
|
+
"build-summary-artifact": name(artifacts.summary),
|
|
32
|
+
"credential-island-macos-artifact": name(artifacts.credential?.payload),
|
|
33
|
+
"credential-island-macos-manifest-artifact": name(
|
|
34
|
+
artifacts.credential?.manifest,
|
|
35
|
+
),
|
|
36
|
+
"build-diagnostics-summary-artifact": name(artifacts.diagnostics),
|
|
37
|
+
"release-candidate-passport-artifact": name(artifacts.release_candidate),
|
|
38
|
+
"release-candidate-artifact": name(artifacts.release_candidate),
|
|
39
|
+
"release-candidate-passport-json": artifacts.release_candidate
|
|
40
|
+
? json(
|
|
41
|
+
readJson(
|
|
42
|
+
file(".buildchain/artifacts/release-candidate-passport.json"),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
: "",
|
|
46
|
+
"build-summary-json": json(summary),
|
|
47
|
+
"artifact-coordinates-json": json(
|
|
48
|
+
readJson(file(".buildchain/artifacts/artifact-coordinates.json")),
|
|
49
|
+
),
|
|
50
|
+
"build-diagnostics-summary-json": json(
|
|
51
|
+
readJson(file(".buildchain/artifacts/diagnostics-summary.json")),
|
|
52
|
+
),
|
|
53
|
+
"trusted-event": gate.trustedEvent,
|
|
54
|
+
"buildchain-runtime-ref": runtime.ref,
|
|
55
|
+
"buildchain-runtime-sha": runtime.sha,
|
|
56
|
+
"buildchain-runtime-class": runtime.class,
|
|
57
|
+
"buildchain-runtime-override": runtime.override,
|
|
58
|
+
"buildchain-runtime-trust-decision": runtime.trustDecision,
|
|
59
|
+
"buildchain-contract-lock-status": plan.admission.lock_status,
|
|
60
|
+
"buildchain-contract-lock-drift": plan.admission.lock_drift,
|
|
61
|
+
"buildchain-contract-digest": plan.admission.contract_digest,
|
|
62
|
+
"publish-channel": gate.channel,
|
|
63
|
+
"publish-allowed": gate.allowed,
|
|
64
|
+
"publish-reason": gate.reason,
|
|
65
|
+
"publish-source-ref": source.ref,
|
|
66
|
+
"publish-source-sha": source.sha,
|
|
67
|
+
"publish-source-tree-sha": plan.source.tree_sha,
|
|
68
|
+
"publish-source-locked": source.locked,
|
|
69
|
+
"publish-source-channel": source.channel,
|
|
70
|
+
"publish-source-line": source.line,
|
|
71
|
+
"publish-source-consumer-version": source.consumerVersion,
|
|
72
|
+
"release-manifest-json": source.releaseManifest,
|
|
73
|
+
"controller-plan-artifact": name(artifacts.controller_plan),
|
|
74
|
+
"controller-plan-json": json(plan.controller),
|
|
75
|
+
"controller-plan-digest": plan.controller.digest,
|
|
76
|
+
"controller-receipt-artifact": name(artifacts.controller_receipt),
|
|
77
|
+
"controller-receipt-json": json(controller.receipt),
|
|
78
|
+
"controller-receipt-digest": controller.receipt.digest,
|
|
79
|
+
"controller-receipt-status": controller.receipt.status,
|
|
80
|
+
};
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
Object.entries(outputs).map(([key, value]) => {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
throw new Error(`Missing historical build evidence: ${key}`);
|
|
85
|
+
return [key, String(value)];
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -113,3 +113,11 @@ export function assertRuntimeContractAccepted(result) {
|
|
|
113
113
|
`Buildchain contract lock rejected: ${(result.evaluation.reasons || []).join("; ")}`,
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
// Compare lock metadata with the selected runtime, independently of checkout paths.
|
|
118
|
+
export function runtimeContractLockDrift(lock, runtimeSha, contractDigest) {
|
|
119
|
+
if (lock.schema === "buildchain.consumer-contract-lock/v2")
|
|
120
|
+
return lock.runtime.sha !== runtimeSha;
|
|
121
|
+
return lock.buildchain.resolvedSha !== runtimeSha ||
|
|
122
|
+
lock.buildchain.contractDigest !== contractDigest;
|
|
123
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
|
|
1
2
|
export function deliverySubmissionRequest(
|
|
2
3
|
input,
|
|
3
4
|
{ repository, branch, sourceProofRoot, affectedPaths },
|
|
@@ -9,7 +10,7 @@ export function deliverySubmissionRequest(
|
|
|
9
10
|
branch,
|
|
10
11
|
pullRequestNumber: Number(input["expected-pr-number"]),
|
|
11
12
|
sourceHead: input["expected-head-sha"],
|
|
12
|
-
sourceRoot: input["source-root"],
|
|
13
|
+
...devDeliverySourceBinding({ sourceRoot: input["source-root"], assignmentRoot: input["assignment-root"], initiativeRoot: input["initiative-root"] }),
|
|
13
14
|
sourceIdentityRoot: input["source-identity-root"],
|
|
14
15
|
sourcePatchRoot: input["source-patch-root"],
|
|
15
16
|
sourceProofRoot,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import {
|
|
@@ -55,7 +56,7 @@ function submitCandidate(state, options) {
|
|
|
55
56
|
{
|
|
56
57
|
pullRequestNumber: options.pullRequestNumber,
|
|
57
58
|
sourceHead: options.sourceHead,
|
|
58
|
-
|
|
59
|
+
...devDeliverySourceBinding(options),
|
|
59
60
|
sourceIdentityRoot: options.sourceIdentityRoot,
|
|
60
61
|
sourcePatchRoot: options.sourcePatchRoot,
|
|
61
62
|
sourceProofRoot: options.sourceProofRoot,
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
} from "../dev-delivery-authority-landing.js";
|
|
8
8
|
import { defaultDevDeliveryStateRef } from "../warrant/values.js";
|
|
9
9
|
import { GitHubDevDeliveryStore } from "../../providers/dev-delivery/store.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
flag,
|
|
12
|
+
hasFlag,
|
|
13
|
+
historicalDeliveryRootOptions,
|
|
14
|
+
} from "./dev-delivery-warrant-options.mjs";
|
|
11
15
|
import { runDevDeliveryAuthorityCommandAdapter } from "./dev-delivery-authority-command-adapters.mjs";
|
|
12
16
|
function text(value = "") {
|
|
13
17
|
return String(value ?? "").trim();
|
|
@@ -247,10 +251,7 @@ export function devDeliveryAuthorityCliOptions(
|
|
|
247
251
|
environment.BUILDCHAIN_WORK_SOURCE_ROOT ||
|
|
248
252
|
environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_ROOT,
|
|
249
253
|
),
|
|
250
|
-
|
|
251
|
-
// New authority submissions accept only the Work sourceRoot above.
|
|
252
|
-
// Retired CLI flags are intentionally ignored by this parser.
|
|
253
|
-
// This prevents producers from reconstructing obsolete identities.
|
|
254
|
+
...historicalDeliveryRootOptions(rest, environment),
|
|
254
255
|
sourceIdentityRoot: flag(
|
|
255
256
|
rest,
|
|
256
257
|
"source-identity-root",
|
|
@@ -79,8 +79,9 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
79
79
|
"source-root",
|
|
80
80
|
environment.BUILDCHAIN_WORK_SOURCE_ROOT ||
|
|
81
81
|
environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_ROOT ||
|
|
82
|
-
|
|
82
|
+
rootFromGitHubEvent(environment),
|
|
83
83
|
),
|
|
84
|
+
...historicalDeliveryRootOptions(rest, environment),
|
|
84
85
|
// prettier-ignore
|
|
85
86
|
...{ expectedSourceHead: flag(rest, "expected-source-head", environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_SOURCE_HEAD), observedSourceHead: flag(rest, "observed-source-head", environment.BUILDCHAIN_DEV_DELIVERY_OBSERVED_SOURCE_HEAD) },
|
|
86
87
|
|
|
@@ -278,6 +279,24 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
// prettier-ignore
|
|
281
|
-
function
|
|
282
|
-
try { const event = JSON.parse(fs.readFileSync(environment.GITHUB_EVENT_PATH, "utf8")); return event.client_payload?.candidate?.
|
|
282
|
+
function rootFromGitHubEvent(environment, key = "sourceRoot") {
|
|
283
|
+
try { const event = JSON.parse(fs.readFileSync(environment.GITHUB_EVENT_PATH, "utf8")); return event.client_payload?.candidate?.[key] || JSON.parse(event.inputs?.["native-roots-json"] || "{}")[key] || ""; } catch { return ""; }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function historicalDeliveryRootOptions(args, environment) {
|
|
287
|
+
const result = {};
|
|
288
|
+
for (const [key, name] of [
|
|
289
|
+
["assignmentRoot", "assignment-root"],
|
|
290
|
+
["initiativeRoot", "initiative-root"],
|
|
291
|
+
]) {
|
|
292
|
+
const value = flag(
|
|
293
|
+
args,
|
|
294
|
+
name,
|
|
295
|
+
environment[
|
|
296
|
+
`BUILDCHAIN_DEV_DELIVERY_${name.replaceAll("-", "_").toUpperCase()}`
|
|
297
|
+
] || rootFromGitHubEvent(environment, key),
|
|
298
|
+
);
|
|
299
|
+
if (value) result[key] = value;
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
283
302
|
}
|
|
@@ -18,9 +18,18 @@ export const EXACT_DEV_DELIVERY_PROOF_FIELDS = Object.freeze([
|
|
|
18
18
|
"sourceWorkflowRunId",
|
|
19
19
|
]);
|
|
20
20
|
export function devDeliverySourceBinding(input = {}) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const legacy = ["assignmentRoot", "initiativeRoot"].some(
|
|
22
|
+
(key) => Object.hasOwn(input, key) && input[key] !== undefined && input[key] !== "",
|
|
23
|
+
);
|
|
24
|
+
if (!legacy) return { sourceRoot: exactRoot(input.sourceRoot, "sourceRoot") };
|
|
25
|
+
if (input.sourceRoot)
|
|
26
|
+
throw new Error("provide sourceRoot alone; do not mix historical producer roots");
|
|
27
|
+
// Keep the original identity preimage so persisted queues and Warrants survive
|
|
28
|
+
// a runtime upgrade without changing candidate IDs or their receipt roots.
|
|
29
|
+
return {
|
|
30
|
+
assignmentRoot: exactRoot(input.assignmentRoot, "assignmentRoot"),
|
|
31
|
+
initiativeRoot: exactRoot(input.initiativeRoot, "initiativeRoot"),
|
|
32
|
+
};
|
|
24
33
|
}
|
|
25
34
|
export function matchesExactDevDeliveryCandidate(existing, attempted) {
|
|
26
35
|
return (
|
|
@@ -44,7 +53,7 @@ export function reuseExactActiveDevDeliverySourceProof(active, input) {
|
|
|
44
53
|
);
|
|
45
54
|
const exact =
|
|
46
55
|
admittedPhase &&
|
|
47
|
-
["sourceRoot"].every(
|
|
56
|
+
["sourceRoot", "assignmentRoot", "initiativeRoot"].every(
|
|
48
57
|
(field) => active[field] === input[field],
|
|
49
58
|
) &&
|
|
50
59
|
[
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { devDeliverySourceBinding } from "./dev-delivery-candidate-identity.js";
|
|
1
2
|
import {
|
|
2
3
|
devDeliveryClone as clone,
|
|
3
4
|
devDeliveryContentRoot,
|
|
@@ -489,6 +490,8 @@ function createDevDeliverySuccessorWake(queue, now) {
|
|
|
489
490
|
"pullRequestNumber",
|
|
490
491
|
"sourceHead",
|
|
491
492
|
"sourceRoot",
|
|
493
|
+
"assignmentRoot",
|
|
494
|
+
"initiativeRoot",
|
|
492
495
|
"sourceIdentityRoot",
|
|
493
496
|
"sourcePatchRoot",
|
|
494
497
|
"sourceProofRoot",
|
|
@@ -584,5 +587,5 @@ export function observeDevDeliveryQueue(
|
|
|
584
587
|
}
|
|
585
588
|
|
|
586
589
|
function candidateSourceBinding(candidate) {
|
|
587
|
-
return
|
|
590
|
+
return devDeliverySourceBinding(candidate);
|
|
588
591
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { devDeliverySourceBinding } from "../dev-delivery-candidate-identity.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { requireValue } from "../../runtime/action-process.mjs";
|
|
3
4
|
import { writeJson } from "../native/files.js";
|
|
@@ -19,7 +20,7 @@ export function handoffInputs(
|
|
|
19
20
|
"expected-pr-number": warrant.pullRequestNumber,
|
|
20
21
|
"expected-head-sha": warrant.sourceHead,
|
|
21
22
|
"source-workflow-run-id": warrant.sourceWorkflowRunId,
|
|
22
|
-
"native-roots-json": JSON.stringify(
|
|
23
|
+
"native-roots-json": JSON.stringify(devDeliverySourceBinding(warrant)),
|
|
23
24
|
"source-identity-root": warrant.sourceIdentityRoot,
|
|
24
25
|
"source-patch-root": warrant.sourcePatchRoot,
|
|
25
26
|
"plan-root": warrant.planRoot,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
settleDevDeliveryTerminalEvent,
|
|
14
14
|
submitDevDeliveryCandidate,
|
|
15
15
|
} from "../dev-delivery-warrant.js";
|
|
16
|
-
import { reuseExactActiveDevDeliverySourceProof } from "../dev-delivery-candidate-identity.js";
|
|
16
|
+
import { devDeliverySourceBinding, reuseExactActiveDevDeliverySourceProof } from "../dev-delivery-candidate-identity.js";
|
|
17
17
|
import { runDeliveryWarrantReadCandidate } from "../delivery-warrant-read-candidate.js";
|
|
18
18
|
|
|
19
19
|
import {
|
|
@@ -83,7 +83,7 @@ export function transitionFor(command, queue, options) {
|
|
|
83
83
|
"pullRequestNumber",
|
|
84
84
|
),
|
|
85
85
|
sourceHead: exactSha(options.sourceHead, "sourceHead"),
|
|
86
|
-
|
|
86
|
+
...devDeliverySourceBinding(options),
|
|
87
87
|
sourceIdentityRoot: exactRoot(
|
|
88
88
|
options.sourceIdentityRoot,
|
|
89
89
|
"sourceIdentityRoot",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readConsumerUpgrade, renderCompatibilityWorkflow } from "../consumer/compatibility-workflows.js";
|
|
3
4
|
|
|
4
5
|
import { createPublicationAuthorityRegistry } from "../publication/publication-authority.js";
|
|
5
6
|
|
|
@@ -70,16 +71,11 @@ export function buildchainPublicationAuthorityDescriptors({ root = process.cwd()
|
|
|
70
71
|
environmentMode,
|
|
71
72
|
runnerPolicy: publicationCapable ? "qualified-measured" : "unqualified",
|
|
72
73
|
}));
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const taxonomy = JSON.parse(fs.readFileSync(taxonomyPath, "utf8"));
|
|
76
|
-
for (const entry of taxonomy.entries) {
|
|
77
|
-
if (!entry.compatibility) continue;
|
|
78
|
-
const previous = descriptors.find((item) => item.workflowPath === entry.compatibility.path);
|
|
74
|
+
for (const entry of readConsumerUpgrade(root)?.entries || []) {
|
|
75
|
+
const previous = descriptors.find((item) => item.workflowPath === entry.target);
|
|
79
76
|
if (!previous) continue;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (fs.readFileSync(path.join(root, workflowPath), "utf8") !== fs.readFileSync(path.join(root, entry.compatibility.path), "utf8")) {
|
|
77
|
+
const workflowPath = entry.path;
|
|
78
|
+
if (fs.readFileSync(path.join(root, workflowPath), "utf8") !== renderCompatibilityWorkflow(entry, fs.readFileSync(path.join(root, entry.target), "utf8"))) {
|
|
83
79
|
throw new Error(`publication authority alias differs from canonical workflow: ${workflowPath}`);
|
|
84
80
|
}
|
|
85
81
|
descriptors.push({ ...previous, workflowPath, publisherWorkflowPath: previous.publisherWorkflowPath === previous.workflowPath ? workflowPath : previous.publisherWorkflowPath });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { historicalPromotionContext } from "./promotion/compatibility-context.js";
|
|
4
|
+
|
|
5
|
+
export const RELEASE_METADATA_NAME = "buildchain.release-metadata.json";
|
|
6
|
+
const schema = "buildchain.github-release-metadata/v1";
|
|
7
|
+
|
|
8
|
+
// The metadata is an ordinary content-rooted release asset. The provider reads
|
|
9
|
+
// it only after validating every local asset against its qualified root.
|
|
10
|
+
export function historicalReleaseMetadata(context, outputDir) {
|
|
11
|
+
const inputs = historicalPromotionContext(context)?.inputs;
|
|
12
|
+
if (
|
|
13
|
+
!inputs ||
|
|
14
|
+
!(inputs["github-release-title"] || inputs["github-release-notes"])
|
|
15
|
+
)
|
|
16
|
+
return [];
|
|
17
|
+
const metadata = {
|
|
18
|
+
schema,
|
|
19
|
+
...(inputs["github-release-title"]
|
|
20
|
+
? { name: inputs["github-release-title"] }
|
|
21
|
+
: {}),
|
|
22
|
+
...(inputs["github-release-notes"]
|
|
23
|
+
? { body: inputs["github-release-notes"] }
|
|
24
|
+
: {}),
|
|
25
|
+
};
|
|
26
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
27
|
+
const file = path.join(outputDir, RELEASE_METADATA_NAME);
|
|
28
|
+
fs.writeFileSync(file, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
29
|
+
return [file];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function releaseMetadata(artifacts) {
|
|
33
|
+
const files = artifacts.filter(({ name }) => name === RELEASE_METADATA_NAME);
|
|
34
|
+
if (!files.length) return null;
|
|
35
|
+
if (files.length !== 1) throw new Error("Ambiguous GitHub Release metadata");
|
|
36
|
+
const value = JSON.parse(fs.readFileSync(files[0].path, "utf8"));
|
|
37
|
+
if (
|
|
38
|
+
!value ||
|
|
39
|
+
value.schema !== schema ||
|
|
40
|
+
Object.keys(value).some(
|
|
41
|
+
(key) => !["schema", "name", "body"].includes(key),
|
|
42
|
+
) ||
|
|
43
|
+
!["name", "body"].some((key) => typeof value[key] === "string") ||
|
|
44
|
+
["name", "body"].some(
|
|
45
|
+
(key) => Object.hasOwn(value, key) && typeof value[key] !== "string",
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
throw new Error("Invalid GitHub Release metadata");
|
|
49
|
+
const { schema: _schema, ...fields } = value;
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function releaseMetadataMatches(metadata, release) {
|
|
54
|
+
return (
|
|
55
|
+
!metadata ||
|
|
56
|
+
Object.entries(metadata).every(([key, value]) => release[key] === value)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as github from "@actions/github";
|
|
2
|
-
import {
|
|
2
|
+
import { publishCandidate } from "./publication.js";
|
|
3
3
|
import { selectedRecordRuntime } from "../discussion/session.js";
|
|
4
4
|
export async function candidatePublicationAction(core, env) {
|
|
5
5
|
const required = new Set([
|
|
@@ -39,6 +39,8 @@ export async function candidatePublicationAction(core, env) {
|
|
|
39
39
|
"publish-package-main",
|
|
40
40
|
"publish-package-set-order",
|
|
41
41
|
"publisher-workflow-sha",
|
|
42
|
+
"publisher-workflow-path",
|
|
43
|
+
"historical-inputs-json",
|
|
42
44
|
"repository",
|
|
43
45
|
"required-artifacts-path",
|
|
44
46
|
"required-status-check",
|
|
@@ -70,7 +72,7 @@ export async function candidatePublicationAction(core, env) {
|
|
|
70
72
|
request["artifact-paths"] = core
|
|
71
73
|
.getMultilineInput("artifact-paths")
|
|
72
74
|
.filter(Boolean);
|
|
73
|
-
return
|
|
75
|
+
return publishCandidate(request, {
|
|
74
76
|
octokit: github.getOctokit(request.token),
|
|
75
77
|
mutationOctokit: github.getOctokit(
|
|
76
78
|
request["mutation-token"] || request.token,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { verifyHistoricalEvidence } from "../promotion/historical-evidence.js";
|
|
1
2
|
import {
|
|
2
3
|
resolveCandidateBuildSummaryPath,
|
|
3
4
|
resolveCandidateProviderInputs,
|
|
@@ -28,6 +29,11 @@ export async function prepareCandidatePublication(
|
|
|
28
29
|
declaredPath: request["candidate-build-summary-path"],
|
|
29
30
|
});
|
|
30
31
|
const candidate = read(candidatePassportPath);
|
|
32
|
+
verifyHistoricalEvidence({
|
|
33
|
+
context: request["historical-inputs-json"],
|
|
34
|
+
candidateDirectory: path.resolve(".buildchain/release-candidate"),
|
|
35
|
+
sourceSha: candidate.source?.headSha,
|
|
36
|
+
});
|
|
31
37
|
const stageCapsules = read(request["stage-capsules-path"]);
|
|
32
38
|
const qualification = read(request["publication-qualification-path"]);
|
|
33
39
|
const token = request["token"];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assertHistoricalPromotionEffects } from "../promotion/compatibility-qualification.js";
|
|
2
|
+
import { publishWithDiscussion } from "../discussion/publication.js";
|
|
3
|
+
import { promoteReleaseCandidate } from "./transaction.js";
|
|
4
|
+
import {
|
|
5
|
+
historicalPromotionContext,
|
|
6
|
+
HISTORICAL_PUBLISHER,
|
|
7
|
+
} from "../promotion/compatibility-context.js";
|
|
8
|
+
|
|
9
|
+
// Both transports execute the same qualified provider transaction. Historical
|
|
10
|
+
// callers did not grant Discussions access; their evidence stays in the existing
|
|
11
|
+
// Actions artifacts and provider receipts instead of creating a Discussion.
|
|
12
|
+
export function publishCandidate(
|
|
13
|
+
request,
|
|
14
|
+
context,
|
|
15
|
+
{
|
|
16
|
+
historical = promoteReleaseCandidate,
|
|
17
|
+
current = publishWithDiscussion,
|
|
18
|
+
} = {},
|
|
19
|
+
) {
|
|
20
|
+
assertHistoricalPromotionEffects(request);
|
|
21
|
+
const retained = historicalPromotionContext(
|
|
22
|
+
request["historical-inputs-json"],
|
|
23
|
+
);
|
|
24
|
+
if (
|
|
25
|
+
Boolean(retained) !==
|
|
26
|
+
(request["publisher-workflow-path"] === HISTORICAL_PUBLISHER)
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Historical publisher requires its admitted compatibility context",
|
|
30
|
+
);
|
|
31
|
+
if (retained && request["resume-discussion-id"])
|
|
32
|
+
throw new Error(
|
|
33
|
+
"Historical publication cannot resume a Discussion transaction",
|
|
34
|
+
);
|
|
35
|
+
return (retained ? historical : current)(request, context);
|
|
36
|
+
}
|
|
@@ -50,7 +50,7 @@ export async function createReleaseDocuments({
|
|
|
50
50
|
schema: RELEASE_INVOCATION_CONTRACT,
|
|
51
51
|
publisher: {
|
|
52
52
|
repository: "kungfu-systems/buildchain",
|
|
53
|
-
workflow: ".github/workflows/.release-promote.yml",
|
|
53
|
+
workflow: request["publisher-workflow-path"] || ".github/workflows/.release-promote.yml",
|
|
54
54
|
workflowSha: request["publisher-workflow-sha"],
|
|
55
55
|
job: "apply",
|
|
56
56
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { historicalRecoveryPins, verifyHistoricalCandidateRuntime } from "./compatibility-qualification.js";
|
|
1
2
|
import { recoverDiscussionCandidate } from "../discussion/recovery.js";
|
|
2
3
|
import { resolveReleaseCandidateArtifacts } from "../candidate/resolve.js";
|
|
3
4
|
import { resumeFromCandidateRun } from "../recovery/candidate.js";
|
|
@@ -20,6 +21,7 @@ export async function qualifyPromotionCandidate(
|
|
|
20
21
|
recoverDiscussion = recoverDiscussionCandidate,
|
|
21
22
|
} = {},
|
|
22
23
|
) {
|
|
24
|
+
const expectedRuntime = historicalRecoveryPins(request, runtimeSha);
|
|
23
25
|
const shared = {
|
|
24
26
|
token,
|
|
25
27
|
apiUrl,
|
|
@@ -41,7 +43,7 @@ export async function qualifyPromotionCandidate(
|
|
|
41
43
|
targetRef: intent["target-ref"],
|
|
42
44
|
});
|
|
43
45
|
if (request["resume-candidate-run-id"])
|
|
44
|
-
return recover({
|
|
46
|
+
return verifyHistoricalCandidateRuntime(await recover({
|
|
45
47
|
...shared,
|
|
46
48
|
repository: request["resume-candidate-repository"],
|
|
47
49
|
targetRepository: repository,
|
|
@@ -63,7 +65,7 @@ export async function qualifyPromotionCandidate(
|
|
|
63
65
|
runtimeRoot,
|
|
64
66
|
recoveryRunId,
|
|
65
67
|
recoveryRunAttempt,
|
|
66
|
-
});
|
|
68
|
+
}), expectedRuntime);
|
|
67
69
|
return fresh({
|
|
68
70
|
...shared,
|
|
69
71
|
repository,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { installationRoot } from "../../runtime/installation-root.js";
|
|
4
|
+
|
|
5
|
+
export const HISTORICAL_PROMOTION_SCHEMA = "buildchain.historical-promotion/v1";
|
|
6
|
+
export const HISTORICAL_PUBLISHER = ".github/workflows/.release-historical.yml";
|
|
7
|
+
let fields, current;
|
|
8
|
+
function loadHistoricalFields() {
|
|
9
|
+
if (fields) return;
|
|
10
|
+
const root = installationRoot(import.meta.url);
|
|
11
|
+
fields = JSON.parse(
|
|
12
|
+
fs.readFileSync(
|
|
13
|
+
path.join(
|
|
14
|
+
root,
|
|
15
|
+
"contracts/fixtures/consumer-upgrade/release-v4.0.0.json",
|
|
16
|
+
),
|
|
17
|
+
"utf8",
|
|
18
|
+
),
|
|
19
|
+
).inputs;
|
|
20
|
+
current = JSON.parse(
|
|
21
|
+
fs.readFileSync(
|
|
22
|
+
path.join(root, "contracts/promotion-request-v1.schema.json"),
|
|
23
|
+
"utf8",
|
|
24
|
+
),
|
|
25
|
+
).properties;
|
|
26
|
+
}
|
|
27
|
+
export const historicalEvidenceFileKeys = [
|
|
28
|
+
"release-passport-kfd-1-witness-jsons",
|
|
29
|
+
"release-passport-kfd-2-claim-jsons",
|
|
30
|
+
"release-passport-kfd-3-prebuild-witness-jsons",
|
|
31
|
+
"release-passport-kfd-3-artifact-witness-jsons",
|
|
32
|
+
"release-passport-impact-json",
|
|
33
|
+
];
|
|
34
|
+
const excluded = new Set([
|
|
35
|
+
"buildchain-channel",
|
|
36
|
+
"buildchain-repository",
|
|
37
|
+
"buildchain-ref",
|
|
38
|
+
"buildchain-contract-lock-path",
|
|
39
|
+
"buildchain-alpha-contract-lock-path",
|
|
40
|
+
"buildchain-stable-contract-lock-path",
|
|
41
|
+
"branch-protection-bypass-users",
|
|
42
|
+
"branch-protection-bypass-teams",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export function historicalPromotionContext(value) {
|
|
46
|
+
if (!value) return null;
|
|
47
|
+
loadHistoricalFields();
|
|
48
|
+
const context = typeof value === "string" ? JSON.parse(value) : value;
|
|
49
|
+
if (
|
|
50
|
+
!context ||
|
|
51
|
+
context.schema !== HISTORICAL_PROMOTION_SCHEMA ||
|
|
52
|
+
Object.keys(context).some((key) => !["schema", "inputs"].includes(key)) ||
|
|
53
|
+
!context.inputs ||
|
|
54
|
+
typeof context.inputs !== "object" ||
|
|
55
|
+
Array.isArray(context.inputs)
|
|
56
|
+
)
|
|
57
|
+
throw new Error("Invalid historical promotion context");
|
|
58
|
+
for (const [key, value] of Object.entries(context.inputs)) {
|
|
59
|
+
const field = fields[key];
|
|
60
|
+
if (
|
|
61
|
+
!field ||
|
|
62
|
+
excluded.has(key) ||
|
|
63
|
+
(current[key] &&
|
|
64
|
+
![
|
|
65
|
+
"publish-artifact-kind",
|
|
66
|
+
"publish-mode",
|
|
67
|
+
...historicalEvidenceFileKeys,
|
|
68
|
+
].includes(key)) ||
|
|
69
|
+
typeof value !== field.type ||
|
|
70
|
+
(field.type === "number" && !Number.isFinite(value))
|
|
71
|
+
)
|
|
72
|
+
throw new Error(`Unsupported historical promotion context field: ${key}`);
|
|
73
|
+
if (
|
|
74
|
+
/^resume-.*runtime-sha$/u.test(key) &&
|
|
75
|
+
value &&
|
|
76
|
+
!/^[a-f0-9]{40}$/u.test(value)
|
|
77
|
+
)
|
|
78
|
+
throw new Error(`Historical ${key} requires an exact runtime SHA`);
|
|
79
|
+
if (key === "publish-mode" && value !== "publish-final-version")
|
|
80
|
+
throw new Error(
|
|
81
|
+
"Historical publish-mode translation requires publish-final-version",
|
|
82
|
+
);
|
|
83
|
+
if (key === "publish-artifact-kind" && value !== "binary")
|
|
84
|
+
throw new Error("Historical artifact-kind translation requires binary");
|
|
85
|
+
}
|
|
86
|
+
return context;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function historicalPublisherPath(value) {
|
|
90
|
+
return historicalPromotionContext(value)
|
|
91
|
+
? HISTORICAL_PUBLISHER
|
|
92
|
+
: ".github/workflows/.release-promote.yml";
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { historicalEvidenceKeys } from "./historical-evidence.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { historicalPromotionContext } from "./compatibility-context.js";
|
|
4
|
+
|
|
5
|
+
export function historicalRecoveryPins(request, runtimeSha) {
|
|
6
|
+
const inputs = historicalPromotionContext(
|
|
7
|
+
request["historical-inputs-json"],
|
|
8
|
+
)?.inputs;
|
|
9
|
+
if (!inputs) return "";
|
|
10
|
+
const selected = inputs["resume-buildchain-runtime-sha"];
|
|
11
|
+
if (selected && selected !== runtimeSha)
|
|
12
|
+
throw new Error("Historical recovery runtime does not match its exact pin");
|
|
13
|
+
const expected = inputs["resume-expected-candidate-runtime-sha"] || "";
|
|
14
|
+
if (expected && !request["resume-candidate-run-id"])
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Historical candidate runtime pin requires a candidate recovery run",
|
|
17
|
+
);
|
|
18
|
+
return expected;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function verifyHistoricalCandidateRuntime(candidate, expected) {
|
|
22
|
+
if (expected) {
|
|
23
|
+
const passport = JSON.parse(
|
|
24
|
+
fs.readFileSync(candidate.paths.passport, "utf8"),
|
|
25
|
+
);
|
|
26
|
+
if (passport.buildchain?.sha !== expected)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Historical recovered candidate does not match its runtime pin",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A retained input must never be accepted and then silently ignored. Each
|
|
35
|
+
// command requires its original execution/evidence contract before enabling it.
|
|
36
|
+
export function assertHistoricalPromotionEffects(request) {
|
|
37
|
+
const inputs =
|
|
38
|
+
historicalPromotionContext(request["historical-inputs-json"])?.inputs || {};
|
|
39
|
+
for (const [key, value] of Object.entries(inputs))
|
|
40
|
+
if (
|
|
41
|
+
key.endsWith("-command") &&
|
|
42
|
+
value &&
|
|
43
|
+
!historicalEvidenceKeys.includes(key)
|
|
44
|
+
)
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Historical promotion command adapter is not implemented: ${key}`,
|
|
47
|
+
);
|
|
48
|
+
}
|