@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
|
@@ -1,37 +1,23 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
assertPlainObject,
|
|
5
|
+
assertString,
|
|
6
|
+
RELEASE_PROPAGATION_PLAN_CONTRACT,
|
|
7
|
+
normalizeChannel,
|
|
8
|
+
optionalString,
|
|
9
|
+
sha256Json,
|
|
10
|
+
stableJson,
|
|
11
|
+
} from "./release-propagation-common.js";
|
|
12
|
+
import { normalizeExecutionProfile } from "./release-propagation-execution-profile.js";
|
|
13
|
+
import { normalizeUpstreamRelease } from "./release-propagation-release.js";
|
|
4
14
|
|
|
5
15
|
export const RELEASE_PROPAGATION_GRAPH_CONTRACT = "kungfu-buildchain-release-propagation-graph";
|
|
6
|
-
export
|
|
16
|
+
export { RELEASE_PROPAGATION_PLAN_CONTRACT };
|
|
7
17
|
export const RELEASE_PROPAGATION_LOCK_CONTRACT = "kungfu-buildchain-release-propagation-lock";
|
|
8
18
|
export const RELEASE_PROPAGATION_RECEIPT_CONTRACT = "kungfu-buildchain-release-propagation-receipt";
|
|
9
|
-
|
|
10
|
-
const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
|
|
11
19
|
const SUPPORTED_CHANNEL_POLICIES = new Set(["preserve", "explicit"]);
|
|
12
20
|
|
|
13
|
-
function stableJson(value) {
|
|
14
|
-
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function sortJson(value) {
|
|
18
|
-
if (Array.isArray(value)) {
|
|
19
|
-
return value.map(sortJson);
|
|
20
|
-
}
|
|
21
|
-
if (!value || typeof value !== "object") {
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
return Object.fromEntries(
|
|
25
|
-
Object.entries(value)
|
|
26
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
27
|
-
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function sha256Json(value) {
|
|
32
|
-
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
21
|
function releaseVersion(upstreamRelease) {
|
|
36
22
|
return upstreamRelease.package?.version || upstreamRelease.publicationArtifact?.version || "";
|
|
37
23
|
}
|
|
@@ -75,32 +61,6 @@ function propagationBranch({ upstreamRelease, targetNode }) {
|
|
|
75
61
|
return `buildchain/release-propagation/${repository}/${version}-${upstreamRelease.channel}-${digest}`;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
function assertPlainObject(value, label) {
|
|
79
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
80
|
-
throw new Error(`${label} must be an object`);
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function assertString(value, label) {
|
|
86
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
87
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
88
|
-
}
|
|
89
|
-
return value.trim();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function optionalString(value) {
|
|
93
|
-
return value === undefined || value === null ? "" : String(value).trim();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function normalizeChannel(value, label) {
|
|
97
|
-
const channel = assertString(value, label);
|
|
98
|
-
if (!SUPPORTED_CHANNELS.has(channel)) {
|
|
99
|
-
throw new Error(`${label} must be alpha or release`);
|
|
100
|
-
}
|
|
101
|
-
return channel;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
64
|
function normalizeChannelMap(edge, label) {
|
|
105
65
|
const policy = edge.channelPolicy || "preserve";
|
|
106
66
|
if (!SUPPORTED_CHANNEL_POLICIES.has(policy)) {
|
|
@@ -130,6 +90,7 @@ function normalizeNode(node, index) {
|
|
|
130
90
|
lockPath: optionalString(node.lockPath || node.lock_path),
|
|
131
91
|
baseRef: optionalString(node.baseRef || node.base_ref),
|
|
132
92
|
workflow: optionalString(node.workflow),
|
|
93
|
+
executionProfile: normalizeExecutionProfile(node.executionProfile || node.execution_profile, `nodes[${index}].executionProfile`),
|
|
133
94
|
};
|
|
134
95
|
if (!/^[^/\s]+\/[^/\s]+$/.test(normalized.repository)) {
|
|
135
96
|
throw new Error(`nodes[${index}].repository must be owner/repo`);
|
|
@@ -235,112 +196,6 @@ export function resolvePropagationChannel(edge, upstreamChannel) {
|
|
|
235
196
|
return downstreamChannel ? normalizeChannel(downstreamChannel, `edge ${edge.id} downstream channel`) : "";
|
|
236
197
|
}
|
|
237
198
|
|
|
238
|
-
function normalizeReleasePassport(passport = {}) {
|
|
239
|
-
if (!passport || typeof passport !== "object" || Array.isArray(passport)) {
|
|
240
|
-
throw new Error("upstreamRelease.releasePassport must be an object");
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
url: assertString(passport.url, "upstreamRelease.releasePassport.url"),
|
|
244
|
-
sha256: assertString(passport.sha256 || passport.digest, "upstreamRelease.releasePassport.sha256"),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function normalizePackageFact(pkg = {}) {
|
|
249
|
-
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
250
|
-
throw new Error("upstreamRelease.package must be an object");
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
name: assertString(pkg.name, "upstreamRelease.package.name"),
|
|
254
|
-
version: assertString(pkg.version, "upstreamRelease.package.version"),
|
|
255
|
-
integrity: assertString(pkg.integrity, "upstreamRelease.package.integrity"),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function normalizeDigestUrlFact(value = {}, label) {
|
|
260
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
261
|
-
throw new Error(`${label} must be an object`);
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
url: assertString(value.url, `${label}.url`),
|
|
265
|
-
sha256: assertString(value.sha256 || value.digest, `${label}.sha256`),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function normalizeOptionalPathDigestUrlFact(value = undefined, label) {
|
|
270
|
-
if (value === undefined || value === null) {
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
const normalized = normalizeDigestUrlFact(value, label);
|
|
274
|
-
return {
|
|
275
|
-
path: optionalString(value.path),
|
|
276
|
-
bytes: value.bytes === undefined ? undefined : Number(value.bytes),
|
|
277
|
-
...normalized,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function normalizePublicationArtifactFact(value = undefined) {
|
|
282
|
-
if (value === undefined || value === null) {
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
const artifact = assertPlainObject(value, "upstreamRelease.publicationArtifact");
|
|
286
|
-
return {
|
|
287
|
-
id: optionalString(artifact.id),
|
|
288
|
-
kind: optionalString(artifact.kind),
|
|
289
|
-
version: assertString(artifact.version, "upstreamRelease.publicationArtifact.version"),
|
|
290
|
-
canonicalUrl: assertString(
|
|
291
|
-
artifact.canonicalUrl || artifact.canonical_url,
|
|
292
|
-
"upstreamRelease.publicationArtifact.canonicalUrl",
|
|
293
|
-
),
|
|
294
|
-
latestUrl: assertString(
|
|
295
|
-
artifact.latestUrl || artifact.latest_url,
|
|
296
|
-
"upstreamRelease.publicationArtifact.latestUrl",
|
|
297
|
-
),
|
|
298
|
-
latestEvidenceUrl: optionalString(artifact.latestEvidenceUrl || artifact.latest_evidence_url),
|
|
299
|
-
immutableVersionUrl: assertString(
|
|
300
|
-
artifact.immutableVersionUrl || artifact.immutable_version_url || artifact.immutableVersionPrefix || artifact.immutable_version_prefix,
|
|
301
|
-
"upstreamRelease.publicationArtifact.immutableVersionUrl",
|
|
302
|
-
),
|
|
303
|
-
immutableVersionPrefix: optionalString(artifact.immutableVersionPrefix || artifact.immutable_version_prefix),
|
|
304
|
-
registry: normalizeDigestUrlFact(artifact.registry, "upstreamRelease.publicationArtifact.registry"),
|
|
305
|
-
manifest: normalizeDigestUrlFact(artifact.manifest, "upstreamRelease.publicationArtifact.manifest"),
|
|
306
|
-
passport: normalizeDigestUrlFact(artifact.passport, "upstreamRelease.publicationArtifact.passport"),
|
|
307
|
-
primaryArtifact: normalizeOptionalPathDigestUrlFact(
|
|
308
|
-
artifact.primaryArtifact || artifact.primary_artifact,
|
|
309
|
-
"upstreamRelease.publicationArtifact.primaryArtifact",
|
|
310
|
-
),
|
|
311
|
-
sourceBundle: normalizeOptionalPathDigestUrlFact(
|
|
312
|
-
artifact.sourceBundle || artifact.source_bundle,
|
|
313
|
-
"upstreamRelease.publicationArtifact.sourceBundle",
|
|
314
|
-
),
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function normalizeUpstreamRelease(input) {
|
|
319
|
-
const release = assertPlainObject(input, "upstreamRelease");
|
|
320
|
-
const publicationArtifact = normalizePublicationArtifactFact(release.publicationArtifact || release.publication_artifact);
|
|
321
|
-
const packageFact = release.package === undefined ? undefined : normalizePackageFact(release.package);
|
|
322
|
-
if (!packageFact && !publicationArtifact) {
|
|
323
|
-
throw new Error("upstreamRelease requires package or publicationArtifact");
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
repository: assertString(release.repository, "upstreamRelease.repository"),
|
|
327
|
-
channel: normalizeChannel(release.channel, "upstreamRelease.channel"),
|
|
328
|
-
tag: assertString(release.tag, "upstreamRelease.tag"),
|
|
329
|
-
sourceSha: assertString(release.sourceSha || release.source_sha, "upstreamRelease.sourceSha"),
|
|
330
|
-
package: packageFact,
|
|
331
|
-
publicationArtifact,
|
|
332
|
-
releasePassport: normalizeReleasePassport(release.releasePassport || release.release_passport),
|
|
333
|
-
siteBundle: release.siteBundle || release.site_bundle
|
|
334
|
-
? {
|
|
335
|
-
manifestSha256: assertString(
|
|
336
|
-
release.siteBundle?.manifestSha256 || release.site_bundle?.manifest_sha256,
|
|
337
|
-
"upstreamRelease.siteBundle.manifestSha256",
|
|
338
|
-
),
|
|
339
|
-
}
|
|
340
|
-
: undefined,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
199
|
export function createReleasePropagationLock({
|
|
345
200
|
graph,
|
|
346
201
|
edge,
|
|
@@ -359,6 +214,7 @@ export function createReleasePropagationLock({
|
|
|
359
214
|
repository: upstreamRelease.repository,
|
|
360
215
|
channel: upstreamRelease.channel,
|
|
361
216
|
tag: upstreamRelease.tag,
|
|
217
|
+
tagTargetSha: upstreamRelease.tagTargetSha,
|
|
362
218
|
sourceSha: upstreamRelease.sourceSha,
|
|
363
219
|
package: upstreamRelease.package,
|
|
364
220
|
publicationArtifact: upstreamRelease.publicationArtifact,
|
|
@@ -371,6 +227,7 @@ export function createReleasePropagationLock({
|
|
|
371
227
|
channel: downstreamChannel,
|
|
372
228
|
baseRef: edge.prBaseRef || targetNode.baseRef,
|
|
373
229
|
lockPath: edge.lockPath || targetNode.lockPath || ".buildchain/upstream-release.lock.json",
|
|
230
|
+
executionProfile: targetNode.executionProfile,
|
|
374
231
|
},
|
|
375
232
|
propagation: {
|
|
376
233
|
graphContract: graph.contract,
|
|
@@ -423,6 +280,7 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
|
|
|
423
280
|
channel: downstreamChannel,
|
|
424
281
|
baseRef: lock.downstream.baseRef,
|
|
425
282
|
lockPath: lock.downstream.lockPath,
|
|
283
|
+
executionProfile: lock.downstream.executionProfile,
|
|
426
284
|
propagationKey: lock.propagation.propagationKey,
|
|
427
285
|
branch: lock.propagation.branch,
|
|
428
286
|
lock,
|
|
@@ -548,6 +406,7 @@ export function createReleasePropagationReceipt({
|
|
|
548
406
|
name: upstreamRelease.package.name,
|
|
549
407
|
version: upstreamRelease.package.version,
|
|
550
408
|
integrity: upstreamRelease.package.integrity,
|
|
409
|
+
gitHead: upstreamRelease.package.gitHead,
|
|
551
410
|
}
|
|
552
411
|
: null,
|
|
553
412
|
},
|
|
@@ -576,6 +435,7 @@ export function createReleasePropagationReceipt({
|
|
|
576
435
|
upstream: {
|
|
577
436
|
repository: upstreamRelease.repository,
|
|
578
437
|
tag: upstreamRelease.tag,
|
|
438
|
+
tagTargetSha: upstreamRelease.tagTargetSha,
|
|
579
439
|
sourceSha: upstreamRelease.sourceSha,
|
|
580
440
|
releasePassport: upstreamRelease.releasePassport,
|
|
581
441
|
},
|
|
@@ -595,3 +455,19 @@ export function createReleasePropagationReceipt({
|
|
|
595
455
|
receiptSha256: sha256Json(body),
|
|
596
456
|
};
|
|
597
457
|
}
|
|
458
|
+
|
|
459
|
+
export {
|
|
460
|
+
RELEASE_PROPAGATION_STAGE_RECEIPT_CONTRACT,
|
|
461
|
+
RELEASE_PROPAGATION_WORK_CONTRACT,
|
|
462
|
+
RELEASE_PROPAGATION_WORK_STAGES,
|
|
463
|
+
createReleasePropagationStageReceipt,
|
|
464
|
+
createReleasePropagationWork,
|
|
465
|
+
resumeReleasePropagationWork,
|
|
466
|
+
verifyReleasePropagationWork,
|
|
467
|
+
} from "./release-propagation-work.js";
|
|
468
|
+
export {
|
|
469
|
+
claimReleasePropagationWork,
|
|
470
|
+
completeReleasePropagationWork,
|
|
471
|
+
recordReleasePropagationStage,
|
|
472
|
+
repairReleasePropagationWork,
|
|
473
|
+
} from "./release-propagation-work-transitions.js";
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { digest } from "./aws-runner-burst-core.mjs";
|
|
4
|
+
import {
|
|
5
|
+
WINDOWS_EC2_JIT,
|
|
6
|
+
windowsJitRunnerLabel,
|
|
7
|
+
windowsJitRunnerLabels,
|
|
8
|
+
} from "./aws-windows-jit-core.mjs";
|
|
9
|
+
|
|
10
|
+
export const AWS_WINDOWS_JIT_CONTROLLER_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-aws-windows-jit-controller/v1";
|
|
12
|
+
|
|
13
|
+
function exact(value, pattern, label) {
|
|
14
|
+
const normalized = String(value || "").trim();
|
|
15
|
+
if (!pattern.test(normalized)) {
|
|
16
|
+
throw new Error(`${label} is invalid`);
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function exactSha(value) {
|
|
22
|
+
return exact(value, /^[0-9a-f]{40}$/i, "sourceSha").toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function iso(value, label) {
|
|
26
|
+
const parsed = new Date(value || "");
|
|
27
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
28
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
29
|
+
}
|
|
30
|
+
return parsed.toISOString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function qualificationId(value) {
|
|
34
|
+
return exact(
|
|
35
|
+
value,
|
|
36
|
+
/^win-(?:smoke|full-0[1-3]|cancel|timeout)$/,
|
|
37
|
+
"qualificationId",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tag(key, value) {
|
|
42
|
+
return { Key: key, Value: String(value) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createWindowsJitLaunchPlan(values = {}) {
|
|
46
|
+
const repository = exact(
|
|
47
|
+
values.repository || WINDOWS_EC2_JIT.repository,
|
|
48
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/,
|
|
49
|
+
"repository",
|
|
50
|
+
);
|
|
51
|
+
if (repository !== WINDOWS_EC2_JIT.repository) {
|
|
52
|
+
throw new Error(`repository must be ${WINDOWS_EC2_JIT.repository}`);
|
|
53
|
+
}
|
|
54
|
+
const runId = exact(values.runId, /^\d+$/, "runId");
|
|
55
|
+
const runAttempt = exact(
|
|
56
|
+
values.runAttempt || "1",
|
|
57
|
+
/^[1-9]\d*$/,
|
|
58
|
+
"runAttempt",
|
|
59
|
+
);
|
|
60
|
+
const jobId = exact(values.jobId, /^\d+$/, "jobId");
|
|
61
|
+
const qualification = qualificationId(values.qualificationId);
|
|
62
|
+
const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
|
|
63
|
+
const expectedLabel = `${WINDOWS_EC2_JIT.labelPrefix}${qualification}`;
|
|
64
|
+
if (runnerLabel !== expectedLabel) {
|
|
65
|
+
throw new Error(`runnerLabel must be ${expectedLabel}`);
|
|
66
|
+
}
|
|
67
|
+
const runnerName = exact(
|
|
68
|
+
values.runnerName,
|
|
69
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/,
|
|
70
|
+
"runnerName",
|
|
71
|
+
);
|
|
72
|
+
const sourceSha = exactSha(values.sourceSha);
|
|
73
|
+
const sourceRef = exact(
|
|
74
|
+
values.sourceRef,
|
|
75
|
+
/^refs\/heads\/[A-Za-z0-9._/-]+$/,
|
|
76
|
+
"sourceRef",
|
|
77
|
+
);
|
|
78
|
+
const region = exact(
|
|
79
|
+
values.region || WINDOWS_EC2_JIT.region,
|
|
80
|
+
/^us-[a-z]+-\d$/,
|
|
81
|
+
"region",
|
|
82
|
+
);
|
|
83
|
+
if (region !== WINDOWS_EC2_JIT.region) {
|
|
84
|
+
throw new Error(`region must be ${WINDOWS_EC2_JIT.region}`);
|
|
85
|
+
}
|
|
86
|
+
const instanceType = exact(
|
|
87
|
+
values.instanceType || WINDOWS_EC2_JIT.instanceType,
|
|
88
|
+
/^[a-z0-9.]+$/,
|
|
89
|
+
"instanceType",
|
|
90
|
+
);
|
|
91
|
+
if (instanceType !== WINDOWS_EC2_JIT.instanceType) {
|
|
92
|
+
throw new Error(`instanceType must be ${WINDOWS_EC2_JIT.instanceType}`);
|
|
93
|
+
}
|
|
94
|
+
const amiId = exact(values.amiId, /^ami-[0-9a-f]+$/, "amiId");
|
|
95
|
+
const amiName = exact(values.amiName, /^[A-Za-z0-9._-]+$/, "amiName");
|
|
96
|
+
const subnetId = exact(values.subnetId, /^subnet-[0-9a-f]+$/, "subnetId");
|
|
97
|
+
const securityGroupId = exact(
|
|
98
|
+
values.securityGroupId,
|
|
99
|
+
/^sg-[0-9a-f]+$/,
|
|
100
|
+
"securityGroupId",
|
|
101
|
+
);
|
|
102
|
+
const instanceProfileName = exact(
|
|
103
|
+
values.instanceProfileName,
|
|
104
|
+
/^[A-Za-z0-9+=,.@_-]{1,128}$/,
|
|
105
|
+
"instanceProfileName",
|
|
106
|
+
);
|
|
107
|
+
const evidenceBucket = exact(
|
|
108
|
+
values.evidenceBucket,
|
|
109
|
+
/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
|
|
110
|
+
"evidenceBucket",
|
|
111
|
+
);
|
|
112
|
+
const launchedAt = iso(values.launchedAt, "launchedAt");
|
|
113
|
+
const clientToken = `kungfu-${runId}-${runAttempt}-${qualification}`;
|
|
114
|
+
const jitParameterName =
|
|
115
|
+
String(values.jitParameterName || "").trim() ||
|
|
116
|
+
`${WINDOWS_EC2_JIT.jitParameterPrefix}${runId}/${runAttempt}/${qualification}`;
|
|
117
|
+
if (
|
|
118
|
+
!jitParameterName.startsWith(WINDOWS_EC2_JIT.jitParameterPrefix) ||
|
|
119
|
+
!/^\/[A-Za-z0-9._/-]+$/.test(jitParameterName)
|
|
120
|
+
) {
|
|
121
|
+
throw new Error("jitParameterName must use the dedicated Windows prefix");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ownershipTags = [
|
|
125
|
+
tag("kungfu:owner", "buildchain"),
|
|
126
|
+
tag("kungfu:plane", "aws-us-elastic-runner-burst"),
|
|
127
|
+
tag("kungfu:provider", "windows-ec2-jit"),
|
|
128
|
+
tag("kungfu:github-run-id", runId),
|
|
129
|
+
tag("kungfu:github-run-attempt", runAttempt),
|
|
130
|
+
tag("kungfu:qualification-id", qualification),
|
|
131
|
+
tag("kungfu:source-sha", sourceSha),
|
|
132
|
+
tag("kungfu:runner-label", runnerLabel),
|
|
133
|
+
];
|
|
134
|
+
const instanceTags = [
|
|
135
|
+
...ownershipTags,
|
|
136
|
+
tag("kungfu:jit-parameter", jitParameterName),
|
|
137
|
+
];
|
|
138
|
+
const volumeTags = [...ownershipTags];
|
|
139
|
+
const plan = {
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
|
|
142
|
+
kind: "launch-plan",
|
|
143
|
+
repository,
|
|
144
|
+
source: { sha: sourceSha, ref: sourceRef },
|
|
145
|
+
github: {
|
|
146
|
+
runId,
|
|
147
|
+
runAttempt,
|
|
148
|
+
jobId,
|
|
149
|
+
qualificationId: qualification,
|
|
150
|
+
event: "workflow_dispatch",
|
|
151
|
+
},
|
|
152
|
+
runner: {
|
|
153
|
+
name: runnerName,
|
|
154
|
+
label: runnerLabel,
|
|
155
|
+
labels: windowsJitRunnerLabels(runnerLabel),
|
|
156
|
+
oneJobJit: true,
|
|
157
|
+
},
|
|
158
|
+
aws: {
|
|
159
|
+
region,
|
|
160
|
+
instanceType,
|
|
161
|
+
amiId,
|
|
162
|
+
amiName,
|
|
163
|
+
subnetId,
|
|
164
|
+
securityGroupId,
|
|
165
|
+
instanceProfileName,
|
|
166
|
+
evidenceBucket,
|
|
167
|
+
jitParameterName,
|
|
168
|
+
launchedAt,
|
|
169
|
+
clientToken,
|
|
170
|
+
rootVolume: {
|
|
171
|
+
deviceName: "/dev/sda1",
|
|
172
|
+
deleteOnTermination: true,
|
|
173
|
+
encrypted: true,
|
|
174
|
+
volumeSizeGiB: 120,
|
|
175
|
+
volumeType: "gp3",
|
|
176
|
+
},
|
|
177
|
+
metadata: {
|
|
178
|
+
httpEndpoint: "enabled",
|
|
179
|
+
httpTokens: "required",
|
|
180
|
+
httpPutResponseHopLimit: 1,
|
|
181
|
+
instanceMetadataTags: "disabled",
|
|
182
|
+
},
|
|
183
|
+
instanceTags,
|
|
184
|
+
volumeTags,
|
|
185
|
+
},
|
|
186
|
+
safety: {
|
|
187
|
+
applyMode: values.execute === true ? "execute" : "dry-run",
|
|
188
|
+
exactSourceRequired: true,
|
|
189
|
+
queuedJobRequired: true,
|
|
190
|
+
activeInstanceCeiling: WINDOWS_EC2_JIT.maxConcurrentInstances,
|
|
191
|
+
awsDryRunRequiredBeforeLaunch: true,
|
|
192
|
+
userDataTransport: "fileb://rendered-bootstrap",
|
|
193
|
+
jitConfigTransport: "0600-temporary-file-to-ssm-secure-string",
|
|
194
|
+
jitConfigInUserData: false,
|
|
195
|
+
jitConfigInArgv: false,
|
|
196
|
+
cleanupOnLaunchFailure: [
|
|
197
|
+
"ec2-instance",
|
|
198
|
+
"ssm-parameter",
|
|
199
|
+
"github-runner-registration",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
return { ...plan, digest: digest(plan) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function windowsRunInstancesArgs(
|
|
207
|
+
plan,
|
|
208
|
+
{ bootstrapPath = "<rendered-bootstrap>", dryRun = false } = {},
|
|
209
|
+
) {
|
|
210
|
+
if (plan?.contract !== AWS_WINDOWS_JIT_CONTROLLER_CONTRACT) {
|
|
211
|
+
throw new Error("Windows JIT launch plan contract is invalid");
|
|
212
|
+
}
|
|
213
|
+
const resolvedBootstrap = exact(
|
|
214
|
+
bootstrapPath,
|
|
215
|
+
/^(?:<rendered-bootstrap>|\/[^\0]+)$/,
|
|
216
|
+
"bootstrapPath",
|
|
217
|
+
);
|
|
218
|
+
const args = [
|
|
219
|
+
"ec2",
|
|
220
|
+
"run-instances",
|
|
221
|
+
"--image-id",
|
|
222
|
+
plan.aws.amiId,
|
|
223
|
+
"--instance-type",
|
|
224
|
+
plan.aws.instanceType,
|
|
225
|
+
"--count",
|
|
226
|
+
"1",
|
|
227
|
+
"--client-token",
|
|
228
|
+
plan.aws.clientToken,
|
|
229
|
+
"--iam-instance-profile",
|
|
230
|
+
JSON.stringify({ Name: plan.aws.instanceProfileName }),
|
|
231
|
+
"--subnet-id",
|
|
232
|
+
plan.aws.subnetId,
|
|
233
|
+
"--security-group-ids",
|
|
234
|
+
plan.aws.securityGroupId,
|
|
235
|
+
"--associate-public-ip-address",
|
|
236
|
+
"--user-data",
|
|
237
|
+
`fileb://${resolvedBootstrap}`,
|
|
238
|
+
"--block-device-mappings",
|
|
239
|
+
JSON.stringify([
|
|
240
|
+
{
|
|
241
|
+
DeviceName: plan.aws.rootVolume.deviceName,
|
|
242
|
+
Ebs: {
|
|
243
|
+
DeleteOnTermination: plan.aws.rootVolume.deleteOnTermination,
|
|
244
|
+
Encrypted: plan.aws.rootVolume.encrypted,
|
|
245
|
+
VolumeSize: plan.aws.rootVolume.volumeSizeGiB,
|
|
246
|
+
VolumeType: plan.aws.rootVolume.volumeType,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]),
|
|
250
|
+
"--metadata-options",
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
HttpEndpoint: plan.aws.metadata.httpEndpoint,
|
|
253
|
+
HttpTokens: plan.aws.metadata.httpTokens,
|
|
254
|
+
HttpPutResponseHopLimit: plan.aws.metadata.httpPutResponseHopLimit,
|
|
255
|
+
InstanceMetadataTags: plan.aws.metadata.instanceMetadataTags,
|
|
256
|
+
}),
|
|
257
|
+
"--instance-initiated-shutdown-behavior",
|
|
258
|
+
"terminate",
|
|
259
|
+
"--tag-specifications",
|
|
260
|
+
JSON.stringify([
|
|
261
|
+
{ ResourceType: "instance", Tags: plan.aws.instanceTags },
|
|
262
|
+
{ ResourceType: "volume", Tags: plan.aws.volumeTags },
|
|
263
|
+
]),
|
|
264
|
+
"--output",
|
|
265
|
+
"json",
|
|
266
|
+
];
|
|
267
|
+
if (dryRun) args.splice(2, 0, "--dry-run");
|
|
268
|
+
return args;
|
|
269
|
+
}
|