@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.4

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 (59) hide show
  1. package/architecture/agent-change-map.md +7 -4
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +2 -0
  4. package/architecture/maintainability-debt.json +52 -124
  5. package/architecture/maintainability-policy.json +15 -10
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +1 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  10. package/architecture/v4-release-invocation-fixtures.json +139 -0
  11. package/architecture/v4-release-topology.json +243 -0
  12. package/architecture/v4-runtime-semantic-closure.json +4 -1
  13. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  14. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  15. package/contracts/v4-release-invocation-v1.schema.json +134 -0
  16. package/dist/site/buildchain-contract.json +9 -9
  17. package/dist/site/buildchain-site.json +7 -7
  18. package/dist/site/kfd-claims.json +24 -5
  19. package/dist/site/kfd-upstream-aggregate.json +1 -1
  20. package/dist/site/manual-registry.json +1 -1
  21. package/dist/site/node-api-registry.json +90 -15
  22. package/dist/site/page-registry.json +2 -2
  23. package/dist/site/public-surface-audit.json +34 -9
  24. package/dist/site/publication-authority-registry.json +2 -3
  25. package/dist/site/publication-registry.json +4 -4
  26. package/dist/site/site-manifest.json +5 -5
  27. package/dist/site/workflow-registry.json +36 -11
  28. package/docs/node-api-reference.md +16 -13
  29. package/package.json +2 -2
  30. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  31. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  32. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  33. package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
  34. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  35. package/packages/core/dev-delivery-warrant-state.js +54 -54
  36. package/packages/core/dev-delivery-warrant.js +8 -0
  37. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  38. package/packages/core/v4-canonical-contracts.js +10 -0
  39. package/packages/core/v4-floating-consumer-policy.js +4 -1
  40. package/packages/core/v4-protected-publication-source.js +93 -0
  41. package/packages/core/v4-publication-qualification.js +1 -0
  42. package/packages/core/v4-release-invocation.js +425 -0
  43. package/scripts/audit-publication-control-plane.mjs +0 -1
  44. package/scripts/check-inventory.mjs +27 -59
  45. package/scripts/check-maintainability.mjs +13 -5
  46. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  47. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  48. package/scripts/check-v4-release-topology.mjs +383 -0
  49. package/scripts/dev-delivery-warrant.mjs +26 -5
  50. package/scripts/generate-channel-promotion-workflow.mjs +14 -55
  51. package/scripts/release-candidate-resolver.mjs +17 -17
  52. package/scripts/resume-from-candidate-run.mjs +15 -16
  53. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  54. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  55. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  56. package/scripts/capture-package-release-propagation.mjs +0 -263
  57. package/scripts/publication-commit-evidence.mjs +0 -444
  58. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  59. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -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,383 @@
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
+
16
+ function read(relative) {
17
+ return fs.readFileSync(path.join(root, relative), "utf8");
18
+ }
19
+
20
+ function productionFiles(relative, extensions) {
21
+ const absolute = path.join(root, relative);
22
+ if (!fs.existsSync(absolute)) return [];
23
+ const entries = fs.readdirSync(absolute, { withFileTypes: true });
24
+ return entries.flatMap((entry) => {
25
+ const child = path.posix.join(relative, entry.name);
26
+ if (
27
+ entry.isDirectory() &&
28
+ !["dist", "node_modules", ".git"].includes(entry.name)
29
+ )
30
+ return productionFiles(child, extensions);
31
+ if (
32
+ entry.isFile() &&
33
+ extensions.some((extension) => entry.name.endsWith(extension))
34
+ )
35
+ return [child];
36
+ return [];
37
+ });
38
+ }
39
+
40
+ function matchingProductionFiles(roots, extensions, pattern) {
41
+ return roots
42
+ .flatMap((relative) => productionFiles(relative, extensions))
43
+ .filter((relative) => relative !== "scripts/check-v4-release-topology.mjs")
44
+ .filter((relative) => pattern.test(read(relative)))
45
+ .sort();
46
+ }
47
+
48
+ export function discoverV4ReleaseAuthorityClosure() {
49
+ const providerPlane = matchingProductionFiles(
50
+ ["actions", "packages/core"],
51
+ [".js", ".mjs", ".cjs"],
52
+ /(?:release-tail-provider-(?:adapters|plane)|create(?:GitHubReleaseAssets|SignedStaticChannel|SiteReleaseActivation|ReleasedEvidence)Adapter)/u,
53
+ );
54
+ const runtimeSelectors = matchingProductionFiles(
55
+ [".github/workflows", "scripts", "packages/core"],
56
+ [".yml", ".yaml", ".js", ".mjs", ".cjs"],
57
+ /(?:promotion-runtime-sha|resume-buildchain-runtime-sha|BUILDCHAIN_(?:CURRENT_RUNTIME_SHA|RUNTIME_REF|RESUME_CANDIDATE_RUN_ID)|authorizeV4RuntimeSelection|scanV4RuntimeSelectorPersistence|v4-release-candidate-adapter)/u,
58
+ );
59
+ const terminalProjections = matchingProductionFiles(
60
+ [".github/workflows", "actions", "packages/core", "scripts"],
61
+ [".yml", ".yaml", ".js", ".mjs", ".cjs"],
62
+ /(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
63
+ );
64
+ return {
65
+ providerAdapters: [
66
+ ...new Set([
67
+ ...providerPlane,
68
+ "packages/core/release-tail-provider-plane.js",
69
+ ]),
70
+ ].sort(),
71
+ runtimeSelectors,
72
+ runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
73
+ terminalProjections,
74
+ };
75
+ }
76
+
77
+ function jobBlock(source, jobId) {
78
+ const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
79
+ return (
80
+ source.match(
81
+ new RegExp(
82
+ `^ ${escaped}:\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_.-]+:|(?![\\s\\S]))`,
83
+ "mu",
84
+ ),
85
+ )?.[1] || ""
86
+ );
87
+ }
88
+
89
+ function permission(block, name) {
90
+ return (
91
+ new RegExp(`^ ${name}:\\s*([^#\\n]+)`, "mu")
92
+ .exec(block)?.[1]
93
+ ?.trim() || null
94
+ );
95
+ }
96
+
97
+ function workflowSnapshot(relative) {
98
+ const source = read(relative);
99
+ const document = parseWorkflowDocument(source);
100
+ const jobs = document.jobs.map((job) => {
101
+ const block = jobBlock(source, job.id);
102
+ const uses = job.uses || null;
103
+ return {
104
+ id: job.id,
105
+ kind: uses ? "reusable-call" : "runner",
106
+ uses,
107
+ permissions: {
108
+ contents: permission(block, "contents"),
109
+ idToken: permission(block, "id-token"),
110
+ },
111
+ carriers: {
112
+ artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
113
+ artifactUpload: /uses:\s+actions\/upload-artifact@/u.test(block),
114
+ jobOutput: /GITHUB_OUTPUT/u.test(block),
115
+ },
116
+ mutationSignals: [
117
+ /contents:\s*write/u.test(block) && "contents-write",
118
+ /id-token:\s*write/u.test(block) && "oidc-write",
119
+ /(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
120
+ block,
121
+ ) && "promotion-runtime",
122
+ /(?:git push|npm publish|gh release (?:create|upload))/u.test(block) &&
123
+ "direct-publication-command",
124
+ ].filter(Boolean),
125
+ };
126
+ });
127
+ return {
128
+ path: relative,
129
+ triggers: document.triggers,
130
+ jobs,
131
+ reusableEdges: parseYamlUses(source)
132
+ .map(({ value }) => value)
133
+ .filter((value) => /\.github\/workflows\//u.test(value))
134
+ .sort(),
135
+ };
136
+ }
137
+
138
+ export function discoverV4ReleaseTopology(
139
+ workflowPaths,
140
+ semanticWorkflowPaths = workflowPaths,
141
+ ) {
142
+ const workflows = workflowPaths.map(workflowSnapshot);
143
+ const jobs = workflows.flatMap((workflow) => workflow.jobs);
144
+ const mutationJobs = jobs.filter((job) => job.mutationSignals.length > 0);
145
+ const semanticPaths = new Set(semanticWorkflowPaths);
146
+ const semanticJobs = workflows
147
+ .filter((workflow) => semanticPaths.has(workflow.path))
148
+ .flatMap((workflow) => workflow.jobs);
149
+ const semanticMutationJobs = semanticJobs.filter(
150
+ (job) => job.kind === "runner" && job.mutationSignals.length > 0,
151
+ );
152
+ return {
153
+ jobRecordFields: [
154
+ "id",
155
+ "kind",
156
+ "uses",
157
+ "contentsPermission",
158
+ "oidcPermission",
159
+ "carriers",
160
+ "mutationSignals",
161
+ ],
162
+ workflows: workflows
163
+ .filter((workflow) => semanticPaths.has(workflow.path))
164
+ .map((workflow) => ({
165
+ path: workflow.path,
166
+ triggers: workflow.triggers,
167
+ jobs: workflow.jobs.map((job) =>
168
+ [
169
+ job.id,
170
+ job.kind,
171
+ job.uses || "-",
172
+ job.permissions.contents || "-",
173
+ job.permissions.idToken || "-",
174
+ Object.entries(job.carriers)
175
+ .filter(([, present]) => present)
176
+ .map(([name]) => name)
177
+ .join(",") || "-",
178
+ job.mutationSignals.join(",") || "-",
179
+ ].join("|"),
180
+ ),
181
+ reusableEdges: workflow.reusableEdges,
182
+ })),
183
+ metrics: {
184
+ workflowCount: workflows.length,
185
+ jobCount: jobs.length,
186
+ reusableEdgeCount: workflows.reduce(
187
+ (count, workflow) => count + workflow.reusableEdges.length,
188
+ 0,
189
+ ),
190
+ mutationRelevantNodeCount: jobs.filter(
191
+ (job) =>
192
+ job.kind === "reusable-call" ||
193
+ job.mutationSignals.length > 0 ||
194
+ job.carriers.artifactDownload ||
195
+ job.carriers.artifactUpload,
196
+ ).length,
197
+ contentsWriteJobCount: mutationJobs.filter((job) =>
198
+ job.mutationSignals.includes("contents-write"),
199
+ ).length,
200
+ oidcWriteJobCount: mutationJobs.filter((job) =>
201
+ job.mutationSignals.includes("oidc-write"),
202
+ ).length,
203
+ },
204
+ semanticMetrics: {
205
+ workflowCount: semanticPaths.size,
206
+ mutationRelevantNodeCount: semanticJobs.filter(
207
+ (job) =>
208
+ job.kind === "reusable-call" ||
209
+ job.mutationSignals.length > 0 ||
210
+ job.carriers.artifactDownload ||
211
+ job.carriers.artifactUpload,
212
+ ).length,
213
+ contentsWriteJobCount: semanticMutationJobs.filter((job) =>
214
+ job.mutationSignals.includes("contents-write"),
215
+ ).length,
216
+ oidcWriteJobCount: semanticMutationJobs.filter((job) =>
217
+ job.mutationSignals.includes("oidc-write"),
218
+ ).length,
219
+ },
220
+ };
221
+ }
222
+
223
+ export function findUnknownV4ReleaseTopology(
224
+ workflowPaths,
225
+ allWorkflowPaths = fs
226
+ .readdirSync(path.join(root, ".github/workflows"))
227
+ .filter((name) => /\.ya?ml$/u.test(name))
228
+ .map((name) => `.github/workflows/${name}`),
229
+ readWorkflow = read,
230
+ ) {
231
+ const declared = new Set(workflowPaths);
232
+ return allWorkflowPaths
233
+ .filter((relative) => !declared.has(relative))
234
+ .filter((relative) => {
235
+ const source = readWorkflow(relative);
236
+ const usesReleaseAuthority = parseYamlUses(source).some(({ value }) =>
237
+ /(?:release-candidate-promote|promote-buildchain-ref|v4-release-candidate-promote|release-tail)/u.test(
238
+ value,
239
+ ),
240
+ );
241
+ const hasReleaseLanguage =
242
+ /(?:^|[-_ ])(?:release|publish|promotion|distribution|tag)(?:$|[-_ :])/imu.test(
243
+ source,
244
+ );
245
+ const hasMutationAuthority =
246
+ /(?:contents|id-token):\s*write/u.test(source) ||
247
+ /(?:git push|npm publish|gh release (?:create|upload)|createRef|updateRef)/u.test(
248
+ source,
249
+ );
250
+ return (
251
+ usesReleaseAuthority || (hasReleaseLanguage && hasMutationAuthority)
252
+ );
253
+ })
254
+ .sort();
255
+ }
256
+
257
+ function assertClosedWorld(workflowPaths) {
258
+ const unknown = findUnknownV4ReleaseTopology(workflowPaths);
259
+ assert.deepEqual(
260
+ unknown,
261
+ [],
262
+ `unknown v4 release topology workflows: ${unknown.join(", ")}`,
263
+ );
264
+ }
265
+
266
+ function assertAuthorityClosure(ledger) {
267
+ const closure = ledger.authorityClosure;
268
+ assert.ok(
269
+ closure && typeof closure === "object",
270
+ "authority closure missing",
271
+ );
272
+ for (const className of [
273
+ "providerAdapters",
274
+ "runtimeSelectors",
275
+ "runtimeEngines",
276
+ "terminalProjections",
277
+ ]) {
278
+ assert.ok(
279
+ Array.isArray(closure[className]) && closure[className].length > 0,
280
+ `authority closure class ${className} is empty`,
281
+ );
282
+ for (const relative of closure[className])
283
+ assert.ok(
284
+ fs.statSync(path.join(root, relative)).isFile(),
285
+ `authority closure path is not a file: ${relative}`,
286
+ );
287
+ }
288
+ const discovered = discoverV4ReleaseAuthorityClosure();
289
+ for (const className of Object.keys(discovered))
290
+ assert.deepEqual(
291
+ closure[className],
292
+ discovered[className],
293
+ `authority closure class drifted: ${className}`,
294
+ );
295
+ assert.deepEqual(closure.runtimeEngines, [
296
+ "actions/v4-release-candidate-promote/index.js",
297
+ ]);
298
+ assert.equal(
299
+ closure.freshEntry,
300
+ ".github/workflows/release-candidate-promote.yml",
301
+ );
302
+ assert.equal(
303
+ closure.recoveryEntry,
304
+ ".github/workflows/buildchain-ref-promotion-recovery.yml",
305
+ );
306
+ const engineSurface = [
307
+ ".github/workflows/.release-candidate-promote.yml",
308
+ ...closure.runtimeEngines,
309
+ ]
310
+ .map(read)
311
+ .join("\n");
312
+ for (const pattern of closure.forbiddenLegacyEnginePatterns)
313
+ assert.doesNotMatch(
314
+ engineSurface,
315
+ new RegExp(pattern, "u"),
316
+ `legacy release engine remains reachable: ${pattern}`,
317
+ );
318
+ const canonicalWorkflow = read(
319
+ ".github/workflows/.release-candidate-promote.yml",
320
+ );
321
+ assert.match(
322
+ canonicalWorkflow,
323
+ /scripts\/v4-release-candidate-adapter\.mjs/u,
324
+ );
325
+ assert.match(
326
+ canonicalWorkflow,
327
+ /release-invocation\.json[\s\S]*release-transaction\.json[\s\S]*release-receipt\.json/u,
328
+ );
329
+ }
330
+
331
+ export function checkV4ReleaseTopology() {
332
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
333
+ assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
334
+ assertClosedWorld(ledger.closedWorld.workflowPaths);
335
+ assertAuthorityClosure(ledger);
336
+ const actual = discoverV4ReleaseTopology(
337
+ ledger.closedWorld.workflowPaths,
338
+ ledger.semanticScope.workflowPaths,
339
+ );
340
+ assert.deepEqual(actual, ledger.observedTopology);
341
+ assert.equal(
342
+ ledger.targetBudgets.maximumMutationRelevantNodeCount,
343
+ Math.floor(ledger.baselineMetrics.semanticMutationRelevantNodeCount / 2),
344
+ );
345
+ if (ledger.enforcement === "converged") {
346
+ assert.ok(
347
+ actual.semanticMetrics.mutationRelevantNodeCount <=
348
+ ledger.targetBudgets.maximumMutationRelevantNodeCount,
349
+ );
350
+ assert.equal(actual.semanticMetrics.contentsWriteJobCount, 1);
351
+ assert.equal(actual.semanticMetrics.oidcWriteJobCount, 1);
352
+ const publisher = actual.workflows.find(
353
+ ({ path: workflowPath }) =>
354
+ workflowPath === ".github/workflows/.release-candidate-promote.yml",
355
+ );
356
+ assert.deepEqual(
357
+ publisher.jobs.map((job) => job.split("|", 1)[0]),
358
+ ["apply", "qualify", "settle"],
359
+ );
360
+ }
361
+ return actual;
362
+ }
363
+
364
+ if (process.argv.includes("--print-closure")) {
365
+ process.stdout.write(
366
+ `${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
367
+ );
368
+ } else if (process.argv.includes("--print")) {
369
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
370
+ process.stdout.write(
371
+ `${JSON.stringify(
372
+ discoverV4ReleaseTopology(
373
+ ledger.closedWorld.workflowPaths,
374
+ ledger.semanticScope.workflowPaths,
375
+ ),
376
+ null,
377
+ 2,
378
+ )}\n`,
379
+ );
380
+ } else {
381
+ checkV4ReleaseTopology();
382
+ process.stdout.write("v4 release topology: ok\n");
383
+ }
@@ -6,6 +6,7 @@ import {
6
6
  closeDevDeliveryWarrant,
7
7
  createDevDeliveryQueue,
8
8
  createNativeCommandContract,
9
+ fenceDevDeliveryWriterProtocol,
9
10
  heartbeatDevDeliveryWarrant,
10
11
  observeDevDeliveryQueue,
11
12
  qualifyDevDeliveryWarrant,
@@ -91,6 +92,13 @@ function requireTerminalEvidenceCas(options) {
91
92
  "terminal evidence reconciliation execute requires expected-old CAS",
92
93
  );
93
94
  }
95
+ if (
96
+ options.command === "fence-writer-protocol" &&
97
+ options.execute &&
98
+ !options.expectedOldStateRoot
99
+ ) {
100
+ throw new Error("writer protocol fence execute requires expected-old CAS");
101
+ }
94
102
  }
95
103
 
96
104
  function normalizeRepository(value) {
@@ -175,6 +183,9 @@ function jsonObject(value, label) {
175
183
  }
176
184
 
177
185
  function transitionFor(command, queue, options) {
186
+ if (command === "fence-writer-protocol") {
187
+ return fenceDevDeliveryWriterProtocol(queue, { now: options.now });
188
+ }
178
189
  if (command === "submit") {
179
190
  const nativeCommandContract = options.environmentRoot
180
191
  ? createNativeCommandContract(options.nativeCommand)
@@ -430,20 +441,29 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
430
441
  options.command,
431
442
  ),
432
443
  });
444
+ let concurrencyRecovery = null;
433
445
  if (
434
446
  options.expectedOldStateRoot &&
435
447
  loaded.queue.stateRoot !== options.expectedOldStateRoot
436
448
  ) {
437
- throw new Error(
438
- `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
439
- );
449
+ if (options.command !== "heartbeat") {
450
+ throw new Error(
451
+ `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
452
+ );
453
+ }
454
+ concurrencyRecovery = {
455
+ schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
456
+ action: "heartbeat-state-root-rebased",
457
+ requestedStateRoot: options.expectedOldStateRoot,
458
+ observedStateRoot: loaded.queue.stateRoot,
459
+ observedCommitSha: loaded.commitSha,
460
+ };
440
461
  }
441
462
  if (options.command === "observe") return observeQueue(loaded, options);
442
463
  const initialLoaded = loaded;
443
464
  let changed = transitionFor(options.command, loaded.queue, options);
444
465
  let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
445
466
  let write = null;
446
- let concurrencyRecovery = null;
447
467
  if (options.execute && mutates) {
448
468
  if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
449
469
  throw new Error(
@@ -508,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
508
528
  }
509
529
 
510
530
  function usage() {
511
- 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";
531
+ return "Usage:\n buildchain dev warrant <fence-writer-protocol|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\nWriter protocol fence:\n fence-writer-protocol --expected-old sha256:... [--execute]\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";
512
532
  }
513
533
 
514
534
  async function main() {
@@ -520,6 +540,7 @@ async function main() {
520
540
  const options = devDeliveryCliOptions(args);
521
541
  if (
522
542
  ![
543
+ "fence-writer-protocol",
523
544
  "submit",
524
545
  "select",
525
546
  "heartbeat",
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
74
74
  function publicOutputs(outputBlock) {
75
75
  const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
76
76
  /^ value:.*$/m,
77
- ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
77
+ ` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
78
78
  ));
79
79
  const routed = [
80
80
  ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
@@ -214,8 +214,9 @@ function consumerAdmissionJob() {
214
214
  id: policy
215
215
  env:
216
216
  BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
217
+ BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
217
218
  BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
218
- BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
219
+ BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
219
220
  BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
220
221
  BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
221
222
  BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
@@ -292,7 +293,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
292
293
  },
293
294
  };
294
295
  const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
295
- const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
+ validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
297
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
297
298
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
298
299
  const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
@@ -300,8 +301,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
300
301
  for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
301
302
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
302
303
  }
303
- const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
304
- const stableForwarded = routedInputs(inputNames, stableRoute, major);
304
+ const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
305
305
  return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
306
306
  name: Release Candidate Promote
307
307
 
@@ -336,7 +336,7 @@ jobs:
336
336
  target-ref: \${{ steps.route.outputs.target-ref }}
337
337
  router-ref: \${{ steps.router.outputs.ref }}
338
338
  router-sha: \${{ steps.router.outputs.sha }}
339
- shell-ref: \${{ steps.route.outputs.shell-ref }}
339
+ shell-ref: \${{ steps.identities.outputs.shell-ref }}
340
340
  shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
341
341
  shell-sha: \${{ steps.identities.outputs.shell-sha }}
342
342
  runtime-ref: \${{ steps.route.outputs.runtime-ref }}
@@ -425,32 +425,17 @@ jobs:
425
425
  shell: bash
426
426
  env:
427
427
  GITHUB_TOKEN: \${{ github.token }}
428
- CHANNEL: \${{ steps.route.outputs.channel }}
429
- SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
430
- ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
431
- ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
432
- STABLE_SHELL_REF: ${stableRoute.logicalRef}
433
- STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
428
+ SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
434
429
  run: |
435
430
  set -euo pipefail
436
- if [[ "\${CHANNEL}" = "alpha" ]]; then
437
- expected_ref="\${ALPHA_SHELL_REF}"
438
- call_ref="\${ALPHA_SHELL_CALL_REF}"
439
- else
440
- expected_ref="\${STABLE_SHELL_REF}"
441
- call_ref="\${STABLE_SHELL_CALL_REF}"
442
- fi
443
- if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
444
- echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
445
- exit 1
446
- fi
447
431
  node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
448
432
  --repository "\${{ steps.router.outputs.repository }}" \\
449
433
  --router-ref "\${{ steps.router.outputs.ref }}" \\
450
434
  --router-sha "\${{ steps.router.outputs.sha }}" \\
451
435
  --shell-ref "\${SELECTED_SHELL_REF}" \\
452
- --shell-call-ref "\${call_ref}" \\
436
+ --shell-call-ref "\${SELECTED_SHELL_REF}" \\
453
437
  --runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
438
+ echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
454
439
 
455
440
  - name: Checkout selected promotion workflow shell
456
441
  uses: actions/checkout@v7.0.0
@@ -500,29 +485,21 @@ jobs:
500
485
  - name: Verify immutable promotion checkouts
501
486
  shell: bash
502
487
  env:
503
- CHANNEL: \${{ steps.route.outputs.channel }}
504
- ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
505
- STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
488
+ SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
506
489
  ROUTER_SHA: \${{ steps.router.outputs.sha }}
507
490
  SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
508
491
  RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
509
492
  run: |
510
493
  set -euo pipefail
511
- if [[ "\${CHANNEL}" = "alpha" ]]; then
512
- workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
513
- else
514
- workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
515
- fi
516
- test -f ".buildchain/shell/\${workflow_path}"
494
+ test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
517
495
  [[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
518
496
  [[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
519
497
  [[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
520
498
 
521
499
  ${consumerAdmissionJob()}
522
- alpha:
523
- name: Promote with alpha workflow shell
500
+ invoke:
501
+ name: Invoke the single v4 publisher adapter
524
502
  needs: [resolve-promotion, consumer-admission]
525
- if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
526
503
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
527
504
  permissions:
528
505
  actions: write
@@ -534,25 +511,7 @@ ${consumerAdmissionJob()}
534
511
  issues: write
535
512
  pull-requests: write
536
513
  with:
537
- ${alphaForwarded}
538
- secrets: inherit
539
-
540
- stable:
541
- name: Promote with stable workflow shell
542
- needs: [resolve-promotion, consumer-admission]
543
- if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
544
- uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
545
- permissions:
546
- actions: write
547
- artifact-metadata: write
548
- attestations: write
549
- checks: write
550
- contents: write
551
- id-token: write
552
- issues: write
553
- pull-requests: write
554
- with:
555
- ${stableForwarded}
514
+ ${invokeForwarded}
556
515
  secrets: inherit
557
516
  `;
558
517
  }