@kungfu-tech/buildchain 4.1.3-alpha.9 → 4.1.4-alpha.0

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 (205) hide show
  1. package/AGENTS.md +11 -13
  2. package/CONTRIBUTING.md +32 -9
  3. package/README.md +56 -140
  4. package/actions/release/promotion/ref/README.md +1 -1
  5. package/architecture/action-taxonomy.json +1 -1
  6. package/architecture/agent-change-map.md +32 -45
  7. package/architecture/build-orchestration.json +1 -1
  8. package/architecture/capability-state-machine-manifest.json +5 -5
  9. package/architecture/ci-lane-change-budget.json +92 -92
  10. package/architecture/code-layout.json +2 -12
  11. package/architecture/core-mechanisms.json +38 -39
  12. package/architecture/cross-platform-adopter-qualification.json +1 -1
  13. package/architecture/dev-delivery-orchestration.json +2 -2
  14. package/architecture/floating-consumer-policy.json +3 -3
  15. package/architecture/internal-capabilities.json +27 -28
  16. package/architecture/maintainability-debt.json +60 -94
  17. package/architecture/maintainability-policy.json +7 -13
  18. package/architecture/minimal-consumer-migration.json +43 -5014
  19. package/architecture/platform-stage-checkpoints.json +1 -1
  20. package/architecture/release-tail-contract.json +2 -2
  21. package/architecture/release-topology.json +33 -25
  22. package/architecture/stage-capsule-qualification.json +1 -1
  23. package/architecture/universal-workflow-bootstrap.json +60 -60
  24. package/architecture/universal-workflow-capability-policy.json +3 -3
  25. package/architecture/universal-workflow-fault-campaign.json +7 -7
  26. package/architecture/workflow-taxonomy.json +59 -60
  27. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  28. package/dist/site/buildchain-contract.json +138 -66
  29. package/dist/site/buildchain-site.json +257 -331
  30. package/dist/site/capability-registry.json +4 -4
  31. package/dist/site/cli-registry.json +6 -142
  32. package/dist/site/controller-registry.json +19 -19
  33. package/dist/site/kfd-claims.json +481 -529
  34. package/dist/site/kfd-upstream-aggregate.json +1 -1
  35. package/dist/site/manual-registry.json +22 -22
  36. package/dist/site/node-api-registry.json +82 -339
  37. package/dist/site/page-registry.json +229 -304
  38. package/dist/site/public-surface-audit.json +544 -754
  39. package/dist/site/publication-authority-registry.json +316 -316
  40. package/dist/site/publication-registry.json +1 -1
  41. package/dist/site/site-manifest.json +23 -23
  42. package/dist/site/workflow-registry.json +1071 -1027
  43. package/docs/MAP.md +28 -20
  44. package/docs/auditable-demo.md +17 -49
  45. package/docs/bootstrap-recovery.md +46 -37
  46. package/docs/cli-reference.md +2 -46
  47. package/docs/cli.md +97 -186
  48. package/docs/code-organization.md +7 -5
  49. package/docs/dev-alpha-candidate-patrol.md +19 -42
  50. package/docs/dev-delivery-orchestration.md +15 -11
  51. package/docs/dev-delivery-qualification-landing-adr.md +10 -9
  52. package/docs/dev-delivery-warrant.md +17 -16
  53. package/docs/dev-qualification-patrol.md +25 -20
  54. package/docs/getting-started.md +80 -139
  55. package/docs/github-artifact-attestation.md +31 -81
  56. package/docs/install.md +66 -30
  57. package/docs/lifecycle-protocol.md +19 -57
  58. package/docs/migration-inventory.md +17 -45
  59. package/docs/next-development-transition.md +13 -13
  60. package/docs/node-api-reference.md +122 -138
  61. package/docs/oci-publication.md +13 -10
  62. package/docs/publication-artifacts.md +128 -486
  63. package/docs/publication-authority.md +15 -45
  64. package/docs/publish-transaction.md +13 -11
  65. package/docs/release-candidate.md +126 -355
  66. package/docs/release-discussions.md +36 -67
  67. package/docs/release-flow.md +80 -339
  68. package/docs/release-governance.md +114 -962
  69. package/docs/release-promotion-request.md +45 -43
  70. package/docs/release-propagation.md +13 -27
  71. package/docs/release-tail-provider-plane.md +38 -65
  72. package/docs/reusable-build-surface.md +78 -196
  73. package/docs/runtime-train-validation.md +8 -6
  74. package/docs/stable-candidate-patrol.md +13 -66
  75. package/docs/v4-adopter-delivery.md +15 -15
  76. package/docs/v4-declarative-release-tail-migration.md +19 -17
  77. package/docs/v4-production-release.md +11 -6
  78. package/docs/v4-publication-rehearsal.md +12 -27
  79. package/docs/v4-stage-capsule.md +14 -20
  80. package/docs/v4-tail-reseal.md +34 -20
  81. package/docs/web-surface-deployments.md +26 -264
  82. package/docs/workflow-catalog.md +31 -31
  83. package/docs/workflow-path-migration.md +10 -4
  84. package/package.json +5 -4
  85. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  86. package/packages/core/adoption/cli/output.mjs +1 -9
  87. package/packages/core/adoption/cli/product-gate.mjs +1 -15
  88. package/packages/core/adoption/cli/project.mjs +9 -10
  89. package/packages/core/adoption/cli/standards.mjs +1 -14
  90. package/packages/core/adoption/cli/support.mjs +0 -2
  91. package/packages/core/adoption/cli/surface-register.mjs +0 -2
  92. package/packages/core/adoption/commands/init-repo.mjs +77 -485
  93. package/packages/core/adoption/consumer-init.js +279 -0
  94. package/packages/core/adoption/qualification/admission.js +1 -1
  95. package/packages/core/build/cli/cache.mjs +0 -2
  96. package/packages/core/build/cli/facts.mjs +0 -3
  97. package/packages/core/build/cli/homebrew.mjs +0 -4
  98. package/packages/core/build/cli/lifecycle.mjs +0 -3
  99. package/packages/core/build/github-artifact-attestation.js +2 -2
  100. package/packages/core/build/platform-stage-checkpoints.js +1 -1
  101. package/packages/core/build/signing/dispatch.js +1 -1
  102. package/packages/core/consumer/buildchain-config.js +9 -0
  103. package/packages/core/consumer/contract/inspection.js +351 -9
  104. package/packages/core/consumer/contract/javascript-syntax.js +439 -0
  105. package/packages/core/consumer/contract/javascript-values.js +61 -0
  106. package/packages/core/consumer/contract/javascript.js +555 -0
  107. package/packages/core/consumer/contract/local-validation.js +84 -0
  108. package/packages/core/contracts/buildchain-contract.js +39 -8
  109. package/packages/core/contracts/cli/introspection.mjs +1 -9
  110. package/packages/core/contracts/public-surface-cli.js +0 -1
  111. package/packages/core/dev-delivery/dev-delivery-contract-surface.js +2 -2
  112. package/packages/core/governance/buildchain-publication-authority.js +24 -24
  113. package/packages/core/governance/cli/doctor.mjs +58 -42
  114. package/packages/core/governance/commands/architecture.mjs +6 -1
  115. package/packages/core/governance/commands/audit-publication-control-plane.mjs +1 -1
  116. package/packages/core/governance/commands/workflow-friction-report.mjs +1 -1
  117. package/packages/core/governance/github-governance-authority.js +2 -2
  118. package/packages/core/governance/publication/collection.js +1 -1
  119. package/packages/core/index.js +1 -3
  120. package/packages/core/observability/cli/candidate.mjs +0 -2
  121. package/packages/core/observability/cli/context.mjs +0 -5
  122. package/packages/core/observability/cli/diagnostics.mjs +0 -7
  123. package/packages/core/observability/cli/events.mjs +2 -10
  124. package/packages/core/paper/commands/paper-work-fleet-cli.mjs +1 -1
  125. package/packages/core/paper/commands/paper.mjs +33 -422
  126. package/packages/core/paper/consumer-status.js +72 -0
  127. package/packages/core/paper/operations/identity.js +0 -3
  128. package/packages/core/paper/operations/migration.js +460 -0
  129. package/packages/core/paper/operations/preflight-checks.js +8 -68
  130. package/packages/core/paper/operations/scaffold.js +16 -326
  131. package/packages/core/paper/operations/status.js +27 -53
  132. package/packages/core/paper/paper-agent-entry.js +36 -147
  133. package/packages/core/paper/paper-fleet.js +24 -39
  134. package/packages/core/paper/paper-repository.js +16 -0
  135. package/packages/core/paper/paper-runtime-channels.js +1 -1
  136. package/packages/core/paper/paper-scaffold-content.js +153 -162
  137. package/packages/core/paper/paper-work.js +3 -10
  138. package/packages/core/paper/paper.js +43 -5
  139. package/packages/core/providers/github/artifact-attestation.js +1 -1
  140. package/packages/core/providers/github/pipeline-context-artifacts.js +172 -0
  141. package/packages/core/providers/github/pipeline-release-evidence.js +17 -98
  142. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  143. package/packages/core/providers/github/pipeline-stable-source.js +22 -4
  144. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  145. package/packages/core/publication/authority/control-plane.js +2 -2
  146. package/packages/core/publication/authority/seal.js +1 -1
  147. package/packages/core/publication/cli/dispatch.mjs +1 -12
  148. package/packages/core/publication/pipeline/actions.js +16 -9
  149. package/packages/core/publication/pipeline/apply.js +17 -9
  150. package/packages/core/publication/pipeline/context.js +29 -0
  151. package/packages/core/publication/pipeline/journal.js +13 -1
  152. package/packages/core/publication/pipeline/prepare.js +15 -6
  153. package/packages/core/publication/pipeline/recovery-import.js +1 -1
  154. package/packages/core/publication/pipeline/recovery-materials.js +63 -1
  155. package/packages/core/publication/pipeline/recovery-prepare.js +21 -9
  156. package/packages/core/publication/pipeline/stable-entry.js +12 -3
  157. package/packages/core/publication/pipeline/stable-products.js +21 -19
  158. package/packages/core/publication/pipeline/version-actions.js +15 -6
  159. package/packages/core/publication/publication-rehearsal-projection.js +0 -9
  160. package/packages/core/release/cli/dispatch.mjs +1 -4
  161. package/packages/core/release/cli/inspection-handlers.mjs +1 -33
  162. package/packages/core/release/cli/release-handlers.mjs +2 -19
  163. package/packages/core/release/cli/verification-handlers.mjs +4 -25
  164. package/packages/core/release/next-development-projection.js +13 -13
  165. package/packages/core/release/passport/locations.js +13 -8
  166. package/packages/core/release/release-passport.js +70 -5
  167. package/packages/core/web/cli/badges.mjs +0 -4
  168. package/packages/core/web/publication-admission.js +1 -1
  169. package/packages/core/web/publication-capability.js +1 -1
  170. package/packages/core/web/readme-badges.js +1 -1
  171. package/packages/core/workflow/cli/main.mjs +1 -4
  172. package/packages/core/workflow/cli/process.mjs +1 -1
  173. package/packages/core/workflow/commands/buildchain-cli-help.mjs +2 -8
  174. package/packages/core/workflow/engine/conformance.js +1 -1
  175. package/packages/core/workflow/pipeline/build.js +21 -1
  176. package/packages/core/workflow/pipeline/delivery-observation.js +6 -2
  177. package/packages/core/workflow/workflow-taxonomy.mjs +61 -17
  178. package/scripts/check-code-layout.mjs +2 -0
  179. package/scripts/check-core-mechanism-inventory.mjs +29 -13
  180. package/scripts/check-floating-consumer-policy-contract.mjs +3 -3
  181. package/scripts/check-inventory.mjs +27 -27
  182. package/scripts/check-maintainability.mjs +28 -5
  183. package/scripts/check-public-dogfood-contract.mjs +3 -3
  184. package/scripts/check-release-topology.mjs +1 -1
  185. package/scripts/check-self-consumer-contract.mjs +7 -7
  186. package/scripts/check-universal-workflow-bootstrap.mjs +1 -1
  187. package/scripts/generate-channel-promotion-workflow.mjs +1 -1
  188. package/scripts/generate-minimal-consumer-contract.mjs +21 -1
  189. package/scripts/generate-site-bundle.mjs +2 -1
  190. package/scripts/generate-tail-reseal-fixture.mjs +3 -3
  191. package/scripts/inventory/promotion.mjs +1 -1
  192. package/scripts/next-development-self-dogfood-harness.mjs +1 -1
  193. package/scripts/next-development-self-dogfood.mjs +4 -4
  194. package/scripts/site-capability-metadata.mjs +2 -6
  195. package/scripts/source-metric-lineage.mjs +62 -7
  196. package/scripts/verify-golden-path.mjs +31 -35
  197. package/templates/native-dev-delivery.yml +1 -1
  198. package/templates/universal-buildchain-bootstrap.yml +1 -1
  199. package/packages/core/paper/operations/bootstrap.js +0 -75
  200. package/packages/core/paper/operations/plans.js +0 -213
  201. package/packages/core/paper/operations/provisioning.js +0 -113
  202. package/packages/core/paper/operations/scaffold-config.js +0 -77
  203. package/packages/core/paper/operations/scaffold-workflows.js +0 -116
  204. package/packages/core/paper/operations/workspace.js +0 -60
  205. package/packages/core/paper/paper-npm-bootstrap.js +0 -492
@@ -1,14 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { collectPaperAgentEntry } from "./paper-agent-entry.js";
3
3
  import {
4
- PAPER_PATHS,
5
4
  PAPER_WORK_BRANCH_PATTERN,
6
5
  gitResult,
7
6
  gitValue,
8
7
  normalizedWorkBranch,
9
8
  paperDevelopmentRef,
10
9
  paperWorkSource,
11
- readJson,
12
10
  remoteBranchObservation,
13
11
  rootedPlan,
14
12
  workCheck,
@@ -30,12 +28,7 @@ function failedActions(checks) {
30
28
  }
31
29
 
32
30
  function agentEntryForWork(cwd, buildchainSha) {
33
- const policy = readJson(path.resolve(cwd, PAPER_PATHS.agentEntry)).value;
34
- return collectPaperAgentEntry({
35
- cwd,
36
- buildchainSha: buildchainSha || policy?.runtime?.sourceSha || "",
37
- mode: "contract",
38
- });
31
+ return collectPaperAgentEntry({ cwd, buildchainSha, mode: "contract" });
39
32
  }
40
33
 
41
34
  export function createPaperWorkStartPlan({
@@ -74,7 +67,7 @@ export function createPaperWorkStartPlan({
74
67
  workCheck(
75
68
  "agent-entry.current",
76
69
  agentEntry.ok,
77
- "The mandatory Buildchain Paper agent-entry contract is current.",
70
+ "The Paper product configuration and shared consumer wiring are current.",
78
71
  "buildchain paper migrate --write --json",
79
72
  ),
80
73
  workCheck(
@@ -252,7 +245,7 @@ export function createPaperWorkSubmitPlan({
252
245
  workCheck(
253
246
  "agent-entry.current",
254
247
  agentEntry.ok,
255
- "The mandatory Buildchain Paper agent-entry contract is current.",
248
+ "The Paper product configuration and shared consumer wiring are current.",
256
249
  "buildchain paper migrate --write --json",
257
250
  ),
258
251
  workCheck(
@@ -1,3 +1,4 @@
1
+ import { paperConsumerStatus } from "./consumer-status.js";
1
2
  import path from "node:path";
2
3
  import {
3
4
  paperConfig,
@@ -44,15 +45,52 @@ import {
44
45
  paperPreflightNextActions,
45
46
  } from "./operations/preflight-checks.js";
46
47
  export function collectPaperSourcePolicy({ cwd = process.cwd() } = {}) {
48
+ const consumer = paperConsumerStatus(cwd);
49
+ if (consumer) return consumer;
47
50
  const agentEntry = collectPaperAgentEntry({ cwd, mode: "ci" });
48
51
  const provisioning = validatePaperProvisioningAuthority(cwd);
49
- const checks = [...agentEntry.checks, { id: "provisioning.authority", status: provisioning.valid ? "pass" : "fail", message: provisioning.errors.join("; ") }];
50
- try { validateBuildchainConfig(cwd, { requireLifecycleStages: ["verify"] }); }
51
- catch (error) { checks.push({ id: "config.publication", status: "fail", message: error.message }); }
52
- return { ok: checks.every(check => check.status === "pass" || check.status === "skip"), checks };
52
+ const checks = [
53
+ ...agentEntry.checks,
54
+ {
55
+ id: "provisioning.authority",
56
+ status: provisioning.valid ? "pass" : "fail",
57
+ message: provisioning.errors.join("; "),
58
+ },
59
+ ];
60
+ try {
61
+ validateBuildchainConfig(cwd, { requireLifecycleStages: ["verify"] });
62
+ } catch (error) {
63
+ checks.push({
64
+ id: "config.publication",
65
+ status: "fail",
66
+ message: error.message,
67
+ });
68
+ }
69
+ return {
70
+ ok: checks.every(
71
+ (check) => check.status === "pass" || check.status === "skip",
72
+ ),
73
+ checks,
74
+ };
75
+ }
76
+
77
+ export function collectPaperPreflight(options = {}) {
78
+ const consumer = paperConsumerStatus(
79
+ path.resolve(options.cwd || process.cwd()),
80
+ );
81
+ if (!consumer) return collectLegacyPaperPreflight(options);
82
+ const result =
83
+ options.agentEntryMode && options.agentEntryMode !== "contract"
84
+ ? collectPaperAgentEntry({
85
+ cwd: consumer.cwd,
86
+ mode: options.agentEntryMode,
87
+ env: options.env,
88
+ })
89
+ : consumer;
90
+ return { ...result, contract: PAPER_PREFLIGHT_CONTRACT };
53
91
  }
54
92
 
55
- export function collectPaperPreflight({
93
+ function collectLegacyPaperPreflight({
56
94
  cwd = process.cwd(),
57
95
  buildchainRoot = process.cwd(),
58
96
  buildchainVersion = "",
@@ -10,7 +10,7 @@ export function verifyArtifactAttestationSigner(request, execute = command) {
10
10
  "--repo",
11
11
  request.repository,
12
12
  "--signer-workflow",
13
- "kungfu-systems/buildchain/.github/workflows/public-release-artifact-attestation.yml",
13
+ "kungfu-systems/buildchain/.github/workflows/.release-artifact-attestation.yml",
14
14
  "--signer-digest",
15
15
  request.signerDigest,
16
16
  "--source-digest",
@@ -0,0 +1,172 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import artifact from "@actions/artifact";
4
+ import {
5
+ canonicalJson,
6
+ recordDigest,
7
+ } from "../../release/discussion/envelope.js";
8
+ import { publicationPath } from "../../publication/pipeline/files.js";
9
+
10
+ const INLINE_LIMIT = 32 * 1024;
11
+ const MATERIAL_LIMIT = 16 * 1024 * 1024;
12
+ const REFERENCE = "buildchain.pipeline-context-reference/v1";
13
+ const SCHEMAS = [
14
+ "buildchain.pipeline-publication-context/v1",
15
+ "buildchain.pipeline-version-context/v1",
16
+ ];
17
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
18
+
19
+ function execution(env) {
20
+ const result = {
21
+ repository: env.GITHUB_REPOSITORY,
22
+ runId: Number(env.GITHUB_RUN_ID),
23
+ runAttempt: Number(env.GITHUB_RUN_ATTEMPT),
24
+ };
25
+ if (
26
+ !/^[\w.-]+\/[\w.-]+$/u.test(result.repository || "") ||
27
+ !Number.isSafeInteger(result.runId) ||
28
+ result.runId < 1 ||
29
+ !Number.isSafeInteger(result.runAttempt) ||
30
+ result.runAttempt < 1
31
+ )
32
+ throw new Error("Context transfer requires its exact provider execution");
33
+ return result;
34
+ }
35
+
36
+ function projection(value, env) {
37
+ const current = execution(env);
38
+ if (
39
+ !SCHEMAS.includes(value.schema) ||
40
+ value.runId !== current.runId ||
41
+ value.runAttempt !== current.runAttempt
42
+ )
43
+ throw new Error("Context transfer belongs to another provider execution");
44
+ const source =
45
+ value.schema === SCHEMAS[0]
46
+ ? value.materialization?.source
47
+ : value.preparation?.source;
48
+ if (
49
+ source?.repository !== current.repository ||
50
+ !/^[0-9a-f]{40}$/u.test(source?.commit || "")
51
+ )
52
+ throw new Error("Context transfer requires an exact repository source");
53
+ return {
54
+ schema: REFERENCE,
55
+ contextSchema: value.schema,
56
+ ...current,
57
+ contextRoot: recordDigest(value),
58
+ ...(value.schema === SCHEMAS[0]
59
+ ? { materialization: { source } }
60
+ : { preparation: { source } }),
61
+ };
62
+ }
63
+
64
+ function workspace(env) {
65
+ const directory = path.join(
66
+ env.GITHUB_WORKSPACE,
67
+ ".buildchain/context-material",
68
+ );
69
+ fs.mkdirSync(directory, { recursive: true });
70
+ return fs.mkdtempSync(path.join(directory, "transfer-"));
71
+ }
72
+
73
+ // Only an immutable reference and the checkout projection cross job outputs.
74
+ // The original context remains unchanged in the authoritative attempt journal.
75
+ export async function writePipelineContext(value, env, client = artifact) {
76
+ const serialized = canonicalJson(value);
77
+ const bytes = Buffer.byteLength(serialized);
78
+ if (bytes <= INLINE_LIMIT) return serialized;
79
+ if (bytes > MATERIAL_LIMIT)
80
+ throw new Error("Pipeline context exceeds its retained material bound");
81
+ const selected = projection(value, env);
82
+ const directory = workspace(env);
83
+ try {
84
+ const file = path.join(directory, "context.json");
85
+ fs.writeFileSync(file, serialized, { flag: "wx" });
86
+ const uploaded = await client.uploadArtifact(
87
+ `buildchain-context-${selected.runAttempt}-${selected.contextRoot.slice(7)}`,
88
+ [file],
89
+ directory,
90
+ { retentionDays: 30, compressionLevel: 0 },
91
+ );
92
+ const digest = `sha256:${String(uploaded.digest || "").replace(/^sha256:/u, "")}`;
93
+ if (
94
+ !Number.isSafeInteger(uploaded.id) ||
95
+ uploaded.id < 1 ||
96
+ !DIGEST.test(digest)
97
+ )
98
+ throw new Error("Context upload omitted immutable provider coordinates");
99
+ const encoded = canonicalJson({
100
+ ...selected,
101
+ bytes,
102
+ artifact: { id: uploaded.id, digest },
103
+ });
104
+ if (Buffer.byteLength(encoded) > INLINE_LIMIT)
105
+ throw new Error(
106
+ "Context checkout projection exceeds the job output bound",
107
+ );
108
+ return encoded;
109
+ } finally {
110
+ fs.rmSync(directory, { recursive: true, force: true });
111
+ }
112
+ }
113
+
114
+ function validateReference(value, env) {
115
+ const current = execution(env);
116
+ if (
117
+ !SCHEMAS.includes(value.contextSchema) ||
118
+ value.repository !== current.repository ||
119
+ value.runId !== current.runId ||
120
+ value.runAttempt !== current.runAttempt ||
121
+ !DIGEST.test(value.contextRoot || "") ||
122
+ !Number.isSafeInteger(value.bytes) ||
123
+ value.bytes <= INLINE_LIMIT ||
124
+ value.bytes > MATERIAL_LIMIT ||
125
+ !Number.isSafeInteger(value.artifact?.id) ||
126
+ value.artifact.id < 1 ||
127
+ !DIGEST.test(value.artifact?.digest || "")
128
+ )
129
+ throw new Error(
130
+ "Context reference changed its execution or material boundary",
131
+ );
132
+ }
133
+
134
+ export async function readPipelineContext(encoded, env, client = artifact) {
135
+ if (Buffer.byteLength(encoded) > INLINE_LIMIT)
136
+ throw new Error("Pipeline context must use bounded job output transport");
137
+ const reference = JSON.parse(encoded);
138
+ if (reference.schema !== REFERENCE) return reference;
139
+ validateReference(reference, env);
140
+ const directory = workspace(env);
141
+ try {
142
+ // No findBy or repository token: the artifact SDK scopes this read to the
143
+ // current workflow execution, including credentialless product build jobs.
144
+ const downloaded = await client.downloadArtifact(reference.artifact.id, {
145
+ path: directory,
146
+ expectedHash: reference.artifact.digest,
147
+ });
148
+ if (
149
+ downloaded.digestMismatch ||
150
+ canonicalJson(fs.readdirSync(directory)) !== '["context.json"]'
151
+ )
152
+ throw new Error("Context artifact changed its archive or file inventory");
153
+ const file = publicationPath(directory, "context.json");
154
+ if (fs.statSync(file).size !== reference.bytes)
155
+ throw new Error("Context artifact changed its retained byte length");
156
+ const value = JSON.parse(
157
+ new TextDecoder("utf-8", { fatal: true }).decode(fs.readFileSync(file)),
158
+ );
159
+ const expected = {
160
+ ...projection(value, env),
161
+ bytes: reference.bytes,
162
+ artifact: reference.artifact,
163
+ };
164
+ if (recordDigest(expected) !== recordDigest(reference))
165
+ throw new Error(
166
+ "Context artifact differs from its exact checkout projection or root",
167
+ );
168
+ return value;
169
+ } finally {
170
+ fs.rmSync(directory, { recursive: true, force: true });
171
+ }
172
+ }
@@ -1,5 +1,4 @@
1
- import { createHash } from "node:crypto";
2
- import { recordDigest } from "../../release/discussion/envelope.js";
1
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
3
2
 
4
3
  const NAMES = [
5
4
  "plan",
@@ -9,103 +8,23 @@ const NAMES = [
9
8
  "attestation",
10
9
  "release",
11
10
  ];
12
- const LIMIT = 8 * 1024 * 1024;
13
-
14
- async function inventory(request, prefix) {
15
- const assets = [],
16
- ids = new Set(),
17
- names = new Set();
18
- for (let page = 1; page <= 20; page++) {
19
- const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
20
- if (!Array.isArray(values) || values.length > 100)
21
- throw new Error("Published evidence asset inventory is incomplete");
22
- for (const value of values) {
23
- if (
24
- !Number.isSafeInteger(value.id) ||
25
- value.id < 1 ||
26
- typeof value.name !== "string" ||
27
- !value.name ||
28
- ids.has(value.id) ||
29
- names.has(value.name)
30
- )
31
- throw new Error("Published evidence asset inventory is ambiguous");
32
- ids.add(value.id);
33
- names.add(value.name);
34
- assets.push(value);
35
- }
36
- if (values.length < 100) return assets;
37
- }
38
- throw new Error(
39
- "Published evidence inventory exceeds its complete-read bound",
40
- );
41
- }
42
-
43
- function selectedAssets(assets) {
44
- return NAMES.map((id) => {
45
- const name = `buildchain.${id}.json`;
46
- const asset = assets.find((value) => value.name === name);
47
- if (!asset) return { name, missing: true };
48
- if (
49
- asset.state !== "uploaded" ||
50
- !Number.isSafeInteger(asset.size) ||
51
- asset.size < 1 ||
52
- asset.size > LIMIT ||
53
- !/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
54
- )
55
- throw new Error("Published evidence asset lacks bounded immutable bytes");
56
- return { id: asset.id, name, size: asset.size, digest: asset.digest };
57
- });
58
- }
59
-
60
- // Download only fixed evidence names from the exact already-observed release.
61
- // There is no caller-supplied URL, artifact selector or provider write here.
62
11
  export async function readPipelineReleaseEvidence(host, candidate) {
63
- const repository = host.repository;
64
- if (
65
- !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
66
- !Number.isSafeInteger(candidate.id) ||
67
- candidate.id < 1
68
- )
69
- throw new Error("Published evidence requires exact release coordinates");
70
- const prefix = `/repos/${repository}/releases/${candidate.id}`;
71
- const selected = selectedAssets(await inventory(host.request, prefix));
72
- const missing = selected
73
- .filter((value) => value.missing)
74
- .map(({ name }) => name);
75
- if (missing.length) return { status: "missing", missing };
76
- const [owner, repo] = repository.split("/");
12
+ const result = await readReleaseEvidenceAssets(
13
+ host,
14
+ candidate,
15
+ NAMES.map((id) => `buildchain.${id}.json`),
16
+ );
17
+ if (result.status !== "present") return result;
77
18
  const values = {};
78
- let bundle;
79
- for (const asset of selected) {
80
- const result = await host.github.rest.repos.getReleaseAsset({
81
- owner,
82
- repo,
83
- asset_id: asset.id,
84
- headers: { accept: "application/octet-stream" },
85
- });
86
- const bytes = Buffer.from(result.data);
87
- if (
88
- bytes.length !== asset.size ||
89
- bytes.length > LIMIT ||
90
- `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
91
- asset.digest
92
- )
93
- throw new Error(
94
- "Published evidence download differs from provider bytes",
95
- );
96
- const id = asset.name.slice("buildchain.".length, -".json".length);
97
- if (id === "attestation") bundle = bytes;
98
- else
99
- values[id] = JSON.parse(
100
- new TextDecoder("utf-8", { fatal: true }).decode(bytes),
101
- );
102
- }
103
- if (
104
- recordDigest(selectedAssets(await inventory(host.request, prefix))) !==
105
- recordDigest(selected)
106
- )
107
- throw new Error(
108
- "Published evidence assets changed during provider readback",
19
+ for (const id of NAMES.filter((name) => name !== "attestation"))
20
+ values[id] = JSON.parse(
21
+ new TextDecoder("utf-8", { fatal: true }).decode(
22
+ result.values[`buildchain.${id}.json`],
23
+ ),
109
24
  );
110
- return { status: "present", values, bundle, assets: selected };
25
+ return {
26
+ ...result,
27
+ values,
28
+ bundle: result.values["buildchain.attestation.json"],
29
+ };
111
30
  }
@@ -0,0 +1,86 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ SETTLEMENT_ASSET,
4
+ verifyPublicationSettlement,
5
+ } from "../../publication/settlement/transaction.js";
6
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
7
+
8
+ // Earlier publications materialized the floating entry after the exact tag.
9
+ // Only their completed immutable settlement can bind those two commits.
10
+ export async function readPipelineStableBaseline(plan, host, release, prior) {
11
+ if ((prior?.commit || null) === plan.previousChannelCommit) return null;
12
+ if (!release || !prior || !plan.previousChannelCommit)
13
+ throw new Error(
14
+ "Stable comparison differs from the retained published channel",
15
+ );
16
+ const evidence = await readReleaseEvidenceAssets(host, release, [
17
+ SETTLEMENT_ASSET,
18
+ "buildchain.release.json",
19
+ ]);
20
+ if (evidence.status !== "present")
21
+ throw new Error(
22
+ "Stable channel differs without a completed publication settlement",
23
+ );
24
+ const read = (name) =>
25
+ JSON.parse(
26
+ new TextDecoder("utf-8", { fatal: true }).decode(evidence.values[name]),
27
+ );
28
+ const settlement = read(SETTLEMENT_ASSET),
29
+ publicPassport = read("buildchain.release.json"),
30
+ repository = plan.source.repository,
31
+ tag = release.tag,
32
+ sourceSha = prior.commit;
33
+ if (
34
+ settlement.schemaVersion !== 1 ||
35
+ settlement.contract !== "buildchain-v4-publication-settlement/v1" ||
36
+ settlement.release?.tag !== tag ||
37
+ settlement.release?.sourceSha !== sourceSha ||
38
+ settlement.release?.channel !== "stable"
39
+ )
40
+ throw new Error(
41
+ "Stable baseline settlement has different release coordinates",
42
+ );
43
+ const verified = verifyPublicationSettlement(settlement.documents, {
44
+ repository,
45
+ tag,
46
+ sourceSha,
47
+ publicPassport,
48
+ });
49
+ const { product, providerState, productState, invocation, passport } =
50
+ settlement.documents;
51
+ const ref = `refs/tags/v${plan.version.split(".")[0]}`,
52
+ updates = product.updates.filter((value) => value.ref === ref);
53
+ const subject = {
54
+ repository,
55
+ sourceSha,
56
+ tag,
57
+ version: tag.slice(1),
58
+ channel: "stable",
59
+ };
60
+ if (
61
+ invocation.target.channel !== "stable" ||
62
+ passport.release.channel !== "stable" ||
63
+ product.promotedSha !== plan.previousChannelCommit ||
64
+ updates.length !== 1 ||
65
+ updates[0].action !== "converged-release-tag" ||
66
+ updates[0].sha !== plan.previousChannelCommit ||
67
+ recordDigest(productState.subject) !== recordDigest(subject) ||
68
+ recordDigest(providerState.subject) !==
69
+ recordDigest({
70
+ ...subject,
71
+ sourceSha: plan.previousChannelCommit,
72
+ })
73
+ )
74
+ throw new Error(
75
+ "Stable baseline settlement does not bind the retained floating channel",
76
+ );
77
+ return {
78
+ releaseId: release.id,
79
+ tag,
80
+ sourceSha,
81
+ channelRef: ref,
82
+ channelCommit: plan.previousChannelCommit,
83
+ receiptRoot: verified.receiptRoot,
84
+ assets: evidence.assets,
85
+ };
86
+ }
@@ -5,6 +5,7 @@ import { compareDevelopmentVersions } from "../../publication/publication-develo
5
5
  import { githubPipelineSource } from "./pipeline-source.js";
6
6
  import { githubPipelineVersion } from "./pipeline-version.js";
7
7
  import { normalInputs } from "../../consumer/contract/entries.js";
8
+ import { readPipelineStableBaseline } from "./pipeline-stable-baseline.js";
8
9
 
9
10
  const SHA = /^[0-9a-f]{40}$/u;
10
11
  const STABLE = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
@@ -295,10 +296,12 @@ export async function readPipelineStableSource(plan, host) {
295
296
  const prior = selected.comparison
296
297
  ? await exactTag(get, selected.comparison.tag)
297
298
  : null;
298
- if ((prior?.commit || null) !== plan.previousChannelCommit)
299
- throw new Error(
300
- "Stable comparison differs from the retained published channel",
301
- );
299
+ const baseline = await readPipelineStableBaseline(
300
+ plan,
301
+ { ...host, request },
302
+ selected.comparison,
303
+ prior,
304
+ );
302
305
  const current = await treeFiles(get, pointer.commit);
303
306
  if (current.tree !== candidate.identity.tree)
304
307
  throw new Error(
@@ -335,6 +338,20 @@ export async function readPipelineStableSource(plan, host) {
335
338
  prior,
336
339
  latest,
337
340
  });
341
+ if (
342
+ baseline &&
343
+ recordDigest(
344
+ await readPipelineStableBaseline(
345
+ plan,
346
+ { ...host, request },
347
+ selected.comparison,
348
+ prior,
349
+ ),
350
+ ) !== recordDigest(baseline)
351
+ )
352
+ throw new Error(
353
+ "Stable baseline evidence changed during provider readback",
354
+ );
338
355
  const body = {
339
356
  schema: "buildchain.pipeline-stable-source/v1",
340
357
  planRoot: plan.root,
@@ -347,6 +364,7 @@ export async function readPipelineStableSource(plan, host) {
347
364
  comparisonStable: selected.comparison
348
365
  ? { ...selected.comparison, sha: prior.commit, tree: previous.tree }
349
366
  : null,
367
+ ...(baseline ? { comparisonChannel: baseline } : {}),
350
368
  impact,
351
369
  changedPaths: changedPaths(previous.files, current.files),
352
370
  };
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ const LIMIT = 8 * 1024 * 1024;
5
+
6
+ async function inventory(request, prefix) {
7
+ const assets = [],
8
+ ids = new Set(),
9
+ names = new Set();
10
+ for (let page = 1; page <= 20; page++) {
11
+ const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
12
+ if (!Array.isArray(values) || values.length > 100)
13
+ throw new Error("Published evidence asset inventory is incomplete");
14
+ for (const value of values) {
15
+ if (
16
+ !Number.isSafeInteger(value.id) ||
17
+ value.id < 1 ||
18
+ typeof value.name !== "string" ||
19
+ !value.name ||
20
+ ids.has(value.id) ||
21
+ names.has(value.name)
22
+ )
23
+ throw new Error("Published evidence asset inventory is ambiguous");
24
+ ids.add(value.id);
25
+ names.add(value.name);
26
+ assets.push(value);
27
+ }
28
+ if (values.length < 100) return assets;
29
+ }
30
+ throw new Error(
31
+ "Published evidence inventory exceeds its complete-read bound",
32
+ );
33
+ }
34
+
35
+ function selectedAssets(assets, names) {
36
+ return names.map((name) => {
37
+ const asset = assets.find((value) => value.name === name);
38
+ if (!asset) return { name, missing: true };
39
+ if (
40
+ asset.state !== "uploaded" ||
41
+ !Number.isSafeInteger(asset.size) ||
42
+ asset.size < 1 ||
43
+ asset.size > LIMIT ||
44
+ !/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
45
+ )
46
+ throw new Error("Published evidence asset lacks bounded immutable bytes");
47
+ return { id: asset.id, name, size: asset.size, digest: asset.digest };
48
+ });
49
+ }
50
+
51
+ // Download only fixed evidence names from the exact already-observed release.
52
+ // There is no caller-supplied URL, artifact selector or provider write here.
53
+ export async function readReleaseEvidenceAssets(host, candidate, names) {
54
+ const repository = host.repository;
55
+ if (
56
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
57
+ !Number.isSafeInteger(candidate.id) ||
58
+ candidate.id < 1
59
+ )
60
+ throw new Error("Published evidence requires exact release coordinates");
61
+ const prefix = `/repos/${repository}/releases/${candidate.id}`;
62
+ const selected = selectedAssets(await inventory(host.request, prefix), names);
63
+ const missing = selected
64
+ .filter((value) => value.missing)
65
+ .map(({ name }) => name);
66
+ if (missing.length) return { status: "missing", missing };
67
+ const [owner, repo] = repository.split("/");
68
+ const values = {};
69
+ for (const asset of selected) {
70
+ const result = await host.github.rest.repos.getReleaseAsset({
71
+ owner,
72
+ repo,
73
+ asset_id: asset.id,
74
+ headers: { accept: "application/octet-stream" },
75
+ });
76
+ const bytes = Buffer.from(result.data);
77
+ if (
78
+ bytes.length !== asset.size ||
79
+ bytes.length > LIMIT ||
80
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
81
+ asset.digest
82
+ )
83
+ throw new Error(
84
+ "Published evidence download differs from provider bytes",
85
+ );
86
+ values[asset.name] = bytes;
87
+ }
88
+ if (
89
+ recordDigest(
90
+ selectedAssets(await inventory(host.request, prefix), names),
91
+ ) !== recordDigest(selected)
92
+ )
93
+ throw new Error(
94
+ "Published evidence assets changed during provider readback",
95
+ );
96
+ return { status: "present", values, assets: selected };
97
+ }
@@ -1,8 +1,8 @@
1
1
  export function publicationControlPlaneRequest(request) {
2
2
  const kind = request.autoAdmissionKind;
3
3
  const defaults = {
4
- "release-candidate": ".github/workflows/public-release-promote.yml",
5
- "publication-artifact": ".github/workflows/public-release-paper.yml",
4
+ "release-candidate": ".github/workflows/.release-candidate-promote.yml",
5
+ "publication-artifact": ".github/workflows/.release-paper.yml",
6
6
  "binary-release-assets": ".github/workflows/.release-binary-assets.yml",
7
7
  };
8
8
  if (!defaults[kind])
@@ -26,7 +26,7 @@ export function sealManagedPublicationAdmission({
26
26
  );
27
27
  const suffix = binary ? ":binary-release-assets" : paper ? ":paper" : "";
28
28
  const defaults = {
29
- "release-candidate": ".github/workflows/public-release-promote.yml",
29
+ "release-candidate": ".github/workflows/.release-candidate-promote.yml",
30
30
  "publication-artifact": ".github/workflows/public-release-paper-sealed.yml",
31
31
  "binary-release-assets": ".github/workflows/.release-binary-assets.yml",
32
32
  };