@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,401 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { AWS_MACOS_JIT_CONTROLLER_CONTRACT } from "./aws-macos-jit-controller-core.mjs";
7
+ import {
8
+ assertOwnership,
9
+ awsArgs,
10
+ commandResult,
11
+ ghJson,
12
+ jsonResult,
13
+ requireSuccess,
14
+ } from "./aws-macos-jit-controller-runtime.mjs";
15
+ import { renderMacosJitBootstrap } from "./aws-macos-jit-core.mjs";
16
+
17
+ function assertExactJobPreflight(plan, profile) {
18
+ const run = ghJson(
19
+ ["api", `repos/${plan.repository}/actions/runs/${plan.github.runId}`],
20
+ undefined,
21
+ "GitHub run preflight",
22
+ );
23
+ if (
24
+ run.event !== plan.github.event ||
25
+ run.head_sha !== plan.source.sha ||
26
+ run.head_repository?.full_name !== plan.repository ||
27
+ !["queued", "in_progress"].includes(run.status)
28
+ ) {
29
+ throw new Error("GitHub run is not the trusted exact-source dispatch");
30
+ }
31
+ const job = ghJson(
32
+ ["api", `repos/${plan.repository}/actions/jobs/${plan.github.jobId}`],
33
+ undefined,
34
+ "GitHub job preflight",
35
+ );
36
+ if (job.status !== "queued") {
37
+ throw new Error(
38
+ `GitHub job must be queued, observed ${job.status || "unknown"}`,
39
+ );
40
+ }
41
+ if (
42
+ !Array.isArray(job.labels) ||
43
+ !plan.runner.labels.every((label) => job.labels.includes(label))
44
+ ) {
45
+ throw new Error("GitHub job labels do not match the macOS JIT job plan");
46
+ }
47
+ const host = jsonResult(
48
+ commandResult(
49
+ "aws",
50
+ awsArgs(plan, profile, [
51
+ "ec2",
52
+ "describe-hosts",
53
+ "--host-ids",
54
+ plan.aws.hostId,
55
+ "--output",
56
+ "json",
57
+ ]),
58
+ ),
59
+ "macOS campaign host preflight",
60
+ ).Hosts?.[0];
61
+ if (
62
+ !host ||
63
+ host.State !== "available" ||
64
+ host.AvailabilityZone !== plan.aws.availabilityZone ||
65
+ host.HostProperties?.InstanceType !== plan.aws.instanceType
66
+ ) {
67
+ throw new Error("macOS campaign host identity or state mismatch");
68
+ }
69
+ assertOwnership(host.Tags, plan);
70
+ const instance = jsonResult(
71
+ commandResult(
72
+ "aws",
73
+ awsArgs(plan, profile, [
74
+ "ec2",
75
+ "describe-instances",
76
+ "--instance-ids",
77
+ plan.aws.instanceId,
78
+ "--output",
79
+ "json",
80
+ ]),
81
+ ),
82
+ "macOS campaign instance preflight",
83
+ ).Reservations?.[0]?.Instances?.[0];
84
+ if (
85
+ !instance ||
86
+ instance.State?.Name !== "running" ||
87
+ instance.Placement?.HostId !== plan.aws.hostId ||
88
+ instance.ImageId !== plan.aws.amiId ||
89
+ instance.InstanceType !== plan.aws.instanceType
90
+ ) {
91
+ throw new Error("macOS campaign instance identity or state mismatch");
92
+ }
93
+ assertOwnership(instance.Tags, plan);
94
+ const parameters = jsonResult(
95
+ commandResult(
96
+ "aws",
97
+ awsArgs(plan, profile, [
98
+ "ssm",
99
+ "describe-parameters",
100
+ "--parameter-filters",
101
+ `Key=Name,Option=Equals,Values=${plan.aws.jitParameterName}`,
102
+ "--output",
103
+ "json",
104
+ ]),
105
+ ),
106
+ "macOS JIT parameter preflight",
107
+ );
108
+ if ((parameters.Parameters || []).length !== 0) {
109
+ throw new Error("macOS JIT parameter already exists");
110
+ }
111
+ return {
112
+ runStatus: run.status,
113
+ jobStatus: job.status,
114
+ hostState: host.State,
115
+ instanceState: instance.State.Name,
116
+ };
117
+ }
118
+
119
+ function deleteRunnerRegistration(plan) {
120
+ const runners = ghJson(
121
+ ["api", `repos/${plan.repository}/actions/runners?per_page=100`],
122
+ undefined,
123
+ "GitHub runner cleanup lookup",
124
+ ).runners;
125
+ const runner = (runners || []).find(
126
+ (entry) => entry.name === plan.runner.name,
127
+ );
128
+ if (!runner) return false;
129
+ requireSuccess(
130
+ commandResult("gh", [
131
+ "api",
132
+ "--method",
133
+ "DELETE",
134
+ `repos/${plan.repository}/actions/runners/${runner.id}`,
135
+ ]),
136
+ "GitHub runner cleanup",
137
+ );
138
+ return true;
139
+ }
140
+
141
+ function waitForSsmOnline(plan, profile, timeoutMs = 15 * 60 * 1000) {
142
+ const deadline = Date.now() + timeoutMs;
143
+ while (Date.now() < deadline) {
144
+ const response = jsonResult(
145
+ commandResult(
146
+ "aws",
147
+ awsArgs(plan, profile, [
148
+ "ssm",
149
+ "describe-instance-information",
150
+ "--filters",
151
+ `Key=InstanceIds,Values=${plan.aws.instanceId}`,
152
+ "--output",
153
+ "json",
154
+ ]),
155
+ ),
156
+ "macOS SSM online preflight",
157
+ );
158
+ const found = (response.InstanceInformationList || []).find(
159
+ (entry) =>
160
+ entry.InstanceId === plan.aws.instanceId &&
161
+ entry.PingStatus === "Online",
162
+ );
163
+ if (found) return found;
164
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
165
+ }
166
+ throw new Error("macOS campaign instance did not become SSM Online in time");
167
+ }
168
+
169
+ function cleanupFailedCommand(plan, profile, parameterCreated) {
170
+ const failures = [];
171
+ const attempt = (cleanup) => {
172
+ try {
173
+ cleanup();
174
+ } catch (error) {
175
+ failures.push(error.message || String(error));
176
+ }
177
+ };
178
+ if (parameterCreated) {
179
+ attempt(() =>
180
+ requireSuccess(
181
+ commandResult(
182
+ "aws",
183
+ awsArgs(plan, profile, [
184
+ "ssm",
185
+ "delete-parameter",
186
+ "--name",
187
+ plan.aws.jitParameterName,
188
+ ]),
189
+ ),
190
+ "macOS JIT parameter cleanup",
191
+ ),
192
+ );
193
+ }
194
+ attempt(() => deleteRunnerRegistration(plan));
195
+ return failures;
196
+ }
197
+
198
+ function createJitParameter(plan, profile, parameterInputPath) {
199
+ const jit = ghJson(
200
+ [
201
+ "api",
202
+ "--method",
203
+ "POST",
204
+ `repos/${plan.repository}/actions/runners/generate-jitconfig`,
205
+ "--input",
206
+ "-",
207
+ ],
208
+ JSON.stringify({
209
+ name: plan.runner.name,
210
+ runner_group_id: 1,
211
+ labels: plan.runner.labels,
212
+ work_folder: "_work",
213
+ }),
214
+ "GitHub JIT configuration",
215
+ ).encoded_jit_config;
216
+ if (!jit || typeof jit !== "string") {
217
+ throw new Error("GitHub JIT configuration was empty");
218
+ }
219
+ fs.writeFileSync(
220
+ parameterInputPath,
221
+ JSON.stringify({
222
+ Name: plan.aws.jitParameterName,
223
+ Description: `One-shot macOS GitHub Actions JIT config for ${plan.repository} run ${plan.github.runId}`,
224
+ Type: "SecureString",
225
+ Tier: "Advanced",
226
+ Value: jit,
227
+ Tags: [
228
+ { Key: "kungfu:owner", Value: "buildchain" },
229
+ { Key: "kungfu:plane", Value: "aws-us-elastic-runner-burst" },
230
+ { Key: "kungfu:provider", Value: "macos-ec2-jit" },
231
+ { Key: "kungfu:campaign-id", Value: plan.campaign.id },
232
+ { Key: "kungfu:github-run-id", Value: plan.github.runId },
233
+ { Key: "kungfu:qualification-id", Value: plan.github.qualificationId },
234
+ ],
235
+ }),
236
+ { mode: 0o600 },
237
+ );
238
+ requireSuccess(
239
+ commandResult(
240
+ "aws",
241
+ awsArgs(plan, profile, [
242
+ "ssm",
243
+ "put-parameter",
244
+ "--cli-input-json",
245
+ `file://${parameterInputPath}`,
246
+ "--output",
247
+ "json",
248
+ ]),
249
+ ),
250
+ "macOS JIT parameter creation",
251
+ );
252
+ }
253
+
254
+ function sendBootstrap(plan, profile, { bootstrapPath, commandInputPath }) {
255
+ fs.writeFileSync(
256
+ bootstrapPath,
257
+ renderMacosJitBootstrap(
258
+ fs.readFileSync(
259
+ path.resolve(
260
+ "infra/aws-us-elastic-runner-burst-plane/macos-jit-bootstrap.sh",
261
+ ),
262
+ "utf8",
263
+ ),
264
+ {
265
+ region: plan.aws.region,
266
+ jitParameterName: plan.aws.jitParameterName,
267
+ evidenceBucket: plan.aws.evidenceBucket,
268
+ runnerLabel: plan.runner.label,
269
+ sourceSha: plan.source.sha,
270
+ githubRunId: plan.github.runId,
271
+ githubRunAttempt: plan.github.runAttempt,
272
+ amiId: plan.aws.amiId,
273
+ amiName: plan.aws.amiName,
274
+ hostId: plan.aws.hostId,
275
+ instanceType: plan.aws.instanceType,
276
+ hostAllocatedAt: plan.aws.hostAllocatedAt,
277
+ },
278
+ ),
279
+ { mode: 0o600 },
280
+ );
281
+ const ssm = waitForSsmOnline(plan, profile);
282
+ const remotePath = `/private/tmp/kungfu-macos-jit-${plan.github.runId}-${plan.github.qualificationId}.sh`;
283
+ const bootstrapBase64 = fs.readFileSync(bootstrapPath).toString("base64");
284
+ fs.writeFileSync(
285
+ commandInputPath,
286
+ JSON.stringify({
287
+ DocumentName: "AWS-RunShellScript",
288
+ InstanceIds: [plan.aws.instanceId],
289
+ Comment: `kungfu macOS JIT ${plan.campaign.id} ${plan.github.qualificationId}`,
290
+ TimeoutSeconds: 10800,
291
+ Parameters: {
292
+ commands: [
293
+ `printf '%s' '${bootstrapBase64}' | base64 --decode > '${remotePath}'`,
294
+ `chmod 700 '${remotePath}'`,
295
+ `sudo /bin/bash '${remotePath}'`,
296
+ `status=$?; rm -f '${remotePath}'; exit $status`,
297
+ ],
298
+ executionTimeout: ["10800"],
299
+ },
300
+ }),
301
+ { mode: 0o600 },
302
+ );
303
+ const sent = jsonResult(
304
+ commandResult(
305
+ "aws",
306
+ awsArgs(plan, profile, [
307
+ "ssm",
308
+ "send-command",
309
+ "--cli-input-json",
310
+ `file://${commandInputPath}`,
311
+ "--output",
312
+ "json",
313
+ ]),
314
+ ),
315
+ "macOS JIT SSM command",
316
+ );
317
+ const commandId = sent.Command?.CommandId;
318
+ if (!/^[0-9a-f-]{36}$/.test(String(commandId || ""))) {
319
+ throw new Error("SSM SendCommand returned no command identity");
320
+ }
321
+ return { commandId, ssmAgentVersion: ssm.AgentVersion };
322
+ }
323
+
324
+ function dispatchMacosJitJob(plan, profile, files, state) {
325
+ createJitParameter(plan, profile, files.parameterInputPath);
326
+ state.parameterCreated = true;
327
+ requireSuccess(
328
+ commandResult(
329
+ "aws",
330
+ awsArgs(plan, profile, [
331
+ "ec2",
332
+ "create-tags",
333
+ "--resources",
334
+ plan.aws.instanceId,
335
+ "--tags",
336
+ `Key=kungfu:jit-parameter,Value=${plan.aws.jitParameterName}`,
337
+ ]),
338
+ ),
339
+ "macOS campaign instance JIT tag update",
340
+ );
341
+ return sendBootstrap(plan, profile, files);
342
+ }
343
+
344
+ export function executeMacosJitJob(plan, { profile = "" } = {}) {
345
+ if (
346
+ plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
347
+ plan.kind !== "job-run-plan"
348
+ ) {
349
+ throw new Error("macOS JIT job plan contract is invalid");
350
+ }
351
+ const preflight = assertExactJobPreflight(plan, profile);
352
+ const tempRoot = fs.mkdtempSync(
353
+ path.join(os.tmpdir(), "buildchain-macos-jit-controller-"),
354
+ );
355
+ fs.chmodSync(tempRoot, 0o700);
356
+ const files = {
357
+ parameterInputPath: path.join(tempRoot, "ssm-parameter.json"),
358
+ bootstrapPath: path.join(tempRoot, "bootstrap.sh"),
359
+ commandInputPath: path.join(tempRoot, "ssm-command.json"),
360
+ };
361
+ const state = { parameterCreated: false };
362
+ try {
363
+ const sent = dispatchMacosJitJob(plan, profile, files, state);
364
+ return {
365
+ schemaVersion: 1,
366
+ contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
367
+ kind: "job-command-result",
368
+ status: "command-sent",
369
+ campaign: plan.campaign,
370
+ source: plan.source,
371
+ github: plan.github,
372
+ runner: plan.runner,
373
+ aws: {
374
+ region: plan.aws.region,
375
+ hostId: plan.aws.hostId,
376
+ instanceId: plan.aws.instanceId,
377
+ ...sent,
378
+ jitParameterName: plan.aws.jitParameterName,
379
+ },
380
+ preflight,
381
+ planDigest: plan.digest,
382
+ };
383
+ } catch (error) {
384
+ const failures = cleanupFailedCommand(
385
+ plan,
386
+ profile,
387
+ state.parameterCreated,
388
+ );
389
+ if (failures.length) {
390
+ throw new Error(
391
+ `${error.message || error}; cleanup failed: ${failures.join("; ")}`,
392
+ );
393
+ }
394
+ throw error;
395
+ } finally {
396
+ for (const file of Object.values(files)) {
397
+ if (fs.existsSync(file)) fs.unlinkSync(file);
398
+ }
399
+ fs.rmdirSync(tempRoot);
400
+ }
401
+ }
@@ -0,0 +1,296 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import {
4
+ WINDOWS_EC2_JIT,
5
+ windowsJitCampaignId,
6
+ } from "./aws-windows-jit-core.mjs";
7
+
8
+ export const AWS_WINDOWS_JIT_CAMPAIGN_CONTRACT =
9
+ "kungfu-buildchain-aws-windows-jit-campaign/v1";
10
+
11
+ function exact(value, pattern, label) {
12
+ const normalized = String(value || "").trim();
13
+ if (!pattern.test(normalized)) throw new Error(`${label} is invalid`);
14
+ return normalized;
15
+ }
16
+
17
+ function exactSha(value) {
18
+ return exact(value, /^[0-9a-f]{40}$/i, "sourceSha").toLowerCase();
19
+ }
20
+
21
+ function epoch(value, label) {
22
+ const milliseconds = new Date(value || "").getTime();
23
+ if (!Number.isFinite(milliseconds)) {
24
+ throw new Error(`${label} must be an ISO timestamp`);
25
+ }
26
+ return Math.floor(milliseconds / 1000);
27
+ }
28
+
29
+ function tableName(value) {
30
+ return exact(
31
+ value,
32
+ /^kungfu-buildchain-windows-jit(?:-[A-Za-z0-9_.-]+)?$/,
33
+ "stateTable",
34
+ );
35
+ }
36
+
37
+ function number(value) {
38
+ return { N: String(value) };
39
+ }
40
+
41
+ function money(value, label) {
42
+ const normalized = String(value ?? "").trim();
43
+ if (!normalized) {
44
+ throw new Error(`${label} is required`);
45
+ }
46
+ const parsed = Number(normalized);
47
+ if (!Number.isFinite(parsed) || parsed < 0) {
48
+ throw new Error(`${label} must be a non-negative finite number`);
49
+ }
50
+ return Math.round(parsed * 100_000_000) / 100_000_000;
51
+ }
52
+
53
+ function acceptedInstances(value) {
54
+ const normalized = String(value ?? "").trim();
55
+ if (!normalized) return WINDOWS_EC2_JIT.maxAcceptedInstances;
56
+ const parsed = Number(normalized);
57
+ if (
58
+ !Number.isInteger(parsed) ||
59
+ parsed < 1 ||
60
+ parsed > WINDOWS_EC2_JIT.maxAcceptedInstances
61
+ ) {
62
+ throw new Error(
63
+ `maxAcceptedInstances must be an integer from 1 through ${WINDOWS_EC2_JIT.maxAcceptedInstances}`,
64
+ );
65
+ }
66
+ return parsed;
67
+ }
68
+
69
+ function string(value) {
70
+ return { S: String(value) };
71
+ }
72
+
73
+ function runKey(plan) {
74
+ return `RUN#${plan.github.runId}#${plan.github.runAttempt}#${plan.github.qualificationId}`;
75
+ }
76
+
77
+ export function createWindowsJitCampaignArmPlan(values = {}) {
78
+ const armedAt = epoch(values.armedAt, "armedAt");
79
+ const expiresAt = epoch(values.expiresAt, "expiresAt");
80
+ if (expiresAt <= armedAt || expiresAt - armedAt > 86400) {
81
+ throw new Error("campaign expiry must be within 24 hours after arming");
82
+ }
83
+ const reservationUsd =
84
+ (WINDOWS_EC2_JIT.pricePerHourUsd *
85
+ WINDOWS_EC2_JIT.maximumInstanceLifetimeMinutes) /
86
+ 60;
87
+ const phaseSpendBaselineUsd = money(
88
+ values.phaseSpendBaselineUsd,
89
+ "phaseSpendBaselineUsd",
90
+ );
91
+ const maxAcceptedInstances = acceptedInstances(values.maxAcceptedInstances);
92
+ const campaignReservationCeilingUsd = reservationUsd * maxAcceptedInstances;
93
+ const campaignSafetyCeilingUsd =
94
+ campaignReservationCeilingUsd +
95
+ reservationUsd * WINDOWS_EC2_JIT.maxConcurrentInstances;
96
+ const remainingPhaseBudgetUsd =
97
+ WINDOWS_EC2_JIT.budgetLimitUsd - phaseSpendBaselineUsd;
98
+ if (campaignSafetyCeilingUsd >= remainingPhaseBudgetUsd) {
99
+ throw new Error(
100
+ "campaign safety envelope must remain below the remaining Windows phase budget",
101
+ );
102
+ }
103
+ return {
104
+ schemaVersion: 1,
105
+ contract: AWS_WINDOWS_JIT_CAMPAIGN_CONTRACT,
106
+ kind: "campaign-arm-plan",
107
+ campaign: {
108
+ id: windowsJitCampaignId(values.campaignId),
109
+ armedAt,
110
+ expiresAt,
111
+ },
112
+ source: { sha: exactSha(values.sourceSha) },
113
+ aws: {
114
+ region: exact(
115
+ values.region || WINDOWS_EC2_JIT.region,
116
+ /^us-[a-z]+-\d$/,
117
+ "region",
118
+ ),
119
+ stateTable: tableName(values.stateTable),
120
+ },
121
+ limits: {
122
+ maxAcceptedInstances,
123
+ reservationUsd,
124
+ budgetLimitUsd: WINDOWS_EC2_JIT.budgetLimitUsd,
125
+ phaseSpendBaselineUsd,
126
+ remainingPhaseBudgetUsd,
127
+ campaignReservationCeilingUsd,
128
+ campaignSafetyCeilingUsd,
129
+ reservationLimitUsd: remainingPhaseBudgetUsd - reservationUsd,
130
+ },
131
+ };
132
+ }
133
+
134
+ export function windowsCampaignArmItems(plan) {
135
+ if (plan?.contract !== AWS_WINDOWS_JIT_CAMPAIGN_CONTRACT) {
136
+ throw new Error("Windows JIT campaign arm plan contract is invalid");
137
+ }
138
+ const table = plan.aws.stateTable;
139
+ const campaignPk = `CAMPAIGN#${plan.campaign.id}`;
140
+ return [
141
+ {
142
+ Put: {
143
+ TableName: table,
144
+ Item: {
145
+ pk: string("CONTROL"),
146
+ state: string("ARMED"),
147
+ campaign_id: string(plan.campaign.id),
148
+ source_sha: string(plan.source.sha),
149
+ phase_spend_baseline_usd: number(plan.limits.phaseSpendBaselineUsd),
150
+ budget_limit_usd: number(plan.limits.budgetLimitUsd),
151
+ armed_at: number(plan.campaign.armedAt),
152
+ expires_epoch: number(plan.campaign.expiresAt),
153
+ },
154
+ ConditionExpression: "attribute_not_exists(pk)",
155
+ },
156
+ },
157
+ {
158
+ Put: {
159
+ TableName: table,
160
+ Item: {
161
+ pk: string(campaignPk),
162
+ state: string("ARMED"),
163
+ source_sha: string(plan.source.sha),
164
+ accepted_instances: number(0),
165
+ reserved_usd: number(0),
166
+ max_accepted_instances: number(plan.limits.maxAcceptedInstances),
167
+ reservation_usd: number(plan.limits.reservationUsd),
168
+ budget_limit_usd: number(plan.limits.budgetLimitUsd),
169
+ phase_spend_baseline_usd: number(plan.limits.phaseSpendBaselineUsd),
170
+ remaining_phase_budget_usd: number(
171
+ plan.limits.remainingPhaseBudgetUsd,
172
+ ),
173
+ campaign_reservation_ceiling_usd: number(
174
+ plan.limits.campaignReservationCeilingUsd,
175
+ ),
176
+ campaign_safety_ceiling_usd: number(
177
+ plan.limits.campaignSafetyCeilingUsd,
178
+ ),
179
+ reservation_limit_usd: number(plan.limits.reservationLimitUsd),
180
+ armed_at: number(plan.campaign.armedAt),
181
+ expires_epoch: number(plan.campaign.expiresAt),
182
+ },
183
+ ConditionExpression: "attribute_not_exists(pk)",
184
+ },
185
+ },
186
+ ];
187
+ }
188
+
189
+ export function windowsCampaignReservationItems(plan, observedAt) {
190
+ const now = epoch(observedAt, "observedAt");
191
+ const runPk = runKey(plan);
192
+ const campaignPk = `CAMPAIGN#${plan.campaign.id}`;
193
+ const reservation = plan.safety.campaignReservationUsd;
194
+ return [
195
+ {
196
+ ConditionCheck: {
197
+ TableName: plan.aws.stateTable,
198
+ Key: { pk: string("CONTROL") },
199
+ ConditionExpression:
200
+ "#state = :armed AND campaign_id = :campaign AND source_sha = :source AND expires_epoch >= :now",
201
+ ExpressionAttributeNames: { "#state": "state" },
202
+ ExpressionAttributeValues: {
203
+ ":armed": string("ARMED"),
204
+ ":campaign": string(plan.campaign.id),
205
+ ":source": string(plan.source.sha),
206
+ ":now": number(now),
207
+ },
208
+ },
209
+ },
210
+ {
211
+ Put: {
212
+ TableName: plan.aws.stateTable,
213
+ Item: {
214
+ pk: string(runPk),
215
+ state: string("RESERVED"),
216
+ campaign_id: string(plan.campaign.id),
217
+ source_sha: string(plan.source.sha),
218
+ github_run_id: string(plan.github.runId),
219
+ github_run_attempt: string(plan.github.runAttempt),
220
+ qualification_id: string(plan.github.qualificationId),
221
+ reserved_usd: number(reservation),
222
+ observed_at: number(now),
223
+ expires_at: number(now + 1209600),
224
+ },
225
+ ConditionExpression: "attribute_not_exists(pk)",
226
+ },
227
+ },
228
+ {
229
+ Update: {
230
+ TableName: plan.aws.stateTable,
231
+ Key: { pk: string(campaignPk) },
232
+ UpdateExpression:
233
+ "ADD accepted_instances :one, reserved_usd :reservation SET updated_at = :now",
234
+ ConditionExpression:
235
+ "#state = :armed AND source_sha = :source AND accepted_instances < max_accepted_instances AND reserved_usd <= reservation_limit_usd",
236
+ ExpressionAttributeNames: { "#state": "state" },
237
+ ExpressionAttributeValues: {
238
+ ":armed": string("ARMED"),
239
+ ":source": string(plan.source.sha),
240
+ ":one": number(1),
241
+ ":reservation": number(reservation),
242
+ ":now": number(now),
243
+ },
244
+ },
245
+ },
246
+ ];
247
+ }
248
+
249
+ export function windowsCampaignMarkLaunchedArgs(plan, instanceId, observedAt) {
250
+ return [
251
+ "dynamodb",
252
+ "update-item",
253
+ "--table-name",
254
+ plan.aws.stateTable,
255
+ "--key",
256
+ JSON.stringify({ pk: string(runKey(plan)) }),
257
+ "--update-expression",
258
+ "SET #state = :launched, instance_id = :instance, launched_at = :now",
259
+ "--condition-expression",
260
+ "#state = :reserved",
261
+ "--expression-attribute-names",
262
+ JSON.stringify({ "#state": "state" }),
263
+ "--expression-attribute-values",
264
+ JSON.stringify({
265
+ ":launched": string("LAUNCHED"),
266
+ ":reserved": string("RESERVED"),
267
+ ":instance": string(instanceId),
268
+ ":now": number(epoch(observedAt, "observedAt")),
269
+ }),
270
+ "--output",
271
+ "json",
272
+ ];
273
+ }
274
+
275
+ export function windowsCampaignKillArgs(stateTable, reason, observedAt) {
276
+ return [
277
+ "dynamodb",
278
+ "update-item",
279
+ "--table-name",
280
+ tableName(stateTable),
281
+ "--key",
282
+ JSON.stringify({ pk: string("CONTROL") }),
283
+ "--update-expression",
284
+ "SET #state = :killed, reason = :reason, killed_at = :now",
285
+ "--expression-attribute-names",
286
+ JSON.stringify({ "#state": "state" }),
287
+ "--expression-attribute-values",
288
+ JSON.stringify({
289
+ ":killed": string("KILLED"),
290
+ ":reason": string(exact(reason, /^[a-z0-9][a-z0-9-]{2,63}$/, "reason")),
291
+ ":now": number(epoch(observedAt, "observedAt")),
292
+ }),
293
+ "--output",
294
+ "json",
295
+ ];
296
+ }