@kungfu-tech/buildchain 2.14.2 → 2.14.3-alpha.1

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 (57) hide show
  1. package/dist/site/agent-index.json +2 -0
  2. package/dist/site/artifact-schemas.json +4 -0
  3. package/dist/site/badge-endpoint-registry.json +320 -0
  4. package/dist/site/badges/v1/kfd-5/aligned.json +14 -0
  5. package/dist/site/badges/v1/kfd-5/declared.json +14 -0
  6. package/dist/site/badges/v1/kfd-5/downgraded.json +14 -0
  7. package/dist/site/badges/v1/kfd-5/draft.json +14 -0
  8. package/dist/site/badges/v1/kfd-5/failed.json +14 -0
  9. package/dist/site/badges/v1/kfd-5/missing.json +14 -0
  10. package/dist/site/badges/v1/kfd-5/passed.json +14 -0
  11. package/dist/site/badges/v1/kfd-5/planned.json +14 -0
  12. package/dist/site/badges/v1/kfd-6/aligned.json +14 -0
  13. package/dist/site/badges/v1/kfd-6/declared.json +14 -0
  14. package/dist/site/badges/v1/kfd-6/downgraded.json +14 -0
  15. package/dist/site/badges/v1/kfd-6/draft.json +14 -0
  16. package/dist/site/badges/v1/kfd-6/failed.json +14 -0
  17. package/dist/site/badges/v1/kfd-6/missing.json +14 -0
  18. package/dist/site/badges/v1/kfd-6/passed.json +14 -0
  19. package/dist/site/badges/v1/kfd-6/planned.json +14 -0
  20. package/dist/site/buildchain-contract.json +149 -28
  21. package/dist/site/buildchain-site.json +25 -23
  22. package/dist/site/capability-registry.json +2 -2
  23. package/dist/site/controller-registry.json +61 -5
  24. package/dist/site/kfd-claims.json +129 -11
  25. package/dist/site/kfd-upstream-aggregate.json +12 -12
  26. package/dist/site/manual-registry.json +6 -6
  27. package/dist/site/node-api-registry.json +20 -7
  28. package/dist/site/page-registry.json +12 -12
  29. package/dist/site/public-surface-audit.json +134 -8
  30. package/dist/site/publication-authority-registry.json +26 -1
  31. package/dist/site/publication-registry.json +4 -4
  32. package/dist/site/release-model.json +3 -1
  33. package/dist/site/release-passport-check-manifest.json +167 -0
  34. package/dist/site/release-provenance.json +3 -0
  35. package/dist/site/schemas/release-passport-v1.schema.json +198 -0
  36. package/dist/site/site-manifest.json +10 -10
  37. package/dist/site/workflow-registry.json +128 -4
  38. package/docs/migration-inventory.md +8 -0
  39. package/docs/release-candidate.md +3 -0
  40. package/docs/release-governance.md +5 -4
  41. package/docs/release-passport.md +19 -0
  42. package/docs/release-propagation.md +19 -7
  43. package/docs/reusable-build-surface.md +26 -1
  44. package/package.json +5 -2
  45. package/packages/core/buildchain-contract.js +41 -0
  46. package/packages/core/buildchain-kfd-claims.js +4 -0
  47. package/packages/core/buildchain-publication-authority.js +1 -0
  48. package/packages/core/controller-evidence.js +1 -1
  49. package/packages/core/index.js +9 -0
  50. package/packages/core/package-manager.js +32 -0
  51. package/packages/core/release-passport-contract.js +249 -0
  52. package/packages/core/release-passport.js +39 -0
  53. package/scripts/check-inventory.mjs +37 -3
  54. package/scripts/generate-channel-promotion-workflow.mjs +331 -0
  55. package/scripts/generate-site-bundle.mjs +15 -0
  56. package/scripts/promotion-channel-router.mjs +132 -0
  57. package/scripts/validate-package-manager-contract.mjs +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.14.2",
3
+ "version": "2.14.3-alpha.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -38,6 +38,7 @@
38
38
  "./release-candidate": "./packages/core/release-candidate.js",
39
39
  "./stable-candidate-ledger": "./packages/core/stable-candidate-ledger.js",
40
40
  "./release-passport": "./packages/core/release-passport.js",
41
+ "./release-passport-contract": "./packages/core/release-passport-contract.js",
41
42
  "./release-propagation": "./packages/core/release-propagation.js",
42
43
  "./surface-manifest": "./packages/core/surface-manifest.js",
43
44
  "./buildchain-kfd-claims": "./packages/core/buildchain-kfd-claims.js",
@@ -57,6 +58,8 @@
57
58
  "./site/kfd-upstream-aggregate.json": "./dist/site/kfd-upstream-aggregate.json",
58
59
  "./site/release-model.json": "./dist/site/release-model.json",
59
60
  "./site/artifact-schemas.json": "./dist/site/artifact-schemas.json",
61
+ "./site/release-passport-check-manifest.json": "./dist/site/release-passport-check-manifest.json",
62
+ "./site/schemas/release-passport-v1.schema.json": "./dist/site/schemas/release-passport-v1.schema.json",
60
63
  "./site/buildchain-contract.json": "./dist/site/buildchain-contract.json",
61
64
  "./site/product-mechanism.json": "./dist/site/product-mechanism.json",
62
65
  "./site/release-provenance.json": "./dist/site/release-provenance.json",
@@ -104,7 +107,7 @@
104
107
  "vitest": "^4.1.9"
105
108
  },
106
109
  "dependencies": {
107
- "@kungfu-tech/kfd": "1.0.0-alpha.21",
110
+ "@kungfu-tech/kfd": "1.0.0-alpha.29",
108
111
  "smol-toml": "1.7.0"
109
112
  }
110
113
  }
@@ -89,6 +89,7 @@ export function createBuildchainContractWorld({
89
89
  "publication-artifact",
90
90
  "paper-release",
91
91
  "release-candidate-promote",
92
+ ".release-candidate-promote",
92
93
  "release-propagation",
93
94
  ]);
94
95
  const hasCompleteControllerSource = [...controllerWorkflowIds]
@@ -173,11 +174,17 @@ export function createBuildchainContractWorld({
173
174
  requiredInputs: ["channel"],
174
175
  requiredOutputs: ["promoted-sha", "built-source-sha", "release-candidate-artifact"],
175
176
  breakingDefaults: {
177
+ channelDefault: "auto",
178
+ alphaShellDefault: `${majorLine}-alpha`,
179
+ stableShellDefault: majorLine,
176
180
  promoteOnlyReleaseCandidate: true,
177
181
  requiredStatusCheck: "check / check",
178
182
  },
179
183
  optionalInputs: [
184
+ "buildchain-channel",
180
185
  "buildchain-ref",
186
+ "buildchain-alpha-contract-lock-path",
187
+ "buildchain-stable-contract-lock-path",
181
188
  "release-candidate-workflow-file",
182
189
  "release-candidate-workflow-name",
183
190
  "publish-required-artifacts-json",
@@ -210,6 +217,9 @@ export function createBuildchainContractWorld({
210
217
  ],
211
218
  },
212
219
  guarantees: [
220
+ "one declarative promotion job selects matching alpha or stable workflow shell, runtime, and contract lock",
221
+ "router, shell, runtime, lock, publication channel, target ref, and override identities are bound before candidate download",
222
+ "train and exact-SHA runtime overrides remain limited to trusted workflow_dispatch actors",
213
223
  "promotion reuses PR-stage release-candidate artifacts",
214
224
  "promotion does not run the heavy native build matrix",
215
225
  "built source and promotion channel SHA are recorded separately",
@@ -218,6 +228,37 @@ export function createBuildchainContractWorld({
218
228
  "GitHub Release passport and evidence publication is delegated to promote-buildchain-ref after the semver release transaction completes",
219
229
  ],
220
230
  }),
231
+ surface(root, {
232
+ id: "advanced-release-candidate-promote",
233
+ kind: "workflow",
234
+ path: ".github/workflows/.release-candidate-promote.yml",
235
+ publicRef: `${pkg.repository ? "kungfu-systems/buildchain" : "buildchain"}/.github/workflows/.release-candidate-promote.yml@${majorLine}`,
236
+ requiredInputs: ["channel"],
237
+ requiredOutputs: ["promoted-sha", "built-source-sha", "release-candidate-artifact"],
238
+ breakingDefaults: {
239
+ promoteOnlyReleaseCandidate: true,
240
+ requiredStatusCheck: "check / check",
241
+ },
242
+ optionalInputs: [
243
+ "buildchain-ref",
244
+ "buildchain-contract-lock-path",
245
+ "promotion-router-ref",
246
+ "promotion-router-sha",
247
+ "promotion-shell-ref",
248
+ "promotion-shell-sha",
249
+ "promotion-runtime-ref",
250
+ "promotion-runtime-sha",
251
+ "promotion-contract-lock-path",
252
+ "promotion-contract-lock-digest",
253
+ "promotion-publication-channel",
254
+ "promotion-target-ref",
255
+ "promotion-override-used",
256
+ ],
257
+ guarantees: [
258
+ "advanced promotion verifies routed shell, runtime, lock, publication channel, and target bindings before candidate resolution",
259
+ "promotion reuses PR-stage release-candidate artifacts and does not run the heavy native build matrix",
260
+ ],
261
+ }),
221
262
  surface(root, {
222
263
  id: "web-surface",
223
264
  kind: "workflow",
@@ -82,6 +82,7 @@ const SCHEMA_AND_STANDARD_FILES = Object.freeze([
82
82
 
83
83
  const WORKFLOW_AND_ACTION_FILES = Object.freeze([
84
84
  ".github/workflows/.build.yml",
85
+ ".github/workflows/.release-candidate-promote.yml",
85
86
  ".github/workflows/release-candidate-promote.yml",
86
87
  ".github/workflows/buildchain-ref-promotion.yml",
87
88
  ".github/workflows/release-propagation.yml",
@@ -372,6 +373,7 @@ export function createBuildchainPublicClaimDefinitions() {
372
373
  "packages/core/buildchain-contract.js",
373
374
  "scripts/buildchain-contract-lock.mjs",
374
375
  ".github/workflows/.build.yml",
376
+ ".github/workflows/.release-candidate-promote.yml",
375
377
  ".github/workflows/release-candidate-promote.yml",
376
378
  "docs/reusable-build-surface.md",
377
379
  ],
@@ -387,6 +389,7 @@ export function createBuildchainPublicClaimDefinitions() {
387
389
  "actions/promote-buildchain-ref/index.js",
388
390
  "actions/promote-buildchain-ref/action.yml",
389
391
  "scripts/ensure-github-release.mjs",
392
+ ".github/workflows/.release-candidate-promote.yml",
390
393
  ".github/workflows/release-candidate-promote.yml",
391
394
  "docs/release-governance.md",
392
395
  ],
@@ -415,6 +418,7 @@ export function createBuildchainPublicClaimDefinitions() {
415
418
  sourcePaths: [
416
419
  "scripts/npm-publish-transaction.mjs",
417
420
  "actions/promote-buildchain-ref/index.js",
421
+ ".github/workflows/.release-candidate-promote.yml",
418
422
  ".github/workflows/release-candidate-promote.yml",
419
423
  "docs/publish-transaction.md",
420
424
  ],
@@ -5,6 +5,7 @@ import { createPublicationAuthorityRegistry } from "./publication-authority.js";
5
5
 
6
6
  const DESCRIPTORS = Object.freeze([
7
7
  [".github/workflows/.build.yml", "non-publication-oidc"],
8
+ [".github/workflows/.release-candidate-promote.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "caller-bound"],
8
9
  [".github/workflows/.publication-authority.yml", "evidence-publication"],
9
10
  [".github/workflows/.release-verify.yml", "governance-write"],
10
11
  [".github/workflows/.sam-release.yml", "retired-deny"],
@@ -72,7 +72,7 @@ const CONTROLLER_SPECS = [
72
72
  },
73
73
  {
74
74
  id: "release-candidate-promotion",
75
- workflowId: "release-candidate-promote",
75
+ workflowId: ".release-candidate-promote",
76
76
  version: 1,
77
77
  capabilities: ["promotion-preflight", "artifact-admission", "publication-authority", "publish-transaction", "release-passport"],
78
78
  stages: ["preflight", "admit-release-candidate", "publication-authority", "publish", "passport", "aggregate"],
@@ -37,6 +37,7 @@ export {
37
37
  detectPackageManager,
38
38
  getWorkspaceInfo,
39
39
  shellJoin,
40
+ validatePackageManagerContract,
40
41
  } from "./package-manager.js";
41
42
 
42
43
  export {
@@ -320,6 +321,14 @@ export {
320
321
  verifyReleasePassport,
321
322
  } from "./release-passport.js";
322
323
 
324
+ export {
325
+ RELEASE_PASSPORT_CHECK_MANIFEST_CONTRACT,
326
+ RELEASE_PASSPORT_SCHEMA,
327
+ RELEASE_PASSPORT_SCHEMA_ID,
328
+ createReleasePassportCheckManifest,
329
+ validateReleasePassportSchema,
330
+ } from "./release-passport-contract.js";
331
+
323
332
  export {
324
333
  BUILDCHAIN_CONSUMER_ISSUE_CONTRACT,
325
334
  BUILDCHAIN_WORKFLOW_FRICTION_ISSUE_CONTRACT,
@@ -15,6 +15,37 @@ function parsePackageManager(value) {
15
15
  return match ? match[1] : null;
16
16
  }
17
17
 
18
+ export function validatePackageManagerContract({ cwd = process.cwd(), expectedManager = "" } = {}) {
19
+ const resolvedCwd = path.resolve(cwd);
20
+ const pkg = readJsonIfExists(path.join(resolvedCwd, "package.json"));
21
+ const declaredSpec = String(pkg?.packageManager || "").trim();
22
+ const declaredMatch = declaredSpec.match(/^([A-Za-z0-9._-]+)@([^\s]+)$/);
23
+
24
+ if (declaredSpec && !declaredMatch) {
25
+ throw new Error(
26
+ `Invalid packageManager declaration: ${declaredSpec}. Expected <manager>@<version>.`,
27
+ );
28
+ }
29
+
30
+ const declaredName = declaredMatch?.[1] || "";
31
+ const detected = declaredName && !KNOWN_MANAGERS.has(declaredName)
32
+ ? { name: "custom", reason: "packageManager" }
33
+ : detectPackageManager(resolvedCwd);
34
+ const expected = String(expectedManager || "").trim();
35
+ if (expected && expected !== "custom" && detected.name !== expected) {
36
+ throw new Error(
37
+ `Package manager mismatch: workflow requested ${expected}, but the consumer declares ${detected.name}.`,
38
+ );
39
+ }
40
+
41
+ return {
42
+ name: detected.name,
43
+ reason: detected.reason,
44
+ declaredSpec,
45
+ declaredVersion: declaredMatch?.[2] || "",
46
+ };
47
+ }
48
+
18
49
  export function assertPackageManager(manager) {
19
50
  if (!KNOWN_MANAGERS.has(manager)) {
20
51
  throw new Error(`Unsupported package manager: ${manager}`);
@@ -288,4 +319,5 @@ export default {
288
319
  getWorkspaceInfo,
289
320
  getYarnLockInfo,
290
321
  shellJoin,
322
+ validatePackageManagerContract,
291
323
  };
@@ -0,0 +1,249 @@
1
+ import kfdStandards from "@kungfu-tech/kfd/standards.json" with { type: "json" };
2
+ import {
3
+ RELEASE_CHECK_REPORT_CONTRACT,
4
+ RELEASE_PASSPORT_CONTRACT,
5
+ } from "./release-passport.js";
6
+
7
+ export const RELEASE_PASSPORT_SCHEMA_ID =
8
+ "https://buildchain.libkungfu.dev/schemas/release-passport-v1.schema.json";
9
+ export const RELEASE_PASSPORT_CHECK_MANIFEST_CONTRACT =
10
+ "kungfu-buildchain-release-passport-check-manifest";
11
+
12
+ const OBJECT = { type: "object" };
13
+ const STRING = { type: "string", minLength: 1 };
14
+ const OPTIONAL_STRING = { type: "string" };
15
+
16
+ export const RELEASE_PASSPORT_SCHEMA = {
17
+ $schema: "https://json-schema.org/draft/2020-12/schema",
18
+ $id: RELEASE_PASSPORT_SCHEMA_ID,
19
+ title: "Buildchain Release Passport v1",
20
+ description:
21
+ "Standalone structural schema for the Buildchain-owned release-passport envelope. KFD subsection semantics remain owned by their canonical KFD schemas.",
22
+ type: "object",
23
+ required: [
24
+ "schemaVersion",
25
+ "contract",
26
+ "product",
27
+ "release",
28
+ "artifacts",
29
+ "evidence",
30
+ "recovery",
31
+ ],
32
+ properties: {
33
+ schemaVersion: { const: 1 },
34
+ contract: { const: RELEASE_PASSPORT_CONTRACT },
35
+ generatedAt: { type: "string", format: "date-time" },
36
+ product: {
37
+ type: "object",
38
+ required: ["name", "repository", "mechanism"],
39
+ properties: {
40
+ name: STRING,
41
+ repository: STRING,
42
+ mechanism: STRING,
43
+ },
44
+ additionalProperties: true,
45
+ },
46
+ release: {
47
+ type: "object",
48
+ required: ["tag"],
49
+ properties: {
50
+ tag: STRING,
51
+ sourceSha: OPTIONAL_STRING,
52
+ releaseSha: OPTIONAL_STRING,
53
+ releaseMaterialSha: OPTIONAL_STRING,
54
+ targetRef: OPTIONAL_STRING,
55
+ channel: OPTIONAL_STRING,
56
+ line: OPTIONAL_STRING,
57
+ package: OBJECT,
58
+ },
59
+ additionalProperties: true,
60
+ },
61
+ artifacts: {
62
+ type: "array",
63
+ minItems: 1,
64
+ items: {
65
+ type: "object",
66
+ required: ["name"],
67
+ properties: { name: STRING },
68
+ additionalProperties: true,
69
+ },
70
+ },
71
+ evidence: {
72
+ type: "object",
73
+ required: ["artifactEvidence", "impact", "agentIndex"],
74
+ properties: {
75
+ artifactEvidence: STRING,
76
+ impact: STRING,
77
+ agentIndex: STRING,
78
+ publishEvidence: OPTIONAL_STRING,
79
+ transactionState: OPTIONAL_STRING,
80
+ },
81
+ additionalProperties: true,
82
+ },
83
+ recovery: OBJECT,
84
+ workflow: OBJECT,
85
+ runnerPolicy: OBJECT,
86
+ versionImpact: OBJECT,
87
+ surfaceImpacts: { type: "array", items: OBJECT },
88
+ packageSet: OBJECT,
89
+ anchorManifest: OBJECT,
90
+ trustedPublishing: OBJECT,
91
+ transaction: OBJECT,
92
+ surfaceTimestampPolicy: OBJECT,
93
+ buildFacts: { type: "array", items: OBJECT },
94
+ buildSummary: OBJECT,
95
+ platformArtifactManifests: { type: "array", items: OBJECT },
96
+ distTagPromotion: OBJECT,
97
+ controllerReceipts: { type: "array", items: OBJECT },
98
+ "kfd-1": OBJECT,
99
+ "kfd-2": OBJECT,
100
+ "kfd-3": OBJECT,
101
+ },
102
+ additionalProperties: true,
103
+ };
104
+
105
+ const BUILDCHAIN_AGGREGATION_FIELDS = [
106
+ "product",
107
+ "release",
108
+ "workflow",
109
+ "runnerPolicy",
110
+ "versionImpact",
111
+ "surfaceImpacts",
112
+ "packageSet",
113
+ "anchorManifest",
114
+ "trustedPublishing",
115
+ "transaction",
116
+ "surfaceTimestampPolicy",
117
+ "buildFacts",
118
+ "buildSummary",
119
+ "platformArtifactManifests",
120
+ "distTagPromotion",
121
+ "controllerReceipts",
122
+ "artifacts",
123
+ "evidence",
124
+ "recovery",
125
+ ];
126
+
127
+ function kfdSectionAuthority(id, standards = kfdStandards) {
128
+ const standard = standards?.standards?.[id] || {};
129
+ return {
130
+ section: id,
131
+ owner: "KFD",
132
+ package: standards?.source?.package || "@kungfu-tech/kfd",
133
+ metadataSchema: standards?.metadataSchema || {},
134
+ schemaIds: standard.schemaIds || {},
135
+ schemaPaths: standard.schemaPaths || {},
136
+ compatibility: standard.compatibility || {},
137
+ rule: `Buildchain aggregates and checks ${id} evidence but does not redefine its schema semantics.`,
138
+ };
139
+ }
140
+
141
+ export function createReleasePassportCheckManifest({ standards = kfdStandards } = {}) {
142
+ return {
143
+ schemaVersion: 1,
144
+ contract: RELEASE_PASSPORT_CHECK_MANIFEST_CONTRACT,
145
+ passport: {
146
+ contract: RELEASE_PASSPORT_CONTRACT,
147
+ schemaVersion: 1,
148
+ schema: {
149
+ id: RELEASE_PASSPORT_SCHEMA_ID,
150
+ path: "schemas/release-passport-v1.schema.json",
151
+ npmExport: "@kungfu-tech/buildchain/site/schemas/release-passport-v1.schema.json",
152
+ },
153
+ checker: {
154
+ contract: RELEASE_CHECK_REPORT_CONTRACT,
155
+ command: "buildchain verify release-passport <buildchain.release.json>",
156
+ nodeApi: "@kungfu-tech/buildchain/release-passport#verifyReleasePassport",
157
+ result: "check-report.json",
158
+ },
159
+ },
160
+ ownership: {
161
+ envelopeOwner: "Buildchain",
162
+ aggregationFields: BUILDCHAIN_AGGREGATION_FIELDS,
163
+ kfdSections: ["kfd-1", "kfd-2", "kfd-3"].map((id) =>
164
+ kfdSectionAuthority(id, standards),
165
+ ),
166
+ },
167
+ localClosure: {
168
+ entrypoint: "buildchain.release.json",
169
+ resolutionBase: "directory containing the passport",
170
+ requiredSiblings: [
171
+ "product.mechanism",
172
+ "evidence.artifactEvidence",
173
+ "evidence.impact",
174
+ "evidence.agentIndex",
175
+ ],
176
+ conditionalSiblings: [
177
+ {
178
+ when: "evidence.publishEvidence is present or packageSet is present",
179
+ pointer: "evidence.publishEvidence",
180
+ },
181
+ {
182
+ when: "evidence.transactionState is present",
183
+ pointer: "evidence.transactionState",
184
+ },
185
+ ],
186
+ rule:
187
+ "The normative checker resolves every declared sibling relative to the passport and fails closed on missing required evidence, digest drift, or semantic mismatch.",
188
+ },
189
+ compatibility: {
190
+ envelope:
191
+ "Compatible optional fields may be added while schemaVersion remains 1; removing or changing required-field semantics requires a new schema id or contract version.",
192
+ siblings:
193
+ "New optional siblings may be added in v1; making a sibling required requires a new schema id or contract version.",
194
+ kfdSections:
195
+ "Each KFD subsection follows the compatibility metadata published by @kungfu-tech/kfd; Buildchain must not silently reinterpret a KFD schema version.",
196
+ unknownFields: "Independent readers must preserve or ignore unknown additive fields.",
197
+ },
198
+ };
199
+ }
200
+
201
+ function matchesType(value, type) {
202
+ if (type === "object") return Boolean(value) && typeof value === "object" && !Array.isArray(value);
203
+ if (type === "array") return Array.isArray(value);
204
+ if (type === "integer") return Number.isInteger(value);
205
+ return typeof value === type;
206
+ }
207
+
208
+ function validateSchemaNode(schema, value, pointer, issues) {
209
+ if (schema.type && !matchesType(value, schema.type)) {
210
+ issues.push({ pointer, code: "type", message: `${pointer} must be ${schema.type}` });
211
+ return;
212
+ }
213
+ if (Object.hasOwn(schema, "const") && value !== schema.const) {
214
+ issues.push({ pointer, code: "const", message: `${pointer} must equal ${JSON.stringify(schema.const)}` });
215
+ }
216
+ if (schema.minLength && typeof value === "string" && value.length < schema.minLength) {
217
+ issues.push({ pointer, code: "minLength", message: `${pointer} must not be empty` });
218
+ }
219
+ if (schema.minItems && Array.isArray(value) && value.length < schema.minItems) {
220
+ issues.push({ pointer, code: "minItems", message: `${pointer} must contain at least ${schema.minItems} item(s)` });
221
+ }
222
+ if (Array.isArray(value) && schema.items) {
223
+ value.forEach((entry, index) => validateSchemaNode(schema.items, entry, `${pointer}/${index}`, issues));
224
+ }
225
+ if (matchesType(value, "object")) {
226
+ for (const key of schema.required || []) {
227
+ if (!Object.hasOwn(value, key)) {
228
+ issues.push({ pointer: `${pointer}/${key}`, code: "required", message: `${pointer}/${key} is required` });
229
+ }
230
+ }
231
+ for (const [key, childSchema] of Object.entries(schema.properties || {})) {
232
+ if (Object.hasOwn(value, key)) {
233
+ validateSchemaNode(childSchema, value[key], `${pointer}/${key}`, issues);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ export function validateReleasePassportSchema(passport) {
240
+ const issues = [];
241
+ validateSchemaNode(RELEASE_PASSPORT_SCHEMA, passport, "#", issues);
242
+ return {
243
+ schemaVersion: 1,
244
+ contract: "kungfu-buildchain-release-passport-schema-validation",
245
+ schemaId: RELEASE_PASSPORT_SCHEMA_ID,
246
+ ok: issues.length === 0,
247
+ issues,
248
+ };
249
+ }
@@ -926,6 +926,43 @@ function normalizeTransaction(value = undefined) {
926
926
  };
927
927
  }
928
928
 
929
+ function normalizePromotionRouting(value = undefined) {
930
+ if (value === undefined) return undefined;
931
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
932
+ throw new Error("release.promotionRouting must be a JSON object");
933
+ }
934
+ if (value.contract !== "buildchain.promotion-routing/v1") {
935
+ throw new Error("release.promotionRouting contract must be buildchain.promotion-routing/v1");
936
+ }
937
+ for (const [label, field] of [
938
+ ["router.ref", value.router?.ref],
939
+ ["router.sha", value.router?.sha],
940
+ ["shell.ref", value.shell?.ref],
941
+ ["shell.sha", value.shell?.sha],
942
+ ["runtime.requestedRef", value.runtime?.requestedRef],
943
+ ["runtime.resolvedSha", value.runtime?.resolvedSha],
944
+ ["contractLock.path", value.contractLock?.path],
945
+ ["contractLock.digest", value.contractLock?.digest],
946
+ ["publication.channel", value.publication?.channel],
947
+ ["publication.targetRef", value.publication?.targetRef],
948
+ ]) {
949
+ if (!String(field || "").trim()) throw new Error(`release.promotionRouting.${label} is required`);
950
+ }
951
+ for (const [label, sha] of [
952
+ ["router.sha", value.router.sha],
953
+ ["shell.sha", value.shell.sha],
954
+ ["runtime.resolvedSha", value.runtime.resolvedSha],
955
+ ]) {
956
+ if (!/^[0-9a-f]{40}$/i.test(String(sha))) {
957
+ throw new Error(`release.promotionRouting.${label} must be a 40-character Git SHA`);
958
+ }
959
+ }
960
+ if (!/^sha256:[0-9a-f]{64}$/i.test(String(value.contractLock.digest))) {
961
+ throw new Error("release.promotionRouting.contractLock.digest must be a sha256 digest");
962
+ }
963
+ return JSON.parse(JSON.stringify(value));
964
+ }
965
+
929
966
  export function createArtifactEvidence({ assets = [], repository = "", tag = "", sourceSha = "", workflow = {} } = {}) {
930
967
  const normalizedAssets = assets.map((asset, index) => normalizeAsset(asset, index));
931
968
  return {
@@ -1002,6 +1039,7 @@ export function createReleasePassport({
1002
1039
  const normalizedPackageSet = normalizePackageSet(packageSet, { packageName, packageVersion, publish });
1003
1040
  const normalizedTrustedPublishing = normalizeTrustedPublishing(trustedPublishing, { workflow, publish });
1004
1041
  const normalizedTransaction = normalizeTransaction(transaction);
1042
+ const normalizedPromotionRouting = normalizePromotionRouting(release.promotionRouting);
1005
1043
  const normalizedBuildSummary = buildSummary ? normalizeEvidenceDocument(buildSummary, "buildSummary") : undefined;
1006
1044
  const normalizedBuildFacts = (buildFacts || [])
1007
1045
  .map((fact, index) => normalizeEvidenceDocument(fact, `buildFacts[${index}]`))
@@ -1150,6 +1188,7 @@ export function createReleasePassport({
1150
1188
  ...(anchorManifest ? { anchorManifest } : {}),
1151
1189
  ...(normalizedTrustedPublishing ? { trustedPublishing: normalizedTrustedPublishing } : {}),
1152
1190
  ...(normalizedTransaction ? { transaction: normalizedTransaction } : {}),
1191
+ ...(normalizedPromotionRouting ? { promotionRouting: normalizedPromotionRouting } : {}),
1153
1192
  ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
1154
1193
  ...(normalizedBuildFacts.length > 0 ? { buildFacts: normalizedBuildFacts } : {}),
1155
1194
  ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
@@ -6,6 +6,7 @@ import {
6
6
  } from "../packages/core/public-surface-audit.js";
7
7
  import { evaluateBuildchainContractLock } from "../packages/core/buildchain-contract.js";
8
8
  import { generateChannelBuildWorkflow } from "./generate-channel-build-workflow.mjs";
9
+ import { generateChannelPromotionWorkflow } from "./generate-channel-promotion-workflow.mjs";
9
10
 
10
11
  const root = process.cwd();
11
12
  const sharedActionTsupConfig = fs.readFileSync(path.join(root, "scripts/tsup-action.config.mjs"), "utf8");
@@ -44,6 +45,8 @@ const requiredPaths = [
44
45
  "scripts/release-line-dry-run.mjs",
45
46
  "scripts/buildchain-channel-router.mjs",
46
47
  "scripts/generate-channel-build-workflow.mjs",
48
+ "scripts/promotion-channel-router.mjs",
49
+ "scripts/generate-channel-promotion-workflow.mjs",
47
50
  "scripts/build-standalone-binary.mjs",
48
51
  "scripts/create-release-bundle.mjs",
49
52
  "scripts/ensure-github-release.mjs",
@@ -81,6 +84,7 @@ const requiredPaths = [
81
84
  ".github/workflows/buildchain-patrol-monthly.yml",
82
85
  ".github/workflows/buildchain-alpha-self-dogfood.yml",
83
86
  ".github/workflows/release-candidate-promote.yml",
87
+ ".github/workflows/.release-candidate-promote.yml",
84
88
  ".github/workflows/release-propagation.yml",
85
89
  ".github/workflows/npm-publish.yml",
86
90
  ".github/workflows/paper-release.yml",
@@ -174,6 +178,30 @@ const channelBuildWorkflow = fs.readFileSync(
174
178
  if (channelBuildWorkflow !== generateChannelBuildWorkflow(reusableBuildWorkflow)) {
175
179
  throw new Error("generated channel build workflow is stale");
176
180
  }
181
+ const advancedPromotionWorkflow = fs.readFileSync(
182
+ path.join(root, ".github/workflows/.release-candidate-promote.yml"),
183
+ "utf8",
184
+ );
185
+ const channelPromotionWorkflow = fs.readFileSync(
186
+ path.join(root, ".github/workflows/release-candidate-promote.yml"),
187
+ "utf8",
188
+ );
189
+ if (channelPromotionWorkflow !== generateChannelPromotionWorkflow(advancedPromotionWorkflow, {
190
+ major: Number(selfDogfoodMajor),
191
+ })) {
192
+ throw new Error("generated channel promotion workflow is stale");
193
+ }
194
+ for (const requiredSnippet of [
195
+ "buildchain-channel:",
196
+ `/.github/workflows/.release-candidate-promote.yml@v${selfDogfoodMajor}-alpha`,
197
+ `/.github/workflows/.release-candidate-promote.yml@v${selfDogfoodMajor}`,
198
+ "promotion-contract-lock-digest:",
199
+ "promotion runtime override is only allowed for trusted workflow_dispatch runs",
200
+ ]) {
201
+ if (!channelPromotionWorkflow.includes(requiredSnippet)) {
202
+ throw new Error(`channel promotion workflow missing routing contract: ${requiredSnippet}`);
203
+ }
204
+ }
177
205
  for (const requiredSnippet of [
178
206
  "buildchain-channel:",
179
207
  "uses: ./.github/workflows/.build.yml",
@@ -270,6 +298,9 @@ if (rootPackage.exports?.["./kfd-gate"] !== "./packages/core/kfd-gate.js") {
270
298
  if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-passport.js") {
271
299
  throw new Error("root package must export @kungfu-tech/buildchain/release-passport");
272
300
  }
301
+ if (rootPackage.exports?.["./release-passport-contract"] !== "./packages/core/release-passport-contract.js") {
302
+ throw new Error("root package must export @kungfu-tech/buildchain/release-passport-contract");
303
+ }
273
304
  if (rootPackage.exports?.["./release-propagation"] !== "./packages/core/release-propagation.js") {
274
305
  throw new Error("root package must export @kungfu-tech/buildchain/release-propagation");
275
306
  }
@@ -627,6 +658,9 @@ for (const requiredSnippet of [
627
658
  "--kfd-3-artifact-verify-cmd",
628
659
  "currently named `kfd-3`",
629
660
  "KFD-3 collaboration-interface release gate",
661
+ "release-passport-v1.schema.json",
662
+ "release-passport-check-manifest.json",
663
+ "Buildchain owns envelope aggregation",
630
664
  ]) {
631
665
  if (!releasePassportDoc.includes(requiredSnippet)) {
632
666
  throw new Error(`release passport doc missing KFD-1 gate snippet: ${requiredSnippet}`);
@@ -808,7 +842,7 @@ for (const forbiddenSnippet of [
808
842
  for (const requiredSnippet of [
809
843
  "id-token: write",
810
844
  "actions: read",
811
- "uses: ./.github/workflows/release-candidate-promote.yml",
845
+ "uses: ./.github/workflows/.release-candidate-promote.yml",
812
846
  "github.event.workflow_run.event == 'push'",
813
847
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
814
848
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
@@ -822,7 +856,7 @@ for (const requiredSnippet of [
822
856
  throw new Error(`buildchain ref promotion workflow missing npm transaction snippet: ${requiredSnippet}`);
823
857
  }
824
858
  }
825
- const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows/release-candidate-promote.yml"), "utf8");
859
+ const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows/.release-candidate-promote.yml"), "utf8");
826
860
  for (const requiredSnippet of [
827
861
  "release-passport-kfd-1-witness-jsons:",
828
862
  "release-passport-kfd-1-witness-jsons: ${{ inputs.release-passport-kfd-1-witness-jsons }}",
@@ -1071,7 +1105,7 @@ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-rele
1071
1105
  throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
1072
1106
  }
1073
1107
 
1074
- for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
1108
+ for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "publication-registry.json", "page-registry.json", "capability-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "controller-registry.json", "public-surface-audit.json", "release-model.json", "release-passport-check-manifest.json", "schemas/release-passport-v1.schema.json", "buildchain-contract.json"]) {
1075
1109
  if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
1076
1110
  throw new Error(`site bundle missing ${siteFile}`);
1077
1111
  }