@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
|
@@ -96,6 +96,10 @@ export function enumerateCliCommandsFromBin({
|
|
|
96
96
|
} = {}) {
|
|
97
97
|
const binSource = readText(root, binPath);
|
|
98
98
|
const helpSource = readText(root, "scripts/buildchain-cli-help.mjs");
|
|
99
|
+
const registrySource = readText(
|
|
100
|
+
root,
|
|
101
|
+
"bin/internal/command-registry.mjs",
|
|
102
|
+
);
|
|
99
103
|
const dispatchSource = [
|
|
100
104
|
binSource,
|
|
101
105
|
...listFiles(root, "bin/internal", (name) => name.endsWith(".mjs")).map(
|
|
@@ -106,7 +110,14 @@ export function enumerateCliCommandsFromBin({
|
|
|
106
110
|
const dispatch = [
|
|
107
111
|
...dispatchSource.matchAll(/if\s*\(\s*command\s*===\s*"([^"]+)"/g),
|
|
108
112
|
].map((match) => commandId(match[1]));
|
|
109
|
-
|
|
113
|
+
const registered = [
|
|
114
|
+
...registrySource.matchAll(/\{\s*id:\s*"([^"]+)"/g),
|
|
115
|
+
].map((match) => commandId(match[1]));
|
|
116
|
+
return uniqueSorted([
|
|
117
|
+
...usage.map((entry) => entry.id),
|
|
118
|
+
...dispatch,
|
|
119
|
+
...registered,
|
|
120
|
+
]).map(
|
|
110
121
|
(id) => ({
|
|
111
122
|
id,
|
|
112
123
|
source: "bin/buildchain.mjs",
|
|
@@ -1189,6 +1189,28 @@ export function createArtifactEvidence({ assets = [], repository = "", tag = "",
|
|
|
1189
1189
|
};
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
|
+
function firstTruthy(...values) {
|
|
1193
|
+
for (const value of values) {
|
|
1194
|
+
if (value) return value;
|
|
1195
|
+
}
|
|
1196
|
+
return values.at(-1);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function releaseField(release, camelKey, snakeKey, ...fallbacks) {
|
|
1200
|
+
return optionalString(firstTruthy(release[camelKey], release[snakeKey], ...fallbacks));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function optionalSections(entries) {
|
|
1204
|
+
return Object.fromEntries(entries.filter(([, value]) => value && (!Array.isArray(value) || value.length > 0)));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function acceptedControllerSourceShas(input) {
|
|
1208
|
+
const { treeEquivalent, builtSourceSha, promotionChannelSha, sourceSha, recoveryTreeEquivalent } = input;
|
|
1209
|
+
return treeEquivalent && builtSourceSha && (promotionChannelSha === sourceSha || recoveryTreeEquivalent)
|
|
1210
|
+
? [builtSourceSha]
|
|
1211
|
+
: [];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1192
1214
|
export function createReleasePassport({
|
|
1193
1215
|
cwd = process.cwd(),
|
|
1194
1216
|
repository = "",
|
|
@@ -1272,14 +1294,10 @@ export function createReleasePassport({
|
|
|
1272
1294
|
kfd1Section: normalizedKfd1?.passportSection,
|
|
1273
1295
|
kfd3Section: normalizedKfd3?.passportSection,
|
|
1274
1296
|
});
|
|
1275
|
-
const builtSourceSha =
|
|
1276
|
-
const builtSourceTreeSha =
|
|
1277
|
-
|
|
1278
|
-
);
|
|
1279
|
-
const promotionChannelSha = optionalString(release.promotionChannelSha || release.promotion_channel_sha);
|
|
1280
|
-
const promotionChannelTreeSha = optionalString(
|
|
1281
|
-
release.promotionChannelTreeSha || release.promotion_channel_tree_sha,
|
|
1282
|
-
);
|
|
1297
|
+
const builtSourceSha = releaseField(release, "builtSourceSha", "built_source_sha");
|
|
1298
|
+
const builtSourceTreeSha = releaseField(release, "builtSourceTreeSha", "built_source_tree_sha");
|
|
1299
|
+
const promotionChannelSha = releaseField(release, "promotionChannelSha", "promotion_channel_sha");
|
|
1300
|
+
const promotionChannelTreeSha = releaseField(release, "promotionChannelTreeSha", "promotion_channel_tree_sha");
|
|
1283
1301
|
const treeEquivalent = release.treeEquivalent === true;
|
|
1284
1302
|
const recoveryTreeEquivalent = Boolean(
|
|
1285
1303
|
treeEquivalent &&
|
|
@@ -1291,11 +1309,13 @@ export function createReleasePassport({
|
|
|
1291
1309
|
receipts: controllerReceipts,
|
|
1292
1310
|
references: controllerReceiptReferences,
|
|
1293
1311
|
expectedSourceSha: sourceSha,
|
|
1294
|
-
acceptedSourceShas:
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1312
|
+
acceptedSourceShas: acceptedControllerSourceShas({
|
|
1313
|
+
treeEquivalent,
|
|
1314
|
+
builtSourceSha,
|
|
1315
|
+
promotionChannelSha,
|
|
1316
|
+
sourceSha,
|
|
1317
|
+
recoveryTreeEquivalent,
|
|
1318
|
+
}),
|
|
1299
1319
|
requirePassed: true,
|
|
1300
1320
|
});
|
|
1301
1321
|
const normalizedGitHubArtifactAttestations = (githubArtifactAttestations || [])
|
|
@@ -1306,37 +1326,13 @@ export function createReleasePassport({
|
|
|
1306
1326
|
publishEvidence: normalizedPublishEvidence,
|
|
1307
1327
|
publish,
|
|
1308
1328
|
});
|
|
1309
|
-
const releaseMaterialSha =
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
);
|
|
1315
|
-
const
|
|
1316
|
-
release.releaseSha ||
|
|
1317
|
-
release.release_sha ||
|
|
1318
|
-
normalizedPublishEvidence?.releaseSha ||
|
|
1319
|
-
normalizedTransaction?.releaseSha,
|
|
1320
|
-
);
|
|
1321
|
-
const targetRef = optionalString(release.targetRef || release.target_ref || normalizedPublishEvidence?.targetRef);
|
|
1322
|
-
const packageDisplayVersion = optionalString(
|
|
1323
|
-
packageVersion ||
|
|
1324
|
-
normalizedPackageSet?.main?.version ||
|
|
1325
|
-
normalizedPublishSummary?.packages?.find((entry) => entry.role === "main")?.publishedVersion ||
|
|
1326
|
-
normalizedPublishEvidence?.version ||
|
|
1327
|
-
normalizedTransaction?.version ||
|
|
1328
|
-
readPackageVersion(cwd),
|
|
1329
|
-
);
|
|
1330
|
-
const publishedVersion = optionalString(
|
|
1331
|
-
release.publishedVersion ||
|
|
1332
|
-
release.published_version ||
|
|
1333
|
-
packageDisplayVersion,
|
|
1334
|
-
);
|
|
1335
|
-
const internalVersion = optionalString(
|
|
1336
|
-
release.internalVersion ||
|
|
1337
|
-
release.internal_version ||
|
|
1338
|
-
normalizedTransaction?.version,
|
|
1339
|
-
);
|
|
1329
|
+
const releaseMaterialSha = releaseField(release, "releaseMaterialSha", "release_material_sha", normalizedPublishEvidence?.releaseMaterialSha, normalizedTransaction?.releaseMaterialSha);
|
|
1330
|
+
const releaseSha = releaseField(release, "releaseSha", "release_sha", normalizedPublishEvidence?.releaseSha, normalizedTransaction?.releaseSha);
|
|
1331
|
+
const targetRef = releaseField(release, "targetRef", "target_ref", normalizedPublishEvidence?.targetRef);
|
|
1332
|
+
const mainPublishedVersion = normalizedPublishSummary?.packages?.find((entry) => entry.role === "main")?.publishedVersion;
|
|
1333
|
+
const packageDisplayVersion = optionalString(firstTruthy(packageVersion, normalizedPackageSet?.main?.version, mainPublishedVersion, normalizedPublishEvidence?.version, normalizedTransaction?.version, readPackageVersion(cwd)));
|
|
1334
|
+
const publishedVersion = releaseField(release, "publishedVersion", "published_version", packageDisplayVersion);
|
|
1335
|
+
const internalVersion = releaseField(release, "internalVersion", "internal_version", normalizedTransaction?.version);
|
|
1340
1336
|
return {
|
|
1341
1337
|
schemaVersion: 1,
|
|
1342
1338
|
contract: RELEASE_PASSPORT_CONTRACT,
|
|
@@ -1344,7 +1340,7 @@ export function createReleasePassport({
|
|
|
1344
1340
|
surfaceTimestampPolicy: createSurfaceTimestampPolicy({
|
|
1345
1341
|
generatedAt,
|
|
1346
1342
|
publishedAt: optionalString(release.publishedAt || release.published_at || normalizedPublishEvidence?.publishedAt),
|
|
1347
|
-
sourceRevision: optionalString(sourceSha
|
|
1343
|
+
sourceRevision: optionalString(firstTruthy(sourceSha, releaseSha, normalizedTransaction?.releaseSha)),
|
|
1348
1344
|
timestampPolicy: "ci-injected",
|
|
1349
1345
|
deterministicInputs: [
|
|
1350
1346
|
"release.sourceSha",
|
|
@@ -1366,14 +1362,14 @@ export function createReleasePassport({
|
|
|
1366
1362
|
},
|
|
1367
1363
|
release: {
|
|
1368
1364
|
tag: normalizedTag,
|
|
1369
|
-
publicTag:
|
|
1370
|
-
internalTag:
|
|
1365
|
+
publicTag: releaseField(release, "publicTag", "public_tag", normalizedTag),
|
|
1366
|
+
internalTag: releaseField(release, "internalTag", "internal_tag", normalizedTag),
|
|
1371
1367
|
internalVersion,
|
|
1372
1368
|
publishedVersion,
|
|
1373
|
-
versionLabel:
|
|
1369
|
+
versionLabel: releaseField(release, "versionLabel", "version_label", publishedVersion, normalizedTag),
|
|
1374
1370
|
line: optionalString(line),
|
|
1375
1371
|
sourceSha: optionalString(sourceSha),
|
|
1376
|
-
channel: optionalString(release.channel
|
|
1372
|
+
channel: optionalString(firstTruthy(release.channel, normalizedPublishEvidence?.channel, publish.channel)),
|
|
1377
1373
|
targetRef,
|
|
1378
1374
|
releaseSha,
|
|
1379
1375
|
releaseMaterialSha,
|
|
@@ -1411,28 +1407,28 @@ export function createReleasePassport({
|
|
|
1411
1407
|
compatibilityFixture: "self-hosted",
|
|
1412
1408
|
note: "Runner facts are recorded in artifact evidence; the protocol does not require self-hosted runners.",
|
|
1413
1409
|
},
|
|
1414
|
-
...(
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1410
|
+
...optionalSections([
|
|
1411
|
+
["packageSet", normalizedPackageSet],
|
|
1412
|
+
["publish", normalizedPublishSummary],
|
|
1413
|
+
["anchorManifest", anchorManifest],
|
|
1414
|
+
["versionMaterial", versionMaterial],
|
|
1415
|
+
["trustedPublishing", normalizedTrustedPublishing],
|
|
1416
|
+
["transaction", normalizedTransaction],
|
|
1417
|
+
["promotionRouting", normalizedPromotionRouting],
|
|
1418
|
+
["buildSummary", normalizedBuildSummary],
|
|
1419
|
+
["buildFacts", normalizedBuildFacts],
|
|
1420
|
+
["platformArtifactManifests", normalizedPlatformArtifactManifests],
|
|
1421
|
+
["distTagPromotion", normalizedDistTagPromotionEvidence],
|
|
1422
|
+
[firstTruthy(normalizedKfd1?.key, kfd1Metadata.key), normalizedKfd1?.passportSection],
|
|
1423
|
+
["kfd-2", normalizedKfd2],
|
|
1424
|
+
[firstTruthy(normalizedKfd3?.key, "kfd-3"), normalizedKfd3?.passportSection],
|
|
1425
|
+
["kfdSupport", normalizedKfdSupport],
|
|
1426
|
+
["kfdAgentHub", normalizedKfdAgentHub],
|
|
1427
|
+
["invariantPassports", invariantPassports],
|
|
1428
|
+
["releaseEvidence", releaseEvidence],
|
|
1429
|
+
["controllerReceipts", normalizedControllerReceipts],
|
|
1430
|
+
["githubArtifactAttestations", normalizedGitHubArtifactAttestations],
|
|
1431
|
+
]),
|
|
1436
1432
|
versionImpact: normalizedImpact.versionImpact,
|
|
1437
1433
|
surfaceImpacts: normalizedImpact.surfaceImpacts,
|
|
1438
1434
|
artifacts: [
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
|
|
4
|
+
|
|
5
|
+
export const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
|
|
6
|
+
|
|
7
|
+
export function stableJson(value) {
|
|
8
|
+
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sortJson(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.map(sortJson);
|
|
14
|
+
}
|
|
15
|
+
if (!value || typeof value !== "object") {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
Object.entries(value)
|
|
20
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
21
|
+
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sha256Json(value) {
|
|
26
|
+
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertPlainObject(value, label) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
31
|
+
throw new Error(`${label} must be an object`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assertString(value, label) {
|
|
37
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
38
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
39
|
+
}
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function optionalString(value) {
|
|
44
|
+
return value === undefined || value === null ? "" : String(value).trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeChannel(value, label) {
|
|
48
|
+
const channel = assertString(value, label);
|
|
49
|
+
if (!SUPPORTED_CHANNELS.has(channel)) {
|
|
50
|
+
throw new Error(`${label} must be alpha or release`);
|
|
51
|
+
}
|
|
52
|
+
return channel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export function assertExactFields(value, fields, label) {
|
|
57
|
+
const object = assertPlainObject(value, label);
|
|
58
|
+
const actual = Object.keys(object).sort();
|
|
59
|
+
const expected = [...fields].sort();
|
|
60
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
61
|
+
throw new Error(`${label} has an invalid field set`);
|
|
62
|
+
}
|
|
63
|
+
return object;
|
|
64
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { assertExactFields, assertString } from "./release-propagation-common.js";
|
|
2
|
+
|
|
3
|
+
const WEB_SURFACE_EXECUTION_PROFILE_CONTRACT = "kungfu-buildchain-github-web-surface-execution";
|
|
4
|
+
|
|
5
|
+
export function normalizeExecutionProfile(value, label) {
|
|
6
|
+
if (value === undefined || value === null) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const profile = assertExactFields(value, [
|
|
10
|
+
"contract",
|
|
11
|
+
"workflow",
|
|
12
|
+
"productionReleaseLabel",
|
|
13
|
+
"productionReleaseHeadPrefix",
|
|
14
|
+
"productionStatusUrl",
|
|
15
|
+
"readbackUrls",
|
|
16
|
+
"updateCommand",
|
|
17
|
+
"prepareCommand",
|
|
18
|
+
"verifyCommand",
|
|
19
|
+
], label);
|
|
20
|
+
if (profile.contract !== WEB_SURFACE_EXECUTION_PROFILE_CONTRACT) {
|
|
21
|
+
throw new Error(`${label}.contract must be ${WEB_SURFACE_EXECUTION_PROFILE_CONTRACT}`);
|
|
22
|
+
}
|
|
23
|
+
const productionStatusUrl = assertString(profile.productionStatusUrl, `${label}.productionStatusUrl`);
|
|
24
|
+
if (!/^https:\/\/[^?\s]+$/.test(productionStatusUrl)) {
|
|
25
|
+
throw new Error(`${label}.productionStatusUrl must be a stable HTTPS URL without query parameters`);
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(profile.readbackUrls) || profile.readbackUrls.length === 0) {
|
|
28
|
+
throw new Error(`${label}.readbackUrls must be a non-empty array`);
|
|
29
|
+
}
|
|
30
|
+
const readbackUrls = [...new Set(profile.readbackUrls.map((entry, index) => {
|
|
31
|
+
const url = assertString(entry, `${label}.readbackUrls[${index}]`);
|
|
32
|
+
if (!/^https:\/\/[^?\s]+$/.test(url)) {
|
|
33
|
+
throw new Error(`${label}.readbackUrls[${index}] must be a stable HTTPS URL without query parameters`);
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}))].sort();
|
|
37
|
+
if (JSON.stringify(readbackUrls) !== JSON.stringify(profile.readbackUrls)) {
|
|
38
|
+
throw new Error(`${label}.readbackUrls must be sorted and unique`);
|
|
39
|
+
}
|
|
40
|
+
const command = (entry, field) => {
|
|
41
|
+
const text = assertString(entry, `${label}.${field}`);
|
|
42
|
+
if (/[\r\n]/.test(text)) {
|
|
43
|
+
throw new Error(`${label}.${field} must be a single-line command`);
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
contract: WEB_SURFACE_EXECUTION_PROFILE_CONTRACT,
|
|
49
|
+
workflow: assertString(profile.workflow, `${label}.workflow`),
|
|
50
|
+
productionReleaseLabel: assertString(profile.productionReleaseLabel, `${label}.productionReleaseLabel`),
|
|
51
|
+
productionReleaseHeadPrefix: assertString(profile.productionReleaseHeadPrefix, `${label}.productionReleaseHeadPrefix`),
|
|
52
|
+
productionStatusUrl,
|
|
53
|
+
readbackUrls,
|
|
54
|
+
updateCommand: command(profile.updateCommand, "updateCommand"),
|
|
55
|
+
prepareCommand: command(profile.prepareCommand, "prepareCommand"),
|
|
56
|
+
verifyCommand: command(profile.verifyCommand, "verifyCommand"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertPlainObject,
|
|
3
|
+
assertString,
|
|
4
|
+
normalizeChannel,
|
|
5
|
+
optionalString,
|
|
6
|
+
} from "./release-propagation-common.js";
|
|
7
|
+
import { assertCommitSha } from "./release-propagation-work-control.js";
|
|
8
|
+
|
|
9
|
+
function normalizeReleasePassport(passport = {}) {
|
|
10
|
+
if (!passport || typeof passport !== "object" || Array.isArray(passport)) {
|
|
11
|
+
throw new Error("upstreamRelease.releasePassport must be an object");
|
|
12
|
+
}
|
|
13
|
+
const normalized = {
|
|
14
|
+
url: assertString(passport.url, "upstreamRelease.releasePassport.url"),
|
|
15
|
+
sha256: assertString(passport.sha256 || passport.digest, "upstreamRelease.releasePassport.sha256"),
|
|
16
|
+
};
|
|
17
|
+
if (!/^[0-9a-f]{64}$/.test(normalized.sha256)) {
|
|
18
|
+
throw new Error("upstreamRelease.releasePassport.sha256 must be an exact SHA-256 digest");
|
|
19
|
+
}
|
|
20
|
+
if (/[?&](?:token|signature|x-amz-|x-goog-)/i.test(normalized.url)) {
|
|
21
|
+
throw new Error("upstreamRelease.releasePassport.url must not contain signed or credential parameters");
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizePackageFact(pkg = {}) {
|
|
27
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
28
|
+
throw new Error("upstreamRelease.package must be an object");
|
|
29
|
+
}
|
|
30
|
+
const normalized = {
|
|
31
|
+
name: assertString(pkg.name, "upstreamRelease.package.name"),
|
|
32
|
+
version: assertString(pkg.version, "upstreamRelease.package.version"),
|
|
33
|
+
integrity: assertString(pkg.integrity, "upstreamRelease.package.integrity"),
|
|
34
|
+
gitHead: assertCommitSha(pkg.gitHead || pkg.git_head, "upstreamRelease.package.gitHead"),
|
|
35
|
+
};
|
|
36
|
+
if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(normalized.integrity)) {
|
|
37
|
+
throw new Error("upstreamRelease.package.integrity must be an exact sha512 SRI value");
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeDigestUrlFact(value = {}, label) {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
44
|
+
throw new Error(`${label} must be an object`);
|
|
45
|
+
}
|
|
46
|
+
const normalized = {
|
|
47
|
+
url: assertString(value.url, `${label}.url`),
|
|
48
|
+
sha256: assertString(value.sha256 || value.digest, `${label}.sha256`),
|
|
49
|
+
};
|
|
50
|
+
if (!/^[0-9a-f]{64}$/.test(normalized.sha256)) {
|
|
51
|
+
throw new Error(`${label}.sha256 must be an exact SHA-256 digest`);
|
|
52
|
+
}
|
|
53
|
+
if (/[?&](?:token|signature|x-amz-|x-goog-)/i.test(normalized.url)) {
|
|
54
|
+
throw new Error(`${label}.url must not contain signed or credential parameters`);
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeOptionalPathDigestUrlFact(value = undefined, label) {
|
|
60
|
+
if (value === undefined || value === null) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const normalized = normalizeDigestUrlFact(value, label);
|
|
64
|
+
return {
|
|
65
|
+
path: optionalString(value.path),
|
|
66
|
+
bytes: value.bytes === undefined ? undefined : Number(value.bytes),
|
|
67
|
+
...normalized,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizePublicationArtifactFact(value = undefined) {
|
|
72
|
+
if (value === undefined || value === null) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const artifact = assertPlainObject(value, "upstreamRelease.publicationArtifact");
|
|
76
|
+
return {
|
|
77
|
+
id: optionalString(artifact.id),
|
|
78
|
+
kind: optionalString(artifact.kind),
|
|
79
|
+
version: assertString(artifact.version, "upstreamRelease.publicationArtifact.version"),
|
|
80
|
+
canonicalUrl: assertString(
|
|
81
|
+
artifact.canonicalUrl || artifact.canonical_url,
|
|
82
|
+
"upstreamRelease.publicationArtifact.canonicalUrl",
|
|
83
|
+
),
|
|
84
|
+
latestUrl: assertString(
|
|
85
|
+
artifact.latestUrl || artifact.latest_url,
|
|
86
|
+
"upstreamRelease.publicationArtifact.latestUrl",
|
|
87
|
+
),
|
|
88
|
+
latestEvidenceUrl: optionalString(artifact.latestEvidenceUrl || artifact.latest_evidence_url),
|
|
89
|
+
immutableVersionUrl: assertString(
|
|
90
|
+
artifact.immutableVersionUrl || artifact.immutable_version_url || artifact.immutableVersionPrefix || artifact.immutable_version_prefix,
|
|
91
|
+
"upstreamRelease.publicationArtifact.immutableVersionUrl",
|
|
92
|
+
),
|
|
93
|
+
immutableVersionPrefix: optionalString(artifact.immutableVersionPrefix || artifact.immutable_version_prefix),
|
|
94
|
+
registry: normalizeDigestUrlFact(artifact.registry, "upstreamRelease.publicationArtifact.registry"),
|
|
95
|
+
manifest: normalizeDigestUrlFact(artifact.manifest, "upstreamRelease.publicationArtifact.manifest"),
|
|
96
|
+
passport: normalizeDigestUrlFact(artifact.passport, "upstreamRelease.publicationArtifact.passport"),
|
|
97
|
+
primaryArtifact: normalizeOptionalPathDigestUrlFact(
|
|
98
|
+
artifact.primaryArtifact || artifact.primary_artifact,
|
|
99
|
+
"upstreamRelease.publicationArtifact.primaryArtifact",
|
|
100
|
+
),
|
|
101
|
+
sourceBundle: normalizeOptionalPathDigestUrlFact(
|
|
102
|
+
artifact.sourceBundle || artifact.source_bundle,
|
|
103
|
+
"upstreamRelease.publicationArtifact.sourceBundle",
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeUpstreamRelease(input) {
|
|
109
|
+
const release = assertPlainObject(input, "upstreamRelease");
|
|
110
|
+
const publicationArtifact = normalizePublicationArtifactFact(release.publicationArtifact || release.publication_artifact);
|
|
111
|
+
const packageFact = release.package === undefined ? undefined : normalizePackageFact(release.package);
|
|
112
|
+
if (!packageFact && !publicationArtifact) {
|
|
113
|
+
throw new Error("upstreamRelease requires package or publicationArtifact");
|
|
114
|
+
}
|
|
115
|
+
const repository = assertString(release.repository, "upstreamRelease.repository");
|
|
116
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
|
|
117
|
+
throw new Error("upstreamRelease.repository must be owner/repo");
|
|
118
|
+
}
|
|
119
|
+
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
|
+
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
|
+
}
|
|
135
|
+
if (packageFact && publicationArtifact && packageFact.version !== publicationArtifact.version) {
|
|
136
|
+
throw new Error("upstreamRelease package and publication artifact versions must match");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
repository,
|
|
140
|
+
channel: normalizeChannel(release.channel, "upstreamRelease.channel"),
|
|
141
|
+
tag,
|
|
142
|
+
tagTargetSha,
|
|
143
|
+
sourceSha,
|
|
144
|
+
package: packageFact,
|
|
145
|
+
publicationArtifact,
|
|
146
|
+
releasePassport: normalizeReleasePassport(release.releasePassport || release.release_passport),
|
|
147
|
+
siteBundle: release.siteBundle || release.site_bundle
|
|
148
|
+
? {
|
|
149
|
+
manifestSha256: assertString(
|
|
150
|
+
release.siteBundle?.manifestSha256 || release.site_bundle?.manifest_sha256,
|
|
151
|
+
"upstreamRelease.siteBundle.manifestSha256",
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
: undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function verifyReleaseLockBinding({ release, lock, downstream, propagationKey }) {
|
|
159
|
+
const normalizedRelease = normalizeUpstreamRelease(release);
|
|
160
|
+
const lockRelease = normalizeUpstreamRelease({
|
|
161
|
+
repository: lock.upstream?.repository,
|
|
162
|
+
channel: lock.upstream?.channel,
|
|
163
|
+
tag: lock.upstream?.tag,
|
|
164
|
+
tagTargetSha: lock.upstream?.tagTargetSha,
|
|
165
|
+
sourceSha: lock.upstream?.sourceSha,
|
|
166
|
+
package: lock.upstream?.package,
|
|
167
|
+
publicationArtifact: lock.upstream?.publicationArtifact,
|
|
168
|
+
releasePassport: lock.upstream?.releasePassport,
|
|
169
|
+
siteBundle: lock.upstream?.siteBundle,
|
|
170
|
+
});
|
|
171
|
+
if (JSON.stringify(lockRelease) !== JSON.stringify(normalizedRelease)) {
|
|
172
|
+
throw new Error("release propagation work upstream release disagrees with its lock");
|
|
173
|
+
}
|
|
174
|
+
const expectedCoordinates = {
|
|
175
|
+
target: lock.downstream?.node,
|
|
176
|
+
repository: lock.downstream?.repository,
|
|
177
|
+
channel: lock.downstream?.channel,
|
|
178
|
+
baseRef: lock.downstream?.baseRef,
|
|
179
|
+
branch: lock.propagation?.branch,
|
|
180
|
+
lockPath: lock.downstream?.lockPath,
|
|
181
|
+
propagationKey: lock.propagation?.propagationKey,
|
|
182
|
+
};
|
|
183
|
+
const actualCoordinates = {
|
|
184
|
+
target: downstream.target,
|
|
185
|
+
repository: downstream.repository,
|
|
186
|
+
channel: downstream.channel,
|
|
187
|
+
baseRef: downstream.baseRef,
|
|
188
|
+
branch: downstream.branch,
|
|
189
|
+
lockPath: downstream.lockPath,
|
|
190
|
+
propagationKey,
|
|
191
|
+
};
|
|
192
|
+
if (JSON.stringify(actualCoordinates) !== JSON.stringify(expectedCoordinates)) {
|
|
193
|
+
throw new Error("release propagation work downstream coordinates disagree with its lock");
|
|
194
|
+
}
|
|
195
|
+
return normalizedRelease;
|
|
196
|
+
}
|