@kungfu-tech/buildchain 2.14.18 → 2.14.19-alpha.1
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/dist/site/buildchain-contract.json +51 -21
- package/dist/site/buildchain-site.json +26 -5
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +43 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/node-api-registry.json +16 -3
- package/dist/site/page-registry.json +22 -1
- package/dist/site/public-surface-audit.json +21 -6
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +4 -4
- package/dist/site/workflow-registry.json +14 -3
- package/docs/release-activation-transaction.md +38 -0
- package/package.json +2 -1
- package/packages/core/index.js +14 -0
- package/packages/core/publication-authority.js +5 -1
- package/packages/core/release-activation-transaction.js +325 -0
- package/packages/core/self-dogfood-version.js +85 -0
- package/scripts/check-inventory.mjs +21 -9
- package/scripts/generate-site-bundle.mjs +2 -0
- package/scripts/installer-publication.mjs +15 -5
- package/scripts/publication-commit-evidence.mjs +298 -12
- package/scripts/release-candidate-resolver.mjs +50 -3
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
6
7
|
|
|
7
8
|
const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
|
|
9
|
+
const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
|
|
8
10
|
|
|
9
11
|
function requiredString(value, label) {
|
|
10
12
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -52,14 +54,194 @@ function publicHttps(value, label) {
|
|
|
52
54
|
return normalized;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function canonical(value) {
|
|
58
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
59
|
+
if (value && typeof value === "object") {
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(value)
|
|
62
|
+
.filter(([, item]) => item !== undefined)
|
|
63
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
64
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function digest(bytes) {
|
|
71
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function semanticRoot(value) {
|
|
75
|
+
return digest(Buffer.from(JSON.stringify(canonical(value))));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function expectedContentType(assetPath) {
|
|
79
|
+
if (assetPath.endsWith(".json")) return "application/json; charset=utf-8";
|
|
80
|
+
if (assetPath.endsWith(".sh")) return "text/x-shellscript; charset=utf-8";
|
|
81
|
+
if (assetPath.endsWith(".ps1")) return "text/plain; charset=utf-8";
|
|
82
|
+
throw new Error(`installer bundle asset type is unsupported: ${assetPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateInstallerBundle(evidence, expected) {
|
|
86
|
+
const bundle = evidence.publication?.installerBundle;
|
|
87
|
+
if (!bundle) return null;
|
|
88
|
+
if (bundle.schema !== INSTALLER_BUNDLE_SCHEMA) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`installer bundle schema must be ${INSTALLER_BUNDLE_SCHEMA}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const bundleRoot = sha256Root(
|
|
94
|
+
bundle.bundleRoot,
|
|
95
|
+
"installerBundle.bundleRoot",
|
|
96
|
+
);
|
|
97
|
+
if (
|
|
98
|
+
bundleRoot !== evidence.publication.payloadRoot ||
|
|
99
|
+
bundleRoot !== evidence.readback?.payloadRoot
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"installer bundle root must be the publication payload root",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
|
|
107
|
+
expected.sourceSha ||
|
|
108
|
+
!["alpha", "stable"].includes(bundle.channel)
|
|
109
|
+
) {
|
|
110
|
+
throw new Error("installer bundle release identity mismatch");
|
|
111
|
+
}
|
|
112
|
+
sha256Root(bundle.channelPayloadRoot, "installerBundle.channelPayloadRoot");
|
|
113
|
+
sha256Root(bundle.channelFileDigest, "installerBundle.channelFileDigest");
|
|
114
|
+
sha256Root(
|
|
115
|
+
bundle.releasePassport?.root,
|
|
116
|
+
"installerBundle.releasePassport.root",
|
|
117
|
+
);
|
|
118
|
+
sha256Root(bundle.manifestDigest, "installerBundle.manifestDigest");
|
|
119
|
+
if (
|
|
120
|
+
evidence.readback?.manifestDigest !== bundle.manifestDigest ||
|
|
121
|
+
!Array.isArray(bundle.assets) ||
|
|
122
|
+
bundle.assets.length !== 7
|
|
123
|
+
) {
|
|
124
|
+
throw new Error("installer bundle read-back or asset set is incomplete");
|
|
125
|
+
}
|
|
126
|
+
const paths = new Set();
|
|
127
|
+
const topLevel = new Set([
|
|
128
|
+
"installer-publication.json",
|
|
129
|
+
"channel-index.json",
|
|
130
|
+
"trusted-keys.json",
|
|
131
|
+
"install.sh",
|
|
132
|
+
"install.ps1",
|
|
133
|
+
]);
|
|
134
|
+
const expectedRoles = new Map([
|
|
135
|
+
["installer-publication.json", "publication-manifest"],
|
|
136
|
+
["channel-index.json", "signed-channel-index"],
|
|
137
|
+
["trusted-keys.json", "public-trust-anchors"],
|
|
138
|
+
["install.sh", "friendly-installer"],
|
|
139
|
+
["install.ps1", "friendly-installer"],
|
|
140
|
+
]);
|
|
141
|
+
const immutableDirectories = new Set();
|
|
142
|
+
let immutableShell = 0;
|
|
143
|
+
let immutablePowerShell = 0;
|
|
144
|
+
const releaseBaseUrl =
|
|
145
|
+
`https://github.com/kungfu-systems/kungfu/releases/download/` +
|
|
146
|
+
expected.releaseTag;
|
|
147
|
+
for (const asset of bundle.assets) {
|
|
148
|
+
const assetPath = requiredString(
|
|
149
|
+
asset.path,
|
|
150
|
+
"installer bundle asset path",
|
|
151
|
+
).replaceAll("\\", "/");
|
|
152
|
+
if (
|
|
153
|
+
assetPath.startsWith("/") ||
|
|
154
|
+
assetPath.endsWith("/") ||
|
|
155
|
+
assetPath.split("/").some((part) => part === "" || part === "..") ||
|
|
156
|
+
paths.has(assetPath)
|
|
157
|
+
) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`unsafe or duplicate installer bundle asset: ${assetPath}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
paths.add(assetPath);
|
|
163
|
+
topLevel.delete(assetPath);
|
|
164
|
+
if (assetPath.includes("/") && assetPath.endsWith("/install.sh")) {
|
|
165
|
+
immutableShell += 1;
|
|
166
|
+
immutableDirectories.add(path.posix.dirname(assetPath));
|
|
167
|
+
}
|
|
168
|
+
if (assetPath.includes("/") && assetPath.endsWith("/install.ps1")) {
|
|
169
|
+
immutablePowerShell += 1;
|
|
170
|
+
immutableDirectories.add(path.posix.dirname(assetPath));
|
|
171
|
+
}
|
|
172
|
+
if (!Number.isSafeInteger(asset.size) || asset.size < 1) {
|
|
173
|
+
throw new Error(`installer bundle asset size is invalid: ${assetPath}`);
|
|
174
|
+
}
|
|
175
|
+
sha256Root(asset.digest, `installer bundle asset digest: ${assetPath}`);
|
|
176
|
+
const releaseAsset = requiredString(
|
|
177
|
+
asset.releaseAsset,
|
|
178
|
+
`installer bundle release asset: ${assetPath}`,
|
|
179
|
+
);
|
|
180
|
+
if (
|
|
181
|
+
asset.contentType !== expectedContentType(assetPath) ||
|
|
182
|
+
publicHttps(
|
|
183
|
+
asset.releaseUrl,
|
|
184
|
+
`installer bundle asset URL: ${assetPath}`,
|
|
185
|
+
) !== `${releaseBaseUrl}/${releaseAsset}` ||
|
|
186
|
+
!/^kungfu-[a-z0-9.-]+$/.test(releaseAsset)
|
|
187
|
+
) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`installer bundle asset transport metadata is invalid: ${assetPath}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const expectedRole = assetPath.includes("/")
|
|
193
|
+
? "immutable-installer"
|
|
194
|
+
: expectedRoles.get(assetPath);
|
|
195
|
+
if (asset.role !== expectedRole) {
|
|
196
|
+
throw new Error(`installer bundle asset role is invalid: ${assetPath}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const immutableDirectory = [...immutableDirectories][0] || "";
|
|
200
|
+
const immutableParts = immutableDirectory.split("/");
|
|
201
|
+
if (
|
|
202
|
+
topLevel.size !== 0 ||
|
|
203
|
+
immutableShell !== 1 ||
|
|
204
|
+
immutablePowerShell !== 1 ||
|
|
205
|
+
immutableDirectories.size !== 1 ||
|
|
206
|
+
immutableParts.length !== 5 ||
|
|
207
|
+
immutableParts[0] !== "installers" ||
|
|
208
|
+
immutableParts[1] !== "v1" ||
|
|
209
|
+
immutableParts[2] !== bundle.channel ||
|
|
210
|
+
immutableParts[3] !== expected.version ||
|
|
211
|
+
!/^[a-f0-9]{64}$/.test(immutableParts[4])
|
|
212
|
+
) {
|
|
213
|
+
throw new Error("installer bundle asset topology is incomplete");
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
bundle.cachePolicy?.friendly !== "public,max-age=300,must-revalidate" ||
|
|
217
|
+
bundle.cachePolicy?.immutable !== "public,max-age=31536000,immutable"
|
|
218
|
+
) {
|
|
219
|
+
throw new Error("installer bundle cache policy is invalid");
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
evidence.siteHandoff?.state !== "deferred-to-site-owned-consumer" ||
|
|
223
|
+
evidence.siteHandoff?.productionAvailable !== false ||
|
|
224
|
+
evidence.siteHandoff?.requiredBundleRoot !== bundleRoot
|
|
225
|
+
) {
|
|
226
|
+
throw new Error("installer bundle site handoff must remain deferred");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
schema: bundle.schema,
|
|
230
|
+
bundleRoot,
|
|
231
|
+
manifestDigest: bundle.manifestDigest,
|
|
232
|
+
channel: bundle.channel,
|
|
233
|
+
channelPayloadRoot: bundle.channelPayloadRoot,
|
|
234
|
+
channelFileDigest: bundle.channelFileDigest,
|
|
235
|
+
releasePassport: bundle.releasePassport,
|
|
236
|
+
cachePolicy: bundle.cachePolicy,
|
|
237
|
+
immutablePath: immutableDirectory,
|
|
238
|
+
assets: bundle.assets,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
55
242
|
export function validatePublicationCommitEvidence(
|
|
56
243
|
evidence,
|
|
57
|
-
{
|
|
58
|
-
version,
|
|
59
|
-
sourceSha,
|
|
60
|
-
releaseSha,
|
|
61
|
-
releaseTag,
|
|
62
|
-
} = {},
|
|
244
|
+
{ version, sourceSha, releaseSha, releaseTag } = {},
|
|
63
245
|
) {
|
|
64
246
|
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
|
|
65
247
|
throw new Error("publication commit evidence must be an object");
|
|
@@ -107,6 +289,7 @@ export function validatePublicationCommitEvidence(
|
|
|
107
289
|
"publication recovery must preserve or explicitly declare no previous authority",
|
|
108
290
|
);
|
|
109
291
|
}
|
|
292
|
+
const installerBundle = validateInstallerBundle(evidence, expected);
|
|
110
293
|
return {
|
|
111
294
|
schema: SCHEMA,
|
|
112
295
|
status: "passed",
|
|
@@ -117,10 +300,109 @@ export function validatePublicationCommitEvidence(
|
|
|
117
300
|
previousAuthority,
|
|
118
301
|
rollbackReference,
|
|
119
302
|
},
|
|
303
|
+
...(installerBundle ? { installerBundle } : {}),
|
|
120
304
|
};
|
|
121
305
|
}
|
|
122
306
|
|
|
123
|
-
function
|
|
307
|
+
export async function verifyInstallerBundleReadback(
|
|
308
|
+
result,
|
|
309
|
+
fetchImpl = globalThis.fetch,
|
|
310
|
+
) {
|
|
311
|
+
const bundle = result.installerBundle;
|
|
312
|
+
if (!bundle) return null;
|
|
313
|
+
if (typeof fetchImpl !== "function") {
|
|
314
|
+
throw new Error("installer bundle read-back requires fetch");
|
|
315
|
+
}
|
|
316
|
+
const manifestResponse = await fetchImpl(result.publicUrl, {
|
|
317
|
+
redirect: "manual",
|
|
318
|
+
cache: "no-store",
|
|
319
|
+
});
|
|
320
|
+
if (manifestResponse.status !== 200) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`installer bundle manifest read-back failed: HTTP ${manifestResponse.status}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
|
|
326
|
+
if (digest(manifestBytes) !== bundle.manifestDigest) {
|
|
327
|
+
throw new Error("installer bundle manifest digest mismatch");
|
|
328
|
+
}
|
|
329
|
+
const manifest = JSON.parse(manifestBytes);
|
|
330
|
+
const unsigned = Object.fromEntries(
|
|
331
|
+
Object.entries(manifest).filter(([key]) => key !== "bundleRoot"),
|
|
332
|
+
);
|
|
333
|
+
if (
|
|
334
|
+
manifest.schema !== INSTALLER_BUNDLE_SCHEMA ||
|
|
335
|
+
manifest.bundleRoot !== bundle.bundleRoot ||
|
|
336
|
+
semanticRoot(unsigned) !== bundle.bundleRoot ||
|
|
337
|
+
manifest.package?.name !== "@kungfu-tech/site" ||
|
|
338
|
+
typeof manifest.package?.version !== "string" ||
|
|
339
|
+
manifest.identity?.sourceCommit !== result.identity.sourceSha ||
|
|
340
|
+
manifest.identity?.releaseSha !== result.identity.releaseSha ||
|
|
341
|
+
manifest.identity?.releaseTag !== result.identity.releaseTag ||
|
|
342
|
+
manifest.identity?.version !== result.identity.version ||
|
|
343
|
+
manifest.identity?.channel !== bundle.channel ||
|
|
344
|
+
manifest.identity?.channelPayloadRoot !== bundle.channelPayloadRoot ||
|
|
345
|
+
manifest.identity?.channelFileDigest !== bundle.channelFileDigest ||
|
|
346
|
+
manifest.identity?.releasePassport?.root !== bundle.releasePassport.root ||
|
|
347
|
+
manifest.distribution?.repository !== "kungfu-systems/kungfu" ||
|
|
348
|
+
manifest.routes?.immutablePath !== bundle.immutablePath ||
|
|
349
|
+
manifest.routes?.friendly?.["install.sh"] !==
|
|
350
|
+
"https://kungfu.tech/install.sh" ||
|
|
351
|
+
manifest.routes?.friendly?.["install.ps1"] !==
|
|
352
|
+
"https://kungfu.tech/install.ps1" ||
|
|
353
|
+
JSON.stringify(canonical(manifest.cachePolicy)) !==
|
|
354
|
+
JSON.stringify(canonical(bundle.cachePolicy)) ||
|
|
355
|
+
JSON.stringify(canonical(manifest.assets)) !==
|
|
356
|
+
JSON.stringify(canonical(bundle.assets)) ||
|
|
357
|
+
`${manifest.distribution?.releaseBaseUrl}/` +
|
|
358
|
+
manifest.distribution?.manifestAsset !==
|
|
359
|
+
result.publicUrl
|
|
360
|
+
) {
|
|
361
|
+
throw new Error("installer bundle manifest root mismatch");
|
|
362
|
+
}
|
|
363
|
+
const observations = [];
|
|
364
|
+
const byUrl = new Map();
|
|
365
|
+
for (const asset of bundle.assets) {
|
|
366
|
+
let observation = byUrl.get(asset.releaseUrl);
|
|
367
|
+
if (!observation) {
|
|
368
|
+
const response = await fetchImpl(asset.releaseUrl, {
|
|
369
|
+
redirect: "manual",
|
|
370
|
+
cache: "no-store",
|
|
371
|
+
});
|
|
372
|
+
if (response.status !== 200) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`installer bundle asset read-back failed: HTTP ${response.status}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
378
|
+
observation = {
|
|
379
|
+
releaseUrl: asset.releaseUrl,
|
|
380
|
+
size: bytes.length,
|
|
381
|
+
digest: digest(bytes),
|
|
382
|
+
};
|
|
383
|
+
byUrl.set(asset.releaseUrl, observation);
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
observation.size !== asset.size ||
|
|
387
|
+
observation.digest !== asset.digest
|
|
388
|
+
) {
|
|
389
|
+
throw new Error(`installer bundle asset drifted: ${asset.path}`);
|
|
390
|
+
}
|
|
391
|
+
observations.push({ path: asset.path, ...observation });
|
|
392
|
+
}
|
|
393
|
+
const seal = {
|
|
394
|
+
schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
|
|
395
|
+
bundleRoot: bundle.bundleRoot,
|
|
396
|
+
manifestDigest: bundle.manifestDigest,
|
|
397
|
+
sourceCommit: result.identity.sourceSha,
|
|
398
|
+
releaseTag: result.identity.releaseTag,
|
|
399
|
+
releasePassport: bundle.releasePassport,
|
|
400
|
+
observations,
|
|
401
|
+
};
|
|
402
|
+
return { ...seal, sealRoot: semanticRoot(seal) };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function main(args) {
|
|
124
406
|
const options = {};
|
|
125
407
|
for (let index = 0; index < args.length; index += 1) {
|
|
126
408
|
const value = args[index];
|
|
@@ -138,21 +420,25 @@ function main(args) {
|
|
|
138
420
|
JSON.parse(fs.readFileSync(evidencePath, "utf8")),
|
|
139
421
|
options,
|
|
140
422
|
);
|
|
141
|
-
|
|
423
|
+
const installerBundleSeal = await verifyInstallerBundleReadback(result);
|
|
424
|
+
process.stdout.write(
|
|
425
|
+
`${JSON.stringify({
|
|
426
|
+
...result,
|
|
427
|
+
...(installerBundleSeal ? { installerBundleSeal } : {}),
|
|
428
|
+
})}\n`,
|
|
429
|
+
);
|
|
142
430
|
}
|
|
143
431
|
|
|
144
432
|
if (
|
|
145
433
|
process.argv[1] &&
|
|
146
434
|
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
147
435
|
) {
|
|
148
|
-
|
|
149
|
-
main(process.argv.slice(2));
|
|
150
|
-
} catch (error) {
|
|
436
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
151
437
|
console.error(
|
|
152
438
|
`publication commit evidence failed: ${
|
|
153
439
|
error instanceof Error ? error.message : String(error)
|
|
154
440
|
}`,
|
|
155
441
|
);
|
|
156
442
|
process.exit(1);
|
|
157
|
-
}
|
|
443
|
+
});
|
|
158
444
|
}
|
|
@@ -402,7 +402,9 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
402
402
|
const repoInfo = splitRepository(repository);
|
|
403
403
|
const sha = assertSha(targetSha, "targetSha");
|
|
404
404
|
const normalizedTarget = normalizeBranch(targetRef);
|
|
405
|
-
|
|
405
|
+
const releaseCandidateTarget = /^(alpha|release)\/v\d+\/v\d+\.\d+$/.test(normalizedTarget);
|
|
406
|
+
const majorGateTarget = normalizedTarget === "publish-gate/major" || normalizedTarget === "major-gate";
|
|
407
|
+
if (!releaseCandidateTarget && !majorGateTarget) {
|
|
406
408
|
return {
|
|
407
409
|
enabled: false,
|
|
408
410
|
reason: `target ref ${normalizedTarget || "(empty)"} does not require release-candidate promotion`,
|
|
@@ -414,14 +416,49 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
414
416
|
fetchImpl,
|
|
415
417
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/commits/${sha}/pulls`,
|
|
416
418
|
});
|
|
417
|
-
const
|
|
419
|
+
const channelPullRequest = selectMergedChannelPullRequest({
|
|
418
420
|
pullRequests: Array.isArray(pulls) ? pulls : [],
|
|
419
421
|
targetRef: normalizedTarget,
|
|
420
422
|
repository: repoInfo.fullName,
|
|
421
423
|
});
|
|
422
|
-
if (!
|
|
424
|
+
if (!channelPullRequest) {
|
|
423
425
|
throw new Error(`no same-repository merged channel PR found for ${sha} into ${normalizedTarget}`);
|
|
424
426
|
}
|
|
427
|
+
let pullRequest = channelPullRequest;
|
|
428
|
+
if (majorGateTarget) {
|
|
429
|
+
const releaseRef = normalizeBranch(channelPullRequest.head?.ref || "");
|
|
430
|
+
if (!/^release\/v\d+\/v\d+\.\d+$/.test(releaseRef)) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`major gate ${normalizedTarget} must be merged from a release/vN/vN.M head, got ${releaseRef || "<empty>"}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const releaseSha = assertSha(channelPullRequest.head?.sha, "major gate release head SHA");
|
|
436
|
+
const releasePulls = await githubJson({
|
|
437
|
+
apiUrl,
|
|
438
|
+
token,
|
|
439
|
+
fetchImpl,
|
|
440
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/commits/${releaseSha}/pulls`,
|
|
441
|
+
});
|
|
442
|
+
pullRequest = selectMergedChannelPullRequest({
|
|
443
|
+
pullRequests: Array.isArray(releasePulls) ? releasePulls : [],
|
|
444
|
+
targetRef: releaseRef,
|
|
445
|
+
repository: repoInfo.fullName,
|
|
446
|
+
});
|
|
447
|
+
if (!pullRequest) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`no same-repository merged release-candidate PR found for major gate release head ${releaseSha} into ${releaseRef}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const releaseMergeSha = assertSha(
|
|
453
|
+
pullRequest.merge_commit_sha || pullRequest.mergeCommit?.oid,
|
|
454
|
+
"release-candidate PR merge SHA",
|
|
455
|
+
);
|
|
456
|
+
if (releaseMergeSha !== releaseSha) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`major gate release head ${releaseSha} does not equal release-candidate PR #${pullRequest.number} merge ${releaseMergeSha}`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
425
462
|
const timeoutMs = Number(waitSeconds) * 1000;
|
|
426
463
|
const intervalMs = Number(pollIntervalMs);
|
|
427
464
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
@@ -501,6 +538,16 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
501
538
|
headRef: pullRequest.head?.ref || "",
|
|
502
539
|
baseRef: pullRequest.base?.ref || "",
|
|
503
540
|
},
|
|
541
|
+
...(majorGateTarget
|
|
542
|
+
? {
|
|
543
|
+
promotionPullRequest: {
|
|
544
|
+
number: channelPullRequest.number,
|
|
545
|
+
url: channelPullRequest.html_url || channelPullRequest.url || "",
|
|
546
|
+
headRef: channelPullRequest.head?.ref || "",
|
|
547
|
+
baseRef: channelPullRequest.base?.ref || "",
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
: {}),
|
|
504
551
|
run: {
|
|
505
552
|
id: String(run.id || ""),
|
|
506
553
|
url: run.html_url || run.url || "",
|