@kungfu-tech/buildchain 4.0.2-alpha.43 → 4.0.2-alpha.45

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 (40) hide show
  1. package/architecture/maintainability-debt.json +18 -19
  2. package/architecture/maintainability-policy.json +4 -4
  3. package/architecture/v4-release-topology.json +9 -6
  4. package/architecture/v4-universal-workflow-train-admission.json +1 -1
  5. package/dist/site/buildchain-contract.json +7 -7
  6. package/dist/site/buildchain-site.json +50 -14
  7. package/dist/site/capability-registry.json +2 -2
  8. package/dist/site/kfd-claims.json +29 -5
  9. package/dist/site/kfd-upstream-aggregate.json +1 -1
  10. package/dist/site/manual-registry.json +3 -3
  11. package/dist/site/node-api-registry.json +154 -13
  12. package/dist/site/page-registry.json +43 -7
  13. package/dist/site/public-surface-audit.json +7 -3
  14. package/dist/site/publication-authority-registry.json +5 -17
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +2 -1
  17. package/dist/site/site-manifest.json +7 -7
  18. package/dist/site/workflow-registry.json +5 -5
  19. package/docs/MAP.md +1 -0
  20. package/docs/cli.md +3 -0
  21. package/docs/node-api-reference.md +21 -10
  22. package/docs/oci-publication.md +82 -0
  23. package/package.json +3 -2
  24. package/packages/core/buildchain-v4-domain.wasm +0 -0
  25. package/packages/core/oci-publication-bundle.js +241 -0
  26. package/packages/core/paper-agent-entry.js +26 -2
  27. package/packages/core/paper.js +4 -4
  28. package/packages/core/release-tail-product-capabilities.js +9 -0
  29. package/packages/core/v4-canonical-contracts.js +1 -0
  30. package/packages/core/v4-domain-wasm-artifact.js +2 -2
  31. package/scripts/check-v4-release-topology.mjs +14 -4
  32. package/scripts/generate-channel-promotion-workflow.mjs +2 -19
  33. package/scripts/generate-v4-universal-workflow-facades.mjs +8 -2
  34. package/scripts/publication-candidate-kind.mjs +92 -0
  35. package/scripts/publication-candidate-sealer.mjs +9 -0
  36. package/scripts/release-candidate-resolver.mjs +6 -17
  37. package/scripts/resume-from-candidate-run.mjs +10 -17
  38. package/scripts/site-capability-metadata.mjs +1 -0
  39. package/scripts/v4-product-publication-intent.mjs +1 -1
  40. package/scripts/v4-universal-workflow-engine.mjs +2 -0
@@ -0,0 +1,241 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { v4ContentRoot } from "./v4-canonical-contracts.js";
5
+
6
+ export const OCI_FAMILY_SCHEMA = "kungfu-buildchain-oci-family/v1";
7
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
8
+ const namePattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;
9
+
10
+ export function sealOciPublicationBundle({ bundleRoot, body }) {
11
+ const manifest = {
12
+ ...body,
13
+ root: v4ContentRoot("oci-publication-family", body),
14
+ };
15
+ verifyOciPublicationBundle({
16
+ bundleRoot,
17
+ manifest,
18
+ repository: body.repository,
19
+ sourceSha: body.sourceSha,
20
+ version: body.version,
21
+ });
22
+ return manifest;
23
+ }
24
+
25
+ function requireValue(condition, message) {
26
+ if (!condition) throw new Error(`OCI bundle: ${message}`);
27
+ }
28
+
29
+ export function ociBundleFile(root, relative) {
30
+ requireValue(
31
+ typeof relative === "string" &&
32
+ relative.length > 0 &&
33
+ !relative.includes("\\") &&
34
+ !path.isAbsolute(relative) &&
35
+ !relative.split(/[\\/]/u).some((p) => p === ".." || p === "." || !p),
36
+ "unsafe file path",
37
+ );
38
+ const base = fs.realpathSync(root);
39
+ let current = base;
40
+ for (const segment of relative.split("/")) {
41
+ current = path.join(current, segment);
42
+ requireValue(
43
+ !fs.lstatSync(current).isSymbolicLink(),
44
+ "symlinks are forbidden",
45
+ );
46
+ }
47
+ requireValue(
48
+ fs.statSync(current).isFile() &&
49
+ fs.realpathSync(current).startsWith(`${base}${path.sep}`),
50
+ "file escapes bundle",
51
+ );
52
+ return current;
53
+ }
54
+
55
+ function fileDigest(file) {
56
+ const hash = crypto.createHash("sha256"),
57
+ buffer = Buffer.alloc(1024 * 1024);
58
+ const fd = fs.openSync(file, "r");
59
+ try {
60
+ for (;;) {
61
+ const size = fs.readSync(fd, buffer);
62
+ if (!size) break;
63
+ hash.update(buffer.subarray(0, size));
64
+ }
65
+ } finally {
66
+ fs.closeSync(fd);
67
+ }
68
+ return `sha256:${hash.digest("hex")}`;
69
+ }
70
+
71
+ function verifyImageProvenance(image, config, sourceSha, version) {
72
+ const labels = config.config?.Labels || {};
73
+ requireValue(
74
+ image.content &&
75
+ /^[0-9a-f]{40}$/u.test(image.content.sourceSha) &&
76
+ typeof image.content.version === "string" &&
77
+ image.content.version.length > 0,
78
+ "missing image content provenance",
79
+ );
80
+ requireValue(
81
+ labels["org.opencontainers.image.revision"] === image.content.sourceSha &&
82
+ labels["org.opencontainers.image.version"] === image.content.version,
83
+ "image content provenance mismatch",
84
+ );
85
+ if (image.action === "built")
86
+ requireValue(
87
+ image.content.sourceSha === sourceSha &&
88
+ image.content.version === version,
89
+ "built image is not from the candidate source",
90
+ );
91
+ if (image.contractMajor !== undefined)
92
+ requireValue(
93
+ Number.isInteger(image.contractMajor) &&
94
+ image.contractMajor > 0 &&
95
+ labels["io.kungfu.image.contract-major"] ===
96
+ String(image.contractMajor),
97
+ "image contract major mismatch",
98
+ );
99
+ if (image.parentDigest)
100
+ requireValue(
101
+ digestPattern.test(image.parentDigest) &&
102
+ labels["io.kungfu.image.parent-digest"] === image.parentDigest,
103
+ "image parent digest mismatch",
104
+ );
105
+ if (image.content.materialSha)
106
+ requireValue(
107
+ /^[0-9a-f]{40}$/u.test(image.content.materialSha) &&
108
+ labels["io.kungfu.buildchain.release-material-sha"] ===
109
+ image.content.materialSha,
110
+ "image material provenance mismatch",
111
+ );
112
+ }
113
+
114
+ export function verifyOciPublicationBundle({
115
+ bundleRoot,
116
+ manifest,
117
+ repository,
118
+ sourceSha,
119
+ version,
120
+ }) {
121
+ requireValue(
122
+ manifest?.schema === OCI_FAMILY_SCHEMA,
123
+ "unsupported manifest schema",
124
+ );
125
+ const { root, ...body } = manifest;
126
+ const computed = v4ContentRoot("oci-publication-family", body);
127
+ requireValue(root === computed, "manifest root mismatch");
128
+ requireValue(
129
+ manifest.repository === repository &&
130
+ manifest.sourceSha === sourceSha &&
131
+ manifest.version === version,
132
+ "candidate identity mismatch",
133
+ );
134
+ requireValue(
135
+ /^[a-z0-9-]+\/[a-z0-9._-]+$/u.test(repository),
136
+ "invalid repository",
137
+ );
138
+ requireValue(
139
+ Array.isArray(manifest.images) && manifest.images.length > 0,
140
+ "empty image family",
141
+ );
142
+ const expected = manifest.expectedImages;
143
+ requireValue(
144
+ Array.isArray(expected) &&
145
+ expected.length === new Set(expected).size &&
146
+ expected.every((n) => namePattern.test(n)),
147
+ "invalid expected image family",
148
+ );
149
+ const actual = manifest.images.map((i) => i.name).sort();
150
+ requireValue(
151
+ JSON.stringify(actual) === JSON.stringify([...expected].sort()),
152
+ "incomplete or duplicate image family",
153
+ );
154
+ const files = new Map();
155
+ for (const image of manifest.images) {
156
+ requireValue(
157
+ image.repository === `ghcr.io/${repository}/${image.name}`,
158
+ "destination must belong to the consumer repository",
159
+ );
160
+ requireValue(
161
+ digestPattern.test(image.digest) &&
162
+ ["built", "reused"].includes(image.action),
163
+ "invalid image identity",
164
+ );
165
+ requireValue(
166
+ /^linux\/(amd64|arm64)$/u.test(image.platform),
167
+ "unsupported image platform",
168
+ );
169
+ const prefix = image.layout;
170
+ const descriptorFile = (descriptor) => {
171
+ requireValue(
172
+ digestPattern.test(descriptor.digest) &&
173
+ Number.isSafeInteger(descriptor.size) &&
174
+ descriptor.size >= 0,
175
+ "invalid OCI descriptor",
176
+ );
177
+ const relative = `${prefix}/blobs/sha256/${descriptor.digest.slice(7)}`;
178
+ const file = ociBundleFile(bundleRoot, relative);
179
+ requireValue(
180
+ fs.statSync(file).size === descriptor.size,
181
+ "OCI blob size mismatch",
182
+ );
183
+ if (!files.has(file))
184
+ requireValue(
185
+ fileDigest(file) === descriptor.digest,
186
+ "OCI blob digest mismatch",
187
+ );
188
+ files.set(file, descriptor);
189
+ return file;
190
+ };
191
+ const layout = JSON.parse(
192
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/oci-layout`)),
193
+ );
194
+ requireValue(
195
+ layout.imageLayoutVersion === "1.0.0",
196
+ "unsupported OCI layout",
197
+ );
198
+ const index = JSON.parse(
199
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/index.json`)),
200
+ );
201
+ const selected = (index.manifests || []).filter(
202
+ (descriptor) =>
203
+ descriptor.digest === image.digest &&
204
+ (descriptor.annotations?.["org.opencontainers.image.ref.name"] ===
205
+ image.name ||
206
+ index.manifests.length === 1),
207
+ );
208
+ requireValue(
209
+ index.schemaVersion === 2 && selected.length === 1,
210
+ "OCI index must select one exact image manifest",
211
+ );
212
+ const document = JSON.parse(fs.readFileSync(descriptorFile(selected[0])));
213
+ requireValue(
214
+ document.schemaVersion === 2 &&
215
+ [
216
+ "application/vnd.oci.image.manifest.v1+json",
217
+ "application/vnd.docker.distribution.manifest.v2+json",
218
+ ].includes(document.mediaType) &&
219
+ Array.isArray(document.layers),
220
+ "unsupported OCI image manifest",
221
+ );
222
+ const config = JSON.parse(fs.readFileSync(descriptorFile(document.config)));
223
+ requireValue(
224
+ `${config.os}/${config.architecture}` === image.platform,
225
+ "OCI platform mismatch",
226
+ );
227
+ verifyImageProvenance(image, config, sourceSha, version);
228
+ for (const layer of document.layers) descriptorFile(layer);
229
+ const smokeFile = ociBundleFile(bundleRoot, image.smoke.path);
230
+ requireValue(
231
+ fileDigest(smokeFile) === image.smoke.sha256,
232
+ "smoke evidence digest mismatch",
233
+ );
234
+ const smoke = JSON.parse(fs.readFileSync(smokeFile));
235
+ requireValue(
236
+ smoke.passed === true && smoke.image === image.name,
237
+ "smoke qualification failed",
238
+ );
239
+ }
240
+ return { root: computed, manifest, bundleRoot: fs.realpathSync(bundleRoot) };
241
+ }
@@ -13,7 +13,13 @@ import {
13
13
  stableJson,
14
14
  workCheck,
15
15
  } from "./paper-repository.js";
16
- import { mergeNextDevelopmentAgentInstructions } from "./next-development-projection.js";
16
+ import {
17
+ mergeNextDevelopmentAgentInstructions,
18
+ NEXT_DEVELOPMENT_AGENT_SECTION_START,
19
+ NEXT_DEVELOPMENT_AGENT_SECTION_END,
20
+ NEXT_DEVELOPMENT_LOCAL_COMMAND,
21
+ } from "./next-development-projection.js";
22
+ import { NEXT_DEVELOPMENT_ADR } from "./next-development-transition.js";
17
23
 
18
24
  export const PAPER_AGENT_ENTRY_CONTRACT = "kungfu-buildchain-paper-agent-entry";
19
25
  export const PAPER_AGENT_ENTRY_SCHEMA_VERSION = 1;
@@ -149,6 +155,24 @@ export function createPaperAgentEntry({
149
155
  };
150
156
  }
151
157
 
158
+ function mergePaperNextDevelopmentInstructions(current) {
159
+ const merged = mergeNextDevelopmentAgentInstructions(current);
160
+ const start = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_START);
161
+ const end = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_END);
162
+ const section = merged.slice(start, end)
163
+ .replace(
164
+ `\`${NEXT_DEVELOPMENT_ADR}\``,
165
+ `[Buildchain next-development ADR](https://github.com/kungfu-systems/buildchain/blob/v4/${NEXT_DEVELOPMENT_ADR})`,
166
+ )
167
+ .replace(
168
+ NEXT_DEVELOPMENT_LOCAL_COMMAND,
169
+ NEXT_DEVELOPMENT_LOCAL_COMMAND.replace(
170
+ "node scripts/", "node node_modules/@kungfu-tech/buildchain/scripts/",
171
+ ),
172
+ );
173
+ return `${merged.slice(0, start)}${section}${merged.slice(end)}`;
174
+ }
175
+
152
176
  export function paperAgentEntryFiles({
153
177
  cwd,
154
178
  buildchainVersion,
@@ -169,7 +193,7 @@ export function paperAgentEntryFiles({
169
193
  [PAPER_PATHS.agentEntry, jsonText(entry)],
170
194
  [
171
195
  PAPER_PATHS.agentInstructions,
172
- mergeNextDevelopmentAgentInstructions(
196
+ mergePaperNextDevelopmentInstructions(
173
197
  mergePaperAgentEntryInstructions(currentAgents, {
174
198
  developmentRef: resolvedDevelopmentRef,
175
199
  }),
@@ -410,7 +410,7 @@ jobs:
410
410
  `;
411
411
  }
412
412
 
413
- function scaffoldVerifyWorkflow(buildchainSha) {
413
+ function scaffoldVerifyWorkflow(buildchainSha, buildchainVersion = "") {
414
414
  return `${nextDevelopmentWorkflowHeader()}name: Verify
415
415
 
416
416
  on:
@@ -423,7 +423,7 @@ on:
423
423
  workflow_dispatch:
424
424
 
425
425
  permissions:
426
- contents: read
426
+ ${buildchainVersion.startsWith("4.") ? " actions: read\n contents: read\n pull-requests: read" : " contents: read"}
427
427
 
428
428
  jobs:
429
429
  check:
@@ -621,7 +621,7 @@ function scaffoldFiles({
621
621
  artifactPaths: "_build/main.pdf",
622
622
  releasePassportProductName: title,
623
623
  });
624
- const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha);
624
+ const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha, buildchainVersion);
625
625
  const agentEntry = paperAgentEntryFiles({
626
626
  cwd,
627
627
  buildchainVersion,
@@ -756,7 +756,7 @@ function migrationFiles({
756
756
  artifactPaths: config.publication.artifactPaths.join(","),
757
757
  releasePassportProductName: config.publication.title,
758
758
  });
759
- const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha);
759
+ const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha, runtimeIdentity.version);
760
760
  const agentEntry = paperAgentEntryFiles({
761
761
  cwd,
762
762
  buildchainVersion: runtimeIdentity.version,
@@ -17,6 +17,15 @@ export const RELEASE_TAIL_PRODUCT_CAPABILITIES = Object.freeze([
17
17
  receiptKind: "product-package-publication",
18
18
  transactionState: "publishing",
19
19
  }),
20
+ Object.freeze({
21
+ id: "product.oci.publish",
22
+ executor: "provider-adapter",
23
+ adapter: "oci-image-family",
24
+ effectKind: "oci-family-publication",
25
+ observationKind: "oci-family-readback",
26
+ receiptKind: "oci-family-publication",
27
+ transactionState: "publishing",
28
+ }),
20
29
  Object.freeze({
21
30
  id: "product.release-refs.converge",
22
31
  executor: "provider-adapter",
@@ -58,6 +58,7 @@ const ROOT_DOMAINS = new Set([
58
58
  "partial-mutation-recovery-checkpoint",
59
59
  "partial-mutation-recovery-plan",
60
60
  "v4-product-required-artifacts",
61
+ "oci-publication-family",
61
62
  "v4-product-publication-intent",
62
63
  "v4-product-publication-operation",
63
64
  "v4-product-publication-plan",
@@ -2,6 +2,6 @@
2
2
  export const V4_DOMAIN_WASM_ABI_VERSION = 1;
3
3
  export const V4_DOMAIN_WASM_RUSTC = "rustc 1.96.0 (ac68faa20 2026-05-25)";
4
4
  export const V4_DOMAIN_WASM_SOURCE_SHA256 =
5
- "07d45b2767e22007d3d41fa32c8a2dba28d9c88c11a2b28863e82d54be3a2933";
5
+ "7297279bce5344b8fc58688be5c1933a823306c82dac7dccfabdf07de41d652f";
6
6
  export const V4_DOMAIN_WASM_SHA256 =
7
- "b37c330e9366b7e9cf9dc4b7573b1e9495212dd9520e8b8ec6cdc3fd0ab7cafd";
7
+ "3a4909bb4b2bbd2676eac83f259c22faf1ce43479036992c5e53d5e2dcc512ce";
@@ -233,13 +233,21 @@ function workflowSnapshot(relative) {
233
233
  .map((job) => {
234
234
  const block = jobBlock(source, job.id);
235
235
  const uses = job.uses || null;
236
+ // A built-in publisher still requires mutation authority when the caller supplies its envelope.
237
+ const inheritedPublisher =
238
+ !/^ permissions:/mu.test(block) &&
239
+ /uses:.*actions\/v4-release-candidate-promote/u.test(block);
236
240
  return {
237
241
  id: job.id,
238
242
  kind: uses ? "reusable-call" : "runner",
239
243
  uses,
240
244
  permissions: {
241
- contents: permission(block, "contents"),
242
- idToken: permission(block, "id-token"),
245
+ contents:
246
+ permission(block, "contents") ||
247
+ (inheritedPublisher ? "inherited" : null),
248
+ idToken:
249
+ permission(block, "id-token") ||
250
+ (inheritedPublisher ? "inherited" : null),
243
251
  },
244
252
  carriers: {
245
253
  artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
@@ -247,8 +255,10 @@ function workflowSnapshot(relative) {
247
255
  jobOutput: /GITHUB_OUTPUT/u.test(block),
248
256
  },
249
257
  mutationSignals: [
250
- /contents:\s*write/u.test(block) && "contents-write",
251
- /id-token:\s*write/u.test(block) && "oidc-write",
258
+ (/contents:\s*write/u.test(block) || inheritedPublisher) &&
259
+ "contents-write",
260
+ (/id-token:\s*write/u.test(block) || inheritedPublisher) &&
261
+ "oidc-write",
252
262
  /(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
253
263
  block,
254
264
  ) && "promotion-runtime",
@@ -316,16 +316,7 @@ ${secrets.trimEnd()}
316
316
  outputs:
317
317
  ${publicOutputs(outputs)}
318
318
 
319
- permissions:
320
- actions: write
321
- artifact-metadata: write
322
- attestations: write
323
- checks: write
324
- contents: write
325
- id-token: write
326
- issues: write
327
- pull-requests: write
328
-
319
+ # Provider authority is inherited from the explicit caller envelope.
329
320
  jobs:
330
321
  resolve-promotion:
331
322
  name: Resolve promotion workflow shell and runtime
@@ -503,15 +494,7 @@ ${consumerAdmissionJob()}
503
494
  name: Invoke the single v4 publisher adapter
504
495
  needs: [resolve-promotion, consumer-admission]
505
496
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
506
- permissions:
507
- actions: write
508
- artifact-metadata: write
509
- attestations: write
510
- checks: write
511
- contents: write
512
- id-token: write
513
- issues: write
514
- pull-requests: write
497
+ # Provider authority is inherited from the explicit caller envelope.
515
498
  with:
516
499
  ${invokeForwarded}
517
500
  secrets: inherit
@@ -216,11 +216,17 @@ function applyPublicationFacadePatches(source, relative) {
216
216
  return source;
217
217
  }
218
218
 
219
+ function inheritPublicationProviderAuthority(source, relative) {
220
+ if (relative !== ".github/workflows/.release-candidate-promote.yml") return source;
221
+ return source.replace("permissions:\n contents: read\n", "# Provider authority is inherited from the explicit caller envelope.\n")
222
+ .replace(" permissions:\n actions: read\n checks: write\n contents: write\n id-token: write\n pull-requests: write\n", " # Provider authority is inherited from the explicit caller envelope.\n");
223
+ }
224
+
219
225
  export function migrateV4UniversalWorkflowFacade(source, relative) {
220
- return rewriteRepositoryWorkflowPaths(root, applyPublicationFacadePatches(guardCompatibilityJobs(
226
+ return rewriteRepositoryWorkflowPaths(root, inheritPublicationProviderAuthority(applyPublicationFacadePatches(guardCompatibilityJobs(
221
227
  addUniversalInput(addRuntimeBootstrapDependencies(source, relative), relative),
222
228
  relative,
223
- ), relative));
229
+ ), relative), relative));
224
230
  }
225
231
 
226
232
  function verify(source, relative) {
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { verifyOciPublicationBundle } from "../packages/core/oci-publication-bundle.js";
4
+
5
+ export function resolveOciCandidate({ payloadRoot, passport }) {
6
+ const matches = [];
7
+ function visit(directory) {
8
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
9
+ if (entry.isSymbolicLink())
10
+ throw new Error("OCI candidate cannot contain symlinks");
11
+ const file = path.join(directory, entry.name);
12
+ if (entry.isDirectory()) visit(file);
13
+ else if (entry.name === "oci-family.json") matches.push(file);
14
+ }
15
+ }
16
+ visit(payloadRoot);
17
+ if (matches.length !== 1)
18
+ throw new Error("OCI candidate requires exactly one sealed image family");
19
+ const manifest = JSON.parse(fs.readFileSync(matches[0]));
20
+ const bundleRoot = path.dirname(matches[0]);
21
+ const verified = verifyOciPublicationBundle({
22
+ bundleRoot,
23
+ manifest,
24
+ repository: passport.repository,
25
+ sourceSha: passport.source.headSha,
26
+ version: passport.target.version,
27
+ });
28
+ return {
29
+ root: bundleRoot,
30
+ manifest: verified.manifest,
31
+ requiredArtifacts: manifest.images.map((image) => ({
32
+ kind: "oci",
33
+ name: image.repository,
34
+ ref: `v${manifest.version}`,
35
+ digest: image.digest,
36
+ platform: image.platform,
37
+ required: true,
38
+ })),
39
+ };
40
+ }
41
+
42
+ export function resolveRecoveredPublicationVersion({
43
+ artifactVersion,
44
+ channel,
45
+ rematerializeOnResume = false,
46
+ } = {}) {
47
+ const version = String(artifactVersion || "").trim(),
48
+ match = version.match(
49
+ /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
50
+ );
51
+ if (!match)
52
+ throw new Error(
53
+ `recovered npm artifact has invalid publication version: ${version || "<empty>"}`,
54
+ );
55
+ return channel === "release" && rematerializeOnResume ? match[1] : version;
56
+ }
57
+ export function resolveRecoveredCandidateVersion({
58
+ artifactVersion,
59
+ publicationVersion,
60
+ channel,
61
+ rematerializeOnResume = false,
62
+ targetRef = "",
63
+ candidateRef = "",
64
+ } = {}) {
65
+ const version = String(artifactVersion || "").trim(),
66
+ target = String(targetRef || "").replace(/^refs\/heads\//u, ""),
67
+ candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
68
+ if (channel !== "release" || !rematerializeOnResume) return version;
69
+ const prefix = `publish-gate/${target}/`;
70
+ if (!target || !candidate.startsWith(prefix))
71
+ throw new Error(
72
+ `stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`,
73
+ );
74
+ const candidateVersion = candidate.slice(prefix.length);
75
+ if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion))
76
+ throw new Error(
77
+ `stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`,
78
+ );
79
+ if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion)
80
+ throw new Error(
81
+ `stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`,
82
+ );
83
+ if (
84
+ (version.match(
85
+ /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
86
+ )?.[1] || "") !== publicationVersion
87
+ )
88
+ throw new Error(
89
+ `recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`,
90
+ );
91
+ return candidateVersion;
92
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveOciCandidate } from "./publication-candidate-kind.mjs";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -102,3 +103,11 @@ export function createResolvedPublicationSealedBundle({
102
103
  });
103
104
  return { root: resolvedRoot, manifest, npmArtifacts: resolvedNpmArtifacts, files };
104
105
  }
106
+
107
+ export function resolveCandidatePublicationBundle({ kind, payloadDir, passport, runtimeSha, npmArtifacts, releaseAssetPaths }) {
108
+ if (kind === "oci") return resolveOciCandidate({ payloadRoot: payloadDir, passport });
109
+ if (kind !== "npm") return undefined;
110
+ return createResolvedPublicationSealedBundle({ bundleRoot: payloadDir, repository: passport.repository,
111
+ sourceSha: passport.source?.headSha, sourceTreeSha: passport.source?.treeHash,
112
+ runtimeSha, releaseCandidateRoot: passport.candidateHash, npmArtifacts, releaseAssetPaths });
113
+ }
@@ -7,7 +7,7 @@ import { execFileSync } from "node:child_process";
7
7
  import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { pathToFileURL } from "node:url";
10
- import { createResolvedPublicationSealedBundle } from "./publication-candidate-sealer.mjs";
10
+ import { resolveCandidatePublicationBundle } from "./publication-candidate-sealer.mjs";
11
11
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
12
12
  import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
13
13
  import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
@@ -725,23 +725,12 @@ 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 sealedBundle = publishArtifactKind === "npm"
729
- ? createResolvedPublicationSealedBundle({
730
- bundleRoot: payloadDir,
731
- repository: repoInfo.fullName,
732
- sourceSha: passport.source?.headSha,
733
- sourceTreeSha: passport.source?.treeHash,
734
- runtimeSha: releaseCandidateRuntimeSha(passport),
735
- releaseCandidateRoot: passport.candidateHash,
736
- npmArtifacts: npmTarballPaths.map((tarballPath) => ({
737
- path: tarballPath,
738
- ...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }),
739
- })),
740
- releaseAssetPaths,
741
- })
742
- : undefined;
728
+ const sealedBundle = resolveCandidatePublicationBundle({ kind: publishArtifactKind, payloadDir, passport,
729
+ runtimeSha: releaseCandidateRuntimeSha(passport), releaseAssetPaths,
730
+ npmArtifacts: npmTarballPaths.map((tarballPath) => ({ path: tarballPath,
731
+ ...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }) })) });
743
732
  const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
744
- const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
733
+ const generatedRequiredArtifacts = sealedBundle?.requiredArtifacts || generatePublishRequiredArtifacts({
745
734
  manifests,
746
735
  version: publicationVersion,
747
736
  kind: publishArtifactKind,
@@ -6,6 +6,8 @@ import path from "node:path";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
+ import { resolveOciCandidate, resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
10
+ export { resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
9
11
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
12
  import { compareSemver } from "./publication-registry-hydrate.mjs";
11
13
  import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
@@ -695,22 +697,6 @@ export function createRecoveredPublicationCandidate({
695
697
  };
696
698
  return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
697
699
  }
698
- export function resolveRecoveredPublicationVersion({ artifactVersion, channel, rematerializeOnResume = false } = {}) {
699
- const version = String(artifactVersion || "").trim(), match = version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u);
700
- if (!match) throw new Error(`recovered npm artifact has invalid publication version: ${version || "<empty>"}`);
701
- return channel === "release" && rematerializeOnResume ? match[1] : version;
702
- }
703
- export function resolveRecoveredCandidateVersion({ artifactVersion, publicationVersion, channel, rematerializeOnResume = false, targetRef = "", candidateRef = "" } = {}) {
704
- const version = String(artifactVersion || "").trim(), target = String(targetRef || "").replace(/^refs\/heads\//u, ""), candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
705
- if (channel !== "release" || !rematerializeOnResume) return version;
706
- const prefix = `publish-gate/${target}/`;
707
- if (!target || !candidate.startsWith(prefix)) throw new Error(`stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`);
708
- const candidateVersion = candidate.slice(prefix.length);
709
- if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion)) throw new Error(`stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`);
710
- if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion) throw new Error(`stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`);
711
- if ((version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u)?.[1] || "") !== publicationVersion) throw new Error(`recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`);
712
- return candidateVersion;
713
- }
714
700
  export function createRecoveredPublication({ downloads, bundleRoot, repository, passport, candidateRuntimeSha, publishArtifactKind, publishPackageMain, releasePatterns, platformManifests, channel, targetRef = "", candidateRef = "", rematerializeOnResume = false }) {
715
701
  const allFiles = downloads.flatMap((download) => download.files.map((file) => ({ path: path.relative(bundleRoot, file.absolutePath).split(path.sep).join("/"),
716
702
  size: file.size, sha256: file.sha256.replace(/^sha256:/, ""), absolutePath: file.absolutePath })))
@@ -718,6 +704,13 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
718
704
  const kind = String(publishArtifactKind || "npm");
719
705
  const releaseMatchers = splitPatterns(releasePatterns).map(patternMatcher);
720
706
  const releaseAssets = allFiles.filter((file) => releaseMatchers.some((matcher) => matcher.test(path.basename(file.path))));
707
+ if (kind === "oci") {
708
+ createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
709
+ const sealed = resolveOciCandidate({ payloadRoot: bundleRoot, passport });
710
+ return { manifest: sealed.manifest, bundleRoot: sealed.root, npmArtifacts: [], allFiles, releaseAssets,
711
+ version: sealed.manifest.version, candidateVersion: sealed.manifest.version,
712
+ publishRequiredArtifacts: sealed.requiredArtifacts };
713
+ }
721
714
  if (kind !== "npm") {
722
715
  createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
723
716
  const version = String(passport.target?.version || "").trim();
@@ -1019,7 +1012,7 @@ export async function resumeFromCandidateRun({
1019
1012
  npmTarballs: tarballs,
1020
1013
  releaseAssets: publication.releaseAssets.map((asset) => outputPath(asset.absolutePath)),
1021
1014
  publishRequiredArtifacts: outputPath(requiredArtifactsPath),
1022
- sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
1015
+ sealedBundleRoot: publication.manifest ? outputPath(publication.bundleRoot || bundleRoot) : "",
1023
1016
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
1024
1017
  recoveryReceipt: outputPath(recoveryReceiptPath),
1025
1018
  stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
@@ -206,6 +206,7 @@ export function nodeApiMeta(exportName) {
206
206
  "./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
207
207
  "./publication-package": { group: "reusable-build", summary: "Publication npm package synthesis APIs for Buildchain-managed paper release presets." },
208
208
  "./publication-reproducibility": { group: "reusable-build", summary: "Two-clean-build publication byte reproducibility receipt APIs." },
209
+ "./oci-publication": { group: "reusable-build", summary: "Sealed OCI image family verification, provenance, and smoke evidence APIs." },
209
210
  "./publication-sealed-bundle": { group: "reusable-build", summary: "Build-once publication bundle manifests and exact-byte resume verification APIs." },
210
211
  "./paper": { group: "reusable-build", summary: "Unified Paper work, fleet, scaffold, preflight, npm bootstrap, build, Alpha, status, and resume planning APIs." },
211
212
  "./publication-authority": { group: "release-passport-trust", summary: "Sealed publication authority registry, runner provenance, control-plane audit, admission, and independent verification APIs." },