@kungfu-tech/buildchain 3.0.5 → 3.0.6-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 (83) hide show
  1. package/README.md +31 -0
  2. package/actions/macos-credential-island/README.md +8 -0
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  5. package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
  6. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  8. package/dist/site/buildchain-contract.json +107 -26
  9. package/dist/site/buildchain-site.json +121 -49
  10. package/dist/site/capability-registry.json +3 -3
  11. package/dist/site/cli-registry.json +9 -3
  12. package/dist/site/controller-registry.json +27 -3
  13. package/dist/site/kfd-claims.json +101 -15
  14. package/dist/site/kfd-upstream-aggregate.json +1 -1
  15. package/dist/site/manual-registry.json +10 -9
  16. package/dist/site/node-api-registry.json +540 -31
  17. package/dist/site/page-registry.json +105 -29
  18. package/dist/site/public-surface-audit.json +184 -11
  19. package/dist/site/publication-authority-registry.json +62 -1
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/site-manifest.json +13 -13
  22. package/dist/site/workflow-registry.json +118 -4
  23. package/docs/MAP.md +4 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
  26. package/docs/cli-reference.md +35 -2
  27. package/docs/dev-alpha-candidate-patrol.md +14 -5
  28. package/docs/dev-qualification-patrol.md +108 -0
  29. package/docs/node-api-reference.md +123 -72
  30. package/docs/publish-transaction.md +7 -5
  31. package/docs/release-flow.md +4 -0
  32. package/docs/release-governance.md +39 -0
  33. package/docs/release-propagation.md +93 -0
  34. package/docs/reusable-build-surface.md +53 -12
  35. package/docs/stable-candidate-patrol.md +5 -2
  36. package/package.json +1 -1
  37. package/packages/core/buildchain-contract.js +36 -1
  38. package/packages/core/buildchain-publication-authority.js +3 -0
  39. package/packages/core/public-surface-audit.js +4 -35
  40. package/packages/core/release-propagation-agent-entry.js +185 -0
  41. package/packages/core/release-propagation-pickup.js +387 -0
  42. package/packages/core/release-propagation-push.js +293 -0
  43. package/packages/core/release-propagation-release.js +61 -18
  44. package/packages/core/release-propagation-stage-evidence.js +36 -0
  45. package/packages/core/release-propagation-work.js +8 -4
  46. package/packages/core/release-propagation.js +27 -2
  47. package/packages/core/workflow-call-contract.js +308 -0
  48. package/packages/core/workflow-yaml-contract.js +272 -0
  49. package/scripts/artifact-signing-controller-core.mjs +525 -0
  50. package/scripts/artifact-signing-controller.mjs +302 -0
  51. package/scripts/artifact-signing-delegation.mjs +51 -3
  52. package/scripts/auditable-demo-platform.mjs +564 -0
  53. package/scripts/auditable-demo-renditions.mjs +7 -2
  54. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  55. package/scripts/auditable-demo.mjs +27 -21
  56. package/scripts/aws-macos-jit-controller-core.mjs +389 -0
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
  58. package/scripts/aws-macos-jit-controller.mjs +558 -0
  59. package/scripts/aws-macos-jit-job-controller.mjs +401 -0
  60. package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
  61. package/scripts/aws-windows-jit-campaign.mjs +202 -0
  62. package/scripts/aws-windows-jit-controller-core.mjs +20 -2
  63. package/scripts/aws-windows-jit-controller.mjs +184 -109
  64. package/scripts/aws-windows-jit-core.mjs +21 -3
  65. package/scripts/aws-windows-jit.mjs +2 -0
  66. package/scripts/build-standalone-binary.mjs +6 -0
  67. package/scripts/buildchain-cli-help.mjs +2 -1
  68. package/scripts/capture-package-release-propagation.mjs +24 -1
  69. package/scripts/check-inventory.mjs +6 -0
  70. package/scripts/compiler-cache-evidence.mjs +90 -7
  71. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  72. package/scripts/dev-pr-auto-merge.mjs +4 -4
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
  75. package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
  76. package/scripts/generate-site-bundle.mjs +5 -0
  77. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  78. package/scripts/locked-source-checkout.mjs +17 -3
  79. package/scripts/release-propagation.mjs +150 -2
  80. package/scripts/run-lifecycle-core.mjs +25 -0
  81. package/scripts/site-capability-metadata.mjs +1 -1
  82. package/scripts/stable-candidate-patrol.mjs +30 -4
  83. package/scripts/workflow-call-contract.mjs +133 -0
@@ -0,0 +1,293 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import {
3
+ assertPlainObject,
4
+ assertString,
5
+ sha256Json,
6
+ } from "./release-propagation-common.js";
7
+ import {
8
+ assertCommitSha,
9
+ assertContentRoot,
10
+ contentRoot,
11
+ } from "./release-propagation-work-control.js";
12
+ import { verifyReleasePropagationWork } from "./release-propagation-work.js";
13
+
14
+ export const RELEASE_PROPAGATION_PUSH_PLAN_CONTRACT =
15
+ "kungfu-buildchain-release-propagation-push-plan";
16
+ export const RELEASE_PROPAGATION_PUSH_RESULT_CONTRACT =
17
+ "kungfu-buildchain-release-propagation-push-result";
18
+
19
+ function branchName(value, label) {
20
+ const branch = assertString(value, label);
21
+ if (
22
+ branch.startsWith("-") ||
23
+ branch.startsWith("refs/") ||
24
+ branch.includes("..") ||
25
+ branch.includes("@{") ||
26
+ /[\s~^:?*[\\]/.test(branch) ||
27
+ branch.endsWith(".") ||
28
+ branch.endsWith("/") ||
29
+ branch.includes("//")
30
+ ) {
31
+ throw new Error(`${label} is not a safe Git branch name`);
32
+ }
33
+ return branch;
34
+ }
35
+
36
+ function remoteName(value) {
37
+ const name = assertString(value, "push repository state.remoteName");
38
+ if (!/^[A-Za-z0-9._-]+$/.test(name) || name.startsWith("-")) {
39
+ throw new Error("push repository state.remoteName is unsafe");
40
+ }
41
+ return name;
42
+ }
43
+
44
+ function optionalCommit(value, label) {
45
+ if (value === "") return "";
46
+ return assertCommitSha(value, label);
47
+ }
48
+
49
+ function normalizeRepositoryState(value) {
50
+ const state = assertPlainObject(value, "push repository state");
51
+ return {
52
+ repository: assertString(
53
+ state.repository,
54
+ "push repository state.repository",
55
+ ),
56
+ remoteName: remoteName(state.remoteName),
57
+ currentBranch: branchName(
58
+ state.currentBranch,
59
+ "push repository state.currentBranch",
60
+ ),
61
+ sourceRevision: assertCommitSha(
62
+ state.sourceRevision,
63
+ "push repository state.sourceRevision",
64
+ ),
65
+ remoteBaseRevision: assertCommitSha(
66
+ state.remoteBaseRevision,
67
+ "push repository state.remoteBaseRevision",
68
+ ),
69
+ remoteBranchRevision: optionalCommit(
70
+ state.remoteBranchRevision,
71
+ "push repository state.remoteBranchRevision",
72
+ ),
73
+ expectedBaseIsAncestor: state.expectedBaseIsAncestor === true,
74
+ };
75
+ }
76
+
77
+ export function createReleasePropagationPushPlan({
78
+ work: workInput,
79
+ expectedWorkRoot,
80
+ repositoryState: stateInput,
81
+ } = {}) {
82
+ const status = verifyReleasePropagationWork(workInput);
83
+ const expectedRoot = assertContentRoot(expectedWorkRoot, "expectedWorkRoot");
84
+ if (status.contentRoot !== expectedRoot) {
85
+ throw new Error("propagation work changed before push planning");
86
+ }
87
+ if (
88
+ status.lifecycle !== "ready" ||
89
+ status.currentStage !== "push-branch" ||
90
+ status.work.authority.mode !== "execute"
91
+ ) {
92
+ throw new Error("propagation work is not ready for the push-branch stage");
93
+ }
94
+ const state = normalizeRepositoryState(stateInput);
95
+ const expectedBranch = branchName(
96
+ status.work.downstream.branch,
97
+ "propagation work downstream.branch",
98
+ );
99
+ if (state.repository !== status.work.downstream.repository) {
100
+ throw new Error(
101
+ "push remote repository disagrees with the propagation target",
102
+ );
103
+ }
104
+ if (state.currentBranch !== expectedBranch) {
105
+ throw new Error(
106
+ "current branch disagrees with the exact propagation branch",
107
+ );
108
+ }
109
+ if (state.remoteBaseRevision !== status.work.downstream.expectedBaseSha) {
110
+ throw new Error("downstream base advanced after the propagation capture");
111
+ }
112
+ if (!state.expectedBaseIsAncestor) {
113
+ throw new Error(
114
+ "propagation branch is not based on the expected downstream base",
115
+ );
116
+ }
117
+ const destinationRef = `refs/heads/${expectedBranch}`;
118
+ const body = {
119
+ schemaVersion: 1,
120
+ contract: RELEASE_PROPAGATION_PUSH_PLAN_CONTRACT,
121
+ workId: status.workId,
122
+ expectedWorkRoot: expectedRoot,
123
+ repository: state.repository,
124
+ remoteName: state.remoteName,
125
+ sourceRef: "HEAD",
126
+ sourceRevision: state.sourceRevision,
127
+ destinationRef,
128
+ expectedBaseRevision: status.work.downstream.expectedBaseSha,
129
+ expectedOldRevision: state.remoteBranchRevision,
130
+ pushMode: "fast-forward-only-exact-refspec",
131
+ argv: ["push", "--porcelain", state.remoteName, `HEAD:${destinationRef}`],
132
+ };
133
+ return { ...body, planRoot: sha256Json(body) };
134
+ }
135
+
136
+ function git(cwd, args) {
137
+ return execFileSync("git", args, {
138
+ cwd,
139
+ encoding: "utf8",
140
+ maxBuffer: 16 * 1024 * 1024,
141
+ }).trim();
142
+ }
143
+
144
+ function repositoryFromRemoteUrl(value) {
145
+ const remoteUrl = String(value || "").trim();
146
+ if (!remoteUrl || /[?#]/.test(remoteUrl)) {
147
+ throw new Error("push remote URL must not contain query or fragment data");
148
+ }
149
+ const ssh = remoteUrl.match(
150
+ /^[^@\s]+@github\.com:([^/\s]+\/[^/\s]+?)(?:\.git)?$/,
151
+ );
152
+ if (ssh) return ssh[1];
153
+ const https = remoteUrl.match(
154
+ /^https:\/\/github\.com\/([^/\s]+\/[^/\s]+?)(?:\.git)?$/,
155
+ );
156
+ if (https) return https[1];
157
+ throw new Error("push remote must be an exact GitHub repository URL");
158
+ }
159
+
160
+ function lsRemote(cwd, remote, ref) {
161
+ const output = git(cwd, ["ls-remote", "--refs", remote, ref]);
162
+ if (!output) return "";
163
+ const rows = output.split("\n").filter(Boolean);
164
+ if (rows.length !== 1) {
165
+ throw new Error(`expected exactly one remote ref for ${ref}`);
166
+ }
167
+ const [revision, observedRef] = rows[0].split(/\s+/);
168
+ if (observedRef !== ref) {
169
+ throw new Error(`remote ref lookup disagrees with ${ref}`);
170
+ }
171
+ return assertCommitSha(revision, `remote ${ref}`);
172
+ }
173
+
174
+ export function inspectReleasePropagationPushState({
175
+ work: workInput,
176
+ cwd = process.cwd(),
177
+ remote = "origin",
178
+ } = {}) {
179
+ const status = verifyReleasePropagationWork(workInput);
180
+ const expectedBranch = branchName(
181
+ status.work.downstream.branch,
182
+ "propagation work downstream.branch",
183
+ );
184
+ const baseRef = branchName(
185
+ status.work.downstream.baseRef,
186
+ "propagation work downstream.baseRef",
187
+ );
188
+ const normalizedRemote = remoteName(remote);
189
+ const repository = repositoryFromRemoteUrl(
190
+ git(cwd, ["remote", "get-url", normalizedRemote]),
191
+ );
192
+ const sourceRevision = assertCommitSha(
193
+ git(cwd, ["rev-parse", "HEAD"]),
194
+ "push source revision",
195
+ );
196
+ const ancestor = spawnSync(
197
+ "git",
198
+ [
199
+ "merge-base",
200
+ "--is-ancestor",
201
+ status.work.downstream.expectedBaseSha,
202
+ sourceRevision,
203
+ ],
204
+ { cwd, encoding: "utf8" },
205
+ );
206
+ if (ancestor.error) throw ancestor.error;
207
+ if (ancestor.status !== 0) {
208
+ throw new Error(
209
+ "propagation branch is not based on the expected downstream base",
210
+ );
211
+ }
212
+ return {
213
+ repository,
214
+ remoteName: normalizedRemote,
215
+ currentBranch: git(cwd, ["branch", "--show-current"]),
216
+ sourceRevision,
217
+ remoteBaseRevision: lsRemote(
218
+ cwd,
219
+ normalizedRemote,
220
+ `refs/heads/${baseRef}`,
221
+ ),
222
+ remoteBranchRevision: lsRemote(
223
+ cwd,
224
+ normalizedRemote,
225
+ `refs/heads/${expectedBranch}`,
226
+ ),
227
+ expectedBaseIsAncestor: true,
228
+ };
229
+ }
230
+
231
+ export function executeReleasePropagationPush({
232
+ work,
233
+ expectedWorkRoot,
234
+ cwd = process.cwd(),
235
+ remote = "origin",
236
+ } = {}) {
237
+ const repositoryState = inspectReleasePropagationPushState({
238
+ work,
239
+ cwd,
240
+ remote,
241
+ });
242
+ const plan = createReleasePropagationPushPlan({
243
+ work,
244
+ expectedWorkRoot,
245
+ repositoryState,
246
+ });
247
+ let mutation = true;
248
+ let output = "";
249
+ if (plan.expectedOldRevision === plan.sourceRevision) {
250
+ mutation = false;
251
+ } else {
252
+ output = git(cwd, plan.argv);
253
+ }
254
+ const observedRevision = lsRemote(cwd, plan.remoteName, plan.destinationRef);
255
+ if (observedRevision !== plan.sourceRevision) {
256
+ throw new Error(
257
+ "remote branch readback disagrees with the pushed source revision",
258
+ );
259
+ }
260
+ const claims = {
261
+ provider: "git",
262
+ repository: plan.repository,
263
+ remoteName: plan.remoteName,
264
+ sourceRef: plan.sourceRef,
265
+ sourceRevision: plan.sourceRevision,
266
+ destinationRef: plan.destinationRef,
267
+ expectedBaseRevision: plan.expectedBaseRevision,
268
+ expectedOldRevision: plan.expectedOldRevision,
269
+ observedRevision,
270
+ pushMode: plan.pushMode,
271
+ argv: plan.argv,
272
+ mutation,
273
+ };
274
+ const evidence = {
275
+ kind: "git-branch-reconciliation",
276
+ root: contentRoot(claims),
277
+ locator: "buildchain://release-propagation/branch-reconciliation",
278
+ repository: plan.repository,
279
+ revision: observedRevision,
280
+ httpStatus: null,
281
+ bytes: Buffer.byteLength(output),
282
+ claims,
283
+ };
284
+ const body = {
285
+ schemaVersion: 1,
286
+ contract: RELEASE_PROPAGATION_PUSH_RESULT_CONTRACT,
287
+ status: mutation ? "pushed" : "already-current",
288
+ mutation,
289
+ plan,
290
+ evidence,
291
+ };
292
+ return { ...body, resultRoot: sha256Json(body) };
293
+ }
@@ -31,7 +31,9 @@ function normalizePackageFact(pkg = {}) {
31
31
  name: assertString(pkg.name, "upstreamRelease.package.name"),
32
32
  version: assertString(pkg.version, "upstreamRelease.package.version"),
33
33
  integrity: assertString(pkg.integrity, "upstreamRelease.package.integrity"),
34
- gitHead: assertCommitSha(pkg.gitHead || pkg.git_head, "upstreamRelease.package.gitHead"),
34
+ gitHead: pkg.gitHead || pkg.git_head
35
+ ? assertCommitSha(pkg.gitHead || pkg.git_head, "upstreamRelease.package.gitHead")
36
+ : "",
35
37
  };
36
38
  if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(normalized.integrity)) {
37
39
  throw new Error("upstreamRelease.package.integrity must be an exact sha512 SRI value");
@@ -39,6 +41,57 @@ function normalizePackageFact(pkg = {}) {
39
41
  return normalized;
40
42
  }
41
43
 
44
+ function normalizeRegistryProvenance(value, packageFact, repository, sourceSha) {
45
+ if (!value) return undefined;
46
+ if (!packageFact) throw new Error("upstreamRelease.registryProvenance requires package");
47
+ const input = assertPlainObject(value, "upstreamRelease.registryProvenance");
48
+ const normalized = {
49
+ registry: assertString(input.registry, "upstreamRelease.registryProvenance.registry"),
50
+ attestationUrl: assertString(input.attestationUrl || input.attestation_url, "upstreamRelease.registryProvenance.attestationUrl"),
51
+ predicateType: assertString(input.predicateType || input.predicate_type, "upstreamRelease.registryProvenance.predicateType"),
52
+ subjectSha512: assertString(input.subjectSha512 || input.subject_sha512, "upstreamRelease.registryProvenance.subjectSha512"),
53
+ repository: assertString(input.repository, "upstreamRelease.registryProvenance.repository"),
54
+ sourceSha: assertCommitSha(input.sourceSha || input.source_sha, "upstreamRelease.registryProvenance.sourceSha"),
55
+ workflowPath: assertString(input.workflowPath || input.workflow_path, "upstreamRelease.registryProvenance.workflowPath"),
56
+ workflowRef: assertString(input.workflowRef || input.workflow_ref, "upstreamRelease.registryProvenance.workflowRef"),
57
+ runUrl: assertString(input.runUrl || input.run_url, "upstreamRelease.registryProvenance.runUrl"),
58
+ };
59
+ if (normalized.registry !== "https://registry.npmjs.org") throw new Error("upstreamRelease.registryProvenance.registry must be the public npm registry");
60
+ if (!normalized.attestationUrl.startsWith("https://registry.npmjs.org/-/npm/v1/attestations/")) throw new Error("upstreamRelease.registryProvenance.attestationUrl must use the npm attestation API");
61
+ if (normalized.predicateType !== "https://slsa.dev/provenance/v1") throw new Error("upstreamRelease.registryProvenance must use SLSA provenance v1");
62
+ if (!/^[0-9a-f]{128}$/.test(normalized.subjectSha512)) throw new Error("upstreamRelease.registryProvenance.subjectSha512 must be an exact SHA-512 digest");
63
+ const sriDigest = Buffer.from(packageFact.integrity.slice("sha512-".length), "base64").toString("hex");
64
+ if (normalized.subjectSha512 !== sriDigest) throw new Error("upstreamRelease registry provenance subject must match package integrity");
65
+ if (normalized.repository !== repository || normalized.sourceSha !== sourceSha) throw new Error("upstreamRelease registry provenance source must match the release envelope");
66
+ return normalized;
67
+ }
68
+
69
+ function normalizeReleaseEvidence(release, packageFact, repository, sourceSha, version) {
70
+ const registryProvenance = normalizeRegistryProvenance(
71
+ release.registryProvenance || release.registry_provenance,
72
+ packageFact,
73
+ repository,
74
+ sourceSha,
75
+ );
76
+ const tag = optionalString(release.tag);
77
+ const target = release.tagTargetSha || release.tag_target_sha;
78
+ const tagTargetSha = target ? assertCommitSha(target, "upstreamRelease.tagTargetSha") : "";
79
+ if (!registryProvenance) {
80
+ if (tag !== `v${version}`) throw new Error("upstreamRelease.tag must exactly match the published version");
81
+ if (tagTargetSha !== sourceSha) throw new Error("upstreamRelease tag target must match sourceSha");
82
+ if (packageFact && packageFact.gitHead !== sourceSha) throw new Error("upstreamRelease package gitHead must match sourceSha");
83
+ } else if ((tag && tag !== `v${version}`) || (tagTargetSha && tagTargetSha !== sourceSha)) {
84
+ throw new Error("upstreamRelease optional tag evidence must match the provenance source");
85
+ }
86
+ const passportInput = release.releasePassport || release.release_passport;
87
+ return {
88
+ tag,
89
+ tagTargetSha,
90
+ registryProvenance,
91
+ releasePassport: passportInput ? normalizeReleasePassport(passportInput) : undefined,
92
+ };
93
+ }
94
+
42
95
  function normalizeDigestUrlFact(value = {}, label) {
43
96
  if (!value || typeof value !== "object" || Array.isArray(value)) {
44
97
  throw new Error(`${label} must be an object`);
@@ -117,33 +170,22 @@ export function normalizeUpstreamRelease(input) {
117
170
  throw new Error("upstreamRelease.repository must be owner/repo");
118
171
  }
119
172
  const sourceSha = assertCommitSha(release.sourceSha || release.source_sha, "upstreamRelease.sourceSha");
120
- const tagTargetSha = assertCommitSha(
121
- release.tagTargetSha || release.tag_target_sha,
122
- "upstreamRelease.tagTargetSha",
123
- );
124
173
  const version = packageFact?.version || publicationArtifact?.version || "";
125
- const tag = assertString(release.tag, "upstreamRelease.tag");
126
- if (tag !== `v${version}`) {
127
- throw new Error("upstreamRelease.tag must exactly match the published version");
128
- }
129
- if (tagTargetSha !== sourceSha) {
130
- throw new Error("upstreamRelease tag target must match sourceSha");
131
- }
132
- if (packageFact && packageFact.gitHead !== sourceSha) {
133
- throw new Error("upstreamRelease package gitHead must match sourceSha");
134
- }
174
+ const evidence = normalizeReleaseEvidence(release, packageFact, repository, sourceSha, version);
175
+ if (!evidence.registryProvenance && !evidence.releasePassport) normalizeReleasePassport();
135
176
  if (packageFact && publicationArtifact && packageFact.version !== publicationArtifact.version) {
136
177
  throw new Error("upstreamRelease package and publication artifact versions must match");
137
178
  }
138
179
  return {
139
180
  repository,
140
181
  channel: normalizeChannel(release.channel, "upstreamRelease.channel"),
141
- tag,
142
- tagTargetSha,
182
+ tag: evidence.tag,
183
+ tagTargetSha: evidence.tagTargetSha,
143
184
  sourceSha,
144
185
  package: packageFact,
145
186
  publicationArtifact,
146
- releasePassport: normalizeReleasePassport(release.releasePassport || release.release_passport),
187
+ releasePassport: evidence.releasePassport,
188
+ registryProvenance: evidence.registryProvenance,
147
189
  siteBundle: release.siteBundle || release.site_bundle
148
190
  ? {
149
191
  manifestSha256: assertString(
@@ -166,6 +208,7 @@ export function verifyReleaseLockBinding({ release, lock, downstream, propagatio
166
208
  package: lock.upstream?.package,
167
209
  publicationArtifact: lock.upstream?.publicationArtifact,
168
210
  releasePassport: lock.upstream?.releasePassport,
211
+ registryProvenance: lock.upstream?.registryProvenance,
169
212
  siteBundle: lock.upstream?.siteBundle,
170
213
  });
171
214
  if (JSON.stringify(lockRelease) !== JSON.stringify(normalizedRelease)) {
@@ -4,6 +4,7 @@ import {
4
4
  optionalString,
5
5
  } from "./release-propagation-common.js";
6
6
  import {
7
+ assertCommitSha,
7
8
  assertContentRoot,
8
9
  assertExactFields,
9
10
  assertIsoTimestamp,
@@ -107,6 +108,40 @@ function verifyIndependentReview(work, receipt) {
107
108
  }
108
109
  }
109
110
 
111
+ function verifyPushedBranch(work, receipt) {
112
+ const reconciliation = receipt.evidence.find((entry) => entry.kind === "git-branch-reconciliation");
113
+ const claims = exactClaims(reconciliation, [
114
+ "provider", "repository", "remoteName", "sourceRef", "sourceRevision",
115
+ "destinationRef", "expectedBaseRevision", "expectedOldRevision",
116
+ "observedRevision", "pushMode", "argv", "mutation",
117
+ ], "push-branch evidence claims");
118
+ if (claims.provider !== "git"
119
+ || claims.repository !== work.downstream.repository
120
+ || claims.sourceRef !== "HEAD"
121
+ || claims.destinationRef !== `refs/heads/${work.downstream.branch}`
122
+ || claims.expectedBaseRevision !== work.downstream.expectedBaseSha
123
+ || claims.pushMode !== "fast-forward-only-exact-refspec") {
124
+ throw new Error("push-branch evidence does not bind the exact propagation target");
125
+ }
126
+ assertCommitSha(claims.sourceRevision, "push-branch source revision");
127
+ assertCommitSha(claims.observedRevision, "push-branch observed revision");
128
+ if (claims.expectedOldRevision !== "") {
129
+ assertCommitSha(claims.expectedOldRevision, "push-branch expected-old revision");
130
+ }
131
+ const expectedArgv = ["push", "--porcelain", claims.remoteName, `HEAD:${claims.destinationRef}`];
132
+ if (JSON.stringify(claims.argv) !== JSON.stringify(expectedArgv)
133
+ || claims.argv.some((value) => value === "--force" || value.startsWith("--force-"))) {
134
+ throw new Error("push-branch evidence must use the exact non-force refspec");
135
+ }
136
+ if (claims.sourceRevision !== claims.observedRevision
137
+ || reconciliation.revision !== claims.observedRevision) {
138
+ throw new Error("push-branch evidence readback disagrees with the source revision");
139
+ }
140
+ if (typeof claims.mutation !== "boolean") {
141
+ throw new Error("push-branch evidence mutation must be boolean");
142
+ }
143
+ }
144
+
110
145
  function verifyProductionDeployment(work, receipt) {
111
146
  const deployment = receipt.evidence.find((entry) => entry.kind === "production-deployment");
112
147
  const claims = exactClaims(deployment, [
@@ -237,6 +272,7 @@ export function validateStageEvidence({ work, receipt }) {
237
272
  ]).has(receipt.stage)) {
238
273
  for (const entry of receipt.evidence) assertHttpsLocator(entry.locator, receipt.stage);
239
274
  }
275
+ if (receipt.stage === "push-branch") verifyPushedBranch(work, receipt);
240
276
  if (receipt.stage === "independent-review") verifyIndependentReview(work, receipt);
241
277
  if (receipt.stage === "production-deploy") verifyProductionDeployment(work, receipt);
242
278
  if (receipt.stage === "online-readback") verifyOnlineReadback(work, receipt.evidence);
@@ -77,7 +77,7 @@ function workCommands(executionProfile = null, repository = "", branch = "", bas
77
77
  stages: {
78
78
  materialize: profile.updateCommand || "<consumer update command from the exact execution profile>",
79
79
  "verify-release": profile.verifyCommand || "<consumer verification command from the exact execution profile>",
80
- "push-branch": `git push --force-with-lease=<exact-expected-old> origin HEAD:refs/heads/${branch || "<managed-branch>"}`,
80
+ "push-branch": "buildchain release-propagation work push-branch --work <work.json> --expected-work-root <sha256:...> --cwd <downstream-worktree> --remote origin --execute --json",
81
81
  "pull-request": `gh pr create --repo ${repository || "<downstream-repository>"} --base ${baseRef || "<base-ref>"} --head ${branch || "<managed-branch>"}`,
82
82
  preview: `gh run list --repo ${repository || "<downstream-repository>"} --workflow ${workflow} --branch ${branch || "<managed-branch>"} --event pull_request --json databaseId,status,conclusion,url,headSha`,
83
83
  "independent-review": `gh pr review <pr-number> --repo ${repository || "<downstream-repository>"} --approve`,
@@ -317,8 +317,12 @@ function verifyWorkRelease(work) {
317
317
  downstream.executionProfile,
318
318
  "release propagation work downstream.executionProfile",
319
319
  );
320
- if (!profile
321
- || JSON.stringify(profile) !== JSON.stringify(downstream.releaseLock.downstream.executionProfile)) {
320
+ const lockedProfile = normalizeExecutionProfile(
321
+ downstream.releaseLock.downstream.executionProfile,
322
+ "release propagation work releaseLock.downstream.executionProfile",
323
+ );
324
+ if (!profile || !lockedProfile
325
+ || sha256Json(profile) !== sha256Json(lockedProfile)) {
322
326
  throw new Error("release propagation work execution profile disagrees with the exact release lock");
323
327
  }
324
328
  return { downstream, profile };
@@ -400,7 +404,7 @@ function verifyWorkPlanAndState(work, downstream, profile) {
400
404
  downstream.branch,
401
405
  downstream.baseRef,
402
406
  );
403
- if (JSON.stringify(plan.commands) !== JSON.stringify(expectedCommands)) {
407
+ if (sha256Json(plan.commands) !== sha256Json(expectedCommands)) {
404
408
  throw new Error("release propagation work commands are not canonical");
405
409
  }
406
410
  const state = assertExactFields(
@@ -219,6 +219,7 @@ export function createReleasePropagationLock({
219
219
  package: upstreamRelease.package,
220
220
  publicationArtifact: upstreamRelease.publicationArtifact,
221
221
  releasePassport: upstreamRelease.releasePassport,
222
+ registryProvenance: upstreamRelease.registryProvenance,
222
223
  siteBundle: upstreamRelease.siteBundle,
223
224
  },
224
225
  downstream: {
@@ -406,7 +407,7 @@ export function createReleasePropagationReceipt({
406
407
  name: upstreamRelease.package.name,
407
408
  version: upstreamRelease.package.version,
408
409
  integrity: upstreamRelease.package.integrity,
409
- gitHead: upstreamRelease.package.gitHead,
410
+ gitHead: upstreamRelease.package.gitHead || null,
410
411
  }
411
412
  : null,
412
413
  },
@@ -414,7 +415,8 @@ export function createReleasePropagationReceipt({
414
415
  state: release.channel === "alpha" ? "complete" : "not-applicable",
415
416
  evidence: release.channel === "alpha"
416
417
  ? {
417
- releasePassportSha256: upstreamRelease.releasePassport.sha256,
418
+ releasePassportSha256: upstreamRelease.releasePassport?.sha256 || null,
419
+ registryProvenance: upstreamRelease.registryProvenance || null,
418
420
  tag: upstreamRelease.tag,
419
421
  }
420
422
  : null,
@@ -476,3 +478,26 @@ export {
476
478
  createPackageReleasePropagationCapture,
477
479
  normalizePackageReleasePropagationConfig,
478
480
  } from "./release-propagation-capture.js";
481
+ export {
482
+ MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT,
483
+ MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT,
484
+ createManualUpstreamPickupCapture,
485
+ createManualUpstreamPickupPlan,
486
+ normalizeManualUpstreamPickupConfig,
487
+ resolveNpmRegistryRelease,
488
+ } from "./release-propagation-pickup.js";
489
+ export {
490
+ RELEASE_PROPAGATION_FAILURE_MATRIX,
491
+ RELEASE_PROPAGATION_FAILURE_MATRIX_CONTRACT,
492
+ SITE_UPSTREAM_AGENT_ENTRY_CONTRACT,
493
+ classifyReleasePropagationCondition,
494
+ normalizeSiteUpstreamIntent,
495
+ planSiteUpstreamAgentEntry,
496
+ } from "./release-propagation-agent-entry.js";
497
+ export {
498
+ RELEASE_PROPAGATION_PUSH_PLAN_CONTRACT,
499
+ RELEASE_PROPAGATION_PUSH_RESULT_CONTRACT,
500
+ createReleasePropagationPushPlan,
501
+ executeReleasePropagationPush,
502
+ inspectReleasePropagationPushState,
503
+ } from "./release-propagation-push.js";