@kungfu-tech/buildchain 4.0.2-alpha.2 → 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 (39) hide show
  1. package/architecture/agent-change-map.md +3 -0
  2. package/architecture/internal-capabilities.json +2 -0
  3. package/architecture/maintainability-debt.json +17 -15
  4. package/architecture/maintainability-policy.json +6 -6
  5. package/architecture/v3-core-mechanism-inventory.json +1 -0
  6. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  7. package/architecture/v4-release-invocation-fixtures.json +20 -0
  8. package/architecture/v4-release-topology.json +85 -80
  9. package/architecture/v4-runtime-semantic-closure.json +4 -1
  10. package/contracts/v4-release-invocation-v1.schema.json +50 -2
  11. package/dist/site/buildchain-contract.json +8 -8
  12. package/dist/site/buildchain-site.json +7 -7
  13. package/dist/site/kfd-claims.json +20 -3
  14. package/dist/site/kfd-upstream-aggregate.json +1 -1
  15. package/dist/site/manual-registry.json +1 -1
  16. package/dist/site/node-api-registry.json +88 -13
  17. package/dist/site/page-registry.json +2 -2
  18. package/dist/site/public-surface-audit.json +20 -3
  19. package/dist/site/publication-registry.json +4 -4
  20. package/dist/site/site-manifest.json +5 -5
  21. package/dist/site/workflow-registry.json +22 -5
  22. package/docs/node-api-reference.md +13 -10
  23. package/package.json +1 -1
  24. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  25. package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
  26. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  27. package/packages/core/dev-delivery-warrant-state.js +42 -26
  28. package/packages/core/dev-delivery-warrant.js +8 -0
  29. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  30. package/packages/core/v4-canonical-contracts.js +2 -0
  31. package/packages/core/v4-protected-publication-source.js +93 -0
  32. package/packages/core/v4-publication-qualification.js +1 -0
  33. package/packages/core/v4-release-invocation.js +72 -3
  34. package/scripts/check-v4-release-topology.mjs +169 -23
  35. package/scripts/dev-delivery-warrant.mjs +13 -1
  36. package/scripts/generate-channel-promotion-workflow.mjs +2 -1
  37. package/scripts/release-candidate-resolver.mjs +17 -17
  38. package/scripts/resume-from-candidate-run.mjs +15 -16
  39. package/scripts/v4-release-candidate-adapter.mjs +41 -0
@@ -0,0 +1,93 @@
1
+ import { v4ContentRoot } from "./v4-canonical-contracts.js";
2
+
3
+ const SHA_PATTERN = /^[0-9a-f]{40}$/u;
4
+
5
+ function exactSha(value, label) {
6
+ const normalized = String(value || "").toLowerCase();
7
+ if (!SHA_PATTERN.test(normalized)) {
8
+ throw new Error(`${label} must be an exact Git SHA`);
9
+ }
10
+ return normalized;
11
+ }
12
+
13
+ function exactParents(value, label) {
14
+ if (!Array.isArray(value)) {
15
+ throw new Error(`${label} must be an ordered Git parent list`);
16
+ }
17
+ return value.map((entry, index) => exactSha(entry, `${label}[${index}]`));
18
+ }
19
+
20
+ export function bindV4ProtectedPublicationSource({
21
+ repository,
22
+ protectedCommit,
23
+ candidateCommit,
24
+ pullRequest,
25
+ } = {}) {
26
+ if (!/^[^/\s]+\/[^/\s]+$/u.test(String(repository || ""))) {
27
+ throw new Error("protected publication source requires owner/repository");
28
+ }
29
+ const protectedSource = {
30
+ sha: exactSha(protectedCommit?.sha, "protected source SHA"),
31
+ tree: exactSha(protectedCommit?.tree, "protected source tree"),
32
+ parents: exactParents(protectedCommit?.parents, "protected source parents"),
33
+ };
34
+ const candidateSource = {
35
+ sha: exactSha(candidateCommit?.sha, "candidate source SHA"),
36
+ tree: exactSha(candidateCommit?.tree, "candidate source tree"),
37
+ parents: exactParents(candidateCommit?.parents, "candidate source parents"),
38
+ };
39
+ if (protectedSource.tree !== candidateSource.tree) {
40
+ throw new Error(
41
+ "protected publication source tree does not match the qualified candidate tree",
42
+ );
43
+ }
44
+
45
+ let mode = "exact-commit";
46
+ let pullRequestNumber = null;
47
+ if (protectedSource.sha !== candidateSource.sha) {
48
+ mode = "merge-equivalent";
49
+ const sameParents =
50
+ protectedSource.parents.length === candidateSource.parents.length &&
51
+ protectedSource.parents.every(
52
+ (parent, index) => parent === candidateSource.parents[index],
53
+ );
54
+ if (!sameParents) {
55
+ throw new Error(
56
+ "protected publication source is not parent-equivalent to the qualified merge candidate",
57
+ );
58
+ }
59
+ pullRequestNumber = Number(pullRequest?.number || 0);
60
+ const pullRequestHead = exactSha(
61
+ pullRequest?.headSha,
62
+ "publication pull request head SHA",
63
+ );
64
+ const pullRequestMerge = exactSha(
65
+ pullRequest?.mergeSha,
66
+ "publication pull request merge SHA",
67
+ );
68
+ if (
69
+ !Number.isSafeInteger(pullRequestNumber) ||
70
+ pullRequestNumber <= 0 ||
71
+ pullRequest?.merged !== true ||
72
+ pullRequestMerge !== protectedSource.sha ||
73
+ !candidateSource.parents.includes(pullRequestHead)
74
+ ) {
75
+ throw new Error(
76
+ "protected publication source does not match the merged pull request and qualified merge candidate",
77
+ );
78
+ }
79
+ }
80
+
81
+ const binding = {
82
+ schema: "kungfu.buildchain.v4-protected-publication-source/v1",
83
+ repository,
84
+ mode,
85
+ protectedSource,
86
+ candidateSource,
87
+ pullRequestNumber,
88
+ };
89
+ return {
90
+ ...binding,
91
+ bindingRoot: v4ContentRoot("candidate-identity", binding),
92
+ };
93
+ }
@@ -207,6 +207,7 @@ export function validateV4PublicationQualificationReceipt(
207
207
  export function assertV4DeclarativePromotionInputs(inputs) {
208
208
  const forbidden = Object.entries(inputs || {}).filter(
209
209
  ([name, value]) =>
210
+ name !== "dry-run" &&
210
211
  /(command|cmd|script|shell|run)$/iu.test(name) &&
211
212
  String(value || "").trim() !== "",
212
213
  );
@@ -13,6 +13,8 @@ export const V4_RELEASE_TRANSACTION_CONTRACT =
13
13
  "kungfu-buildchain-v4-release-transaction/v1";
14
14
  export const V4_RELEASE_RECEIPT_CONTRACT =
15
15
  "kungfu-buildchain-v4-release-receipt/v1";
16
+ export const V4_RELEASE_PROVIDER_CONTRACT =
17
+ "kungfu-buildchain-release-tail-provider/v1";
16
18
 
17
19
  const SHA_PATTERN = /^[0-9a-f]{40}$/u;
18
20
  const TAG_PATTERN = /^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
@@ -127,10 +129,59 @@ function validateAuthority(value) {
127
129
  validateV4Root(value[name], `$/authority/${name}`);
128
130
  }
129
131
 
132
+ function validateProvider(value, candidate) {
133
+ exactKeys(value, ["adapter", "contract", "repository"], "$/provider");
134
+ if (
135
+ value.adapter !== "built-in-provider-plane" ||
136
+ value.contract !== V4_RELEASE_PROVIDER_CONTRACT ||
137
+ value.repository !== candidate.repository
138
+ )
139
+ fault(
140
+ "invalid-release-provider",
141
+ "$/provider",
142
+ "provider identity must bind the built-in Provider Plane and candidate repository",
143
+ );
144
+ }
145
+
146
+ function validateParent(value) {
147
+ exactKeys(
148
+ value,
149
+ ["invocationRoot", "transactionRoot", "receiptRoot"],
150
+ "$/parent",
151
+ );
152
+ const roots = [
153
+ value.invocationRoot,
154
+ value.transactionRoot,
155
+ value.receiptRoot,
156
+ ];
157
+ if (roots.every((root) => root === null)) return;
158
+ if (roots.some((root) => root === null))
159
+ fault(
160
+ "invalid-release-parent-lineage",
161
+ "$/parent",
162
+ "parent lineage roots must be either all null or all present",
163
+ );
164
+ roots.forEach((root, index) =>
165
+ validateV4Root(
166
+ root,
167
+ `$/parent/${["invocationRoot", "transactionRoot", "receiptRoot"][index]}`,
168
+ ),
169
+ );
170
+ }
171
+
130
172
  export function createV4ReleaseInvocation(value) {
131
173
  exactKeys(
132
174
  value,
133
- ["schema", "publisher", "runtime", "candidate", "target", "authority"],
175
+ [
176
+ "schema",
177
+ "publisher",
178
+ "runtime",
179
+ "candidate",
180
+ "target",
181
+ "authority",
182
+ "provider",
183
+ "parent",
184
+ ],
134
185
  "$",
135
186
  );
136
187
  if (value.schema !== V4_RELEASE_INVOCATION_CONTRACT)
@@ -144,6 +195,8 @@ export function createV4ReleaseInvocation(value) {
144
195
  validateCandidate(value.candidate);
145
196
  validateTarget(value.target);
146
197
  validateAuthority(value.authority);
198
+ validateProvider(value.provider, value.candidate);
199
+ validateParent(value.parent);
147
200
  v4CanonicalBytes(value);
148
201
  const roots = {
149
202
  publisherRoot: v4ContentRoot(
@@ -160,6 +213,8 @@ export function createV4ReleaseInvocation(value) {
160
213
  "release-invocation-authority",
161
214
  value.authority,
162
215
  ),
216
+ providerRoot: v4ContentRoot("release-invocation-provider", value.provider),
217
+ parentRoot: v4ContentRoot("release-invocation-parent", value.parent),
163
218
  };
164
219
  const invocationRoot = v4ContentRoot("release-invocation", {
165
220
  schema: V4_RELEASE_INVOCATION_CONTRACT,
@@ -261,16 +316,30 @@ export function planV4ReleaseRoute({
261
316
  export function createV4ReleaseTransaction(value) {
262
317
  exactKeys(
263
318
  value,
264
- ["invocationRoot", "publisherRoot", "runtimeRoot"],
319
+ [
320
+ "invocationRoot",
321
+ "publisherRoot",
322
+ "runtimeRoot",
323
+ "providerRoot",
324
+ "parentRoot",
325
+ ],
265
326
  "$transaction",
266
327
  );
267
- for (const name of ["invocationRoot", "publisherRoot", "runtimeRoot"])
328
+ for (const name of [
329
+ "invocationRoot",
330
+ "publisherRoot",
331
+ "runtimeRoot",
332
+ "providerRoot",
333
+ "parentRoot",
334
+ ])
268
335
  validateV4Root(value[name], `$transaction/${name}`);
269
336
  const transaction = {
270
337
  schema: V4_RELEASE_TRANSACTION_CONTRACT,
271
338
  invocationRoot: value.invocationRoot,
272
339
  publisherRoot: value.publisherRoot,
273
340
  runtimeRoot: value.runtimeRoot,
341
+ providerRoot: value.providerRoot,
342
+ parentRoot: value.parentRoot,
274
343
  phases: ["QUALIFY", "APPLY", "SETTLE"],
275
344
  writer: "canonical-v4-apply",
276
345
  };
@@ -12,13 +12,68 @@ import {
12
12
 
13
13
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
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
15
 
18
16
  function read(relative) {
19
17
  return fs.readFileSync(path.join(root, relative), "utf8");
20
18
  }
21
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
+
22
77
  function jobBlock(source, jobId) {
23
78
  const escaped = jobId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
24
79
  return (
@@ -104,25 +159,27 @@ export function discoverV4ReleaseTopology(
104
159
  "carriers",
105
160
  "mutationSignals",
106
161
  ],
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
- })),
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
+ })),
126
183
  metrics: {
127
184
  workflowCount: workflows.length,
128
185
  jobCount: jobs.length,
@@ -174,7 +231,26 @@ export function findUnknownV4ReleaseTopology(
174
231
  const declared = new Set(workflowPaths);
175
232
  return allWorkflowPaths
176
233
  .filter((relative) => !declared.has(relative))
177
- .filter((relative) => releaseMarker.test(readWorkflow(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
+ })
178
254
  .sort();
179
255
  }
180
256
 
@@ -187,10 +263,76 @@ function assertClosedWorld(workflowPaths) {
187
263
  );
188
264
  }
189
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
+
190
331
  export function checkV4ReleaseTopology() {
191
332
  const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
192
333
  assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
193
334
  assertClosedWorld(ledger.closedWorld.workflowPaths);
335
+ assertAuthorityClosure(ledger);
194
336
  const actual = discoverV4ReleaseTopology(
195
337
  ledger.closedWorld.workflowPaths,
196
338
  ledger.semanticScope.workflowPaths,
@@ -219,7 +361,11 @@ export function checkV4ReleaseTopology() {
219
361
  return actual;
220
362
  }
221
363
 
222
- if (process.argv.includes("--print")) {
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")) {
223
369
  const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
224
370
  process.stdout.write(
225
371
  `${JSON.stringify(
@@ -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)
@@ -517,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
517
528
  }
518
529
 
519
530
  function usage() {
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";
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";
521
532
  }
522
533
 
523
534
  async function main() {
@@ -529,6 +540,7 @@ async function main() {
529
540
  const options = devDeliveryCliOptions(args);
530
541
  if (
531
542
  ![
543
+ "fence-writer-protocol",
532
544
  "submit",
533
545
  "select",
534
546
  "heartbeat",
@@ -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 }}
@@ -110,13 +110,13 @@ function digestFileSync(filePath, algorithm, encoding) {
110
110
  return hash.digest(encoding);
111
111
  }
112
112
 
113
- export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, repository }) {
113
+ export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, targetSha = "", repository }) {
114
114
  const normalizedTarget = normalizeBranch(targetRef);
115
115
  const candidates = pullRequests.filter((pr) => {
116
- const baseRef = normalizeBranch(pr.base?.ref || pr.baseRefName || "");
117
- const merged = Boolean(pr.merged_at || pr.mergedAt || pr.state === "closed");
118
- const sameRepo = !repository || !pr.head?.repo?.full_name || pr.head.repo.full_name === repository;
119
- return merged && sameRepo && baseRef === normalizedTarget;
116
+ const baseRepo = pr.base?.repo?.full_name || pr.baseRepository?.nameWithOwner;
117
+ const merged = Boolean(pr.merged_at || pr.mergedAt || pr.merged === true);
118
+ const rooted = !repository || (baseRepo || pr.head?.repo?.full_name) === repository;
119
+ return merged && rooted && (!targetSha || (pr.merge_commit_sha || pr.mergeCommit?.oid) === targetSha) && normalizeBranch(pr.base?.ref || pr.baseRefName || "") === normalizedTarget;
120
120
  });
121
121
  candidates.sort((left, right) => {
122
122
  const leftTime = Date.parse(left.merged_at || left.updated_at || left.closed_at || "");
@@ -513,11 +513,11 @@ export async function resolveReleaseCandidateArtifacts({
513
513
  });
514
514
  const channelPullRequest = selectMergedChannelPullRequest({
515
515
  pullRequests: Array.isArray(pulls) ? pulls : [],
516
- targetRef: normalizedTarget,
516
+ targetRef: normalizedTarget, targetSha: sha,
517
517
  repository: repoInfo.fullName,
518
518
  });
519
519
  if (!channelPullRequest) {
520
- throw new Error(`no same-repository merged channel PR found for ${sha} into ${normalizedTarget}`);
520
+ throw new Error(`no exact merged channel PR rooted in ${repoInfo.fullName} found for ${sha} into ${normalizedTarget}`);
521
521
  }
522
522
  let pullRequest = channelPullRequest;
523
523
  if (majorGateTarget) {
@@ -725,16 +725,6 @@ export async function resolveReleaseCandidateArtifacts({
725
725
  const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
726
726
  throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
727
727
  }
728
- const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
729
- const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
730
- manifests,
731
- version: passport.target?.version || "",
732
- kind: publishArtifactKind,
733
- tarballPaths: npmTarballPaths,
734
- mainPackage: publishPackageMain,
735
- });
736
- const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
737
- fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
738
728
  const sealedBundle = publishArtifactKind === "npm"
739
729
  ? createResolvedPublicationSealedBundle({
740
730
  bundleRoot: payloadDir,
@@ -750,6 +740,16 @@ export async function resolveReleaseCandidateArtifacts({
750
740
  releaseAssetPaths,
751
741
  })
752
742
  : undefined;
743
+ const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
744
+ const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
745
+ manifests,
746
+ version: [sealedBundle?.manifest?.npm?.version, passport.target?.version, ""].find(Boolean),
747
+ kind: publishArtifactKind,
748
+ tarballPaths: npmTarballPaths,
749
+ mainPackage: publishPackageMain,
750
+ });
751
+ const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
752
+ fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
753
753
  const sealedBundleManifestPath = sealedBundle
754
754
  ? path.join(resolvedOutput, "sealed-bundle.json")
755
755
  : "";
@@ -785,14 +785,13 @@ async function recoverCandidateEvidence({
785
785
  for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
786
786
  const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
787
787
  const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
788
- const sidecarFiles = initialDownloads[0].files.filter(
789
- (file) =>
790
- path.basename(file.path) === "release-candidate-stage-capsules.json",
791
- );
792
- const stageCapsuleSidecar =
793
- sidecarFiles.length === 1
794
- ? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
795
- : undefined;
788
+ const [stageCapsuleFile, publicationQualificationFile] = [
789
+ "release-candidate-stage-capsules.json",
790
+ "release-candidate-publication-qualification.json",
791
+ ].map((name) => initialDownloads[0].files.find((file) => path.basename(file.path) === name));
792
+ const stageCapsuleSidecar = stageCapsuleFile
793
+ ? readOnlyJson([stageCapsuleFile], "release-candidate-stage-capsules.json")
794
+ : undefined;
796
795
  const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
797
796
  const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
798
797
  if (chosen.length !== requiredNames.size) {
@@ -804,7 +803,7 @@ async function recoverCandidateEvidence({
804
803
  for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
805
804
  return {
806
805
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
807
- passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
806
+ passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
808
807
  };
809
808
  }
810
809
 
@@ -878,7 +877,7 @@ export async function resumeFromCandidateRun({
878
877
  try {
879
878
  const {
880
879
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
881
- passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
880
+ passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
882
881
  } = await recoverCandidateEvidence({
883
882
  repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
884
883
  outputDir, apiUrl, token, fetchImpl, archiveDir,
@@ -1023,9 +1022,9 @@ export async function resumeFromCandidateRun({
1023
1022
  sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
1024
1023
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
1025
1024
  recoveryReceipt: outputPath(recoveryReceiptPath),
1026
- runtimeResumeEvidence: runtimeResumeEvidencePath
1027
- ? outputPath(runtimeResumeEvidencePath)
1028
- : "",
1025
+ stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
1026
+ publicationQualification: publicationQualificationFile ? outputPath(publicationQualificationFile.absolutePath) : "",
1027
+ runtimeResumeEvidence: runtimeResumeEvidencePath ? outputPath(runtimeResumeEvidencePath) : "",
1029
1028
  },
1030
1029
  };
1031
1030
  } finally {
@@ -1077,10 +1076,10 @@ export async function resumeFromCandidateRunCli() {
1077
1076
  "release-candidate-run-url": result.run.url,
1078
1077
  "release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
1079
1078
  "release-candidate-recovery-root": result.receipt.root,
1079
+ "release-candidate-stage-capsules-path": result.paths.stageCapsules,
1080
+ "release-candidate-publication-qualification-path": result.paths.publicationQualification,
1080
1081
  "v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
1081
- "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
1082
- ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
1083
- : "",
1082
+ "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize" : "",
1084
1083
  "release-candidate-root": result.candidateRoot,
1085
1084
  "release-candidate-artifact-root": result.artifactRoot,
1086
1085
  "publish-sealed-bundle-root": result.paths.sealedBundleRoot,