@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0

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 (83) hide show
  1. package/README.md +31 -0
  2. package/actions/macos-credential-island/README.md +8 -0
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  5. package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
  6. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  8. package/dist/site/buildchain-contract.json +107 -26
  9. package/dist/site/buildchain-site.json +121 -49
  10. package/dist/site/capability-registry.json +3 -3
  11. package/dist/site/cli-registry.json +9 -3
  12. package/dist/site/controller-registry.json +27 -3
  13. package/dist/site/kfd-claims.json +101 -15
  14. package/dist/site/kfd-upstream-aggregate.json +1 -1
  15. package/dist/site/manual-registry.json +10 -9
  16. package/dist/site/node-api-registry.json +540 -31
  17. package/dist/site/page-registry.json +105 -29
  18. package/dist/site/public-surface-audit.json +184 -11
  19. package/dist/site/publication-authority-registry.json +62 -1
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/site-manifest.json +13 -13
  22. package/dist/site/workflow-registry.json +118 -4
  23. package/docs/MAP.md +4 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
  26. package/docs/cli-reference.md +35 -2
  27. package/docs/dev-alpha-candidate-patrol.md +14 -5
  28. package/docs/dev-qualification-patrol.md +108 -0
  29. package/docs/node-api-reference.md +123 -72
  30. package/docs/publish-transaction.md +7 -5
  31. package/docs/release-flow.md +4 -0
  32. package/docs/release-governance.md +39 -0
  33. package/docs/release-propagation.md +93 -0
  34. package/docs/reusable-build-surface.md +53 -12
  35. package/docs/stable-candidate-patrol.md +5 -2
  36. package/package.json +1 -1
  37. package/packages/core/buildchain-contract.js +36 -1
  38. package/packages/core/buildchain-publication-authority.js +3 -0
  39. package/packages/core/public-surface-audit.js +4 -35
  40. package/packages/core/release-propagation-agent-entry.js +185 -0
  41. package/packages/core/release-propagation-pickup.js +387 -0
  42. package/packages/core/release-propagation-push.js +293 -0
  43. package/packages/core/release-propagation-release.js +61 -18
  44. package/packages/core/release-propagation-stage-evidence.js +36 -0
  45. package/packages/core/release-propagation-work.js +8 -4
  46. package/packages/core/release-propagation.js +27 -2
  47. package/packages/core/workflow-call-contract.js +308 -0
  48. package/packages/core/workflow-yaml-contract.js +272 -0
  49. package/scripts/artifact-signing-controller-core.mjs +525 -0
  50. package/scripts/artifact-signing-controller.mjs +302 -0
  51. package/scripts/artifact-signing-delegation.mjs +51 -3
  52. package/scripts/auditable-demo-platform.mjs +564 -0
  53. package/scripts/auditable-demo-renditions.mjs +7 -2
  54. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  55. package/scripts/auditable-demo.mjs +27 -21
  56. package/scripts/aws-macos-jit-controller-core.mjs +389 -0
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
  58. package/scripts/aws-macos-jit-controller.mjs +558 -0
  59. package/scripts/aws-macos-jit-job-controller.mjs +401 -0
  60. package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
  61. package/scripts/aws-windows-jit-campaign.mjs +202 -0
  62. package/scripts/aws-windows-jit-controller-core.mjs +20 -2
  63. package/scripts/aws-windows-jit-controller.mjs +184 -109
  64. package/scripts/aws-windows-jit-core.mjs +21 -3
  65. package/scripts/aws-windows-jit.mjs +2 -0
  66. package/scripts/build-standalone-binary.mjs +6 -0
  67. package/scripts/buildchain-cli-help.mjs +2 -1
  68. package/scripts/capture-package-release-propagation.mjs +24 -1
  69. package/scripts/check-inventory.mjs +6 -0
  70. package/scripts/compiler-cache-evidence.mjs +90 -7
  71. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  72. package/scripts/dev-pr-auto-merge.mjs +4 -4
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
  75. package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
  76. package/scripts/generate-site-bundle.mjs +5 -0
  77. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  78. package/scripts/locked-source-checkout.mjs +17 -3
  79. package/scripts/release-propagation.mjs +150 -2
  80. package/scripts/run-lifecycle-core.mjs +25 -0
  81. package/scripts/site-capability-metadata.mjs +1 -1
  82. package/scripts/stable-candidate-patrol.mjs +30 -4
  83. package/scripts/workflow-call-contract.mjs +133 -0
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { spawnSync } from "node:child_process";
5
+ import { pathToFileURL } from "node:url";
6
+ import {
7
+ createWindowsJitCampaignArmPlan,
8
+ windowsCampaignArmItems,
9
+ windowsCampaignKillArgs,
10
+ } from "./aws-windows-jit-campaign-core.mjs";
11
+
12
+ function arg(name, fallback = "") {
13
+ const index = process.argv.indexOf(`--${name}`);
14
+ return index === -1 ? fallback : process.argv[index + 1] || "";
15
+ }
16
+
17
+ function aws(plan, serviceArgs) {
18
+ const profile = arg("aws-profile");
19
+ const result = spawnSync(
20
+ "aws",
21
+ [
22
+ ...(profile ? ["--profile", profile] : []),
23
+ "--region",
24
+ plan.aws.region,
25
+ ...serviceArgs,
26
+ ],
27
+ { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
28
+ );
29
+ if (result.status !== 0) {
30
+ const detail = String(result.stderr || result.stdout || "")
31
+ .trim()
32
+ .slice(0, 2000);
33
+ throw new Error(
34
+ `AWS campaign mutation failed${detail ? `: ${detail}` : ""}`,
35
+ );
36
+ }
37
+ return result.stdout ? JSON.parse(result.stdout) : {};
38
+ }
39
+
40
+ function armPlan() {
41
+ return createWindowsJitCampaignArmPlan({
42
+ campaignId: arg("campaign-id"),
43
+ sourceSha: arg("source-sha"),
44
+ stateTable: arg("state-table"),
45
+ region: arg("region", "us-east-1"),
46
+ armedAt: arg("armed-at", new Date().toISOString()),
47
+ expiresAt: arg("expires-at"),
48
+ phaseSpendBaselineUsd: arg("phase-spend-baseline-usd"),
49
+ maxAcceptedInstances: arg("max-accepted-instances"),
50
+ });
51
+ }
52
+
53
+ function confirm(
54
+ plan,
55
+ { phaseSpendBaseline = true, maxAcceptedInstances = true } = {},
56
+ ) {
57
+ if (arg("confirm-campaign-id") !== plan.campaign.id) {
58
+ throw new Error("--confirm-campaign-id must equal the campaign id");
59
+ }
60
+ if (arg("confirm-source-sha") !== plan.source.sha) {
61
+ throw new Error("--confirm-source-sha must equal the exact source SHA");
62
+ }
63
+ if (arg("confirm-state-table") !== plan.aws.stateTable) {
64
+ throw new Error(
65
+ "--confirm-state-table must equal the campaign state table",
66
+ );
67
+ }
68
+ if (
69
+ phaseSpendBaseline &&
70
+ (!arg("confirm-phase-spend-baseline-usd").trim() ||
71
+ Number(arg("confirm-phase-spend-baseline-usd")) !==
72
+ plan.limits.phaseSpendBaselineUsd)
73
+ ) {
74
+ throw new Error(
75
+ "--confirm-phase-spend-baseline-usd must equal the phase spend baseline",
76
+ );
77
+ }
78
+ if (
79
+ maxAcceptedInstances &&
80
+ (!arg("confirm-max-accepted-instances").trim() ||
81
+ Number(arg("confirm-max-accepted-instances")) !==
82
+ plan.limits.maxAcceptedInstances)
83
+ ) {
84
+ throw new Error(
85
+ "--confirm-max-accepted-instances must equal the campaign slot ceiling",
86
+ );
87
+ }
88
+ }
89
+
90
+ function killSwitchTopic() {
91
+ const topic = arg("kill-switch-topic");
92
+ if (
93
+ !/^arn:aws:sns:us-east-1:\d{12}:kungfu-buildchain-windows-jit-[A-Za-z0-9_-]+$/.test(
94
+ topic,
95
+ )
96
+ ) {
97
+ throw new Error(
98
+ "--kill-switch-topic must be the dedicated Windows JIT SNS ARN",
99
+ );
100
+ }
101
+ if (arg("confirm-kill-switch-topic") !== topic) {
102
+ throw new Error(
103
+ "--confirm-kill-switch-topic must equal the dedicated kill-switch topic",
104
+ );
105
+ }
106
+ return topic;
107
+ }
108
+
109
+ export function main() {
110
+ const mode = process.argv[2] || "plan-arm";
111
+ if (["plan-arm", "arm-campaign"].includes(mode)) {
112
+ const plan = armPlan();
113
+ if (mode === "plan-arm") {
114
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
115
+ return plan;
116
+ }
117
+ confirm(plan);
118
+ aws(plan, [
119
+ "dynamodb",
120
+ "transact-write-items",
121
+ "--transact-items",
122
+ JSON.stringify(windowsCampaignArmItems(plan)),
123
+ "--output",
124
+ "json",
125
+ ]);
126
+ const result = {
127
+ contract: plan.contract,
128
+ kind: "campaign-arm-result",
129
+ status: "armed",
130
+ campaign: plan.campaign,
131
+ source: plan.source,
132
+ aws: plan.aws,
133
+ limits: plan.limits,
134
+ };
135
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
136
+ return result;
137
+ }
138
+ if (mode === "kill-campaign") {
139
+ const now = new Date();
140
+ const plan = createWindowsJitCampaignArmPlan({
141
+ campaignId: arg("campaign-id"),
142
+ sourceSha: arg("source-sha"),
143
+ stateTable: arg("state-table"),
144
+ region: arg("region", "us-east-1"),
145
+ armedAt: now.toISOString(),
146
+ expiresAt: new Date(now.getTime() + 1000).toISOString(),
147
+ phaseSpendBaselineUsd: 0,
148
+ });
149
+ confirm(plan, {
150
+ phaseSpendBaseline: false,
151
+ maxAcceptedInstances: false,
152
+ });
153
+ const topic = killSwitchTopic();
154
+ aws(
155
+ plan,
156
+ windowsCampaignKillArgs(
157
+ plan.aws.stateTable,
158
+ arg("reason", "operator-kill"),
159
+ new Date().toISOString(),
160
+ ),
161
+ );
162
+ const notification = aws(plan, [
163
+ "sns",
164
+ "publish",
165
+ "--topic-arn",
166
+ topic,
167
+ "--message",
168
+ JSON.stringify({
169
+ contract: plan.contract,
170
+ action: "kill-campaign",
171
+ campaignId: plan.campaign.id,
172
+ sourceSha: plan.source.sha,
173
+ }),
174
+ "--output",
175
+ "json",
176
+ ]);
177
+ const result = {
178
+ contract: plan.contract,
179
+ kind: "campaign-kill-result",
180
+ status: "killed",
181
+ campaign: plan.campaign,
182
+ source: plan.source,
183
+ killSwitchTopic: topic,
184
+ notificationMessageId: notification.MessageId || "",
185
+ };
186
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
187
+ return result;
188
+ }
189
+ throw new Error(`unsupported Windows JIT campaign mode: ${mode}`);
190
+ }
191
+
192
+ if (
193
+ process.argv[1] &&
194
+ import.meta.url === pathToFileURL(process.argv[1]).href
195
+ ) {
196
+ try {
197
+ main();
198
+ } catch (error) {
199
+ console.error(`::error::${error.message || error}`);
200
+ process.exitCode = 1;
201
+ }
202
+ }
@@ -3,12 +3,13 @@
3
3
  import { digest } from "./aws-runner-burst-core.mjs";
4
4
  import {
5
5
  WINDOWS_EC2_JIT,
6
+ windowsJitCampaignId,
6
7
  windowsJitRunnerLabel,
7
8
  windowsJitRunnerLabels,
8
9
  } from "./aws-windows-jit-core.mjs";
9
10
 
10
11
  export const AWS_WINDOWS_JIT_CONTROLLER_CONTRACT =
11
- "kungfu-buildchain-aws-windows-jit-controller/v1";
12
+ "kungfu-buildchain-aws-windows-jit-controller/v2";
12
13
 
13
14
  function exact(value, pattern, label) {
14
15
  const normalized = String(value || "").trim();
@@ -59,8 +60,9 @@ export function createWindowsJitLaunchPlan(values = {}) {
59
60
  );
60
61
  const jobId = exact(values.jobId, /^\d+$/, "jobId");
61
62
  const qualification = qualificationId(values.qualificationId);
63
+ const campaign = windowsJitCampaignId(values.campaignId);
62
64
  const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
63
- const expectedLabel = `${WINDOWS_EC2_JIT.labelPrefix}${qualification}`;
65
+ const expectedLabel = `${WINDOWS_EC2_JIT.labelPrefix}${campaign}-${qualification}`;
64
66
  if (runnerLabel !== expectedLabel) {
65
67
  throw new Error(`runnerLabel must be ${expectedLabel}`);
66
68
  }
@@ -109,6 +111,11 @@ export function createWindowsJitLaunchPlan(values = {}) {
109
111
  /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
110
112
  "evidenceBucket",
111
113
  );
114
+ const stateTable = exact(
115
+ values.stateTable,
116
+ /^kungfu-buildchain-windows-jit(?:-[A-Za-z0-9_.-]+)?$/,
117
+ "stateTable",
118
+ );
112
119
  const launchedAt = iso(values.launchedAt, "launchedAt");
113
120
  const clientToken = `kungfu-${runId}-${runAttempt}-${qualification}`;
114
121
  const jitParameterName =
@@ -125,6 +132,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
125
132
  tag("kungfu:owner", "buildchain"),
126
133
  tag("kungfu:plane", "aws-us-elastic-runner-burst"),
127
134
  tag("kungfu:provider", "windows-ec2-jit"),
135
+ tag("kungfu:campaign-id", campaign),
128
136
  tag("kungfu:github-run-id", runId),
129
137
  tag("kungfu:github-run-attempt", runAttempt),
130
138
  tag("kungfu:qualification-id", qualification),
@@ -141,6 +149,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
141
149
  contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
142
150
  kind: "launch-plan",
143
151
  repository,
152
+ campaign: { id: campaign },
144
153
  source: { sha: sourceSha, ref: sourceRef },
145
154
  github: {
146
155
  runId,
@@ -148,6 +157,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
148
157
  jobId,
149
158
  qualificationId: qualification,
150
159
  event: "workflow_dispatch",
160
+ displayTitle: `AWS Windows JIT ${campaign} ${qualification}`,
151
161
  },
152
162
  runner: {
153
163
  name: runnerName,
@@ -164,6 +174,7 @@ export function createWindowsJitLaunchPlan(values = {}) {
164
174
  securityGroupId,
165
175
  instanceProfileName,
166
176
  evidenceBucket,
177
+ stateTable,
167
178
  jitParameterName,
168
179
  launchedAt,
169
180
  clientToken,
@@ -188,6 +199,13 @@ export function createWindowsJitLaunchPlan(values = {}) {
188
199
  exactSourceRequired: true,
189
200
  queuedJobRequired: true,
190
201
  activeInstanceCeiling: WINDOWS_EC2_JIT.maxConcurrentInstances,
202
+ campaignAcceptedInstanceCeiling: WINDOWS_EC2_JIT.maxAcceptedInstances,
203
+ campaignReservationUsd:
204
+ (WINDOWS_EC2_JIT.pricePerHourUsd *
205
+ WINDOWS_EC2_JIT.maximumInstanceLifetimeMinutes) /
206
+ 60,
207
+ campaignBudgetLimitUsd: WINDOWS_EC2_JIT.budgetLimitUsd,
208
+ persistentCampaignLedgerRequired: true,
191
209
  awsDryRunRequiredBeforeLaunch: true,
192
210
  userDataTransport: "fileb://rendered-bootstrap",
193
211
  jitConfigTransport: "0600-temporary-file-to-ssm-secure-string",
@@ -11,6 +11,10 @@ import {
11
11
  createWindowsJitLaunchPlan,
12
12
  windowsRunInstancesArgs,
13
13
  } from "./aws-windows-jit-controller-core.mjs";
14
+ import {
15
+ windowsCampaignMarkLaunchedArgs,
16
+ windowsCampaignReservationItems,
17
+ } from "./aws-windows-jit-campaign-core.mjs";
14
18
  import { renderWindowsJitBootstrap } from "./aws-windows-jit-core.mjs";
15
19
 
16
20
  function arg(name, fallback = "") {
@@ -75,6 +79,7 @@ function assertLivePreflight(plan, profile) {
75
79
  );
76
80
  if (
77
81
  run.event !== plan.github.event ||
82
+ run.display_title !== plan.github.displayTitle ||
78
83
  run.head_sha !== plan.source.sha ||
79
84
  run.head_repository?.full_name !== plan.repository ||
80
85
  !["queued", "in_progress"].includes(run.status)
@@ -267,6 +272,169 @@ function cleanupFailedLaunch(
267
272
  return failures;
268
273
  }
269
274
 
275
+ function generateJitConfiguration(plan) {
276
+ const jit = ghJson(
277
+ [
278
+ "api",
279
+ "--method",
280
+ "POST",
281
+ `repos/${plan.repository}/actions/runners/generate-jitconfig`,
282
+ "--input",
283
+ "-",
284
+ ],
285
+ JSON.stringify({
286
+ name: plan.runner.name,
287
+ runner_group_id: 1,
288
+ labels: plan.runner.labels,
289
+ work_folder: "_work",
290
+ }),
291
+ "GitHub JIT configuration",
292
+ ).encoded_jit_config;
293
+ if (!jit || typeof jit !== "string") {
294
+ throw new Error("GitHub JIT configuration was empty");
295
+ }
296
+ return jit;
297
+ }
298
+
299
+ function createJitParameter(plan, profile, parameterInputPath, jit) {
300
+ fs.writeFileSync(
301
+ parameterInputPath,
302
+ JSON.stringify({
303
+ Name: plan.aws.jitParameterName,
304
+ Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
305
+ Type: "SecureString",
306
+ Tier: "Advanced",
307
+ Value: jit,
308
+ Tags: [
309
+ { Key: "kungfu:owner", Value: "buildchain" },
310
+ { Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
311
+ { Key: "kungfu:provider", Value: "windows-ec2-jit" },
312
+ { Key: "kungfu:campaign-id", Value: plan.campaign.id },
313
+ { Key: "kungfu:github-run-id", Value: plan.github.runId },
314
+ {
315
+ Key: "kungfu:qualification-id",
316
+ Value: plan.github.qualificationId,
317
+ },
318
+ ],
319
+ }),
320
+ { mode: 0o600 },
321
+ );
322
+ requireSuccess(
323
+ commandResult(
324
+ "aws",
325
+ awsArgs(plan, profile, [
326
+ "ssm",
327
+ "put-parameter",
328
+ "--cli-input-json",
329
+ `file://${parameterInputPath}`,
330
+ "--output",
331
+ "json",
332
+ ]),
333
+ ),
334
+ "SSM JIT parameter creation",
335
+ );
336
+ }
337
+
338
+ function writeBootstrap(plan, bootstrapPath) {
339
+ const template = fs.readFileSync(
340
+ path.resolve(
341
+ "infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
342
+ ),
343
+ "utf8",
344
+ );
345
+ fs.writeFileSync(
346
+ bootstrapPath,
347
+ renderWindowsJitBootstrap(template, {
348
+ region: plan.aws.region,
349
+ campaignId: plan.campaign.id,
350
+ jitParameterName: plan.aws.jitParameterName,
351
+ evidenceBucket: plan.aws.evidenceBucket,
352
+ runnerLabel: plan.runner.label,
353
+ sourceSha: plan.source.sha,
354
+ githubRunId: plan.github.runId,
355
+ githubRunAttempt: plan.github.runAttempt,
356
+ amiId: plan.aws.amiId,
357
+ amiName: plan.aws.amiName,
358
+ instanceType: plan.aws.instanceType,
359
+ launchedAt: plan.aws.launchedAt,
360
+ }),
361
+ { mode: 0o600 },
362
+ );
363
+ }
364
+
365
+ function assertLaunchDryRun(plan, profile, bootstrapPath) {
366
+ const dryRun = commandResult(
367
+ "aws",
368
+ awsArgs(
369
+ plan,
370
+ profile,
371
+ windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
372
+ ),
373
+ );
374
+ if (
375
+ dryRun.status === 0 ||
376
+ !/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
377
+ ) {
378
+ throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
379
+ }
380
+ }
381
+
382
+ function reserveCampaign(plan, profile) {
383
+ requireSuccess(
384
+ commandResult(
385
+ "aws",
386
+ awsArgs(plan, profile, [
387
+ "dynamodb",
388
+ "transact-write-items",
389
+ "--transact-items",
390
+ JSON.stringify(
391
+ windowsCampaignReservationItems(plan, new Date().toISOString()),
392
+ ),
393
+ "--output",
394
+ "json",
395
+ ]),
396
+ ),
397
+ "Windows campaign atomic reservation",
398
+ );
399
+ }
400
+
401
+ function launchInstance(plan, profile, bootstrapPath) {
402
+ const launched = jsonResult(
403
+ commandResult(
404
+ "aws",
405
+ awsArgs(
406
+ plan,
407
+ profile,
408
+ windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
409
+ ),
410
+ ),
411
+ "EC2 RunInstances",
412
+ );
413
+ const instance = launched.Instances?.[0];
414
+ if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
415
+ throw new Error("EC2 RunInstances returned no instance identity");
416
+ }
417
+ return instance;
418
+ }
419
+
420
+ function markCampaignLaunched(plan, profile, instanceId) {
421
+ requireSuccess(
422
+ commandResult(
423
+ "aws",
424
+ awsArgs(
425
+ plan,
426
+ profile,
427
+ windowsCampaignMarkLaunchedArgs(
428
+ plan,
429
+ instanceId,
430
+ new Date().toISOString(),
431
+ ),
432
+ ),
433
+ ),
434
+ "Windows campaign launch ledger update",
435
+ );
436
+ }
437
+
270
438
  export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
271
439
  if (plan?.contract !== AWS_WINDOWS_JIT_CONTROLLER_CONTRACT) {
272
440
  throw new Error("Windows JIT launch plan contract is invalid");
@@ -285,124 +453,23 @@ export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
285
453
  let failure;
286
454
  const cleanupFailures = [];
287
455
  try {
288
- const jit = ghJson(
289
- [
290
- "api",
291
- "--method",
292
- "POST",
293
- `repos/${plan.repository}/actions/runners/generate-jitconfig`,
294
- "--input",
295
- "-",
296
- ],
297
- JSON.stringify({
298
- name: plan.runner.name,
299
- runner_group_id: 1,
300
- labels: plan.runner.labels,
301
- work_folder: "_work",
302
- }),
303
- "GitHub JIT configuration",
304
- ).encoded_jit_config;
305
- if (!jit || typeof jit !== "string") {
306
- throw new Error("GitHub JIT configuration was empty");
307
- }
308
- fs.writeFileSync(
309
- parameterInputPath,
310
- JSON.stringify({
311
- Name: plan.aws.jitParameterName,
312
- Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
313
- Type: "SecureString",
314
- Tier: "Advanced",
315
- Value: jit,
316
- Tags: [
317
- { Key: "kungfu:owner", Value: "buildchain" },
318
- { Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
319
- { Key: "kungfu:provider", Value: "windows-ec2-jit" },
320
- { Key: "kungfu:github-run-id", Value: plan.github.runId },
321
- {
322
- Key: "kungfu:qualification-id",
323
- Value: plan.github.qualificationId,
324
- },
325
- ],
326
- }),
327
- { mode: 0o600 },
328
- );
329
- requireSuccess(
330
- commandResult(
331
- "aws",
332
- awsArgs(plan, profile, [
333
- "ssm",
334
- "put-parameter",
335
- "--cli-input-json",
336
- `file://${parameterInputPath}`,
337
- "--output",
338
- "json",
339
- ]),
340
- ),
341
- "SSM JIT parameter creation",
342
- );
456
+ const jit = generateJitConfiguration(plan);
457
+ createJitParameter(plan, profile, parameterInputPath, jit);
343
458
  parameterCreated = true;
344
- fs.writeFileSync(
345
- bootstrapPath,
346
- renderWindowsJitBootstrap(
347
- fs.readFileSync(
348
- path.resolve(
349
- "infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
350
- ),
351
- "utf8",
352
- ),
353
- {
354
- region: plan.aws.region,
355
- jitParameterName: plan.aws.jitParameterName,
356
- evidenceBucket: plan.aws.evidenceBucket,
357
- runnerLabel: plan.runner.label,
358
- sourceSha: plan.source.sha,
359
- githubRunId: plan.github.runId,
360
- githubRunAttempt: plan.github.runAttempt,
361
- amiId: plan.aws.amiId,
362
- amiName: plan.aws.amiName,
363
- instanceType: plan.aws.instanceType,
364
- launchedAt: plan.aws.launchedAt,
365
- },
366
- ),
367
- { mode: 0o600 },
368
- );
369
- const dryRun = commandResult(
370
- "aws",
371
- awsArgs(
372
- plan,
373
- profile,
374
- windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
375
- ),
376
- );
377
- if (
378
- dryRun.status === 0 ||
379
- !/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
380
- ) {
381
- throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
382
- }
459
+ writeBootstrap(plan, bootstrapPath);
460
+ assertLaunchDryRun(plan, profile, bootstrapPath);
461
+ reserveCampaign(plan, profile);
383
462
  launchAttempted = true;
384
- const launched = jsonResult(
385
- commandResult(
386
- "aws",
387
- awsArgs(
388
- plan,
389
- profile,
390
- windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
391
- ),
392
- ),
393
- "EC2 RunInstances",
394
- );
395
- const instance = launched.Instances?.[0];
396
- if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
397
- throw new Error("EC2 RunInstances returned no instance identity");
398
- }
463
+ const instance = launchInstance(plan, profile, bootstrapPath);
399
464
  launchSucceeded = true;
465
+ markCampaignLaunched(plan, profile, instance.InstanceId);
400
466
  result = {
401
467
  schemaVersion: 1,
402
468
  contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
403
469
  kind: "launch-result",
404
470
  status: "launched",
405
471
  source: plan.source,
472
+ campaign: plan.campaign,
406
473
  github: plan.github,
407
474
  runner: plan.runner,
408
475
  aws: {
@@ -452,6 +519,7 @@ function planFromArgs(execute) {
452
519
  runAttempt: arg("run-attempt", "1"),
453
520
  jobId: arg("job-id"),
454
521
  qualificationId: arg("qualification-id"),
522
+ campaignId: arg("campaign-id"),
455
523
  runnerLabel: arg("runner-label"),
456
524
  runnerName: arg("runner-name"),
457
525
  sourceSha: arg("source-sha"),
@@ -464,6 +532,7 @@ function planFromArgs(execute) {
464
532
  securityGroupId: arg("security-group-id"),
465
533
  instanceProfileName: arg("instance-profile-name"),
466
534
  evidenceBucket: arg("evidence-bucket"),
535
+ stateTable: arg("state-table"),
467
536
  jitParameterName: arg("jit-parameter"),
468
537
  launchedAt: arg("launched-at", new Date().toISOString()),
469
538
  });
@@ -482,6 +551,12 @@ export function main() {
482
551
  if (arg("confirm-run-id") !== plan.github.runId) {
483
552
  throw new Error("--confirm-run-id must equal the exact GitHub run id");
484
553
  }
554
+ if (arg("confirm-campaign-id") !== plan.campaign.id) {
555
+ throw new Error("--confirm-campaign-id must equal the campaign id");
556
+ }
557
+ if (arg("confirm-state-table") !== plan.aws.stateTable) {
558
+ throw new Error("--confirm-state-table must equal the campaign state table");
559
+ }
485
560
  const result = executeWindowsJitLaunch(plan, {
486
561
  profile: arg("aws-profile"),
487
562
  });