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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +126 -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 +103 -16
  14. package/dist/site/kfd-upstream-aggregate.json +9 -9
  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 +110 -29
  18. package/dist/site/public-surface-audit.json +186 -12
  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 +120 -5
  23. package/docs/MAP.md +5 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +181 -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 +47 -1
  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 +3 -2
  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 +295 -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 +185 -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 +15 -15
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +310 -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)
@@ -159,6 +164,7 @@ function assertLivePreflight(plan, profile) {
159
164
  throw new Error("Windows AMI identity or availability mismatch");
160
165
  }
161
166
  return {
167
+ budgetGuard: jsonResult(commandResult("/bin/bash", ["scripts/aws-windows-jit-operator.sh", "launch-gate", "--region", plan.aws.region, ...(profile ? ["--aws-profile", profile] : [])]), "provider Budget launch gate"),
162
168
  runStatus: run.status,
163
169
  jobStatus: job.status,
164
170
  activeInstances: activeInstances.length,
@@ -267,6 +273,169 @@ function cleanupFailedLaunch(
267
273
  return failures;
268
274
  }
269
275
 
276
+ function generateJitConfiguration(plan) {
277
+ const jit = ghJson(
278
+ [
279
+ "api",
280
+ "--method",
281
+ "POST",
282
+ `repos/${plan.repository}/actions/runners/generate-jitconfig`,
283
+ "--input",
284
+ "-",
285
+ ],
286
+ JSON.stringify({
287
+ name: plan.runner.name,
288
+ runner_group_id: 1,
289
+ labels: plan.runner.labels,
290
+ work_folder: "_work",
291
+ }),
292
+ "GitHub JIT configuration",
293
+ ).encoded_jit_config;
294
+ if (!jit || typeof jit !== "string") {
295
+ throw new Error("GitHub JIT configuration was empty");
296
+ }
297
+ return jit;
298
+ }
299
+
300
+ function createJitParameter(plan, profile, parameterInputPath, jit) {
301
+ fs.writeFileSync(
302
+ parameterInputPath,
303
+ JSON.stringify({
304
+ Name: plan.aws.jitParameterName,
305
+ Description: `One-shot GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId} attempt ${plan.github.runAttempt}`,
306
+ Type: "SecureString",
307
+ Tier: "Advanced",
308
+ Value: jit,
309
+ Tags: [
310
+ { Key: "kungfu:owner", Value: "buildchain" },
311
+ { Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
312
+ { Key: "kungfu:provider", Value: "windows-ec2-jit" },
313
+ { Key: "kungfu:campaign-id", Value: plan.campaign.id },
314
+ { Key: "kungfu:github-run-id", Value: plan.github.runId },
315
+ {
316
+ Key: "kungfu:qualification-id",
317
+ Value: plan.github.qualificationId,
318
+ },
319
+ ],
320
+ }),
321
+ { mode: 0o600 },
322
+ );
323
+ requireSuccess(
324
+ commandResult(
325
+ "aws",
326
+ awsArgs(plan, profile, [
327
+ "ssm",
328
+ "put-parameter",
329
+ "--cli-input-json",
330
+ `file://${parameterInputPath}`,
331
+ "--output",
332
+ "json",
333
+ ]),
334
+ ),
335
+ "SSM JIT parameter creation",
336
+ );
337
+ }
338
+
339
+ function writeBootstrap(plan, bootstrapPath) {
340
+ const template = fs.readFileSync(
341
+ path.resolve(
342
+ "infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
343
+ ),
344
+ "utf8",
345
+ );
346
+ fs.writeFileSync(
347
+ bootstrapPath,
348
+ renderWindowsJitBootstrap(template, {
349
+ region: plan.aws.region,
350
+ campaignId: plan.campaign.id,
351
+ jitParameterName: plan.aws.jitParameterName,
352
+ evidenceBucket: plan.aws.evidenceBucket,
353
+ runnerLabel: plan.runner.label,
354
+ sourceSha: plan.source.sha,
355
+ githubRunId: plan.github.runId,
356
+ githubRunAttempt: plan.github.runAttempt,
357
+ amiId: plan.aws.amiId,
358
+ amiName: plan.aws.amiName,
359
+ instanceType: plan.aws.instanceType,
360
+ launchedAt: plan.aws.launchedAt,
361
+ }),
362
+ { mode: 0o600 },
363
+ );
364
+ }
365
+
366
+ function assertLaunchDryRun(plan, profile, bootstrapPath) {
367
+ const dryRun = commandResult(
368
+ "aws",
369
+ awsArgs(
370
+ plan,
371
+ profile,
372
+ windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: true }),
373
+ ),
374
+ );
375
+ if (
376
+ dryRun.status === 0 ||
377
+ !/DryRunOperation/.test(`${dryRun.stdout}\n${dryRun.stderr}`)
378
+ ) {
379
+ throw new Error("EC2 RunInstances DryRun did not return DryRunOperation");
380
+ }
381
+ }
382
+
383
+ function reserveCampaign(plan, profile) {
384
+ requireSuccess(
385
+ commandResult(
386
+ "aws",
387
+ awsArgs(plan, profile, [
388
+ "dynamodb",
389
+ "transact-write-items",
390
+ "--transact-items",
391
+ JSON.stringify(
392
+ windowsCampaignReservationItems(plan, new Date().toISOString()),
393
+ ),
394
+ "--output",
395
+ "json",
396
+ ]),
397
+ ),
398
+ "Windows campaign atomic reservation",
399
+ );
400
+ }
401
+
402
+ function launchInstance(plan, profile, bootstrapPath) {
403
+ const launched = jsonResult(
404
+ commandResult(
405
+ "aws",
406
+ awsArgs(
407
+ plan,
408
+ profile,
409
+ windowsRunInstancesArgs(plan, { bootstrapPath, dryRun: false }),
410
+ ),
411
+ ),
412
+ "EC2 RunInstances",
413
+ );
414
+ const instance = launched.Instances?.[0];
415
+ if (!/^i-[0-9a-f]+$/.test(String(instance?.InstanceId || ""))) {
416
+ throw new Error("EC2 RunInstances returned no instance identity");
417
+ }
418
+ return instance;
419
+ }
420
+
421
+ function markCampaignLaunched(plan, profile, instanceId) {
422
+ requireSuccess(
423
+ commandResult(
424
+ "aws",
425
+ awsArgs(
426
+ plan,
427
+ profile,
428
+ windowsCampaignMarkLaunchedArgs(
429
+ plan,
430
+ instanceId,
431
+ new Date().toISOString(),
432
+ ),
433
+ ),
434
+ ),
435
+ "Windows campaign launch ledger update",
436
+ );
437
+ }
438
+
270
439
  export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
271
440
  if (plan?.contract !== AWS_WINDOWS_JIT_CONTROLLER_CONTRACT) {
272
441
  throw new Error("Windows JIT launch plan contract is invalid");
@@ -285,124 +454,23 @@ export function executeWindowsJitLaunch(plan, { profile = "" } = {}) {
285
454
  let failure;
286
455
  const cleanupFailures = [];
287
456
  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
- );
457
+ const jit = generateJitConfiguration(plan);
458
+ createJitParameter(plan, profile, parameterInputPath, jit);
343
459
  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
- }
460
+ writeBootstrap(plan, bootstrapPath);
461
+ assertLaunchDryRun(plan, profile, bootstrapPath);
462
+ reserveCampaign(plan, profile);
383
463
  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
- }
464
+ const instance = launchInstance(plan, profile, bootstrapPath);
399
465
  launchSucceeded = true;
466
+ markCampaignLaunched(plan, profile, instance.InstanceId);
400
467
  result = {
401
468
  schemaVersion: 1,
402
469
  contract: AWS_WINDOWS_JIT_CONTROLLER_CONTRACT,
403
470
  kind: "launch-result",
404
471
  status: "launched",
405
472
  source: plan.source,
473
+ campaign: plan.campaign,
406
474
  github: plan.github,
407
475
  runner: plan.runner,
408
476
  aws: {
@@ -452,6 +520,7 @@ function planFromArgs(execute) {
452
520
  runAttempt: arg("run-attempt", "1"),
453
521
  jobId: arg("job-id"),
454
522
  qualificationId: arg("qualification-id"),
523
+ campaignId: arg("campaign-id"),
455
524
  runnerLabel: arg("runner-label"),
456
525
  runnerName: arg("runner-name"),
457
526
  sourceSha: arg("source-sha"),
@@ -464,6 +533,7 @@ function planFromArgs(execute) {
464
533
  securityGroupId: arg("security-group-id"),
465
534
  instanceProfileName: arg("instance-profile-name"),
466
535
  evidenceBucket: arg("evidence-bucket"),
536
+ stateTable: arg("state-table"),
467
537
  jitParameterName: arg("jit-parameter"),
468
538
  launchedAt: arg("launched-at", new Date().toISOString()),
469
539
  });
@@ -482,6 +552,12 @@ export function main() {
482
552
  if (arg("confirm-run-id") !== plan.github.runId) {
483
553
  throw new Error("--confirm-run-id must equal the exact GitHub run id");
484
554
  }
555
+ if (arg("confirm-campaign-id") !== plan.campaign.id) {
556
+ throw new Error("--confirm-campaign-id must equal the campaign id");
557
+ }
558
+ if (arg("confirm-state-table") !== plan.aws.stateTable) {
559
+ throw new Error("--confirm-state-table must equal the campaign state table");
560
+ }
485
561
  const result = executeWindowsJitLaunch(plan, {
486
562
  profile: arg("aws-profile"),
487
563
  });