@kungfu-tech/buildchain 4.0.1 → 4.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/architecture/agent-change-map.md +7 -5
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +4 -1
- package/architecture/maintainability-debt.json +45 -119
- package/architecture/maintainability-policy.json +13 -8
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +2 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-release-invocation-fixtures.json +119 -0
- package/architecture/v4-release-topology.json +238 -0
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +86 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +9 -9
- package/dist/site/kfd-claims.json +9 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +23 -10
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +19 -11
- package/docs/dev-delivery-warrant.md +21 -0
- package/docs/node-api-reference.md +3 -3
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
- package/packages/core/dev-delivery-warrant-state.js +12 -28
- package/packages/core/v4-canonical-contracts.js +8 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-release-invocation.js +356 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +237 -0
- package/scripts/dev-delivery-warrant-options.mjs +15 -4
- package/scripts/dev-delivery-warrant.mjs +29 -7
- package/scripts/generate-channel-promotion-workflow.mjs +12 -54
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
|
@@ -196,7 +196,6 @@ function normalizeNpmPublisher(value, { packageName, repository, workflowFilenam
|
|
|
196
196
|
configurationRead: true,
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
-
|
|
200
199
|
function main() {
|
|
201
200
|
const repository = flag("repository");
|
|
202
201
|
const workflowRepository = flag("workflow-repository", repository);
|
|
@@ -127,7 +127,6 @@ const requiredPaths = [
|
|
|
127
127
|
"scripts/npm-publish-transaction.mjs",
|
|
128
128
|
"scripts/paper.mjs",
|
|
129
129
|
"scripts/publication-package.mjs",
|
|
130
|
-
"scripts/publication-commit-evidence.mjs",
|
|
131
130
|
"scripts/publication-reproducibility.mjs",
|
|
132
131
|
"scripts/release-candidate-resolver.mjs",
|
|
133
132
|
"scripts/resume-from-candidate-run.mjs",
|
|
@@ -300,8 +299,8 @@ if (channelPromotionWorkflow !== generateChannelPromotionWorkflow(advancedPromot
|
|
|
300
299
|
throw new Error("generated channel promotion workflow is stale");
|
|
301
300
|
}
|
|
302
301
|
for (const requiredSnippet of [
|
|
303
|
-
"
|
|
304
|
-
"
|
|
302
|
+
"name: Adapt recovery inputs into the canonical v4 publisher",
|
|
303
|
+
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4-alpha",
|
|
305
304
|
"declarative-release-tail: true",
|
|
306
305
|
]) {
|
|
307
306
|
if (!boundedAlphaRecoveryWorkflow.includes(requiredSnippet)) {
|
|
@@ -316,8 +315,8 @@ for (const workflowSource of [boundedAlphaRecoveryWorkflow, channelPromotionWork
|
|
|
316
315
|
for (const requiredSnippet of [
|
|
317
316
|
"buildchain-channel:",
|
|
318
317
|
`/${promotionShellRouting.alpha.workflowPath}@${promotionShellRouting.alpha.callRef}`,
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
`SELECTED_SHELL_REF: v${selfDogfoodMajor}-alpha`,
|
|
319
|
+
"name: Invoke the single v4 publisher adapter",
|
|
321
320
|
"promotion-contract-lock-digest:",
|
|
322
321
|
"authorize-promotion-runtime-override.cjs",
|
|
323
322
|
"BUILDCHAIN_ROUTER_REPOSITORY: ${{ inputs.buildchain-repository }}",
|
|
@@ -329,6 +328,9 @@ for (const requiredSnippet of [
|
|
|
329
328
|
throw new Error(`channel promotion workflow missing routing contract: ${requiredSnippet}`);
|
|
330
329
|
}
|
|
331
330
|
}
|
|
331
|
+
if (channelPromotionWorkflow.includes("Promote with stable workflow shell")) {
|
|
332
|
+
throw new Error("channel promotion workflow retains a parallel stable publisher");
|
|
333
|
+
}
|
|
332
334
|
for (const forbiddenSnippet of ["job.workflow_repository", "job.workflow_sha"]) {
|
|
333
335
|
if (channelPromotionWorkflow.includes(forbiddenSnippet)) {
|
|
334
336
|
throw new Error(`channel promotion workflow uses unsupported GitHub context: ${forbiddenSnippet}`);
|
|
@@ -1046,7 +1048,6 @@ for (const requiredSnippet of [
|
|
|
1046
1048
|
"resume-candidate-run-id:",
|
|
1047
1049
|
"resume-expected-source-tree:",
|
|
1048
1050
|
"resume-buildchain-runtime-ref:",
|
|
1049
|
-
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
|
|
1050
1051
|
"release-passport-buildchain-self-kfd: true",
|
|
1051
1052
|
"declarative-release-tail: true",
|
|
1052
1053
|
"release-passport-impact-json: .buildchain/release-impact.json",
|
|
@@ -1057,61 +1058,28 @@ for (const requiredSnippet of [
|
|
|
1057
1058
|
}
|
|
1058
1059
|
const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows/.release-candidate-promote.yml"), "utf8");
|
|
1059
1060
|
for (const requiredSnippet of [
|
|
1060
|
-
"release
|
|
1061
|
-
"
|
|
1062
|
-
"
|
|
1063
|
-
"
|
|
1064
|
-
"
|
|
1065
|
-
"
|
|
1066
|
-
"
|
|
1067
|
-
"release-
|
|
1068
|
-
"release-
|
|
1069
|
-
"release-
|
|
1070
|
-
"release-passport-kfd-support-matrix-json:", "release-passport-kfd-support-matrix-json: ${{ inputs.release-passport-kfd-support-matrix-json }}",
|
|
1071
|
-
"release-passport-kfd-product-gate-jsons:",
|
|
1072
|
-
"release-passport-kfd-product-gate-jsons: ${{ inputs.release-passport-kfd-product-gate-jsons }}",
|
|
1073
|
-
"release-passport-invariant-passport-jsons:",
|
|
1074
|
-
"release-passport-invariant-passport-jsons: ${{ inputs.release-passport-invariant-passport-jsons }}",
|
|
1075
|
-
"release-passport-invariant-passport-command:",
|
|
1076
|
-
"release-passport-invariant-passport-command: ${{ inputs.release-passport-invariant-passport-command }}",
|
|
1077
|
-
"release-passport-buildchain-self-kfd:",
|
|
1078
|
-
"release-passport-buildchain-self-kfd: ${{ inputs.release-passport-buildchain-self-kfd }}",
|
|
1079
|
-
"github-release:",
|
|
1080
|
-
"default: true",
|
|
1081
|
-
"github-release: ${{ inputs.github-release }}",
|
|
1082
|
-
"github-release-payload-patterns:",
|
|
1083
|
-
"github-release-artifact-paths: ${{ steps.rc.outputs.release-candidate-github-release-artifact-paths }}",
|
|
1084
|
-
"github-release-title: ${{ inputs.github-release-title }}",
|
|
1085
|
-
"github-release-notes: ${{ inputs.github-release-notes }}",
|
|
1086
|
-
"publication-commit-command:",
|
|
1087
|
-
"BUILDCHAIN_PUBLICATION_COMMIT_SIGNING_KEY:",
|
|
1088
|
-
"KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY:",
|
|
1089
|
-
"Commit consumer publication authority last",
|
|
1090
|
-
"node .buildchain/runtime/scripts/publication-commit-evidence.mjs",
|
|
1091
|
-
"require-publish-source-lock: \"true\"",
|
|
1092
|
-
"publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
|
|
1093
|
-
"publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
|
|
1094
|
-
"publish-source-locked: ${{ steps.publish-gate.outputs.locked }}",
|
|
1095
|
-
"expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
1096
|
-
"publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
1097
|
-
"name: Plan exact publication version",
|
|
1098
|
-
"name: Install exact publication planning dependencies",
|
|
1099
|
-
"DRY_RUN: ${{ inputs.dry-run }}",
|
|
1100
|
-
"if (dryRun) {",
|
|
1101
|
-
"Enforce Buildchain stable release canary gate",
|
|
1102
|
-
"BUILDCHAIN_STABLE_RELEASE_POLICY: .buildchain/stable-release-policy.json",
|
|
1103
|
-
"Consumer has no binary-distribution.yml; standalone binary dispatch is not applicable.",
|
|
1104
|
-
"gh workflow view binary-distribution.yml",
|
|
1105
|
-
"resume-candidate-run-id:",
|
|
1106
|
-
"node .buildchain/runtime/scripts/resume-from-candidate-run.mjs",
|
|
1107
|
-
"publish-sealed-bundle-root: ${{ steps.rc.outputs.publish-sealed-bundle-root }}",
|
|
1108
|
-
"BUILDCHAIN_EXPECTED_TRANSACTION_ID: ${{ inputs.resume-transaction-id }}",
|
|
1109
|
-
"if: ${{ inputs.resume-candidate-run-id == '' }}",
|
|
1110
|
-
"Bridge Buildchain self-runtime dependencies",
|
|
1111
|
-
"ln -s .buildchain/runtime/node_modules node_modules",
|
|
1061
|
+
"name: QUALIFY canonical v4 release invocation inputs",
|
|
1062
|
+
"name: APPLY one rooted provider transaction",
|
|
1063
|
+
"name: SETTLE terminal ReleaseReceipt projection",
|
|
1064
|
+
"uses: ./.buildchain/runtime/actions/v4-release-candidate-promote",
|
|
1065
|
+
"publisher-workflow-sha:",
|
|
1066
|
+
"runtime-commit:",
|
|
1067
|
+
"runtime-tree:",
|
|
1068
|
+
"release-invocation-root:",
|
|
1069
|
+
"release-transaction-root:",
|
|
1070
|
+
"release-receipt-root:",
|
|
1112
1071
|
]) {
|
|
1113
1072
|
if (!releaseCandidatePromoteWorkflow.includes(requiredSnippet)) {
|
|
1114
|
-
throw new Error(`release candidate promote workflow missing
|
|
1073
|
+
throw new Error(`release candidate promote workflow missing canonical v4 boundary: ${requiredSnippet}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const retiredSnippet of [
|
|
1077
|
+
"Commit consumer publication authority last",
|
|
1078
|
+
"publish-github-artifact-attestation-evidence.mjs",
|
|
1079
|
+
"capture-package-release-propagation.mjs",
|
|
1080
|
+
]) {
|
|
1081
|
+
if (releaseCandidatePromoteWorkflow.includes(retiredSnippet)) {
|
|
1082
|
+
throw new Error(`release candidate promote workflow retains parallel publication authority: ${retiredSnippet}`);
|
|
1115
1083
|
}
|
|
1116
1084
|
}
|
|
1117
1085
|
const workflowDir = path.join(root, ".github/workflows");
|
|
@@ -20,17 +20,25 @@ import {
|
|
|
20
20
|
evaluateWorkflowBudgets,
|
|
21
21
|
} from "./maintainability-governance.mjs";
|
|
22
22
|
|
|
23
|
-
function collectHotspots(
|
|
23
|
+
function collectHotspots(
|
|
24
|
+
root,
|
|
25
|
+
current,
|
|
26
|
+
limit = 20,
|
|
27
|
+
shallowFallback = [],
|
|
28
|
+
{
|
|
29
|
+
baseRef = process.env.GITHUB_BASE_REF || process.env.GITHUB_REF_NAME || "",
|
|
30
|
+
} = {},
|
|
31
|
+
) {
|
|
24
32
|
const maintained = new Set([
|
|
25
33
|
...Object.keys(current.files || {}),
|
|
26
34
|
...Object.keys(current.tests || {}),
|
|
27
35
|
...Object.keys(current.workflows || {}),
|
|
28
36
|
]);
|
|
29
|
-
//
|
|
30
|
-
// Reuse the audited routes; full clones still re-rank them below.
|
|
31
|
-
// This keeps consumer verification deterministic without inventing history.
|
|
37
|
+
// Release-line reconciliation reuses audited routes across equivalent-tree DAG shapes.
|
|
32
38
|
if (
|
|
33
|
-
gitOutput(root, ["rev-parse", "--is-shallow-repository"]).trim() ===
|
|
39
|
+
gitOutput(root, ["rev-parse", "--is-shallow-repository"]).trim() ===
|
|
40
|
+
"true" ||
|
|
41
|
+
/^(?:alpha|release|publish-gate)\//u.test(baseRef)
|
|
34
42
|
) {
|
|
35
43
|
return shallowFallback.slice(0, limit);
|
|
36
44
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
collectHistoryRows,
|
|
9
9
|
collectRevisionCatalog,
|
|
10
10
|
countsBy,
|
|
11
|
+
ensureCapabilityCutLineage,
|
|
11
12
|
git,
|
|
12
13
|
sha256,
|
|
13
14
|
} from "./v3-v4-capability-catalog.mjs";
|
|
@@ -17,9 +18,8 @@ export const V3_V4_CAPABILITY_INVENTORY_CONTRACT =
|
|
|
17
18
|
export const V3_V4_CAPABILITY_CUTS = Object.freeze({
|
|
18
19
|
priorFamilyV3: "6b96bdad8d9f8ccf9275f27d9370a226a9c78465",
|
|
19
20
|
liveV3: "8493bf140a7f567e76aff3119f3d39ff026afc84",
|
|
20
|
-
liveV4: "
|
|
21
|
+
liveV4: "97edd5b93eb67e5bf36ad55726e08231b47b70c0",
|
|
21
22
|
});
|
|
22
|
-
|
|
23
23
|
const INVENTORY_PATH = "architecture/v3-v4-live-capability-inventory.json";
|
|
24
24
|
const ALLOWED_DISPOSITIONS = new Set([
|
|
25
25
|
"v4-native",
|
|
@@ -92,64 +92,6 @@ function capabilityCutAvailable(root, revision) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function assertCapabilityCutAncestor({
|
|
96
|
-
root = process.cwd(),
|
|
97
|
-
revision,
|
|
98
|
-
descendant = "HEAD",
|
|
99
|
-
label = "capability cut",
|
|
100
|
-
} = {}) {
|
|
101
|
-
try {
|
|
102
|
-
execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
|
|
103
|
-
cwd: root,
|
|
104
|
-
stdio: "ignore",
|
|
105
|
-
});
|
|
106
|
-
} catch {
|
|
107
|
-
throw new Error(
|
|
108
|
-
`${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function repositoryIsShallow(root) {
|
|
114
|
-
return (
|
|
115
|
-
execFileSync("git", ["rev-parse", "--is-shallow-repository"], {
|
|
116
|
-
cwd: root,
|
|
117
|
-
encoding: "utf8",
|
|
118
|
-
}).trim() === "true"
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function ensureCapabilityCutAncestor({
|
|
123
|
-
root = process.cwd(),
|
|
124
|
-
revision,
|
|
125
|
-
descendant = "HEAD",
|
|
126
|
-
label = "capability cut",
|
|
127
|
-
} = {}) {
|
|
128
|
-
try {
|
|
129
|
-
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
130
|
-
return;
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (!repositoryIsShallow(root)) throw error;
|
|
133
|
-
}
|
|
134
|
-
const descendantCommit = execFileSync(
|
|
135
|
-
"git",
|
|
136
|
-
["rev-parse", `${descendant}^{commit}`],
|
|
137
|
-
{ cwd: root, encoding: "utf8" },
|
|
138
|
-
).trim();
|
|
139
|
-
try {
|
|
140
|
-
execFileSync(
|
|
141
|
-
"git",
|
|
142
|
-
["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
|
|
143
|
-
{ cwd: root, stdio: "ignore" },
|
|
144
|
-
);
|
|
145
|
-
} catch {
|
|
146
|
-
throw new Error(
|
|
147
|
-
`${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
95
|
function ensureCapabilityCutsAvailable(root) {
|
|
154
96
|
const revisions = Object.values(V3_V4_CAPABILITY_CUTS);
|
|
155
97
|
const missing = revisions.filter(
|
|
@@ -170,7 +112,7 @@ function ensureCapabilityCutsAvailable(root) {
|
|
|
170
112
|
`v3/v4 capability cuts are unavailable after a bounded origin fetch: ${unavailable.join(", ")}`,
|
|
171
113
|
);
|
|
172
114
|
}
|
|
173
|
-
|
|
115
|
+
ensureCapabilityCutLineage({
|
|
174
116
|
root,
|
|
175
117
|
revision: V3_V4_CAPABILITY_CUTS.liveV4,
|
|
176
118
|
label: "live v4 capability cut",
|
|
@@ -52,16 +52,11 @@ function assertOrdered(relative, markers) {
|
|
|
52
52
|
|
|
53
53
|
export function assertPromotionCertificationWiring(source) {
|
|
54
54
|
for (const marker of [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"path: .buildchain/runtime/policy-caller",
|
|
61
|
-
'--caller-root "${{ github.workspace }}/.buildchain/runtime/policy-caller"',
|
|
62
|
-
'--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
|
|
63
|
-
'--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
|
|
64
|
-
"release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
|
|
55
|
+
"Translate and admit legacy-compatible inputs",
|
|
56
|
+
"BUILDCHAIN_PROMOTION_INPUTS_JSON: ${{ toJSON(inputs) }}",
|
|
57
|
+
"Resolve and qualify the sealed release candidate",
|
|
58
|
+
"BUILDCHAIN_RUNTIME_AUTHORIZATION_JSON",
|
|
59
|
+
"QUALIFY canonical v4 release invocation inputs",
|
|
65
60
|
]) {
|
|
66
61
|
if (!source.includes(marker)) {
|
|
67
62
|
fail(`promotion certification is missing ${marker}`);
|
|
@@ -165,8 +160,7 @@ export function checkV4FloatingConsumerPolicyContract() {
|
|
|
165
160
|
const promotion = read(".github/workflows/release-candidate-promote.yml");
|
|
166
161
|
if (
|
|
167
162
|
!promotion.includes("consumer-admission:") ||
|
|
168
|
-
!jobDependsOn(promotion, "
|
|
169
|
-
!jobDependsOn(promotion, "stable", "consumer-admission")
|
|
163
|
+
!jobDependsOn(promotion, "invoke", "consumer-admission")
|
|
170
164
|
) {
|
|
171
165
|
fail(
|
|
172
166
|
"release candidate promotion is not transitively gated by consumer admission",
|
|
@@ -179,16 +173,12 @@ export function checkV4FloatingConsumerPolicyContract() {
|
|
|
179
173
|
["packages/core/release-candidate.js", "consumerPolicy"],
|
|
180
174
|
["packages/core/release-passport.js", "v4ConsumerPolicy"],
|
|
181
175
|
[
|
|
182
|
-
"actions/
|
|
183
|
-
"
|
|
176
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
177
|
+
"candidate.consumerPolicy?.receiptRoot",
|
|
184
178
|
],
|
|
185
179
|
[
|
|
186
|
-
"actions/
|
|
187
|
-
"
|
|
188
|
-
],
|
|
189
|
-
[
|
|
190
|
-
".github/workflows/.release-candidate-promote.yml",
|
|
191
|
-
"v4-policy-certification",
|
|
180
|
+
"actions/v4-release-candidate-promote/index.js",
|
|
181
|
+
"createV4ReleaseInvocation",
|
|
192
182
|
],
|
|
193
183
|
]) {
|
|
194
184
|
if (!read(relative).includes(marker)) {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
parseWorkflowDocument,
|
|
10
|
+
parseYamlUses,
|
|
11
|
+
} from "../packages/core/workflow-yaml-contract.js";
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
const ledgerPath = path.join(root, "architecture/v4-release-topology.json");
|
|
15
|
+
const releaseMarker =
|
|
16
|
+
/(?:release-candidate-promote\.yml|promote-buildchain-ref|v4-release-candidate-promote|release-tail-runtime)/u;
|
|
17
|
+
|
|
18
|
+
function read(relative) {
|
|
19
|
+
return fs.readFileSync(path.join(root, relative), "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function jobBlock(source, jobId) {
|
|
23
|
+
const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
24
|
+
return (
|
|
25
|
+
source.match(
|
|
26
|
+
new RegExp(
|
|
27
|
+
`^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
|
|
28
|
+
"mu",
|
|
29
|
+
),
|
|
30
|
+
)?.[1] || ""
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function permission(block, name) {
|
|
35
|
+
return (
|
|
36
|
+
new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
|
|
37
|
+
.exec(block)?.[1]
|
|
38
|
+
?.trim() || null
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function workflowSnapshot(relative) {
|
|
43
|
+
const source = read(relative);
|
|
44
|
+
const document = parseWorkflowDocument(source);
|
|
45
|
+
const jobs = document.jobs.map((job) => {
|
|
46
|
+
const block = jobBlock(source, job.id);
|
|
47
|
+
const uses = job.uses || null;
|
|
48
|
+
return {
|
|
49
|
+
id: job.id,
|
|
50
|
+
kind: uses ? "reusable-call" : "runner",
|
|
51
|
+
uses,
|
|
52
|
+
permissions: {
|
|
53
|
+
contents: permission(block, "contents"),
|
|
54
|
+
idToken: permission(block, "id-token"),
|
|
55
|
+
},
|
|
56
|
+
carriers: {
|
|
57
|
+
artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
|
|
58
|
+
artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
|
|
59
|
+
jobOutput: /GITHUB_OUTPUT/u.test(block),
|
|
60
|
+
},
|
|
61
|
+
mutationSignals: [
|
|
62
|
+
/contents:\s*write/u.test(block) && "contents-write",
|
|
63
|
+
/id-token:\s*write/u.test(block) && "oidc-write",
|
|
64
|
+
/(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
|
|
65
|
+
block,
|
|
66
|
+
) && "promotion-runtime",
|
|
67
|
+
/(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
|
|
68
|
+
"direct-publication-command",
|
|
69
|
+
].filter(Boolean),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
path: relative,
|
|
74
|
+
triggers: document.triggers,
|
|
75
|
+
jobs,
|
|
76
|
+
reusableEdges: parseYamlUses(source)
|
|
77
|
+
.map(({ value }) => value)
|
|
78
|
+
.filter((value) => /\.github\/workflows\//u.test(value))
|
|
79
|
+
.sort(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function discoverV4ReleaseTopology(
|
|
84
|
+
workflowPaths,
|
|
85
|
+
semanticWorkflowPaths = workflowPaths,
|
|
86
|
+
) {
|
|
87
|
+
const workflows = workflowPaths.map(workflowSnapshot);
|
|
88
|
+
const jobs = workflows.flatMap((workflow) => workflow.jobs);
|
|
89
|
+
const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
|
|
90
|
+
const semanticPaths = new Set(semanticWorkflowPaths);
|
|
91
|
+
const semanticJobs = workflows
|
|
92
|
+
.filter((workflow) => semanticPaths.has(workflow.path))
|
|
93
|
+
.flatMap((workflow) => workflow.jobs);
|
|
94
|
+
const semanticMutationJobs = semanticJobs.filter(
|
|
95
|
+
(job) => job.kind === "runner" && job.mutationSignals.length > 0,
|
|
96
|
+
);
|
|
97
|
+
return {
|
|
98
|
+
jobRecordFields: [
|
|
99
|
+
"id",
|
|
100
|
+
"kind",
|
|
101
|
+
"uses",
|
|
102
|
+
"contentsPermission",
|
|
103
|
+
"oidcPermission",
|
|
104
|
+
"carriers",
|
|
105
|
+
"mutationSignals",
|
|
106
|
+
],
|
|
107
|
+
workflows: workflows.map((workflow) => ({
|
|
108
|
+
path: workflow.path,
|
|
109
|
+
triggers: workflow.triggers,
|
|
110
|
+
jobs: workflow.jobs.map((job) =>
|
|
111
|
+
[
|
|
112
|
+
job.id,
|
|
113
|
+
job.kind,
|
|
114
|
+
job.uses || "-",
|
|
115
|
+
job.permissions.contents || "-",
|
|
116
|
+
job.permissions.idToken || "-",
|
|
117
|
+
Object.entries(job.carriers)
|
|
118
|
+
.filter(([, present]) => present)
|
|
119
|
+
.map(([name]) => name)
|
|
120
|
+
.join(",") || "-",
|
|
121
|
+
job.mutationSignals.join(",") || "-",
|
|
122
|
+
].join("|"),
|
|
123
|
+
),
|
|
124
|
+
reusableEdges: workflow.reusableEdges,
|
|
125
|
+
})),
|
|
126
|
+
metrics: {
|
|
127
|
+
workflowCount: workflows.length,
|
|
128
|
+
jobCount: jobs.length,
|
|
129
|
+
reusableEdgeCount: workflows.reduce(
|
|
130
|
+
(count, workflow) => count + workflow.reusableEdges.length,
|
|
131
|
+
0,
|
|
132
|
+
),
|
|
133
|
+
mutationRelevantNodeCount: jobs.filter(
|
|
134
|
+
(job) =>
|
|
135
|
+
job.kind === "reusable-call" ||
|
|
136
|
+
job.mutationSignals.length > 0 ||
|
|
137
|
+
job.carriers.artifactDownload ||
|
|
138
|
+
job.carriers.artifactUpload,
|
|
139
|
+
).length,
|
|
140
|
+
contentsWriteJobCount: mutationJobs.filter((job) =>
|
|
141
|
+
job.mutationSignals.includes("contents-write"),
|
|
142
|
+
).length,
|
|
143
|
+
oidcWriteJobCount: mutationJobs.filter((job) =>
|
|
144
|
+
job.mutationSignals.includes("oidc-write"),
|
|
145
|
+
).length,
|
|
146
|
+
},
|
|
147
|
+
semanticMetrics: {
|
|
148
|
+
workflowCount: semanticPaths.size,
|
|
149
|
+
mutationRelevantNodeCount: semanticJobs.filter(
|
|
150
|
+
(job) =>
|
|
151
|
+
job.kind === "reusable-call" ||
|
|
152
|
+
job.mutationSignals.length > 0 ||
|
|
153
|
+
job.carriers.artifactDownload ||
|
|
154
|
+
job.carriers.artifactUpload,
|
|
155
|
+
).length,
|
|
156
|
+
contentsWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
157
|
+
job.mutationSignals.includes("contents-write"),
|
|
158
|
+
).length,
|
|
159
|
+
oidcWriteJobCount: semanticMutationJobs.filter((job) =>
|
|
160
|
+
job.mutationSignals.includes("oidc-write"),
|
|
161
|
+
).length,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function findUnknownV4ReleaseTopology(
|
|
167
|
+
workflowPaths,
|
|
168
|
+
allWorkflowPaths = fs
|
|
169
|
+
.readdirSync(path.join(root, ".github/workflows"))
|
|
170
|
+
.filter((name) => /\.ya?ml$/u.test(name))
|
|
171
|
+
.map((name) => `.github/workflows/${name}`),
|
|
172
|
+
readWorkflow = read,
|
|
173
|
+
) {
|
|
174
|
+
const declared = new Set(workflowPaths);
|
|
175
|
+
return allWorkflowPaths
|
|
176
|
+
.filter((relative) => !declared.has(relative))
|
|
177
|
+
.filter((relative) => releaseMarker.test(readWorkflow(relative)))
|
|
178
|
+
.sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertClosedWorld(workflowPaths) {
|
|
182
|
+
const unknown = findUnknownV4ReleaseTopology(workflowPaths);
|
|
183
|
+
assert.deepEqual(
|
|
184
|
+
unknown,
|
|
185
|
+
[],
|
|
186
|
+
`unknown v4 release topology workflows: ${unknown.join(", ")}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function checkV4ReleaseTopology() {
|
|
191
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
192
|
+
assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
|
|
193
|
+
assertClosedWorld(ledger.closedWorld.workflowPaths);
|
|
194
|
+
const actual = discoverV4ReleaseTopology(
|
|
195
|
+
ledger.closedWorld.workflowPaths,
|
|
196
|
+
ledger.semanticScope.workflowPaths,
|
|
197
|
+
);
|
|
198
|
+
assert.deepEqual(actual, ledger.observedTopology);
|
|
199
|
+
assert.equal(
|
|
200
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
201
|
+
Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
|
|
202
|
+
);
|
|
203
|
+
if (ledger.enforcement === "converged") {
|
|
204
|
+
assert.ok(
|
|
205
|
+
actual.semanticMetrics.mutationRelevantNodeCount <=
|
|
206
|
+
ledger.targetBudgets.maximumMutationRelevantNodeCount,
|
|
207
|
+
);
|
|
208
|
+
assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
|
|
209
|
+
assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
|
|
210
|
+
const publisher = actual.workflows.find(
|
|
211
|
+
({ path: workflowPath }) =>
|
|
212
|
+
workflowPath === ".github/workflows/.release-candidate-promote.yml",
|
|
213
|
+
);
|
|
214
|
+
assert.deepEqual(
|
|
215
|
+
publisher.jobs.map((job) => job.split("|", 1)[0]),
|
|
216
|
+
["apply", "qualify", "settle"],
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return actual;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (process.argv.includes("--print")) {
|
|
223
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
|
|
224
|
+
process.stdout.write(
|
|
225
|
+
`${JSON.stringify(
|
|
226
|
+
discoverV4ReleaseTopology(
|
|
227
|
+
ledger.closedWorld.workflowPaths,
|
|
228
|
+
ledger.semanticScope.workflowPaths,
|
|
229
|
+
),
|
|
230
|
+
null,
|
|
231
|
+
2,
|
|
232
|
+
)}\n`,
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
checkV4ReleaseTopology();
|
|
236
|
+
process.stdout.write("v4 release topology: ok\n");
|
|
237
|
+
}
|
|
@@ -33,11 +33,8 @@ function terminalEvidenceOptions(rest, environment) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
const [command = "", ...rest] = args;
|
|
36
|
+
function authorityStateOptions(rest, environment) {
|
|
38
37
|
return {
|
|
39
|
-
command,
|
|
40
|
-
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
41
38
|
branch: flag(
|
|
42
39
|
rest,
|
|
43
40
|
"branch",
|
|
@@ -53,6 +50,20 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
53
50
|
"expected-old",
|
|
54
51
|
environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_OLD,
|
|
55
52
|
),
|
|
53
|
+
legacyTerminalRecoveryPath: flag(
|
|
54
|
+
rest,
|
|
55
|
+
"legacy-terminal-recovery",
|
|
56
|
+
environment.BUILDCHAIN_DEV_DELIVERY_LEGACY_TERMINAL_RECOVERY,
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
62
|
+
const [command = "", ...rest] = args;
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
66
|
+
...authorityStateOptions(rest, environment),
|
|
56
67
|
pullRequestNumber: flag(
|
|
57
68
|
rest,
|
|
58
69
|
"pull-request",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
settleDevDeliveryTerminalEvent,
|
|
16
16
|
submitDevDeliveryCandidate,
|
|
17
17
|
} from "../packages/core/dev-delivery-warrant.js";
|
|
18
|
+
import { recoverLegacyTerminalDevDeliveryQueue } from "../packages/core/dev-delivery-warrant-legacy-recovery.js";
|
|
18
19
|
import { runV4DeliveryWarrantReadCandidate } from "../packages/core/v4-delivery-warrant-read-candidate.js";
|
|
19
20
|
|
|
20
21
|
import { GitHubDevDeliveryStore } from "./dev-delivery-warrant-store.mjs";
|
|
@@ -80,7 +81,9 @@ function reconcileTerminalEvidenceCommand(queue, options) {
|
|
|
80
81
|
|
|
81
82
|
function requireTerminalEvidenceCas(options) {
|
|
82
83
|
if (
|
|
83
|
-
|
|
84
|
+
["reconcile-terminal-evidence", "recover-legacy-terminal"].includes(
|
|
85
|
+
options.command,
|
|
86
|
+
) &&
|
|
84
87
|
options.execute &&
|
|
85
88
|
!options.expectedOldStateRoot
|
|
86
89
|
) {
|
|
@@ -267,6 +270,13 @@ function transitionFor(command, queue, options) {
|
|
|
267
270
|
}
|
|
268
271
|
if (command === "recover")
|
|
269
272
|
return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
|
|
273
|
+
if (command === "recover-legacy-terminal") {
|
|
274
|
+
return recoverLegacyTerminalDevDeliveryQueue(
|
|
275
|
+
queue,
|
|
276
|
+
jsonFile(options.legacyTerminalRecoveryPath, "legacy terminal recovery"),
|
|
277
|
+
{ now: options.now },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
270
280
|
if (command === "close") {
|
|
271
281
|
return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
|
|
272
282
|
outcome: options.outcome,
|
|
@@ -416,22 +426,33 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
416
426
|
stateRef: options.stateRef,
|
|
417
427
|
protectedBase: options.branch,
|
|
418
428
|
now: options.now,
|
|
419
|
-
allowLegacyV3Readback:
|
|
429
|
+
allowLegacyV3Readback: ["observe", "recover-legacy-terminal"].includes(
|
|
430
|
+
options.command,
|
|
431
|
+
),
|
|
420
432
|
});
|
|
433
|
+
let concurrencyRecovery = null;
|
|
421
434
|
if (
|
|
422
435
|
options.expectedOldStateRoot &&
|
|
423
436
|
loaded.queue.stateRoot !== options.expectedOldStateRoot
|
|
424
437
|
) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
438
|
+
if (options.command !== "heartbeat") {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
concurrencyRecovery = {
|
|
444
|
+
schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
|
|
445
|
+
action: "heartbeat-state-root-rebased",
|
|
446
|
+
requestedStateRoot: options.expectedOldStateRoot,
|
|
447
|
+
observedStateRoot: loaded.queue.stateRoot,
|
|
448
|
+
observedCommitSha: loaded.commitSha,
|
|
449
|
+
};
|
|
428
450
|
}
|
|
429
451
|
if (options.command === "observe") return observeQueue(loaded, options);
|
|
430
452
|
const initialLoaded = loaded;
|
|
431
453
|
let changed = transitionFor(options.command, loaded.queue, options);
|
|
432
454
|
let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
|
|
433
455
|
let write = null;
|
|
434
|
-
let concurrencyRecovery = null;
|
|
435
456
|
if (options.execute && mutates) {
|
|
436
457
|
if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
|
|
437
458
|
throw new Error(
|
|
@@ -496,7 +517,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
496
517
|
}
|
|
497
518
|
|
|
498
519
|
function usage() {
|
|
499
|
-
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
520
|
+
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
500
521
|
}
|
|
501
522
|
|
|
502
523
|
async function main() {
|
|
@@ -513,6 +534,7 @@ async function main() {
|
|
|
513
534
|
"heartbeat",
|
|
514
535
|
"qualify",
|
|
515
536
|
"recover",
|
|
537
|
+
"recover-legacy-terminal",
|
|
516
538
|
"close",
|
|
517
539
|
"settle",
|
|
518
540
|
"reconcile-terminal-evidence",
|