@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.
- package/README.md +31 -0
- package/actions/macos-credential-island/README.md +8 -0
- package/bin/buildchain.mjs +1 -1
- package/contracts/auditable-demo-media-profiles-v1.json +61 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
- package/dist/site/buildchain-contract.json +107 -26
- package/dist/site/buildchain-site.json +121 -49
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +9 -3
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +101 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +105 -29
- package/dist/site/public-surface-audit.json +184 -11
- package/dist/site/publication-authority-registry.json +62 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +118 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
- package/docs/cli-reference.md +35 -2
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +123 -72
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +39 -0
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +53 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +36 -1
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-pickup.js +387 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-release.js +61 -18
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +8 -4
- package/packages/core/release-propagation.js +27 -2
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +564 -0
- package/scripts/auditable-demo-renditions.mjs +7 -2
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/auditable-demo.mjs +27 -21
- package/scripts/aws-macos-jit-controller-core.mjs +389 -0
- package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
- package/scripts/aws-macos-jit-controller.mjs +558 -0
- package/scripts/aws-macos-jit-job-controller.mjs +401 -0
- package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
- package/scripts/aws-windows-jit-campaign.mjs +202 -0
- package/scripts/aws-windows-jit-controller-core.mjs +20 -2
- package/scripts/aws-windows-jit-controller.mjs +184 -109
- package/scripts/aws-windows-jit-core.mjs +21 -3
- package/scripts/aws-windows-jit.mjs +2 -0
- package/scripts/build-standalone-binary.mjs +6 -0
- package/scripts/buildchain-cli-help.mjs +2 -1
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/check-inventory.mjs +6 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/locked-source-checkout.mjs +17 -3
- package/scripts/release-propagation.mjs +150 -2
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
- 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:
|
|
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
|
|
126
|
-
if (
|
|
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:
|
|
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":
|
|
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
|
-
|
|
321
|
-
|
|
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 (
|
|
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
|
|
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";
|