@kungfu-tech/buildchain 3.0.4-alpha.2 → 3.0.4-alpha.3
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/bin/buildchain.mjs +126 -72
- package/bin/internal/trust-release-cli.mjs +15 -537
- package/bin/internal/trust-release-command-handlers.mjs +14 -0
- package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
- package/bin/internal/trust-release-release-handlers.mjs +317 -0
- package/bin/internal/trust-release-verification-handlers.mjs +306 -0
- package/dist/site/buildchain-contract.json +35 -24
- package/dist/site/buildchain-site.json +22 -10
- package/dist/site/controller-registry.json +16 -3
- package/dist/site/kfd-claims.json +9 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +7 -7
- package/dist/site/page-registry.json +9 -4
- package/dist/site/public-surface-audit.json +56 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +7 -0
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +13 -5
- package/docs/MAP.md +1 -1
- package/docs/release-propagation.md +166 -8
- package/package.json +1 -1
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/controller-evidence.js +8 -1
- package/packages/core/index.js +1 -13
- package/packages/core/paper-npm-bootstrap.js +492 -0
- package/packages/core/paper.js +271 -669
- package/packages/core/public-surface-cli.js +12 -1
- package/packages/core/release-passport.js +67 -71
- package/packages/core/release-propagation-common.js +64 -0
- package/packages/core/release-propagation-execution-profile.js +59 -0
- package/packages/core/release-propagation-release.js +196 -0
- package/packages/core/release-propagation-stage-evidence.js +364 -0
- package/packages/core/release-propagation-work-capture.js +64 -0
- package/packages/core/release-propagation-work-constants.js +34 -0
- package/packages/core/release-propagation-work-control.js +203 -0
- package/packages/core/release-propagation-work-transitions.js +145 -0
- package/packages/core/release-propagation-work.js +517 -0
- package/packages/core/release-propagation.js +34 -158
- package/scripts/aws-windows-jit-controller-core.mjs +269 -0
- package/scripts/aws-windows-jit-controller.mjs +502 -0
- package/scripts/check-internal-architecture.mjs +178 -52
- package/scripts/check-maintainability.mjs +76 -17
- package/scripts/generate-site-bundle.mjs +16 -7
- package/scripts/maintainability-metrics.mjs +24 -4
- package/scripts/release-propagation.mjs +126 -0
- package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
- package/scripts/web-surface-core.mjs +46 -207
- package/scripts/web-surface-routing.mjs +286 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertPlainObject,
|
|
3
|
+
assertString,
|
|
4
|
+
optionalString,
|
|
5
|
+
} from "./release-propagation-common.js";
|
|
6
|
+
import {
|
|
7
|
+
assertContentRoot,
|
|
8
|
+
assertExactFields,
|
|
9
|
+
assertIsoTimestamp,
|
|
10
|
+
contentRoot,
|
|
11
|
+
} from "./release-propagation-work-control.js";
|
|
12
|
+
import {
|
|
13
|
+
FAILURE_DISPOSITIONS,
|
|
14
|
+
RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT,
|
|
15
|
+
} from "./release-propagation-work-constants.js";
|
|
16
|
+
|
|
17
|
+
const REQUIRED_EVIDENCE_KINDS = Object.freeze({
|
|
18
|
+
materialize: ["release-lock"],
|
|
19
|
+
"verify-release": ["release-contract-verification"],
|
|
20
|
+
"push-branch": ["git-branch-reconciliation"],
|
|
21
|
+
"pull-request": ["github-pull-request"],
|
|
22
|
+
preview: ["preview-deployment"],
|
|
23
|
+
"independent-review": ["github-pr-approval"],
|
|
24
|
+
"protected-merge": ["github-merge"],
|
|
25
|
+
staging: ["staging-deployment"],
|
|
26
|
+
"production-release": ["production-release-pr"],
|
|
27
|
+
"production-deploy": ["production-deployment", "rollback-coordinate"],
|
|
28
|
+
"online-readback": ["production-status", "online-artifact"],
|
|
29
|
+
complete: ["work-control-decision"],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const COMMIT_BOUND_STAGES = new Set([
|
|
33
|
+
"push-branch",
|
|
34
|
+
"pull-request",
|
|
35
|
+
"preview",
|
|
36
|
+
"independent-review",
|
|
37
|
+
"protected-merge",
|
|
38
|
+
"staging",
|
|
39
|
+
"production-release",
|
|
40
|
+
"production-deploy",
|
|
41
|
+
"online-readback",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function assertHttpsLocator(locator, stage) {
|
|
45
|
+
if (!/^https:\/\/[^?\s]+$/.test(locator)) {
|
|
46
|
+
throw new Error(`${stage} evidence locator must be a stable HTTPS URL without query parameters`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exactClaims(entry, fields, label) {
|
|
51
|
+
const claims = assertExactFields(entry.claims, fields, label);
|
|
52
|
+
if (entry.root !== contentRoot(claims)) {
|
|
53
|
+
throw new Error(`${label} root does not bind its exact claims`);
|
|
54
|
+
}
|
|
55
|
+
return claims;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function releaseClaims(work) {
|
|
59
|
+
return {
|
|
60
|
+
releaseRoot: work.upstream.releaseRoot,
|
|
61
|
+
releaseLockRoot: work.downstream.releaseLockRoot,
|
|
62
|
+
sourceSha: work.upstream.release.sourceSha,
|
|
63
|
+
tagTargetSha: work.upstream.release.tagTargetSha,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertReleaseClaims(work, claims, label) {
|
|
68
|
+
const expected = releaseClaims(work);
|
|
69
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
70
|
+
if (claims[field] !== value) {
|
|
71
|
+
throw new Error(`${label}.${field} does not bind the propagation release`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function verifyIndependentReview(work, receipt) {
|
|
77
|
+
if (receipt.actor.kind !== "github-reviewer") {
|
|
78
|
+
throw new Error("independent-review actor must be a verified GitHub reviewer");
|
|
79
|
+
}
|
|
80
|
+
const approval = receipt.evidence.find((entry) => entry.kind === "github-pr-approval");
|
|
81
|
+
const claims = exactClaims(approval, [
|
|
82
|
+
"provider",
|
|
83
|
+
"reviewState",
|
|
84
|
+
"reviewerIdentity",
|
|
85
|
+
"pullRequestAuthorIdentity",
|
|
86
|
+
"headRevision",
|
|
87
|
+
], "independent-review evidence claims");
|
|
88
|
+
if (claims.provider !== "github" || claims.reviewState !== "APPROVED") {
|
|
89
|
+
throw new Error("independent-review must bind an approved GitHub review");
|
|
90
|
+
}
|
|
91
|
+
if (claims.reviewerIdentity !== receipt.actor.identity
|
|
92
|
+
|| claims.pullRequestAuthorIdentity !== work.authority.sourceControlPrincipal) {
|
|
93
|
+
throw new Error("independent-review identities disagree with the receipt and source-control authority");
|
|
94
|
+
}
|
|
95
|
+
if (new Set([
|
|
96
|
+
work.authority.executionPrincipal,
|
|
97
|
+
work.authority.sourceControlPrincipal,
|
|
98
|
+
]).has(claims.reviewerIdentity)) {
|
|
99
|
+
throw new Error("independent-review identity must differ from the executor and pull request author");
|
|
100
|
+
}
|
|
101
|
+
const pullRequest = [...work.stageReceipts]
|
|
102
|
+
.reverse()
|
|
103
|
+
.find((entry) => entry.stage === "pull-request" && entry.outcome === "success");
|
|
104
|
+
const expectedHead = pullRequest?.evidence.find((entry) => entry.kind === "github-pull-request")?.revision;
|
|
105
|
+
if (!expectedHead || claims.headRevision !== expectedHead || approval.revision !== expectedHead) {
|
|
106
|
+
throw new Error("independent-review must bind the exact propagated pull request head");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function verifyProductionDeployment(work, receipt) {
|
|
111
|
+
const deployment = receipt.evidence.find((entry) => entry.kind === "production-deployment");
|
|
112
|
+
const claims = exactClaims(deployment, [
|
|
113
|
+
"deploymentRunUrl",
|
|
114
|
+
"deployedRevision",
|
|
115
|
+
"releaseRoot",
|
|
116
|
+
"releaseLockRoot",
|
|
117
|
+
"sourceSha",
|
|
118
|
+
"tagTargetSha",
|
|
119
|
+
"artifactRoot",
|
|
120
|
+
"readbackArtifacts",
|
|
121
|
+
], "production-deployment evidence claims");
|
|
122
|
+
assertReleaseClaims(work, claims, "production-deployment evidence claims");
|
|
123
|
+
if (claims.deploymentRunUrl !== deployment.locator
|
|
124
|
+
|| claims.deployedRevision !== deployment.revision) {
|
|
125
|
+
throw new Error("production-deployment evidence must bind its run and deployed revision");
|
|
126
|
+
}
|
|
127
|
+
assertContentRoot(claims.artifactRoot, "production-deployment evidence claims.artifactRoot");
|
|
128
|
+
if (!Array.isArray(claims.readbackArtifacts)) {
|
|
129
|
+
throw new Error("production-deployment readbackArtifacts must be an array");
|
|
130
|
+
}
|
|
131
|
+
const artifacts = claims.readbackArtifacts.map((entry, index) => {
|
|
132
|
+
const artifact = assertExactFields(entry, ["url", "contentSha256"], `readbackArtifacts[${index}]`);
|
|
133
|
+
return {
|
|
134
|
+
url: assertString(artifact.url, `readbackArtifacts[${index}].url`),
|
|
135
|
+
contentSha256: assertContentRoot(
|
|
136
|
+
artifact.contentSha256,
|
|
137
|
+
`readbackArtifacts[${index}].contentSha256`,
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const expectedUrls = work.downstream.executionProfile.readbackUrls;
|
|
142
|
+
if (JSON.stringify(artifacts.map((entry) => entry.url)) !== JSON.stringify(expectedUrls)) {
|
|
143
|
+
throw new Error("production-deployment readback artifacts must cover the exact profile URLs");
|
|
144
|
+
}
|
|
145
|
+
const rollback = receipt.evidence.find((entry) => entry.kind === "rollback-coordinate");
|
|
146
|
+
const rollbackClaims = exactClaims(rollback, [
|
|
147
|
+
"deployedRevision",
|
|
148
|
+
"rollbackRevision",
|
|
149
|
+
], "rollback-coordinate evidence claims");
|
|
150
|
+
if (rollbackClaims.deployedRevision !== deployment.revision
|
|
151
|
+
|| rollbackClaims.rollbackRevision !== rollback.revision) {
|
|
152
|
+
throw new Error("rollback-coordinate must bind the deployed and rollback revisions");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function verifyOnlineReadback(work, evidence) {
|
|
157
|
+
const profile = work.downstream.executionProfile;
|
|
158
|
+
const expectedLocators = [profile.productionStatusUrl, ...profile.readbackUrls].sort();
|
|
159
|
+
const actualLocators = evidence.map((entry) => entry.locator).sort();
|
|
160
|
+
if (JSON.stringify(actualLocators) !== JSON.stringify(expectedLocators)) {
|
|
161
|
+
throw new Error("online-readback evidence must cover the exact production status and artifact URLs");
|
|
162
|
+
}
|
|
163
|
+
for (const entry of evidence) assertHttpsLocator(entry.locator, "online-readback");
|
|
164
|
+
if (evidence.some((entry) => entry.httpStatus !== 200 || entry.bytes < 1)) {
|
|
165
|
+
throw new Error("online-readback evidence must bind HTTP 200 and observed production bytes");
|
|
166
|
+
}
|
|
167
|
+
const status = evidence.find((entry) => entry.locator === profile.productionStatusUrl);
|
|
168
|
+
if (status?.kind !== "production-status") {
|
|
169
|
+
throw new Error("online-readback status URL requires production-status evidence");
|
|
170
|
+
}
|
|
171
|
+
if (evidence.some((entry) => entry !== status && entry.kind !== "online-artifact")) {
|
|
172
|
+
throw new Error("online-readback artifact URLs require online-artifact evidence");
|
|
173
|
+
}
|
|
174
|
+
const deploymentReceipt = [...work.stageReceipts]
|
|
175
|
+
.reverse()
|
|
176
|
+
.find((entry) => entry.stage === "production-deploy" && entry.outcome === "success");
|
|
177
|
+
const deployment = deploymentReceipt?.evidence.find(
|
|
178
|
+
(entry) => entry.kind === "production-deployment",
|
|
179
|
+
);
|
|
180
|
+
if (!deployment) {
|
|
181
|
+
throw new Error("online-readback requires a qualifying production deployment receipt");
|
|
182
|
+
}
|
|
183
|
+
const deploymentClaims = exactClaims(deployment, [
|
|
184
|
+
"deploymentRunUrl",
|
|
185
|
+
"deployedRevision",
|
|
186
|
+
"releaseRoot",
|
|
187
|
+
"releaseLockRoot",
|
|
188
|
+
"sourceSha",
|
|
189
|
+
"tagTargetSha",
|
|
190
|
+
"artifactRoot",
|
|
191
|
+
"readbackArtifacts",
|
|
192
|
+
], "production-deployment evidence claims");
|
|
193
|
+
const expectedArtifacts = new Map(
|
|
194
|
+
deploymentClaims.readbackArtifacts.map((entry) => [entry.url, entry.contentSha256]),
|
|
195
|
+
);
|
|
196
|
+
for (const entry of evidence) {
|
|
197
|
+
const claims = exactClaims(entry, [
|
|
198
|
+
"deployedRevision",
|
|
199
|
+
"releaseRoot",
|
|
200
|
+
"releaseLockRoot",
|
|
201
|
+
"sourceSha",
|
|
202
|
+
"tagTargetSha",
|
|
203
|
+
"artifactRoot",
|
|
204
|
+
"contentSha256",
|
|
205
|
+
], "online-readback evidence claims");
|
|
206
|
+
assertReleaseClaims(work, claims, "online-readback evidence claims");
|
|
207
|
+
assertContentRoot(claims.contentSha256, "online-readback evidence claims.contentSha256");
|
|
208
|
+
if (claims.deployedRevision !== deployment.revision
|
|
209
|
+
|| claims.artifactRoot !== deploymentClaims.artifactRoot
|
|
210
|
+
|| entry.revision !== deployment.revision) {
|
|
211
|
+
throw new Error("online-readback evidence does not bind the production deployment");
|
|
212
|
+
}
|
|
213
|
+
if (entry.kind === "online-artifact"
|
|
214
|
+
&& claims.contentSha256 !== expectedArtifacts.get(entry.locator)) {
|
|
215
|
+
throw new Error("online artifact digest does not match the deployed readback artifact");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function validateStageEvidence({ work, receipt }) {
|
|
221
|
+
if (receipt.outcome !== "success") return;
|
|
222
|
+
const requiredKinds = REQUIRED_EVIDENCE_KINDS[receipt.stage] || [];
|
|
223
|
+
const kinds = new Set(receipt.evidence.map((entry) => entry.kind));
|
|
224
|
+
if (requiredKinds.some((kind) => !kinds.has(kind))) {
|
|
225
|
+
throw new Error(`${receipt.stage} receipt is missing stage-specific evidence`);
|
|
226
|
+
}
|
|
227
|
+
if (receipt.evidence.some((entry) => entry.repository !== work.downstream.repository)) {
|
|
228
|
+
throw new Error(`${receipt.stage} evidence repository disagrees with the propagation target`);
|
|
229
|
+
}
|
|
230
|
+
if (COMMIT_BOUND_STAGES.has(receipt.stage)
|
|
231
|
+
&& receipt.evidence.some((entry) => !/^[0-9a-f]{40}$/.test(entry.revision))) {
|
|
232
|
+
throw new Error(`${receipt.stage} evidence must bind an exact downstream Git revision`);
|
|
233
|
+
}
|
|
234
|
+
if (new Set([
|
|
235
|
+
"pull-request", "preview", "independent-review", "protected-merge",
|
|
236
|
+
"staging", "production-release", "production-deploy",
|
|
237
|
+
]).has(receipt.stage)) {
|
|
238
|
+
for (const entry of receipt.evidence) assertHttpsLocator(entry.locator, receipt.stage);
|
|
239
|
+
}
|
|
240
|
+
if (receipt.stage === "independent-review") verifyIndependentReview(work, receipt);
|
|
241
|
+
if (receipt.stage === "production-deploy") verifyProductionDeployment(work, receipt);
|
|
242
|
+
if (receipt.stage === "online-readback") verifyOnlineReadback(work, receipt.evidence);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeEvidence(value, label) {
|
|
246
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
247
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
248
|
+
}
|
|
249
|
+
return value.map((entry, index) => {
|
|
250
|
+
const evidence = assertExactFields(entry, [
|
|
251
|
+
"kind",
|
|
252
|
+
"root",
|
|
253
|
+
"locator",
|
|
254
|
+
"repository",
|
|
255
|
+
"revision",
|
|
256
|
+
"httpStatus",
|
|
257
|
+
"bytes",
|
|
258
|
+
"claims",
|
|
259
|
+
], `${label}[${index}]`);
|
|
260
|
+
const locator = assertString(evidence.locator, `${label}[${index}].locator`);
|
|
261
|
+
if (/[?&](?:token|signature|x-amz-|x-goog-)/i.test(locator)) {
|
|
262
|
+
throw new Error(`${label}[${index}].locator must not contain signed or credential parameters`);
|
|
263
|
+
}
|
|
264
|
+
const httpStatus = evidence.httpStatus === null ? null : Number(evidence.httpStatus);
|
|
265
|
+
const bytes = Number(evidence.bytes);
|
|
266
|
+
if (httpStatus !== null
|
|
267
|
+
&& (!Number.isInteger(httpStatus) || httpStatus < 100 || httpStatus > 599)) {
|
|
268
|
+
throw new Error(`${label}[${index}].httpStatus must be null or a valid HTTP status`);
|
|
269
|
+
}
|
|
270
|
+
if (!Number.isInteger(bytes) || bytes < 0) {
|
|
271
|
+
throw new Error(`${label}[${index}].bytes must be a non-negative integer`);
|
|
272
|
+
}
|
|
273
|
+
const claims = evidence.claims === null
|
|
274
|
+
? null
|
|
275
|
+
: structuredClone(assertPlainObject(evidence.claims, `${label}[${index}].claims`));
|
|
276
|
+
return {
|
|
277
|
+
kind: assertString(evidence.kind, `${label}[${index}].kind`),
|
|
278
|
+
root: assertContentRoot(evidence.root, `${label}[${index}].root`),
|
|
279
|
+
locator,
|
|
280
|
+
repository: optionalString(evidence.repository),
|
|
281
|
+
revision: optionalString(evidence.revision),
|
|
282
|
+
httpStatus,
|
|
283
|
+
bytes,
|
|
284
|
+
claims,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function normalizeStageReceipt(value, {
|
|
290
|
+
work,
|
|
291
|
+
workId,
|
|
292
|
+
expectedWorkRoot,
|
|
293
|
+
expectedStage,
|
|
294
|
+
allowedOutcomes,
|
|
295
|
+
}) {
|
|
296
|
+
const receipt = assertExactFields(value, [
|
|
297
|
+
"schemaVersion",
|
|
298
|
+
"contract",
|
|
299
|
+
"workId",
|
|
300
|
+
"expectedWorkRoot",
|
|
301
|
+
"stage",
|
|
302
|
+
"outcome",
|
|
303
|
+
"attempt",
|
|
304
|
+
"observedAt",
|
|
305
|
+
"actor",
|
|
306
|
+
"summary",
|
|
307
|
+
"evidence",
|
|
308
|
+
"failure",
|
|
309
|
+
"receiptRoot",
|
|
310
|
+
], "stage receipt");
|
|
311
|
+
if (receipt.schemaVersion !== 1 || receipt.contract !== RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT) {
|
|
312
|
+
throw new Error("stage receipt must use the Buildchain release propagation stage receipt v1 contract");
|
|
313
|
+
}
|
|
314
|
+
if (receipt.workId !== workId || receipt.expectedWorkRoot !== expectedWorkRoot) {
|
|
315
|
+
throw new Error("stage receipt does not bind the expected propagation work cut");
|
|
316
|
+
}
|
|
317
|
+
if (receipt.stage !== expectedStage) {
|
|
318
|
+
throw new Error(`stage receipt must target current stage ${expectedStage}`);
|
|
319
|
+
}
|
|
320
|
+
if (!allowedOutcomes.has(receipt.outcome)) {
|
|
321
|
+
throw new Error(`stage receipt outcome must be one of ${[...allowedOutcomes].join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
if (!Number.isInteger(receipt.attempt) || receipt.attempt < 1) {
|
|
324
|
+
throw new Error("stage receipt attempt must be a positive integer");
|
|
325
|
+
}
|
|
326
|
+
const actor = assertExactFields(receipt.actor, ["kind", "identity"], "stage receipt actor");
|
|
327
|
+
const normalized = {
|
|
328
|
+
schemaVersion: 1,
|
|
329
|
+
contract: RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT,
|
|
330
|
+
workId,
|
|
331
|
+
expectedWorkRoot,
|
|
332
|
+
stage: expectedStage,
|
|
333
|
+
outcome: receipt.outcome,
|
|
334
|
+
attempt: receipt.attempt,
|
|
335
|
+
observedAt: assertIsoTimestamp(receipt.observedAt, "stage receipt observedAt"),
|
|
336
|
+
actor: {
|
|
337
|
+
kind: assertString(actor.kind, "stage receipt actor.kind"),
|
|
338
|
+
identity: assertString(actor.identity, "stage receipt actor.identity"),
|
|
339
|
+
},
|
|
340
|
+
summary: assertString(receipt.summary, "stage receipt summary"),
|
|
341
|
+
evidence: normalizeEvidence(receipt.evidence, "stage receipt evidence"),
|
|
342
|
+
failure: null,
|
|
343
|
+
};
|
|
344
|
+
if (receipt.outcome === "failure") {
|
|
345
|
+
const failure = assertExactFields(receipt.failure, ["class", "code", "summary"], "stage receipt failure");
|
|
346
|
+
const failureClass = assertString(failure.class, "stage receipt failure.class");
|
|
347
|
+
if (!FAILURE_DISPOSITIONS.has(failureClass)) {
|
|
348
|
+
throw new Error(`unsupported propagation failure class: ${failureClass}`);
|
|
349
|
+
}
|
|
350
|
+
normalized.failure = {
|
|
351
|
+
class: failureClass,
|
|
352
|
+
code: assertString(failure.code, "stage receipt failure.code"),
|
|
353
|
+
summary: assertString(failure.summary, "stage receipt failure.summary"),
|
|
354
|
+
};
|
|
355
|
+
} else if (receipt.failure !== null) {
|
|
356
|
+
throw new Error("successful or repair stage receipt cannot carry failure data");
|
|
357
|
+
}
|
|
358
|
+
const expectedReceiptRoot = contentRoot(normalized);
|
|
359
|
+
if (receipt.receiptRoot !== expectedReceiptRoot) {
|
|
360
|
+
throw new Error("stage receipt root does not verify");
|
|
361
|
+
}
|
|
362
|
+
validateStageEvidence({ work, receipt: normalized });
|
|
363
|
+
return { ...normalized, receiptRoot: expectedReceiptRoot };
|
|
364
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { assertExactFields } from "./release-propagation-common.js";
|
|
2
|
+
import {
|
|
3
|
+
contentRoot,
|
|
4
|
+
normalizeWorkRef,
|
|
5
|
+
} from "./release-propagation-work-control.js";
|
|
6
|
+
|
|
7
|
+
function automaticWorkRef({ repository, subject, version, cut }) {
|
|
8
|
+
return normalizeWorkRef({
|
|
9
|
+
schema: "kungfu.assignment-graph.work-ref/v1",
|
|
10
|
+
workspace_identity_root: contentRoot({
|
|
11
|
+
authority: "kungfu-buildchain-release-propagation",
|
|
12
|
+
repository,
|
|
13
|
+
}),
|
|
14
|
+
object_kind: "assignment",
|
|
15
|
+
subject,
|
|
16
|
+
version_root: contentRoot(version),
|
|
17
|
+
cut_root: contentRoot(cut),
|
|
18
|
+
}, `automatic WorkRef ${subject}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function automaticCaptureContext(selectedPlan, selected, propagationKey) {
|
|
22
|
+
const release = selectedPlan.upstreamRelease;
|
|
23
|
+
const version = release.package?.version || release.publicationArtifact?.version;
|
|
24
|
+
return {
|
|
25
|
+
parentWorkRef: automaticWorkRef({
|
|
26
|
+
repository: release.repository,
|
|
27
|
+
subject: `buildchain:release:${release.repository}/${version}`,
|
|
28
|
+
version: release,
|
|
29
|
+
cut: { repository: release.repository, sourceSha: release.sourceSha },
|
|
30
|
+
}),
|
|
31
|
+
childWorkRef: automaticWorkRef({
|
|
32
|
+
repository: selected.repository,
|
|
33
|
+
subject: `buildchain:propagation:${selected.repository}/${propagationKey}`,
|
|
34
|
+
version: selected.lock,
|
|
35
|
+
cut: { propagationKey, expectedTarget: selected.target },
|
|
36
|
+
}),
|
|
37
|
+
familyState: null,
|
|
38
|
+
authority: {
|
|
39
|
+
mode: "capture-only",
|
|
40
|
+
publishToProduction: false,
|
|
41
|
+
allowedActions: [],
|
|
42
|
+
executionPrincipal: null,
|
|
43
|
+
sourceControlPrincipal: null,
|
|
44
|
+
executionWarrant: null,
|
|
45
|
+
},
|
|
46
|
+
supersedesWorkRoot: "",
|
|
47
|
+
bindingState: "pending",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveWorkContext(workContext, selectedPlan, selected, propagationKey) {
|
|
52
|
+
if (workContext === undefined || workContext === null) {
|
|
53
|
+
return automaticCaptureContext(selectedPlan, selected, propagationKey);
|
|
54
|
+
}
|
|
55
|
+
const context = assertExactFields(workContext, [
|
|
56
|
+
"parentWorkRef",
|
|
57
|
+
"childWorkRef",
|
|
58
|
+
"familyState",
|
|
59
|
+
"authority",
|
|
60
|
+
"supersedesWorkRoot",
|
|
61
|
+
], "workContext");
|
|
62
|
+
return { ...context, bindingState: "bound" };
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const RELEASE_PROPAGATION_WORK_CONTRACT = "kungfu-buildchain-release-propagation-work";
|
|
2
|
+
export const RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT = "kungfu-buildchain-release-propagation-stage-receipt";
|
|
3
|
+
|
|
4
|
+
export const RELEASE_PROPAGATION_WORK_STAGES = Object.freeze([
|
|
5
|
+
"materialize",
|
|
6
|
+
"verify-release",
|
|
7
|
+
"push-branch",
|
|
8
|
+
"pull-request",
|
|
9
|
+
"preview",
|
|
10
|
+
"independent-review",
|
|
11
|
+
"protected-merge",
|
|
12
|
+
"staging",
|
|
13
|
+
"production-release",
|
|
14
|
+
"production-deploy",
|
|
15
|
+
"online-readback",
|
|
16
|
+
"complete",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const EXECUTION_ACTIONS = new Set(RELEASE_PROPAGATION_WORK_STAGES);
|
|
20
|
+
export const FAILURE_DISPOSITIONS = new Map([
|
|
21
|
+
["stale-branch", "retry"],
|
|
22
|
+
["expected-old-mismatch", "retry"],
|
|
23
|
+
["lockfile-drift", "retry"],
|
|
24
|
+
["failed-check", "retry"],
|
|
25
|
+
["interrupted-execution", "retry"],
|
|
26
|
+
["ci-delay", "retry"],
|
|
27
|
+
["semantic-ambiguity", "needs-decision"],
|
|
28
|
+
["credential", "needs-decision"],
|
|
29
|
+
["policy-expansion", "needs-decision"],
|
|
30
|
+
["unknown", "needs-decision"],
|
|
31
|
+
["release-contract-mismatch", "hard-safety-gate"],
|
|
32
|
+
["immutable-artifact-conflict", "hard-safety-gate"],
|
|
33
|
+
["destructive-required", "hard-safety-gate"],
|
|
34
|
+
]);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertPlainObject,
|
|
3
|
+
assertString,
|
|
4
|
+
optionalString,
|
|
5
|
+
sha256Json,
|
|
6
|
+
} from "./release-propagation-common.js";
|
|
7
|
+
import { EXECUTION_ACTIONS } from "./release-propagation-work-constants.js";
|
|
8
|
+
|
|
9
|
+
const WORK_REF_SCHEMA = "kungfu.assignment-graph.work-ref/v1";
|
|
10
|
+
const FAMILY_STATE_V2_SCHEMA = "kungfu.work-control.initiative-family-state/v2";
|
|
11
|
+
const TYPED_REFERENCE_FIELDS = new Set([
|
|
12
|
+
"kind",
|
|
13
|
+
"identity",
|
|
14
|
+
"root",
|
|
15
|
+
"factWorld",
|
|
16
|
+
"cutRoot",
|
|
17
|
+
"schema",
|
|
18
|
+
"status",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function contentRoot(value) {
|
|
22
|
+
return `sha256:${sha256Json(value)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertExactFields(value, fields, label) {
|
|
26
|
+
const object = assertPlainObject(value, label);
|
|
27
|
+
const actual = Object.keys(object).sort();
|
|
28
|
+
const expected = [...fields].sort();
|
|
29
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
30
|
+
throw new Error(`${label} has an invalid field set`);
|
|
31
|
+
}
|
|
32
|
+
return object;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertContentRoot(value, label, { optional = false } = {}) {
|
|
36
|
+
const root = optionalString(value);
|
|
37
|
+
if (optional && !root) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(root)) {
|
|
41
|
+
throw new Error(`${label} must be a sha256 content root`);
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertCommitSha(value, label) {
|
|
47
|
+
const sha = assertString(value, label).toLowerCase();
|
|
48
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) {
|
|
49
|
+
throw new Error(`${label} must be an exact 40-character Git SHA`);
|
|
50
|
+
}
|
|
51
|
+
return sha;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertIsoTimestamp(value, label) {
|
|
55
|
+
const timestamp = assertString(value, label);
|
|
56
|
+
if (Number.isNaN(Date.parse(timestamp)) || !/(?:Z|[+-]\d\d:\d\d)$/.test(timestamp)) {
|
|
57
|
+
throw new Error(`${label} must be an ISO-8601 timestamp with timezone`);
|
|
58
|
+
}
|
|
59
|
+
return timestamp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeWorkRef(value, label) {
|
|
63
|
+
const ref = assertExactFields(value, [
|
|
64
|
+
"schema",
|
|
65
|
+
"workspace_identity_root",
|
|
66
|
+
"object_kind",
|
|
67
|
+
"subject",
|
|
68
|
+
"version_root",
|
|
69
|
+
"cut_root",
|
|
70
|
+
], label);
|
|
71
|
+
if (ref.schema !== WORK_REF_SCHEMA) {
|
|
72
|
+
throw new Error(`${label}.schema must be ${WORK_REF_SCHEMA}`);
|
|
73
|
+
}
|
|
74
|
+
if (!new Set(["initiative", "assignment"]).has(ref.object_kind)) {
|
|
75
|
+
throw new Error(`${label}.object_kind must be initiative or assignment`);
|
|
76
|
+
}
|
|
77
|
+
const subject = assertString(ref.subject, `${label}.subject`);
|
|
78
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(subject)) {
|
|
79
|
+
throw new Error(`${label}.subject is not canonical`);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
schema: WORK_REF_SCHEMA,
|
|
83
|
+
workspace_identity_root: assertContentRoot(ref.workspace_identity_root, `${label}.workspace_identity_root`),
|
|
84
|
+
object_kind: ref.object_kind,
|
|
85
|
+
subject,
|
|
86
|
+
version_root: assertContentRoot(ref.version_root, `${label}.version_root`),
|
|
87
|
+
cut_root: assertContentRoot(ref.cut_root, `${label}.cut_root`),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function normalizeTypedReference(value, { kind, status, factWorld = "", cutRoot = "", label }) {
|
|
92
|
+
const reference = assertExactFields(value, TYPED_REFERENCE_FIELDS, label);
|
|
93
|
+
if (reference.kind !== kind) {
|
|
94
|
+
throw new Error(`${label}.kind must be ${kind}`);
|
|
95
|
+
}
|
|
96
|
+
if (reference.status !== status) {
|
|
97
|
+
throw new Error(`${label}.status must be ${status}`);
|
|
98
|
+
}
|
|
99
|
+
const normalized = {
|
|
100
|
+
kind,
|
|
101
|
+
identity: assertString(reference.identity, `${label}.identity`),
|
|
102
|
+
root: assertContentRoot(reference.root, `${label}.root`),
|
|
103
|
+
factWorld: assertString(reference.factWorld, `${label}.factWorld`),
|
|
104
|
+
cutRoot: assertContentRoot(reference.cutRoot, `${label}.cutRoot`),
|
|
105
|
+
schema: assertString(reference.schema, `${label}.schema`),
|
|
106
|
+
status,
|
|
107
|
+
};
|
|
108
|
+
if (factWorld && normalized.factWorld !== factWorld) {
|
|
109
|
+
throw new Error(`${label}.factWorld does not match the Family State v2 fact world`);
|
|
110
|
+
}
|
|
111
|
+
if (cutRoot && normalized.cutRoot !== cutRoot) {
|
|
112
|
+
throw new Error(`${label}.cutRoot does not match the Family State v2 cut`);
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function normalizeFamilyStateReference(value) {
|
|
118
|
+
const reference = assertExactFields(value, [
|
|
119
|
+
"schema",
|
|
120
|
+
"stateRoot",
|
|
121
|
+
"v1ProjectionRoot",
|
|
122
|
+
"typedBindingRoot",
|
|
123
|
+
"factWorld",
|
|
124
|
+
"cutRoot",
|
|
125
|
+
], "workContext.familyState");
|
|
126
|
+
if (reference.schema !== FAMILY_STATE_V2_SCHEMA) {
|
|
127
|
+
throw new Error(`workContext.familyState.schema must be ${FAMILY_STATE_V2_SCHEMA}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
schema: FAMILY_STATE_V2_SCHEMA,
|
|
131
|
+
stateRoot: assertContentRoot(reference.stateRoot, "workContext.familyState.stateRoot"),
|
|
132
|
+
v1ProjectionRoot: assertContentRoot(reference.v1ProjectionRoot, "workContext.familyState.v1ProjectionRoot"),
|
|
133
|
+
typedBindingRoot: assertContentRoot(reference.typedBindingRoot, "workContext.familyState.typedBindingRoot"),
|
|
134
|
+
factWorld: assertString(reference.factWorld, "workContext.familyState.factWorld"),
|
|
135
|
+
cutRoot: assertContentRoot(reference.cutRoot, "workContext.familyState.cutRoot"),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function normalizeAllowedActions(value, label) {
|
|
140
|
+
if (!Array.isArray(value) || value.some((entry) => !EXECUTION_ACTIONS.has(entry))) {
|
|
141
|
+
throw new Error(`${label} must contain only supported propagation actions`);
|
|
142
|
+
}
|
|
143
|
+
const actions = [...new Set(value)].sort();
|
|
144
|
+
if (JSON.stringify(actions) !== JSON.stringify(value)) {
|
|
145
|
+
throw new Error(`${label} must be a sorted unique array`);
|
|
146
|
+
}
|
|
147
|
+
return actions;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function normalizeWorkAuthority(value, familyState) {
|
|
151
|
+
const authority = assertExactFields(value, [
|
|
152
|
+
"mode",
|
|
153
|
+
"publishToProduction",
|
|
154
|
+
"allowedActions",
|
|
155
|
+
"executionPrincipal",
|
|
156
|
+
"sourceControlPrincipal",
|
|
157
|
+
"executionWarrant",
|
|
158
|
+
], "workContext.authority");
|
|
159
|
+
if (!new Set(["capture-only", "execute"]).has(authority.mode)) {
|
|
160
|
+
throw new Error("workContext.authority.mode must be capture-only or execute");
|
|
161
|
+
}
|
|
162
|
+
if (typeof authority.publishToProduction !== "boolean") {
|
|
163
|
+
throw new Error("workContext.authority.publishToProduction must be a boolean");
|
|
164
|
+
}
|
|
165
|
+
const allowedActions = normalizeAllowedActions(authority.allowedActions, "workContext.authority.allowedActions");
|
|
166
|
+
if (authority.mode === "capture-only") {
|
|
167
|
+
if (authority.executionWarrant !== null
|
|
168
|
+
|| authority.executionPrincipal !== null
|
|
169
|
+
|| authority.sourceControlPrincipal !== null
|
|
170
|
+
|| allowedActions.length !== 0) {
|
|
171
|
+
throw new Error("capture-only work cannot carry execution authority");
|
|
172
|
+
}
|
|
173
|
+
return { ...authority, allowedActions };
|
|
174
|
+
}
|
|
175
|
+
if (!authority.publishToProduction) {
|
|
176
|
+
throw new Error("execute authority must explicitly carry publish-to-production intent");
|
|
177
|
+
}
|
|
178
|
+
if (!familyState) {
|
|
179
|
+
throw new Error("execute authority requires a bound Family State v2 reference");
|
|
180
|
+
}
|
|
181
|
+
if (JSON.stringify(allowedActions) !== JSON.stringify([...EXECUTION_ACTIONS].sort())) {
|
|
182
|
+
throw new Error("execute authority must cover the complete propagation stage set");
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
...authority,
|
|
186
|
+
allowedActions,
|
|
187
|
+
executionPrincipal: assertString(
|
|
188
|
+
authority.executionPrincipal,
|
|
189
|
+
"workContext.authority.executionPrincipal",
|
|
190
|
+
),
|
|
191
|
+
sourceControlPrincipal: assertString(
|
|
192
|
+
authority.sourceControlPrincipal,
|
|
193
|
+
"workContext.authority.sourceControlPrincipal",
|
|
194
|
+
),
|
|
195
|
+
executionWarrant: normalizeTypedReference(authority.executionWarrant, {
|
|
196
|
+
kind: "execution-warrant",
|
|
197
|
+
status: "active",
|
|
198
|
+
factWorld: familyState.factWorld,
|
|
199
|
+
cutRoot: familyState.cutRoot,
|
|
200
|
+
label: "workContext.authority.executionWarrant",
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
}
|