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

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.
@@ -0,0 +1,167 @@
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(`AWS campaign mutation failed${detail ? `: ${detail}` : ""}`);
34
+ }
35
+ return result.stdout ? JSON.parse(result.stdout) : {};
36
+ }
37
+
38
+ function armPlan() {
39
+ return createWindowsJitCampaignArmPlan({
40
+ campaignId: arg("campaign-id"),
41
+ sourceSha: arg("source-sha"),
42
+ stateTable: arg("state-table"),
43
+ region: arg("region", "us-east-1"),
44
+ armedAt: arg("armed-at", new Date().toISOString()),
45
+ expiresAt: arg("expires-at"),
46
+ });
47
+ }
48
+
49
+ function confirm(plan) {
50
+ if (arg("confirm-campaign-id") !== plan.campaign.id) {
51
+ throw new Error("--confirm-campaign-id must equal the campaign id");
52
+ }
53
+ if (arg("confirm-source-sha") !== plan.source.sha) {
54
+ throw new Error("--confirm-source-sha must equal the exact source SHA");
55
+ }
56
+ if (arg("confirm-state-table") !== plan.aws.stateTable) {
57
+ throw new Error("--confirm-state-table must equal the campaign state table");
58
+ }
59
+ }
60
+
61
+ function killSwitchTopic() {
62
+ const topic = arg("kill-switch-topic");
63
+ if (
64
+ !/^arn:aws:sns:us-east-1:\d{12}:kungfu-buildchain-windows-jit-[A-Za-z0-9_-]+$/.test(
65
+ topic,
66
+ )
67
+ ) {
68
+ throw new Error("--kill-switch-topic must be the dedicated Windows JIT SNS ARN");
69
+ }
70
+ if (arg("confirm-kill-switch-topic") !== topic) {
71
+ throw new Error(
72
+ "--confirm-kill-switch-topic must equal the dedicated kill-switch topic",
73
+ );
74
+ }
75
+ return topic;
76
+ }
77
+
78
+ export function main() {
79
+ const mode = process.argv[2] || "plan-arm";
80
+ if (["plan-arm", "arm-campaign"].includes(mode)) {
81
+ const plan = armPlan();
82
+ if (mode === "plan-arm") {
83
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
84
+ return plan;
85
+ }
86
+ confirm(plan);
87
+ aws(plan, [
88
+ "dynamodb",
89
+ "transact-write-items",
90
+ "--transact-items",
91
+ JSON.stringify(windowsCampaignArmItems(plan)),
92
+ "--output",
93
+ "json",
94
+ ]);
95
+ const result = {
96
+ contract: plan.contract,
97
+ kind: "campaign-arm-result",
98
+ status: "armed",
99
+ campaign: plan.campaign,
100
+ source: plan.source,
101
+ aws: plan.aws,
102
+ limits: plan.limits,
103
+ };
104
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
105
+ return result;
106
+ }
107
+ if (mode === "kill-campaign") {
108
+ const now = new Date();
109
+ const plan = createWindowsJitCampaignArmPlan({
110
+ campaignId: arg("campaign-id"),
111
+ sourceSha: arg("source-sha"),
112
+ stateTable: arg("state-table"),
113
+ region: arg("region", "us-east-1"),
114
+ armedAt: now.toISOString(),
115
+ expiresAt: new Date(now.getTime() + 1000).toISOString(),
116
+ });
117
+ confirm(plan);
118
+ const topic = killSwitchTopic();
119
+ aws(
120
+ plan,
121
+ windowsCampaignKillArgs(
122
+ plan.aws.stateTable,
123
+ arg("reason", "operator-kill"),
124
+ new Date().toISOString(),
125
+ ),
126
+ );
127
+ const notification = aws(plan, [
128
+ "sns",
129
+ "publish",
130
+ "--topic-arn",
131
+ topic,
132
+ "--message",
133
+ JSON.stringify({
134
+ contract: plan.contract,
135
+ action: "kill-campaign",
136
+ campaignId: plan.campaign.id,
137
+ sourceSha: plan.source.sha,
138
+ }),
139
+ "--output",
140
+ "json",
141
+ ]);
142
+ const result = {
143
+ contract: plan.contract,
144
+ kind: "campaign-kill-result",
145
+ status: "killed",
146
+ campaign: plan.campaign,
147
+ source: plan.source,
148
+ killSwitchTopic: topic,
149
+ notificationMessageId: notification.MessageId || "",
150
+ };
151
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
152
+ return result;
153
+ }
154
+ throw new Error(`unsupported Windows JIT campaign mode: ${mode}`);
155
+ }
156
+
157
+ if (
158
+ process.argv[1] &&
159
+ import.meta.url === pathToFileURL(process.argv[1]).href
160
+ ) {
161
+ try {
162
+ main();
163
+ } catch (error) {
164
+ console.error(`::error::${error.message || error}`);
165
+ process.exitCode = 1;
166
+ }
167
+ }
@@ -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
  });
@@ -75,6 +75,14 @@ export function windowsJitRunnerLabels(value) {
75
75
  return ["self-hosted", "Windows", "X64", windowsJitRunnerLabel(value)];
76
76
  }
77
77
 
78
+ export function windowsJitCampaignId(value) {
79
+ const campaign = String(value || "").trim();
80
+ if (!/^win-[a-z0-9][a-z0-9-]{2,15}$/.test(campaign)) {
81
+ throw new Error("campaignId must be a bounded Windows campaign id");
82
+ }
83
+ return campaign;
84
+ }
85
+
78
86
  export function renderWindowsJitBootstrap(template, values = {}) {
79
87
  const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
80
88
  const jitParameterName = String(values.jitParameterName || "").trim();
@@ -86,6 +94,7 @@ export function renderWindowsJitBootstrap(template, values = {}) {
86
94
  }
87
95
  const bounded = {
88
96
  REGION: String(values.region || WINDOWS_EC2_JIT.region),
97
+ CAMPAIGN_ID: String(values.campaignId || ""),
89
98
  JIT_PARAMETER_NAME: jitParameterName,
90
99
  EVIDENCE_BUCKET: String(values.evidenceBucket || ""),
91
100
  RUNNER_LABEL: runnerLabel,
@@ -99,6 +108,7 @@ export function renderWindowsJitBootstrap(template, values = {}) {
99
108
  };
100
109
  const patterns = {
101
110
  REGION: /^us-[a-z]+-\d$/,
111
+ CAMPAIGN_ID: /^win-[a-z0-9][a-z0-9-]{2,43}$/,
102
112
  EVIDENCE_BUCKET: /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
103
113
  GITHUB_RUN_ID: /^\d+$/,
104
114
  GITHUB_RUN_ATTEMPT: /^\d+$/,
@@ -184,6 +194,7 @@ export function windowsEc2JitPlan(overrides = {}) {
184
194
 
185
195
  export function createWindowsJitEvidence({
186
196
  repository,
197
+ campaignId,
187
198
  sourceSha,
188
199
  sourceRef,
189
200
  githubRunId,
@@ -235,6 +246,7 @@ export function createWindowsJitEvidence({
235
246
  phase: WINDOWS_EC2_JIT.phase,
236
247
  provider: "aws-ec2",
237
248
  repository,
249
+ campaign: { id: windowsJitCampaignId(campaignId) },
238
250
  source: {
239
251
  sha: exactSha(sourceSha, "sourceSha"),
240
252
  ref: String(sourceRef || "").trim(),
@@ -286,6 +298,7 @@ export function createWindowsJitEvidence({
286
298
  }
287
299
 
288
300
  export function verifyWindowsEc2JitQualification({
301
+ campaignId,
289
302
  jobs = [],
290
303
  cancellationCleanup = {},
291
304
  timeoutCleanup = {},
@@ -299,6 +312,7 @@ export function verifyWindowsEc2JitQualification({
299
312
  } = {}) {
300
313
  const plan = windowsEc2JitPlan();
301
314
  const issues = [];
315
+ const campaign = windowsJitCampaignId(campaignId);
302
316
  const accepted = jobs.filter(
303
317
  (job) =>
304
318
  job?.trusted === true &&
@@ -308,6 +322,9 @@ export function verifyWindowsEc2JitQualification({
308
322
  );
309
323
  const smokeJobs = accepted.filter((job) => job.kind === "smoke");
310
324
  const fullJobs = accepted.filter((job) => job.kind === "full");
325
+ if (accepted.some((job) => job.campaignId !== campaign)) {
326
+ issues.push("accepted-jobs-not-bound-to-campaign");
327
+ }
311
328
  if (smokeJobs.length < plan.config.minimumSmokeJobs) {
312
329
  issues.push("runner-profile-smoke-missing");
313
330
  }
@@ -349,6 +366,7 @@ export function verifyWindowsEc2JitQualification({
349
366
  contract: AWS_WINDOWS_JIT_CONTRACT,
350
367
  kind: "phase-verification",
351
368
  phase: plan.config.phase,
369
+ campaign: { id: campaign },
352
370
  status: issues.length ? "failed" : "passed",
353
371
  qualifying: issues.length === 0,
354
372
  metrics: {
@@ -35,6 +35,7 @@ export function main() {
35
35
  if (mode === "evidence") {
36
36
  const result = createWindowsJitEvidence({
37
37
  repository: process.env.GITHUB_REPOSITORY,
38
+ campaignId: process.env.AWS_EC2_CAMPAIGN_ID,
38
39
  sourceSha:
39
40
  process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || process.env.GITHUB_SHA,
40
41
  sourceRef:
@@ -77,6 +78,7 @@ export function main() {
77
78
  fs.readFileSync(path.resolve(template), "utf8"),
78
79
  {
79
80
  region: arg("region", "us-east-1"),
81
+ campaignId: arg("campaign-id"),
80
82
  jitParameterName: arg("jit-parameter"),
81
83
  evidenceBucket: arg("evidence-bucket"),
82
84
  runnerLabel: arg("runner-label"),