@kungfu-tech/buildchain 4.0.2-alpha.48 → 4.0.2-alpha.49

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 (32) hide show
  1. package/architecture/ci-lane-change-budget.json +18 -0
  2. package/architecture/maintainability-policy.json +4 -4
  3. package/architecture/v4-release-topology.json +56 -7
  4. package/architecture/v4-universal-workflow-bootstrap.json +6 -1
  5. package/architecture/v4-universal-workflow-train-admission.json +1 -1
  6. package/architecture/workflow-taxonomy.json +11 -0
  7. package/dist/site/buildchain-contract.json +4 -4
  8. package/dist/site/buildchain-site.json +21 -11
  9. package/dist/site/capability-registry.json +1 -1
  10. package/dist/site/kfd-claims.json +21 -5
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +1 -1
  13. package/dist/site/node-api-registry.json +9 -9
  14. package/dist/site/page-registry.json +16 -6
  15. package/dist/site/public-surface-audit.json +18 -3
  16. package/dist/site/publication-authority-registry.json +23 -1
  17. package/dist/site/publication-registry.json +4 -4
  18. package/dist/site/site-manifest.json +5 -5
  19. package/dist/site/workflow-registry.json +39 -5
  20. package/docs/node-api-reference.md +10 -10
  21. package/docs/oci-publication.md +75 -1
  22. package/docs/workflow-catalog.md +1 -0
  23. package/package.json +1 -1
  24. package/packages/core/buildchain-publication-authority.js +1 -0
  25. package/packages/core/oci-compose-qualification.js +140 -0
  26. package/packages/core/oci-publication-bundle.js +59 -25
  27. package/packages/core/oci-publication-graph.js +206 -0
  28. package/scripts/check-v4-release-topology.mjs +12 -0
  29. package/scripts/generate-v4-universal-workflow-facades.mjs +53 -20
  30. package/scripts/oci-compose-preview.mjs +250 -0
  31. package/scripts/publication-candidate-kind.mjs +2 -1
  32. package/scripts/universal-facade-maintainability.mjs +10 -1
@@ -2,6 +2,10 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { v4ContentRoot } from "./v4-canonical-contracts.js";
5
+ import {
6
+ verifyOciGraph,
7
+ verifyOciDestination,
8
+ } from "./oci-publication-graph.js";
5
9
 
6
10
  export const OCI_FAMILY_SCHEMA = "kungfu-buildchain-oci-family/v1";
7
11
  const digestPattern = /^sha256:[0-9a-f]{64}$/u;
@@ -119,7 +123,9 @@ export function verifyOciPublicationBundle({
119
123
  version,
120
124
  }) {
121
125
  requireValue(
122
- manifest?.schema === OCI_FAMILY_SCHEMA,
126
+ [OCI_FAMILY_SCHEMA, "kungfu-buildchain-oci-family/v2"].includes(
127
+ manifest?.schema,
128
+ ),
123
129
  "unsupported manifest schema",
124
130
  );
125
131
  const { root, ...body } = manifest;
@@ -152,18 +158,18 @@ export function verifyOciPublicationBundle({
152
158
  "incomplete or duplicate image family",
153
159
  );
154
160
  const files = new Map();
161
+ const graphs = new Map();
155
162
  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
- );
163
+ verifyOciDestination(image, manifest, requireValue);
160
164
  requireValue(
161
165
  digestPattern.test(image.digest) &&
162
166
  ["built", "reused"].includes(image.action),
163
167
  "invalid image identity",
164
168
  );
165
169
  requireValue(
166
- /^linux\/(amd64|arm64)$/u.test(image.platform),
170
+ /^linux\/(amd64|arm64)$/u.test(image.platform) ||
171
+ (manifest.schema.endsWith("/v2") &&
172
+ ["multi-platform", "compose"].includes(image.platform)),
167
173
  "unsupported image platform",
168
174
  );
169
175
  const prefix = image.layout;
@@ -171,7 +177,9 @@ export function verifyOciPublicationBundle({
171
177
  requireValue(
172
178
  digestPattern.test(descriptor.digest) &&
173
179
  Number.isSafeInteger(descriptor.size) &&
174
- descriptor.size >= 0,
180
+ descriptor.size >= 0 &&
181
+ !descriptor.urls &&
182
+ !descriptor.data,
175
183
  "invalid OCI descriptor",
176
184
  );
177
185
  const relative = `${prefix}/blobs/sha256/${descriptor.digest.slice(7)}`;
@@ -209,23 +217,44 @@ export function verifyOciPublicationBundle({
209
217
  index.schemaVersion === 2 && selected.length === 1,
210
218
  "OCI index must select one exact image manifest",
211
219
  );
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);
220
+ if (manifest.schema.endsWith("/v2")) {
221
+ graphs.set(
222
+ image.name,
223
+ verifyOciGraph({
224
+ image,
225
+ manifest,
226
+ descriptor: selected[0],
227
+ descriptorFile,
228
+ requireValue,
229
+ verifyProvenance: (config) =>
230
+ verifyImageProvenance(image, config, sourceSha, version),
231
+ }),
232
+ );
233
+ } else {
234
+ const document = JSON.parse(fs.readFileSync(descriptorFile(selected[0])));
235
+ requireValue(
236
+ document.schemaVersion === 2 &&
237
+ [
238
+ "application/vnd.oci.image.manifest.v1+json",
239
+ "application/vnd.docker.distribution.manifest.v2+json",
240
+ ].includes(document.mediaType) &&
241
+ Array.isArray(document.layers),
242
+ "unsupported OCI image manifest",
243
+ );
244
+ const config = JSON.parse(
245
+ fs.readFileSync(descriptorFile(document.config)),
246
+ );
247
+ requireValue(
248
+ `${config.os}/${config.architecture}` === image.platform,
249
+ "OCI platform mismatch",
250
+ );
251
+ verifyImageProvenance(image, config, sourceSha, version);
252
+ for (const layer of document.layers) descriptorFile(layer);
253
+ graphs.set(image.name, {
254
+ manifests: [selected[0]],
255
+ blobs: [document.config, ...document.layers],
256
+ });
257
+ }
229
258
  const smokeFile = ociBundleFile(bundleRoot, image.smoke.path);
230
259
  requireValue(
231
260
  fileDigest(smokeFile) === image.smoke.sha256,
@@ -237,5 +266,10 @@ export function verifyOciPublicationBundle({
237
266
  "smoke qualification failed",
238
267
  );
239
268
  }
240
- return { root: computed, manifest, bundleRoot: fs.realpathSync(bundleRoot) };
269
+ return {
270
+ root: computed,
271
+ manifest,
272
+ graphs,
273
+ bundleRoot: fs.realpathSync(bundleRoot),
274
+ };
241
275
  }
@@ -0,0 +1,206 @@
1
+ import fs from "node:fs";
2
+
3
+ const imageTypes = [
4
+ "application/vnd.oci.image.manifest.v1+json",
5
+ "application/vnd.docker.distribution.manifest.v2+json",
6
+ ];
7
+ const indexTypes = [
8
+ "application/vnd.oci.image.index.v1+json",
9
+ "application/vnd.docker.distribution.manifest.list.v2+json",
10
+ ];
11
+ const composeType = "application/vnd.docker.compose.project";
12
+
13
+ export function ociPublicationTag(image, version) {
14
+ return `${image.kind === "compose" ? "compose-" : ""}v${version}`;
15
+ }
16
+
17
+ export function verifyOciDestination(image, manifest, requireValue) {
18
+ const compose = manifest.schema.endsWith("/v2") && image.kind === "compose";
19
+ requireValue(
20
+ manifest.schema.endsWith("/v2") || !image.kind,
21
+ "artifact kinds require family v2",
22
+ );
23
+ const target =
24
+ compose &&
25
+ manifest.images.find(
26
+ (entry) => entry.name === image.targetImage && entry.kind !== "compose",
27
+ );
28
+ requireValue(
29
+ !image.kind || ["image", "compose"].includes(image.kind),
30
+ "unsupported artifact kind",
31
+ );
32
+ requireValue(!compose || target, "Compose target image is absent");
33
+ requireValue(
34
+ image.repository ===
35
+ `ghcr.io/${manifest.repository}/${compose ? target.name : image.name}`,
36
+ "destination must belong to the consumer repository",
37
+ );
38
+ requireValue(
39
+ manifest.images.filter(
40
+ (entry) =>
41
+ entry.repository === image.repository &&
42
+ ociPublicationTag(entry, manifest.version) ===
43
+ ociPublicationTag(image, manifest.version),
44
+ ).length === 1,
45
+ "duplicate publication destination",
46
+ );
47
+ }
48
+
49
+ export function verifyOciGraph({
50
+ image,
51
+ manifest,
52
+ descriptor,
53
+ descriptorFile,
54
+ requireValue: check,
55
+ verifyProvenance,
56
+ }) {
57
+ const manifests = [],
58
+ blobs = new Map(),
59
+ platforms = [],
60
+ attestations = [];
61
+ const visited = new Set();
62
+ const read = (entry) => JSON.parse(fs.readFileSync(descriptorFile(entry)));
63
+ function blob(entry) {
64
+ descriptorFile(entry);
65
+ blobs.set(entry.digest, entry);
66
+ }
67
+ function visit(entry, depth = 0) {
68
+ check(
69
+ depth < 4 && visited.size < 256 && !visited.has(entry.digest),
70
+ "duplicate or excessive OCI graph",
71
+ );
72
+ visited.add(entry.digest);
73
+ const document = read(entry);
74
+ check(
75
+ document.schemaVersion === 2 && document.mediaType === entry.mediaType,
76
+ "OCI descriptor media type mismatch",
77
+ );
78
+ if (indexTypes.includes(document.mediaType)) {
79
+ check(
80
+ image.kind !== "compose" &&
81
+ Array.isArray(document.manifests) &&
82
+ document.manifests.length > 0,
83
+ "invalid OCI index",
84
+ );
85
+ for (const child of document.manifests) visit(child, depth + 1);
86
+ } else {
87
+ check(
88
+ imageTypes.includes(document.mediaType) &&
89
+ Array.isArray(document.layers),
90
+ "unsupported OCI manifest",
91
+ );
92
+ const config = read(document.config);
93
+ blob(document.config);
94
+ for (const layer of document.layers) blob(layer);
95
+ if (image.kind === "compose") {
96
+ verifyCompose(document, image, manifest, read, check);
97
+ } else if (
98
+ entry.annotations?.["vnd.docker.reference.type"] ===
99
+ "attestation-manifest"
100
+ ) {
101
+ check(
102
+ entry.platform?.os === "unknown" &&
103
+ entry.platform?.architecture === "unknown" &&
104
+ document.layers.length > 0 &&
105
+ document.layers.every(
106
+ (layer) => layer.mediaType === "application/vnd.in-toto+json",
107
+ ),
108
+ "invalid image attestation",
109
+ );
110
+ attestations.push({
111
+ target: entry.annotations["vnd.docker.reference.digest"],
112
+ statements: document.layers.map(read),
113
+ });
114
+ } else {
115
+ const platform = `${config.os}/${config.architecture}`;
116
+ check(
117
+ /^linux\/(amd64|arm64)$/u.test(platform) &&
118
+ (!entry.platform ||
119
+ platform ===
120
+ `${entry.platform.os}/${entry.platform.architecture}`),
121
+ "OCI platform mismatch",
122
+ );
123
+ check(
124
+ !platforms.some((found) => found.platform === platform),
125
+ "duplicate image platform",
126
+ );
127
+ platforms.push({ platform, digest: entry.digest });
128
+ verifyProvenance(config);
129
+ }
130
+ }
131
+ manifests.push(entry);
132
+ }
133
+ visit(descriptor);
134
+ if (image.kind !== "compose") {
135
+ const expected =
136
+ image.platform === "multi-platform" ? image.platforms : [image.platform];
137
+ check(
138
+ Array.isArray(expected) &&
139
+ expected.length > 0 &&
140
+ new Set(expected).size === expected.length &&
141
+ JSON.stringify([...expected].sort()) ===
142
+ JSON.stringify(platforms.map((entry) => entry.platform).sort()),
143
+ "incomplete image platform family",
144
+ );
145
+ for (const attestation of attestations) {
146
+ check(
147
+ platforms.some((entry) => entry.digest === attestation.target) &&
148
+ attestation.statements.every((statement) =>
149
+ statement.subject?.some(
150
+ (subject) =>
151
+ `sha256:${subject.digest?.sha256}` === attestation.target,
152
+ ),
153
+ ),
154
+ "image attestation subject mismatch",
155
+ );
156
+ }
157
+ }
158
+ return { manifests, blobs: [...blobs.values()], platforms };
159
+ }
160
+
161
+ function verifyCompose(document, image, manifest, read, check) {
162
+ if (image.preview)
163
+ check(
164
+ image.preview.alias === "compose-preview" &&
165
+ /^\.github\/workflows\/[a-z0-9-]+\.yml$/u.test(
166
+ image.preview.qualificationWorkflow,
167
+ ) &&
168
+ /^(none|sha256:[0-9a-f]{64})$/u.test(image.preview.previousDigest),
169
+ "invalid Compose preview policy",
170
+ );
171
+ check(
172
+ image.platform === "compose" &&
173
+ document.artifactType === composeType &&
174
+ document.config.mediaType === "application/vnd.oci.empty.v1+json" &&
175
+ document.layers.length === 1 &&
176
+ document.layers[0].mediaType ===
177
+ "application/vnd.docker.compose.file+yaml",
178
+ "unsupported Compose artifact",
179
+ );
180
+ // JSON is a YAML subset; this bounded public form needs no executable YAML loader.
181
+ const project = read(document.layers[0]);
182
+ check(
183
+ project.services && Object.keys(project.services).length > 0,
184
+ "empty Compose application",
185
+ );
186
+ const target = manifest.images.find(
187
+ (entry) => entry.name === image.targetImage,
188
+ );
189
+ const references = Object.values(project.services).map(
190
+ (service) => service.image,
191
+ );
192
+ check(
193
+ references.every(
194
+ (ref) =>
195
+ typeof ref === "string" &&
196
+ /^[a-z0-9./:_-]+@sha256:[0-9a-f]{64}$/u.test(ref),
197
+ ) && references.includes(`${target.repository}@${target.digest}`),
198
+ "Compose images must bind exact digests",
199
+ );
200
+ check(
201
+ image.content?.sourceSha === manifest.sourceSha &&
202
+ image.content?.version === manifest.version &&
203
+ image.action === "built",
204
+ "Compose candidate provenance mismatch",
205
+ );
206
+ }
@@ -25,6 +25,7 @@ const PRIVILEGED_ENTRYPOINTS = [
25
25
  "actions/v4-release-candidate-promote/index.js",
26
26
  "scripts/binary-publication-evidence.mjs",
27
27
  "scripts/next-development-review.mjs",
28
+ "scripts/oci-compose-preview.mjs",
28
29
  "scripts/v4-publication-settlement.mjs",
29
30
  ];
30
31
 
@@ -502,6 +503,17 @@ export function checkV4ReleaseTopology() {
502
503
  assert.equal(ledger.contract, "kungfu-buildchain-v4-release-topology/v1");
503
504
  assertClosedWorld(ledger.closedWorld.workflowPaths);
504
505
  assertAuthorityClosure(ledger);
506
+ const preview = ledger.postPublicationScope;
507
+ assert.deepEqual(preview.workflowPaths, [
508
+ ".github/workflows/public-release-oci-compose-preview.yml",
509
+ ]);
510
+ const previewTopology = discoverV4ReleaseTopology(
511
+ preview.workflowPaths,
512
+ preview.workflowPaths,
513
+ );
514
+ assert.equal(previewTopology.semanticMetrics.contentsWriteJobCount, 1);
515
+ assert.equal(previewTopology.semanticMetrics.oidcWriteJobCount, 0);
516
+ assert.deepEqual(preview.observedTopology, previewTopology);
505
517
  const actual = discoverV4ReleaseTopology(
506
518
  ledger.closedWorld.workflowPaths,
507
519
  ledger.semanticScope.workflowPaths,
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import crypto from "node:crypto";
4
- import { currentWorkflowPath, writeWorkflowSource, rewriteRepositoryWorkflowPaths } from "./workflow-taxonomy.mjs";
4
+ import {
5
+ currentWorkflowPath,
6
+ writeWorkflowSource,
7
+ rewriteRepositoryWorkflowPaths,
8
+ } from "./workflow-taxonomy.mjs";
5
9
  import path from "node:path";
6
10
  import { execFileSync } from "node:child_process";
7
11
  import { fileURLToPath } from "node:url";
@@ -96,6 +100,7 @@ function frozenFacadeSource(relative) {
96
100
  const mutableFacades = new Set([
97
101
  ...(contract.migration.channelGeneratedFacades || []),
98
102
  ".github/workflows/dev-pr-auto-merge.yml",
103
+ ...(contract.migration.postMigrationWorkflows || []),
99
104
  ]);
100
105
 
101
106
  function isMutableFacade(relative) {
@@ -187,9 +192,7 @@ function guardCompatibilityJobs(source, relative) {
187
192
  function addRuntimeBootstrapDependencies(source, relative) {
188
193
  if (
189
194
  relative !== ".github/workflows/.build.yml" ||
190
- source.includes(
191
- ".buildchain/workflow-shell/scripts/github-output.mjs\n",
192
- )
195
+ source.includes(".buildchain/workflow-shell/scripts/github-output.mjs\n")
193
196
  )
194
197
  return source;
195
198
  const anchor =
@@ -201,32 +204,60 @@ function addRuntimeBootstrapDependencies(source, relative) {
201
204
  }
202
205
 
203
206
  function applyPublicationFacadePatches(source, relative) {
204
- const document = JSON.parse(fs.readFileSync(path.join(root,
205
- "architecture/v4-publication-facade-patches.json"), "utf8"));
206
- const patch = document.facades.find(entry => entry.workflow === relative);
207
+ const document = JSON.parse(
208
+ fs.readFileSync(
209
+ path.join(root, "architecture/v4-publication-facade-patches.json"),
210
+ "utf8",
211
+ ),
212
+ );
213
+ const patch = document.facades.find((entry) => entry.workflow === relative);
207
214
  if (!patch) return source;
208
- const digest = value => crypto.createHash("sha256").update(value).digest("hex");
215
+ const digest = (value) =>
216
+ crypto.createHash("sha256").update(value).digest("hex");
209
217
  if (digest(source) === patch.afterSha256) return source;
210
- if (digest(source) !== patch.beforeSha256) fail(`${relative}: publication facade source drift`);
218
+ if (digest(source) !== patch.beforeSha256)
219
+ fail(`${relative}: publication facade source drift`);
211
220
  for (const { before, after } of patch.replacements) {
212
- if (source.split(before).length !== 2) fail(`${relative}: ambiguous publication patch`);
221
+ if (source.split(before).length !== 2)
222
+ fail(`${relative}: ambiguous publication patch`);
213
223
  source = source.replace(before, () => after);
214
224
  }
215
- if (digest(source) !== patch.afterSha256) fail(`${relative}: publication facade result drift`);
225
+ if (digest(source) !== patch.afterSha256)
226
+ fail(`${relative}: publication facade result drift`);
216
227
  return source;
217
228
  }
218
229
 
219
230
  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");
231
+ if (relative !== ".github/workflows/.release-candidate-promote.yml")
232
+ return source;
233
+ return source
234
+ .replace(
235
+ "permissions:\n contents: read\n",
236
+ "# Provider authority is inherited from the explicit caller envelope.\n",
237
+ )
238
+ .replace(
239
+ " permissions:\n actions: read\n checks: write\n contents: write\n id-token: write\n pull-requests: write\n",
240
+ " # Provider authority is inherited from the explicit caller envelope.\n",
241
+ );
223
242
  }
224
243
 
225
244
  export function migrateV4UniversalWorkflowFacade(source, relative) {
226
- return rewriteRepositoryWorkflowPaths(root, inheritPublicationProviderAuthority(applyPublicationFacadePatches(guardCompatibilityJobs(
227
- addUniversalInput(addRuntimeBootstrapDependencies(source, relative), relative),
228
- relative,
229
- ), relative), relative));
245
+ return rewriteRepositoryWorkflowPaths(
246
+ root,
247
+ inheritPublicationProviderAuthority(
248
+ applyPublicationFacadePatches(
249
+ guardCompatibilityJobs(
250
+ addUniversalInput(
251
+ addRuntimeBootstrapDependencies(source, relative),
252
+ relative,
253
+ ),
254
+ relative,
255
+ ),
256
+ relative,
257
+ ),
258
+ relative,
259
+ ),
260
+ );
230
261
  }
231
262
 
232
263
  function verify(source, relative) {
@@ -427,7 +458,8 @@ function main() {
427
458
  );
428
459
  } else {
429
460
  writeWorkflowSource(
430
- root, path.relative(root, recoveryWorkflowPath),
461
+ root,
462
+ path.relative(root, recoveryWorkflowPath),
431
463
  fs.readFileSync(recoveryTemplatePath, "utf8"),
432
464
  );
433
465
  }
@@ -440,7 +472,8 @@ function main() {
440
472
  if (check) verify(source, relative);
441
473
  else
442
474
  writeWorkflowSource(
443
- root, current,
475
+ root,
476
+ current,
444
477
  migrateV4UniversalWorkflowFacade(source, relative),
445
478
  );
446
479
  }