@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.
Files changed (50) hide show
  1. package/architecture/agent-change-map.md +7 -5
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +4 -1
  4. package/architecture/maintainability-debt.json +45 -119
  5. package/architecture/maintainability-policy.json +13 -8
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +2 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-release-invocation-fixtures.json +119 -0
  10. package/architecture/v4-release-topology.json +238 -0
  11. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  12. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  13. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  14. package/dist/site/buildchain-contract.json +9 -9
  15. package/dist/site/buildchain-site.json +9 -9
  16. package/dist/site/kfd-claims.json +9 -7
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +1 -1
  19. package/dist/site/node-api-registry.json +3 -3
  20. package/dist/site/page-registry.json +4 -4
  21. package/dist/site/public-surface-audit.json +23 -10
  22. package/dist/site/publication-authority-registry.json +2 -3
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/site-manifest.json +5 -5
  25. package/dist/site/workflow-registry.json +19 -11
  26. package/docs/dev-delivery-warrant.md +21 -0
  27. package/docs/node-api-reference.md +3 -3
  28. package/package.json +2 -2
  29. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  30. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  31. package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
  32. package/packages/core/dev-delivery-warrant-state.js +12 -28
  33. package/packages/core/v4-canonical-contracts.js +8 -0
  34. package/packages/core/v4-floating-consumer-policy.js +4 -1
  35. package/packages/core/v4-release-invocation.js +356 -0
  36. package/scripts/audit-publication-control-plane.mjs +0 -1
  37. package/scripts/check-inventory.mjs +27 -59
  38. package/scripts/check-maintainability.mjs +13 -5
  39. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  41. package/scripts/check-v4-release-topology.mjs +237 -0
  42. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  43. package/scripts/dev-delivery-warrant.mjs +29 -7
  44. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  45. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  46. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  47. package/scripts/capture-package-release-propagation.mjs +0 -263
  48. package/scripts/publication-commit-evidence.mjs +0 -444
  49. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  50. 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
- "uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@alpha/v4/v4.0",
304
- "promotion-shell-ref: ${{ needs.consumer-admission.outputs.shell-call-ref }}",
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
- `/${promotionShellRouting.stable.workflowPath}@${promotionShellRouting.stable.callRef}`,
320
- `STABLE_SHELL_REF: v${selfDogfoodMajor}`,
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-passport-kfd-1-witness-jsons:",
1061
- "release-passport-kfd-1-witness-jsons: ${{ inputs.release-passport-kfd-1-witness-jsons }}",
1062
- "release-passport-kfd-2-claim-jsons:",
1063
- "release-passport-kfd-2-claim-jsons: ${{ inputs.release-passport-kfd-2-claim-jsons }}",
1064
- "release-passport-kfd-3-prebuild-witness-jsons:",
1065
- "release-passport-kfd-3-prebuild-witness-jsons: ${{ inputs.release-passport-kfd-3-prebuild-witness-jsons }}",
1066
- "release-passport-kfd-3-artifact-witness-jsons:",
1067
- "release-passport-kfd-3-artifact-witness-jsons: ${{ inputs.release-passport-kfd-3-artifact-witness-jsons }}",
1068
- "release-passport-kfd-3-artifact-verify-command:",
1069
- "release-passport-kfd-adopter-manifest-json:", "release-passport-kfd-adopter-manifest-json: ${{ inputs.release-passport-kfd-adopter-manifest-json }}",
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 KFD gate pass-through: ${requiredSnippet}`);
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(root, current, limit = 20, shallowFallback = []) {
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
- // Shallow consumers cannot reproduce repository-wide churn ranking.
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() === "true"
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: "11b4ab21405cdffcc9d6dc10cd947618d9d1bf9c",
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
- ensureCapabilityCutAncestor({
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
- "path: .buildchain/runtime/candidate-policy-runtime",
56
- "BUILDCHAIN_EXPECTED_RUNTIME_SHA: ${{ steps.v4-policy-caller.outputs.runtime-sha }}",
57
- "policy_runtime=.buildchain/runtime",
58
- 'node "${policy_runtime}/scripts/v4-consumer-policy.mjs" certify',
59
- "passport.consumerPolicy?.receipt?.caller?.sourceSha",
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, "alpha", "consumer-admission") ||
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/promote-buildchain-ref/action.yml",
183
- "release-passport-v4-consumer-policy-certification-json",
176
+ "actions/v4-release-candidate-promote/index.js",
177
+ "candidate.consumerPolicy?.receiptRoot",
184
178
  ],
185
179
  [
186
- "actions/promote-buildchain-ref/action.yml",
187
- "release-passport-v4-consumer-policy-certification-root",
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
- export function devDeliveryCliOptions(args = [], environment = process.env) {
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
- options.command === "reconcile-terminal-evidence" &&
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: options.command === "observe",
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
- throw new Error(
426
- `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
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",