@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.
Files changed (49) hide show
  1. package/bin/buildchain.mjs +126 -72
  2. package/bin/internal/trust-release-cli.mjs +15 -537
  3. package/bin/internal/trust-release-command-handlers.mjs +14 -0
  4. package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
  5. package/bin/internal/trust-release-release-handlers.mjs +317 -0
  6. package/bin/internal/trust-release-verification-handlers.mjs +306 -0
  7. package/dist/site/buildchain-contract.json +35 -24
  8. package/dist/site/buildchain-site.json +22 -10
  9. package/dist/site/controller-registry.json +16 -3
  10. package/dist/site/kfd-claims.json +9 -7
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +2 -2
  13. package/dist/site/node-api-registry.json +7 -7
  14. package/dist/site/page-registry.json +9 -4
  15. package/dist/site/public-surface-audit.json +56 -8
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-model.json +7 -0
  18. package/dist/site/site-manifest.json +6 -6
  19. package/dist/site/workflow-registry.json +13 -5
  20. package/docs/MAP.md +1 -1
  21. package/docs/release-propagation.md +166 -8
  22. package/package.json +1 -1
  23. package/packages/core/buildchain-kfd-claims.js +1 -1
  24. package/packages/core/controller-evidence.js +8 -1
  25. package/packages/core/index.js +1 -13
  26. package/packages/core/paper-npm-bootstrap.js +492 -0
  27. package/packages/core/paper.js +271 -669
  28. package/packages/core/public-surface-cli.js +12 -1
  29. package/packages/core/release-passport.js +67 -71
  30. package/packages/core/release-propagation-common.js +64 -0
  31. package/packages/core/release-propagation-execution-profile.js +59 -0
  32. package/packages/core/release-propagation-release.js +196 -0
  33. package/packages/core/release-propagation-stage-evidence.js +364 -0
  34. package/packages/core/release-propagation-work-capture.js +64 -0
  35. package/packages/core/release-propagation-work-constants.js +34 -0
  36. package/packages/core/release-propagation-work-control.js +203 -0
  37. package/packages/core/release-propagation-work-transitions.js +145 -0
  38. package/packages/core/release-propagation-work.js +517 -0
  39. package/packages/core/release-propagation.js +34 -158
  40. package/scripts/aws-windows-jit-controller-core.mjs +269 -0
  41. package/scripts/aws-windows-jit-controller.mjs +502 -0
  42. package/scripts/check-internal-architecture.mjs +178 -52
  43. package/scripts/check-maintainability.mjs +76 -17
  44. package/scripts/generate-site-bundle.mjs +16 -7
  45. package/scripts/maintainability-metrics.mjs +24 -4
  46. package/scripts/release-propagation.mjs +126 -0
  47. package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
  48. package/scripts/web-surface-core.mjs +46 -207
  49. 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 const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
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
+ }