@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8

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 (108) hide show
  1. package/actions/release-tail/README.md +8 -0
  2. package/architecture/ci-lane-change-budget.json +187 -0
  3. package/architecture/internal-capabilities.json +65 -5
  4. package/architecture/maintainability-policy.json +27 -21
  5. package/architecture/release-tail-contract-inventory.json +5 -2
  6. package/architecture/v4-adopter-delivery-parity.json +32 -5
  7. package/architecture/v4-floating-consumer-policy.json +6 -0
  8. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  9. package/bin/buildchain.mjs +7 -1
  10. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  11. package/bin/internal/command-registry.mjs +1 -0
  12. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  13. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  14. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  15. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  16. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  17. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  18. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  19. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  20. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  21. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  22. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  23. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  24. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  25. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  26. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  27. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  28. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  29. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  30. package/dist/site/agent-index.json +3 -0
  31. package/dist/site/artifact-schemas.json +6 -0
  32. package/dist/site/buildchain-contract.json +14 -14
  33. package/dist/site/buildchain-site.json +231 -24
  34. package/dist/site/capability-registry.json +11 -10
  35. package/dist/site/cli-registry.json +53 -1
  36. package/dist/site/kfd-claims.json +279 -14
  37. package/dist/site/kfd-upstream-aggregate.json +1 -1
  38. package/dist/site/manual-registry.json +20 -5
  39. package/dist/site/node-api-registry.json +11569 -9299
  40. package/dist/site/page-registry.json +214 -15
  41. package/dist/site/public-surface-audit.json +206 -11
  42. package/dist/site/publication-authority-registry.json +39 -8
  43. package/dist/site/publication-registry.json +4 -4
  44. package/dist/site/release-provenance.json +15 -0
  45. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  46. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  47. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  48. package/dist/site/site-manifest.json +17 -9
  49. package/dist/site/workflow-registry.json +110 -8
  50. package/docs/MAP.md +3 -1
  51. package/docs/cli-reference.md +68 -0
  52. package/docs/cli.md +8 -0
  53. package/docs/dev-delivery-warrant.md +22 -1
  54. package/docs/node-api-reference.md +332 -172
  55. package/docs/release-tail-provider-plane.md +30 -0
  56. package/docs/v4-adopter-delivery.md +133 -0
  57. package/docs/v4-publication-rehearsal.md +130 -0
  58. package/package.json +18 -3
  59. package/packages/core/adopter-delivery-passport.js +259 -0
  60. package/packages/core/adopter-delivery-vectors.js +158 -0
  61. package/packages/core/adopter-delivery-vectors.json +77 -0
  62. package/packages/core/buildchain-agent-manuals.js +1 -0
  63. package/packages/core/buildchain-config.js +84 -0
  64. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  65. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  66. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  67. package/packages/core/buildchain-publication-authority.js +1 -0
  68. package/packages/core/ci-lane-change-budget.js +247 -0
  69. package/packages/core/dev-delivery-native-proof.js +43 -27
  70. package/packages/core/dev-delivery-warrant-state.js +31 -8
  71. package/packages/core/dev-delivery-warrant.js +8 -3
  72. package/packages/core/github-governance-authority.js +1 -0
  73. package/packages/core/index.js +5 -0
  74. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  75. package/packages/core/published-delivery-authority.js +266 -0
  76. package/packages/core/release-candidate-recovery.js +66 -11
  77. package/packages/core/release-candidate.js +2 -2
  78. package/packages/core/release-passport.js +36 -9
  79. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  80. package/packages/core/v4-adopter-delivery.js +359 -0
  81. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  82. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  83. package/packages/core/v4-floating-consumer-policy.js +138 -35
  84. package/packages/core/v4-floating-consumer-release-passport.js +30 -3
  85. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  86. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  87. package/packages/core/v4-publication-rehearsal.js +481 -0
  88. package/packages/core/workflow-yaml-contract.js +24 -2
  89. package/scripts/buildchain-cli-help.mjs +10 -0
  90. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  91. package/scripts/check-inventory.mjs +3 -3
  92. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  93. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  94. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  95. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  96. package/scripts/dev-delivery-warrant.mjs +5 -1
  97. package/scripts/dev-pr-delivery-warrant.mjs +1 -0
  98. package/scripts/dev-qualification-patrol.mjs +9 -9
  99. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  100. package/scripts/generate-release-candidate-passport.mjs +152 -0
  101. package/scripts/generate-site-bundle.mjs +30 -0
  102. package/scripts/promotion-channel-router.mjs +4 -4
  103. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  104. package/scripts/release-tail.mjs +51 -3
  105. package/scripts/resume-from-candidate-run.mjs +556 -29
  106. package/scripts/site-capability-metadata.mjs +17 -0
  107. package/scripts/v4-consumer-policy.mjs +4 -0
  108. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
@@ -67,8 +67,7 @@ function directEntries(lines, parentLine) {
67
67
  const trimmed = line.trim();
68
68
  const indent = indentation(line);
69
69
  if (trimmed && indent <= parentIndent) break;
70
- if (!trimmed || trimmed.startsWith("#") || indent !== childIndent)
71
- continue;
70
+ if (!trimmed || trimmed.startsWith("#") || indent !== childIndent) continue;
72
71
  const match = line.match(/^\s*([A-Za-z0-9_.-]+):(?:\s*(.*))?$/);
73
72
  if (match) {
74
73
  entries.push({
@@ -263,6 +262,28 @@ export function parseWorkflowCallJobs(text) {
263
262
  .filter((job) => job.uses);
264
263
  }
265
264
 
265
+ export function parseWorkflowJobs(text) {
266
+ const lines = String(text || "").split(/\r?\n/);
267
+ const jobs = topLevelEntry(lines, "jobs");
268
+ if (!jobs) return [];
269
+ return directEntries(lines, jobs.line)
270
+ .map((job) => {
271
+ const properties = directEntries(lines, job.line);
272
+ const property = (name) =>
273
+ properties.find((entry) => entry.key === name) || null;
274
+ return {
275
+ id: job.key,
276
+ name: unquote(property("name")?.value || job.key),
277
+ uses: unquote(property("uses")?.value || ""),
278
+ runsOn: unquote(property("runs-on")?.value || ""),
279
+ timeoutMinutes: property("timeout-minutes")
280
+ ? scalar(property("timeout-minutes").value)
281
+ : null,
282
+ };
283
+ })
284
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
285
+ }
286
+
266
287
  function blockScalarIndent(lines, lineIndex, parentIndent) {
267
288
  for (let index = lineIndex + 1; index < lines.length; index += 1) {
268
289
  if (!lines[index].trim()) continue;
@@ -315,6 +336,7 @@ export function parseWorkflowDocument(text) {
315
336
  return {
316
337
  triggers: parseTriggers(String(text || "").split(/\r?\n/)),
317
338
  interface: parseReusableWorkflowInterface(text),
339
+ jobs: parseWorkflowJobs(text),
318
340
  callJobs: parseWorkflowCallJobs(text),
319
341
  };
320
342
  }
@@ -8,6 +8,11 @@ export const BUILDCHAIN_USAGE = `Usage:
8
8
  buildchain architecture qualify --authority-revision <git-revision>
9
9
  [--candidate-revision <git-revision>]
10
10
  [--cwd <dir>] [--json]
11
+ buildchain adopter-delivery run --input <json-or-path> [--output <path>]
12
+ buildchain adopter-delivery verify --input <json-or-path>
13
+ --readback <json-or-path> [--output <path>]
14
+ buildchain adopter-delivery bootstrap --input <json-or-path> [--output <path>]
15
+ buildchain adopter-delivery archive --input <json-or-path> [--output <path>]
11
16
  buildchain portable-cache plan --manifest <file-or-json> [--output <file>]
12
17
  [--github-output <file>] [--json]
13
18
  buildchain portable-cache receipt --plan <file-or-json> [--matched-key <key>]
@@ -44,6 +49,11 @@ export const BUILDCHAIN_USAGE = `Usage:
44
49
  buildchain release-tail init --declaration <json-or-path> [--state <path>]
45
50
  buildchain release-tail <status|verify> [--state <path>] [--output <path>]
46
51
  buildchain release-tail compat --hooks-json <json-or-path> [--output <path>]
52
+ buildchain release-tail rehearse --capsule <absolute-path>
53
+ --candidate-root <absolute-path>
54
+ --mode <simulate|replay>
55
+ --state <absolute-path>
56
+ --evidence <absolute-path>
47
57
  buildchain tail-reseal plan --request <request.json> [--output <plan.json>]
48
58
  buildchain tail-reseal admit --request <request.json> [--output <admission.json>]
49
59
  buildchain tail-reseal verify-platform --request <request.json> --platform <id>
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { execFileSync } from "node:child_process";
7
+ import { evaluateCiLaneChangeBudget } from "../packages/core/ci-lane-change-budget.js";
8
+
9
+ const root = process.cwd();
10
+ const workflowRoot = path.join(root, ".github", "workflows");
11
+ const policyPath = path.join(
12
+ root,
13
+ "architecture",
14
+ "ci-lane-change-budget.json",
15
+ );
16
+
17
+ const workflows = fs
18
+ .readdirSync(workflowRoot, { withFileTypes: true })
19
+ .filter((entry) => entry.isFile() && /\.(?:yaml|yml)$/u.test(entry.name))
20
+ .map((entry) => {
21
+ const workflowPath = path.posix.join(".github/workflows", entry.name);
22
+ return {
23
+ path: workflowPath,
24
+ text: fs.readFileSync(path.join(workflowRoot, entry.name), "utf8"),
25
+ };
26
+ })
27
+ .sort((left, right) => left.path.localeCompare(right.path));
28
+
29
+ const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
30
+ const baselineRevision = String(policy.baseline?.gitRevision || "").trim();
31
+ if (!/^[0-9a-f]{40}$/u.test(baselineRevision)) {
32
+ throw new Error("baseline.gitRevision must be an exact Git commit");
33
+ }
34
+ const embeddedBaselineLanes = policy.baseline?.lanes;
35
+ if (
36
+ !Array.isArray(embeddedBaselineLanes) ||
37
+ embeddedBaselineLanes.length === 0
38
+ ) {
39
+ throw new Error("baseline.lanes must contain the exact offline lane set");
40
+ }
41
+ const embeddedLaneSetRoot = `sha256:${crypto
42
+ .createHash("sha256")
43
+ .update(`${JSON.stringify(embeddedBaselineLanes)}\n`)
44
+ .digest("hex")}`;
45
+ if (embeddedLaneSetRoot !== policy.baseline?.laneSetRoot) {
46
+ throw new Error("baseline laneSetRoot does not match baseline.lanes");
47
+ }
48
+
49
+ let baselineWorkflows = null;
50
+ try {
51
+ execFileSync("git", ["cat-file", "-e", `${baselineRevision}^{commit}`], {
52
+ cwd: root,
53
+ stdio: "ignore",
54
+ });
55
+ const baselinePaths = execFileSync(
56
+ "git",
57
+ [
58
+ "ls-tree",
59
+ "-r",
60
+ "--name-only",
61
+ baselineRevision,
62
+ "--",
63
+ ".github/workflows",
64
+ ],
65
+ { cwd: root, encoding: "utf8" },
66
+ )
67
+ .split(/\r?\n/u)
68
+ .filter((value) => /\.(?:yaml|yml)$/u.test(value))
69
+ .sort();
70
+ baselineWorkflows = baselinePaths.map((workflowPath) => ({
71
+ path: workflowPath,
72
+ text: execFileSync("git", ["show", `${baselineRevision}:${workflowPath}`], {
73
+ cwd: root,
74
+ encoding: "utf8",
75
+ }),
76
+ }));
77
+ } catch {
78
+ // Shallow and exported source cuts may not contain the pinned commit object.
79
+ // The content-rooted embedded lane set remains the exact offline authority.
80
+ }
81
+
82
+ const embeddedEvaluation = evaluateCiLaneChangeBudget({
83
+ policy,
84
+ workflows,
85
+ });
86
+ const evaluation = baselineWorkflows
87
+ ? evaluateCiLaneChangeBudget({ policy, workflows, baselineWorkflows })
88
+ : embeddedEvaluation;
89
+ if (
90
+ baselineWorkflows &&
91
+ (evaluation.baselineLaneCount !== embeddedEvaluation.baselineLaneCount ||
92
+ JSON.stringify(evaluation.newLanes) !==
93
+ JSON.stringify(embeddedEvaluation.newLanes))
94
+ ) {
95
+ throw new Error(
96
+ "embedded baseline lanes do not match the pinned baseline revision",
97
+ );
98
+ }
99
+ process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
100
+ if (!evaluation.ok) process.exitCode = 1;
@@ -1021,16 +1021,16 @@ for (const forbiddenSnippet of [
1021
1021
  for (const requiredSnippet of [
1022
1022
  "id-token: write",
1023
1023
  "actions: write",
1024
- "uses: ./.github/workflows/.release-candidate-promote.yml",
1024
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4-alpha",
1025
1025
  "github.event.workflow_run.event == 'push'",
1026
1026
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
1027
1027
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
1028
1028
  "resume-candidate-run-id:",
1029
1029
  "resume-expected-source-tree:",
1030
1030
  "resume-buildchain-runtime-sha:",
1031
- "github-release: true",
1031
+ "uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
1032
1032
  "release-passport-buildchain-self-kfd: true",
1033
- "artifact-patterns: ${{ startsWith(github.event.workflow_run.head_branch || inputs['target-ref'], 'alpha/') && 'buildchain-package-*' || '' }}",
1033
+ "buildchain-channel: alpha",
1034
1034
  "release-passport-impact-json: .buildchain/release-impact.json",
1035
1035
  ]) {
1036
1036
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -52,8 +52,13 @@ function assertOrdered(relative, markers) {
52
52
 
53
53
  export function assertPromotionCertificationWiring(source) {
54
54
  for (const marker of [
55
- ".buildchain/runtime/promotion-shell/scripts/v4-consumer-policy.mjs certify",
56
- '--caller-root "${{ github.workspace }}"',
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"',
57
62
  '--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
58
63
  '--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
59
64
  "release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
@@ -78,13 +83,22 @@ function assertPersistedSelectors() {
78
83
  );
79
84
  if (!match) continue;
80
85
  const selector = match[2];
86
+ const protectedBootstrap =
87
+ relative ===
88
+ ".github/workflows/buildchain-ref-promotion-recovery.yml" &&
89
+ match[1] === ".github/workflows/.release-candidate-promote.yml" &&
90
+ selector === "alpha/v4/v4.0";
81
91
  const isV4 =
82
92
  selector === "v4" ||
83
93
  selector === "v4-alpha" ||
84
94
  /^v4(?:[./-]|$)/u.test(selector) ||
85
95
  /^[0-9a-f]{40}$/iu.test(selector) ||
86
96
  selector.includes("${{");
87
- if (isV4 && !["v4", "v4-alpha"].includes(selector)) {
97
+ if (
98
+ (isV4 || protectedBootstrap) &&
99
+ !["v4", "v4-alpha"].includes(selector) &&
100
+ !protectedBootstrap
101
+ ) {
88
102
  offenders.push(`${relative}:${node.line} @${selector}`);
89
103
  }
90
104
  }
@@ -112,7 +126,12 @@ export function checkV4FloatingConsumerPolicyContract() {
112
126
  "Enforce v4 floating consumer policy",
113
127
  "Validate consumer package manager contract",
114
128
  ]);
115
- if (!read(".github/workflows/.build.yml").includes("BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: ${{ inputs.buildchain-expected-channel }}")) fail("channel builds must disambiguate dual-channel caller invocations");
129
+ if (
130
+ !read(".github/workflows/.build.yml").includes(
131
+ "BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: ${{ inputs.buildchain-expected-channel }}",
132
+ )
133
+ )
134
+ fail("channel builds must disambiguate dual-channel caller invocations");
116
135
  assertTrustGatedJobs(read(".github/workflows/.build.yml"), [
117
136
  "resolve-source",
118
137
  "resolve-contract",
@@ -0,0 +1,153 @@
1
+ import crypto from "node:crypto";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+
5
+ const SHA = /^[0-9a-f]{40}$/u;
6
+
7
+ function exactSha(value, label) {
8
+ const normalized = String(value || "")
9
+ .trim()
10
+ .toLowerCase();
11
+ if (!SHA.test(normalized))
12
+ throw new Error(`${label} must be an exact 40-hex Git SHA`);
13
+ return normalized;
14
+ }
15
+
16
+ function git(args, { cwd, encoding = "utf8" }) {
17
+ const result = spawnSync("git", args, {
18
+ cwd,
19
+ encoding,
20
+ maxBuffer: 64 * 1024 * 1024,
21
+ });
22
+ if (result.status !== 0)
23
+ throw new Error(
24
+ `git ${args.join(" ")} failed: ${String(result.stderr || "").trim()}`,
25
+ );
26
+ return encoding === null ? result.stdout : String(result.stdout || "").trim();
27
+ }
28
+
29
+ function patchRoot(base, head, cwd) {
30
+ const patch = git(
31
+ [
32
+ "diff",
33
+ "--binary",
34
+ "--full-index",
35
+ "--no-ext-diff",
36
+ `${base}..${head}`,
37
+ ],
38
+ { cwd, encoding: null },
39
+ );
40
+ return `sha256:${crypto.createHash("sha256").update(patch).digest("hex")}`;
41
+ }
42
+
43
+ function linearRange(base, head, cwd) {
44
+ try {
45
+ git(["merge-base", "--is-ancestor", base, head], { cwd });
46
+ } catch {
47
+ return null;
48
+ }
49
+ const commits = git(
50
+ ["rev-list", "--reverse", "--topo-order", `${base}..${head}`],
51
+ { cwd },
52
+ )
53
+ .split(/\s+/u)
54
+ .filter(Boolean);
55
+ if (commits.length === 0) return null;
56
+ let expectedParent = base;
57
+ const trees = [];
58
+ const patchRoots = [];
59
+ for (const rangeCommit of commits) {
60
+ const [observedCommit, ...parents] = git(
61
+ ["rev-list", "--parents", "-n", "1", rangeCommit],
62
+ { cwd },
63
+ ).split(/\s+/u);
64
+ if (
65
+ observedCommit !== rangeCommit ||
66
+ parents.length !== 1 ||
67
+ parents[0] !== expectedParent
68
+ ) {
69
+ return null;
70
+ }
71
+ trees.push(
72
+ exactSha(
73
+ git(["rev-parse", `${rangeCommit}^{tree}`], { cwd }),
74
+ "replayedCommitTree",
75
+ ),
76
+ );
77
+ patchRoots.push(patchRoot(expectedParent, rangeCommit, cwd));
78
+ expectedParent = rangeCommit;
79
+ }
80
+ return { commits, trees, patchRoots };
81
+ }
82
+
83
+ export function exactMergeGroupBinding(input = {}) {
84
+ const cwd = path.resolve(input.cwd || process.cwd());
85
+ const mergeGroupHead = exactSha(input.mergeGroupHead, "mergeGroupHead");
86
+ const expectedTree = exactSha(input.mergeGroupTree, "mergeGroupTree");
87
+ const qualifiedBase = exactSha(input.qualifiedBase, "qualifiedBase");
88
+ const currentBase = exactSha(input.currentBase, "currentBase");
89
+ const sourceHead = exactSha(input.sourceHead, "sourceHead");
90
+ const sourcePatchRoot = String(input.sourcePatchRoot || "").trim();
91
+ const actualTree = exactSha(
92
+ git(["rev-parse", `${mergeGroupHead}^{tree}`], { cwd }),
93
+ "mergeGroupTree",
94
+ );
95
+ if (actualTree !== expectedTree)
96
+ return { ok: false, reason: "merge-group-tree-mismatch" };
97
+ try {
98
+ git(["merge-base", "--is-ancestor", qualifiedBase, currentBase], { cwd });
99
+ } catch {
100
+ return {
101
+ ok: false,
102
+ reason: "current-base-not-descendant-of-qualified-base",
103
+ };
104
+ }
105
+ if (!/^sha256:[0-9a-f]{64}$/u.test(sourcePatchRoot))
106
+ throw new Error("sourcePatchRoot must be an exact SHA-256 root");
107
+ if (patchRoot(currentBase, mergeGroupHead, cwd) !== sourcePatchRoot)
108
+ return { ok: false, reason: "merge-group-source-patch-mismatch" };
109
+
110
+ const [commit, ...parents] = git(
111
+ ["rev-list", "--parents", "-n", "1", mergeGroupHead],
112
+ { cwd },
113
+ ).split(/\s+/u);
114
+ if (commit !== mergeGroupHead)
115
+ return { ok: false, reason: "merge-group-parent-mismatch" };
116
+ if (
117
+ parents.length === 2 &&
118
+ parents[0] === currentBase &&
119
+ parents[1] === sourceHead
120
+ ) {
121
+ return {
122
+ ok: true,
123
+ mergeGroupHead,
124
+ mergeGroupTree: actualTree,
125
+ parents,
126
+ compositionMode: "two-parent-merge",
127
+ replayedCommitTrees: [],
128
+ replayedCommitPatchRoots: [],
129
+ };
130
+ }
131
+
132
+ const sourceRange = linearRange(qualifiedBase, sourceHead, cwd);
133
+ const mergeGroupRange = linearRange(currentBase, mergeGroupHead, cwd);
134
+ if (
135
+ !sourceRange ||
136
+ !mergeGroupRange ||
137
+ sourceRange.commits.length !== mergeGroupRange.commits.length ||
138
+ sourceRange.patchRoots.some(
139
+ (root, index) => root !== mergeGroupRange.patchRoots[index],
140
+ )
141
+ ) {
142
+ return { ok: false, reason: "merge-group-parent-mismatch" };
143
+ }
144
+ return {
145
+ ok: true,
146
+ mergeGroupHead,
147
+ mergeGroupTree: actualTree,
148
+ parents,
149
+ compositionMode: "linear-replay",
150
+ replayedCommitTrees: mergeGroupRange.trees,
151
+ replayedCommitPatchRoots: mergeGroupRange.patchRoots,
152
+ };
153
+ }
@@ -11,6 +11,7 @@ import {
11
11
  verifySourceQualificationProof,
12
12
  } from "../packages/core/dev-delivery-warrant.js";
13
13
  import { validateControllerReceipt } from "../packages/core/controller-evidence.js";
14
+ import { exactMergeGroupBinding } from "./dev-delivery-source-proof-replay.mjs";
14
15
 
15
16
  const REUSE_DECISION_SCHEMA =
16
17
  "kungfu.buildchain.source-qualification-reuse-decision/v1";
@@ -113,98 +114,6 @@ function gitPathRoot(ref, paths, label, cwd) {
113
114
  });
114
115
  }
115
116
 
116
- function exactMergeGroupBinding(input = {}) {
117
- const cwd = path.resolve(input.cwd || process.cwd());
118
- const mergeGroupHead = exactSha(input.mergeGroupHead, "mergeGroupHead");
119
- const expectedTree = exactSha(input.mergeGroupTree, "mergeGroupTree");
120
- const currentBase = exactSha(input.currentBase, "currentBase");
121
- const sourceHead = exactSha(input.sourceHead, "sourceHead");
122
- const actualTree = exactSha(
123
- git(["rev-parse", `${mergeGroupHead}^{tree}`], { cwd }),
124
- "mergeGroupTree",
125
- );
126
- if (actualTree !== expectedTree)
127
- return { ok: false, reason: "merge-group-tree-mismatch" };
128
- const [commit, ...parents] = git(
129
- ["rev-list", "--parents", "-n", "1", mergeGroupHead],
130
- { cwd },
131
- ).split(/\s+/u);
132
- if (commit !== mergeGroupHead) {
133
- return { ok: false, reason: "merge-group-parent-mismatch" };
134
- }
135
- if (
136
- parents.length === 2 &&
137
- parents[0] === currentBase &&
138
- parents[1] === sourceHead
139
- ) {
140
- return {
141
- ok: true,
142
- mergeGroupHead,
143
- mergeGroupTree: actualTree,
144
- parents,
145
- compositionMode: "two-parent-merge",
146
- replayedCommitTrees: [],
147
- };
148
- }
149
-
150
- const linearRange = (head) => {
151
- try {
152
- git(["merge-base", "--is-ancestor", currentBase, head], { cwd });
153
- } catch {
154
- return null;
155
- }
156
- const commits = git(
157
- ["rev-list", "--reverse", "--topo-order", `${currentBase}..${head}`],
158
- { cwd },
159
- )
160
- .split(/\s+/u)
161
- .filter(Boolean);
162
- if (commits.length === 0) return null;
163
- let expectedParent = currentBase;
164
- const trees = [];
165
- for (const rangeCommit of commits) {
166
- const [observedCommit, ...rangeParents] = git(
167
- ["rev-list", "--parents", "-n", "1", rangeCommit],
168
- { cwd },
169
- ).split(/\s+/u);
170
- if (
171
- observedCommit !== rangeCommit ||
172
- rangeParents.length !== 1 ||
173
- rangeParents[0] !== expectedParent
174
- ) {
175
- return null;
176
- }
177
- trees.push(
178
- exactSha(
179
- git(["rev-parse", `${rangeCommit}^{tree}`], { cwd }),
180
- "replayedCommitTree",
181
- ),
182
- );
183
- expectedParent = rangeCommit;
184
- }
185
- return trees;
186
- };
187
-
188
- const sourceTrees = linearRange(sourceHead);
189
- const mergeGroupTrees = linearRange(mergeGroupHead);
190
- if (
191
- !sourceTrees ||
192
- !mergeGroupTrees ||
193
- sourceTrees.length !== mergeGroupTrees.length ||
194
- sourceTrees.some((tree, index) => tree !== mergeGroupTrees[index])
195
- ) {
196
- return { ok: false, reason: "merge-group-parent-mismatch" };
197
- }
198
- return {
199
- ok: true,
200
- mergeGroupHead,
201
- mergeGroupTree: actualTree,
202
- parents,
203
- compositionMode: "linear-replay",
204
- replayedCommitTrees: mergeGroupTrees,
205
- };
206
- }
207
-
208
117
  export function sourceQualificationPredicates(input = {}) {
209
118
  const cwd = path.resolve(input.cwd || process.cwd());
210
119
  const repository = required(input.repository, "repository");
@@ -397,7 +306,8 @@ export function materializeReuseLifecycleEvidence({
397
306
  decisionRoot: decision.decisionRoot,
398
307
  sourceWorkflowRunId: decision.sourceWorkflowRunId,
399
308
  sourceHead: decision.sourceHead,
400
- qualifiedBase: decision.currentBase,
309
+ qualifiedBase: decision.qualifiedBase,
310
+ currentBase: decision.currentBase,
401
311
  mergeGroupHead: decision.mergeGroupHead,
402
312
  mergeGroupTree: decision.mergeGroupTree,
403
313
  },
@@ -446,7 +356,7 @@ export function verifySourceQualificationReuse(input = {}) {
446
356
  const verification = verifySourceQualificationProof(proof, {
447
357
  repository: required(input.repository, "repository"),
448
358
  protectedBase: required(input.protectedBase, "protectedBase"),
449
- qualifiedBase: exactSha(input.currentBase, "currentBase"),
359
+ qualifiedBase: exactSha(proof.qualifiedBase, "qualifiedBase"),
450
360
  sourceHead: exactSha(input.sourceHead, "sourceHead"),
451
361
  sourceWorkflowRunId: Number(input.sourceWorkflowRunId),
452
362
  controllerReceiptRoot: receipt.digest,
@@ -462,7 +372,7 @@ export function verifySourceQualificationReuse(input = {}) {
462
372
  }
463
373
  const current = sourceQualificationPredicates({
464
374
  ...input,
465
- qualifiedBase: input.currentBase,
375
+ qualifiedBase: proof.qualifiedBase,
466
376
  });
467
377
  const classification = classifyDevDeliveryDelta({
468
378
  proof,
@@ -476,7 +386,11 @@ export function verifySourceQualificationReuse(input = {}) {
476
386
  reason: classification.reason,
477
387
  });
478
388
  }
479
- const mergeGroup = exactMergeGroupBinding(input);
389
+ const mergeGroup = exactMergeGroupBinding({
390
+ ...input,
391
+ qualifiedBase: proof.qualifiedBase,
392
+ sourcePatchRoot: proof.sourcePatchRoot,
393
+ });
480
394
  if (!mergeGroup.ok) {
481
395
  return rootedDecision({
482
396
  schema: REUSE_DECISION_SCHEMA,
@@ -493,12 +407,14 @@ export function verifySourceQualificationReuse(input = {}) {
493
407
  sourceProofRoot: proof.proofRoot,
494
408
  sourceWorkflowRunId: proof.sourceWorkflowRunId,
495
409
  sourceHead: proof.sourceHead,
496
- currentBase: proof.qualifiedBase,
410
+ qualifiedBase: proof.qualifiedBase,
411
+ currentBase: exactSha(input.currentBase, "currentBase"),
497
412
  mergeGroupHead: mergeGroup.mergeGroupHead,
498
413
  mergeGroupTree: mergeGroup.mergeGroupTree,
499
414
  mergeGroupParents: mergeGroup.parents,
500
415
  mergeGroupCompositionMode: mergeGroup.compositionMode,
501
416
  mergeGroupReplayTrees: mergeGroup.replayedCommitTrees,
417
+ mergeGroupReplayPatchRoots: mergeGroup.replayedCommitPatchRoots,
502
418
  predicateRoots: {
503
419
  sourceIdentityRoot: proof.sourceIdentityRoot,
504
420
  sourcePatchRoot: proof.sourcePatchRoot,
@@ -46,7 +46,7 @@ function decodeBlob(blob) {
46
46
  ).toString("utf8");
47
47
  }
48
48
 
49
- function validateStoredState(queue) {
49
+ function validateStoredState(queue, { allowLegacyV3Readback = false } = {}) {
50
50
  const body = structuredClone(queue || {});
51
51
  const embeddedStateRoot = body.stateRoot;
52
52
  delete body.stateRoot;
@@ -59,7 +59,7 @@ function validateStoredState(queue) {
59
59
  ) {
60
60
  normalizeDevDeliveryAuthorityState(queue);
61
61
  } else {
62
- normalizeDevDeliveryQueue(queue);
62
+ normalizeDevDeliveryQueue(queue, { allowLegacyV3Readback });
63
63
  }
64
64
  return queue;
65
65
  }
@@ -108,7 +108,7 @@ export class GitHubDevDeliveryStore {
108
108
  return data;
109
109
  }
110
110
 
111
- async read({ stateRef, protectedBase, now }) {
111
+ async read({ stateRef, protectedBase, now, allowLegacyV3Readback = false }) {
112
112
  let ref;
113
113
  try {
114
114
  ref = await this.request(
@@ -128,11 +128,13 @@ export class GitHubDevDeliveryStore {
128
128
  };
129
129
  }
130
130
  const commitSha = exactSha(ref?.object?.sha, "state ref commit");
131
- const readback = await this.readCommit(commitSha);
131
+ const readback = await this.readCommit(commitSha, {
132
+ allowLegacyV3Readback,
133
+ });
132
134
  return { exists: true, ...readback };
133
135
  }
134
136
 
135
- async readCommit(commitShaInput) {
137
+ async readCommit(commitShaInput, { allowLegacyV3Readback = false } = {}) {
136
138
  const commitSha = exactSha(commitShaInput, "state commit");
137
139
  const commit = await this.request(
138
140
  "GET",
@@ -152,7 +154,9 @@ export class GitHubDevDeliveryStore {
152
154
  `/repos/${this.repository.fullName}/git/blobs/${entry.sha}`,
153
155
  );
154
156
  const bytes = decodeBlob(blob);
155
- const queue = validateStoredState(JSON.parse(bytes));
157
+ const queue = validateStoredState(JSON.parse(bytes), {
158
+ allowLegacyV3Readback,
159
+ });
156
160
  return { commitSha, queue, bytes };
157
161
  }
158
162
 
@@ -295,7 +295,10 @@ async function observeQueue(loaded, options) {
295
295
  const readMode = text(options.readMode || "v3").toLowerCase();
296
296
  if (!["v3", "v4"].includes(readMode))
297
297
  throw new Error("readMode must be v3 or v4");
298
- let observation = observeDevDeliveryQueue(loaded.queue, { now: options.now });
298
+ let observation = observeDevDeliveryQueue(loaded.queue, {
299
+ now: options.now,
300
+ allowLegacyV3Readback: true,
301
+ });
299
302
  let readCandidate;
300
303
  if (readMode === "v4") {
301
304
  const qualification =
@@ -374,6 +377,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
374
377
  stateRef: options.stateRef,
375
378
  protectedBase: options.branch,
376
379
  now: options.now,
380
+ allowLegacyV3Readback: options.command === "observe",
377
381
  });
378
382
  if (
379
383
  options.expectedOldStateRoot &&
@@ -143,6 +143,7 @@ export async function readCurrentDeliveryQueueState(client, repository, targetBr
143
143
  const { queue } = await store.read({
144
144
  stateRef: defaultDevDeliveryStateRef(targetBranch),
145
145
  protectedBase: targetBranch,
146
+ allowLegacyV3Readback: true,
146
147
  });
147
148
  return queue;
148
149
  }
@@ -501,16 +501,16 @@ export function createGitHubDevQualificationClient({
501
501
  sourceBranch,
502
502
  { activeOnly = false } = {},
503
503
  ) {
504
- const query = sourceBranch
505
- ? `?branch=${encodeURIComponent(sourceBranch)}`
506
- : "";
507
- const runs = await paged(
508
- `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflow)}/runs${query}`,
509
- "workflow_runs",
504
+ const query = sourceBranch ? `branch=${encodeRef(sourceBranch)}&` : "";
505
+ const runs = await Promise.all(
506
+ [...(activeOnly ? ACTIVE_STATUSES : [""])].map((status) =>
507
+ paged(
508
+ `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflow)}/runs?${query}${status ? `status=${status}` : ""}`,
509
+ "workflow_runs",
510
+ ),
511
+ ),
510
512
  );
511
- return activeOnly
512
- ? runs.filter((run) => ACTIVE_STATUSES.has(text(run.status)))
513
- : runs;
513
+ return runs.flat();
514
514
  },
515
515
  async listRunJobs(runId, runAttempt) {
516
516
  return paged(