@kungfu-tech/buildchain 3.0.4-alpha.0 → 3.0.4-alpha.10

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 (74) hide show
  1. package/AGENTS.md +14 -5
  2. package/README.md +18 -0
  3. package/bin/buildchain.mjs +136 -73
  4. package/bin/internal/trust-release-cli.mjs +15 -537
  5. package/bin/internal/trust-release-command-handlers.mjs +14 -0
  6. package/bin/internal/trust-release-inspection-handlers.mjs +175 -0
  7. package/bin/internal/trust-release-release-handlers.mjs +317 -0
  8. package/bin/internal/trust-release-verification-handlers.mjs +306 -0
  9. package/dist/site/buildchain-contract.json +36 -25
  10. package/dist/site/buildchain-site.json +1474 -61
  11. package/dist/site/capability-registry.json +8 -5
  12. package/dist/site/cli-registry.json +1869 -0
  13. package/dist/site/controller-registry.json +16 -3
  14. package/dist/site/kfd-claims.json +84 -10
  15. package/dist/site/kfd-upstream-aggregate.json +1 -1
  16. package/dist/site/manual-registry.json +49 -6
  17. package/dist/site/node-api-registry.json +17802 -11
  18. package/dist/site/page-registry.json +1441 -59
  19. package/dist/site/public-surface-audit.json +2863 -358
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/release-model.json +7 -0
  22. package/dist/site/site-manifest.json +34 -10
  23. package/dist/site/workflow-registry.json +13 -5
  24. package/docs/MAP.md +21 -6
  25. package/docs/cli-reference.md +1936 -0
  26. package/docs/cli.md +15 -1
  27. package/docs/getting-started.md +167 -0
  28. package/docs/install.md +7 -8
  29. package/docs/node-api-reference.md +1952 -0
  30. package/docs/release-propagation.md +166 -8
  31. package/docs/site-bundle-contract.md +10 -4
  32. package/docs/versioning.md +5 -4
  33. package/package.json +8 -5
  34. package/packages/core/buildchain-agent-manuals.js +37 -0
  35. package/packages/core/buildchain-kfd-claims.js +4 -37
  36. package/packages/core/controller-evidence.js +8 -1
  37. package/packages/core/index.js +1 -13
  38. package/packages/core/paper-agent-entry.js +14 -11
  39. package/packages/core/paper-fleet.js +34 -7
  40. package/packages/core/paper-npm-bootstrap.js +492 -0
  41. package/packages/core/paper-repository.js +1 -0
  42. package/packages/core/paper.js +356 -674
  43. package/packages/core/public-surface-cli.js +12 -1
  44. package/packages/core/publication-package.js +7 -0
  45. package/packages/core/release-passport.js +67 -71
  46. package/packages/core/release-propagation-common.js +64 -0
  47. package/packages/core/release-propagation-execution-profile.js +59 -0
  48. package/packages/core/release-propagation-release.js +196 -0
  49. package/packages/core/release-propagation-stage-evidence.js +364 -0
  50. package/packages/core/release-propagation-work-capture.js +64 -0
  51. package/packages/core/release-propagation-work-constants.js +34 -0
  52. package/packages/core/release-propagation-work-control.js +203 -0
  53. package/packages/core/release-propagation-work-transitions.js +145 -0
  54. package/packages/core/release-propagation-work.js +517 -0
  55. package/packages/core/release-propagation.js +34 -158
  56. package/scripts/aggregate-build-summary.mjs +67 -4
  57. package/scripts/auditable-demo-renditions.mjs +131 -0
  58. package/scripts/auditable-demo.mjs +88 -91
  59. package/scripts/aws-windows-jit-controller-core.mjs +269 -0
  60. package/scripts/aws-windows-jit-controller.mjs +502 -0
  61. package/scripts/check-internal-architecture.mjs +178 -52
  62. package/scripts/check-inventory.mjs +2 -2
  63. package/scripts/check-maintainability.mjs +76 -17
  64. package/scripts/generate-public-reference.mjs +68 -0
  65. package/scripts/generate-site-bundle.mjs +35 -41
  66. package/scripts/maintainability-metrics.mjs +24 -4
  67. package/scripts/paper-work-fleet-cli.mjs +22 -3
  68. package/scripts/public-reference.mjs +557 -0
  69. package/scripts/release-propagation.mjs +126 -0
  70. package/scripts/resolve-artifact-transfer-mode.mjs +117 -0
  71. package/scripts/site-reference-registry.mjs +174 -0
  72. package/scripts/verify-golden-path.mjs +169 -0
  73. package/scripts/web-surface-core.mjs +46 -207
  74. package/scripts/web-surface-routing.mjs +286 -0
@@ -7,6 +7,10 @@ import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { fileURLToPath } from "node:url";
10
+ import {
11
+ validateRenditionSet,
12
+ validateTerminalCapture,
13
+ } from "./auditable-demo-renditions.mjs";
10
14
 
11
15
  const UTF8 = new TextDecoder("utf-8", { fatal: true });
12
16
  const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
@@ -22,19 +26,16 @@ const REQUIRED_ADAPTER_FILES = [
22
26
  "public-projection.json",
23
27
  "scene.json",
24
28
  ];
25
- const OPTIONAL_ADAPTER_FILES = ["terminal-capture.json"];
29
+ const OPTIONAL_ADAPTER_FILES = [
30
+ "terminal-capture.json",
31
+ "complete-transcript-720p.txt",
32
+ "public-projection-720p.json",
33
+ "scene-720p.json",
34
+ "terminal-capture-720p.json",
35
+ "rendition-set.json",
36
+ ];
26
37
  const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
27
38
  const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
28
- const TERMINAL_CAPTURE_NON_AUTHORITIES = [
29
- "first-party-identity",
30
- "system-identity",
31
- "kfd-compliance",
32
- "product-system-metadata",
33
- "package-metadata",
34
- "registry-history",
35
- "scan-output",
36
- "standalone-generation",
37
- ];
38
39
 
39
40
  function invariant(condition, message) {
40
41
  if (!condition) throw new Error(message);
@@ -249,80 +250,11 @@ function validateProjection(value, scene, transcriptLineCount) {
249
250
  };
250
251
  }
251
252
 
252
- function validateTerminalCapture(value, scene) {
253
- exactKeys(
254
- value,
255
- [
256
- "schema",
257
- "command",
258
- "dimensions",
259
- "durationMs",
260
- "encoding",
261
- "events",
262
- "completion",
263
- "exitCode",
264
- "authority",
265
- ],
266
- [],
267
- "terminalCapture",
268
- );
269
- invariant(value.schema === "kungfu.terminal-capture/v1", "unsupported terminal capture schema");
270
- text(value.command, 1, 160, "terminalCapture.command");
271
- exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
272
- integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
273
- integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
274
- const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
275
- invariant(
276
- durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
277
- "terminal capture duration must end within two seconds of the scene",
278
- );
279
- invariant(value.encoding === "base64", "terminalCapture.encoding must be base64");
280
- invariant(
281
- Array.isArray(value.events)
282
- && value.events.length > 0
283
- && value.events.length <= MAX_TERMINAL_CAPTURE_EVENTS,
284
- `terminalCapture.events must contain 1 through ${MAX_TERMINAL_CAPTURE_EVENTS} events`,
285
- );
286
- let previousAtMs = -1;
287
- let totalBytes = 0;
288
- for (const [index, event] of value.events.entries()) {
289
- exactKeys(event, ["atMs", "data"], [], `terminalCapture.events[${index}]`);
290
- const atMs = integer(event.atMs, 0, durationMs - 1, `terminalCapture.events[${index}].atMs`);
291
- invariant(atMs >= previousAtMs, "terminal capture event timestamps must be monotonic");
292
- invariant(index > 0 || atMs === 0, "the first terminal capture event must start at zero");
293
- previousAtMs = atMs;
294
- totalBytes += decodeBase64(event.data, `terminalCapture.events[${index}].data`).length;
295
- invariant(totalBytes <= MAX_TERMINAL_CAPTURE_BYTES, "terminal capture exceeds the 4 MiB byte bound");
296
- }
297
- exactKeys(
298
- value.completion,
299
- ["schema", "status", "reportRoot", "eventCount"],
300
- [],
301
- "terminalCapture.completion",
302
- );
303
- invariant(
304
- value.completion.schema === "kungfu.agent-work-lab.tui-autoplay/v1"
305
- && value.completion.status === "qualified"
306
- && DIGEST_PATTERN.test(value.completion.reportRoot),
307
- "terminal capture completion sentinel is not a qualified Agent Work Lab autoplay",
308
- );
309
- integer(value.completion.eventCount, 1, 100_000, "terminalCapture.completion.eventCount");
310
- invariant(value.exitCode === 0, "terminal capture exitCode must be zero");
311
- exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "terminalCapture.authority");
312
- invariant(
313
- value.authority.classification === "volatile-terminal-observation",
314
- "terminal capture authority classification must remain observation-only",
315
- );
316
- invariant(
317
- Array.isArray(value.authority.grants) && value.authority.grants.length === 0,
318
- "terminal capture must not grant authority",
319
- );
320
- invariant(
321
- JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(TERMINAL_CAPTURE_NON_AUTHORITIES),
322
- "terminal capture must declare every identity and metadata non-authority",
323
- );
324
- return value;
325
- }
253
+ const RENDITION_VALIDATION_HELPERS = {
254
+ decodeBase64, decodeUtf8, digestPattern: DIGEST_PATTERN, exactKeys, integer, invariant,
255
+ maxBytes: MAX_TERMINAL_CAPTURE_BYTES, maxEvents: MAX_TERMINAL_CAPTURE_EVENTS,
256
+ readJson, readRegular, sha256, text, validateProjection, validateScene,
257
+ };
326
258
 
327
259
  function validateSourceCoordinate(value) {
328
260
  exactKeys(
@@ -379,8 +311,17 @@ function validateAdapterOutput(output, strict = true) {
379
311
  );
380
312
  const terminalCapturePath = path.join(output, "terminal-capture.json");
381
313
  const terminalCapture = fs.existsSync(terminalCapturePath)
382
- ? validateTerminalCapture(readJson(terminalCapturePath, "terminal capture"), scene)
314
+ ? validateTerminalCapture(
315
+ readJson(terminalCapturePath, "terminal capture"),
316
+ scene,
317
+ RENDITION_VALIDATION_HELPERS,
318
+ )
383
319
  : null;
320
+ const renditionSet = validateRenditionSet(output, RENDITION_VALIDATION_HELPERS);
321
+ invariant(
322
+ !renditionSet || terminalCapture,
323
+ "rendition set requires the primary terminal capture",
324
+ );
384
325
  if (strict) {
385
326
  const allowed = new Set([...REQUIRED_ADAPTER_FILES, ...OPTIONAL_ADAPTER_FILES]);
386
327
  for (const member of listFiles(output)) invariant(allowed.has(member), `undeclared adapter output: ${member}`);
@@ -391,6 +332,7 @@ function validateAdapterOutput(output, strict = true) {
391
332
  scene,
392
333
  projection,
393
334
  terminalCapture,
335
+ renditionSet,
394
336
  };
395
337
  }
396
338
 
@@ -1047,6 +989,21 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
1047
989
  const observed = manifest.inputs?.[key]?.root;
1048
990
  invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
1049
991
  }
992
+ if (expectedInputs.renditionSet) {
993
+ invariant(
994
+ manifest.derivation?.policy === "independent-native-frame-sets/v1",
995
+ "renderer did not use independent native frame sets",
996
+ );
997
+ invariant(
998
+ Array.isArray(manifest.inputs?.renditions)
999
+ && manifest.inputs.renditions.length === 2
1000
+ && manifest.inputs.renditions[0]?.role === "primary"
1001
+ && manifest.inputs.renditions[1]?.role === "responsive"
1002
+ && manifest.inputs.renditions[0]?.terminalCapture?.root
1003
+ !== manifest.inputs.renditions[1]?.terminalCapture?.root,
1004
+ "renderer native rendition inputs are not independently bound",
1005
+ );
1006
+ }
1050
1007
  const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
1051
1008
  invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
1052
1009
  const qualification = qualifyRendererOutput(
@@ -1099,11 +1056,10 @@ function finalizeGate(values) {
1099
1056
  fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
1100
1057
  writeJson(path.join(output, "scene.json"), normalized.scene);
1101
1058
  writeJson(path.join(output, "public-projection.json"), normalized.projection);
1102
- if (normalized.terminalCapture) {
1103
- copyFile(
1104
- path.join(adapterOutput, "terminal-capture.json"),
1105
- path.join(output, "terminal-capture.json"),
1106
- );
1059
+ for (const name of OPTIONAL_ADAPTER_FILES) {
1060
+ if (fs.existsSync(path.join(adapterOutput, name))) {
1061
+ copyFile(path.join(adapterOutput, name), path.join(output, name));
1062
+ }
1107
1063
  }
1108
1064
  copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
1109
1065
  copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
@@ -1148,6 +1104,22 @@ function finalizeGate(values) {
1148
1104
  },
1149
1105
  }
1150
1106
  : {}),
1107
+ ...(normalized.renditionSet
1108
+ ? {
1109
+ renditionSet: {
1110
+ schema: normalized.renditionSet.schema,
1111
+ root: sha256(readRegular(path.join(output, "rendition-set.json"), "rendition set")),
1112
+ renditions: normalized.renditionSet.renditions.map((rendition) => ({
1113
+ id: rendition.id,
1114
+ role: rendition.role,
1115
+ captureRoot: rendition.captureRoot,
1116
+ sceneRoot: sha256(readRegular(path.join(output, rendition.scene), `${rendition.id} scene`)),
1117
+ transcriptRoot: sha256(readRegular(path.join(output, rendition.transcript), `${rendition.id} transcript`)),
1118
+ projectionRoot: sha256(readRegular(path.join(output, rendition.projection), `${rendition.id} projection`)),
1119
+ })),
1120
+ },
1121
+ }
1122
+ : {}),
1151
1123
  evidenceClass: normalized.projection.evidenceClass,
1152
1124
  claimBoundary: normalized.projection.claimBoundary,
1153
1125
  },
@@ -1198,6 +1170,27 @@ function verifyGate(values) {
1198
1170
  "gate terminal capture root mismatch",
1199
1171
  );
1200
1172
  }
1173
+ const qualifiedRenditionSet = receipt.qualifiedInputs?.renditionSet;
1174
+ invariant(
1175
+ Boolean(qualifiedRenditionSet) === Boolean(normalized.renditionSet),
1176
+ "gate rendition set presence drifted",
1177
+ );
1178
+ if (normalized.renditionSet) {
1179
+ invariant(
1180
+ qualifiedRenditionSet.schema === normalized.renditionSet.schema
1181
+ && qualifiedRenditionSet.root === sha256(readRegular(path.join(bundle, "rendition-set.json"), "rendition set"))
1182
+ && JSON.stringify(qualifiedRenditionSet.renditions)
1183
+ === JSON.stringify(normalized.renditionSet.renditions.map((rendition) => ({
1184
+ id: rendition.id,
1185
+ role: rendition.role,
1186
+ captureRoot: rendition.captureRoot,
1187
+ sceneRoot: sha256(readRegular(path.join(bundle, rendition.scene), `${rendition.id} scene`)),
1188
+ transcriptRoot: sha256(readRegular(path.join(bundle, rendition.transcript), `${rendition.id} transcript`)),
1189
+ projectionRoot: sha256(readRegular(path.join(bundle, rendition.projection), `${rendition.id} projection`)),
1190
+ }))),
1191
+ "gate native rendition roots mismatch",
1192
+ );
1193
+ }
1201
1194
  invariant(receipt.qualifiedInputs.evidenceClass === normalized.projection.evidenceClass, "gate evidence class drifted");
1202
1195
  }
1203
1196
 
@@ -1228,6 +1221,7 @@ function finalizeMedia(values) {
1228
1221
  "--media-profile": mediaProfile,
1229
1222
  });
1230
1223
  const terminalCapturePath = path.join(gateBundle, "terminal-capture.json");
1224
+ const renditionSetPath = path.join(gateBundle, "rendition-set.json");
1231
1225
  const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
1232
1226
  scene: path.join(gateBundle, "scene.json"),
1233
1227
  transcript: path.join(gateBundle, "complete-transcript.txt"),
@@ -1235,6 +1229,9 @@ function finalizeMedia(values) {
1235
1229
  ...(fs.existsSync(terminalCapturePath)
1236
1230
  ? { terminalCapture: terminalCapturePath }
1237
1231
  : {}),
1232
+ ...(fs.existsSync(renditionSetPath)
1233
+ ? { renditionSet: renditionSetPath }
1234
+ : {}),
1238
1235
  }, {
1239
1236
  mediaProfile,
1240
1237
  inspectMedia: mediaInspection?.inspectMedia,
@@ -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
+ }