@kungfu-tech/buildchain 3.0.9-alpha.2 → 3.0.9-alpha.4

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 (50) hide show
  1. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  2. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  3. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  4. package/contracts/next-development-request-v1.schema.json +66 -0
  5. package/contracts/next-development-transition-v1.schema.json +292 -0
  6. package/dist/site/buildchain-contract.json +4 -4
  7. package/dist/site/buildchain-site.json +89 -18
  8. package/dist/site/capability-registry.json +4 -3
  9. package/dist/site/kfd-claims.json +65 -5
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +19 -3
  12. package/dist/site/node-api-registry.json +1017 -37
  13. package/dist/site/page-registry.json +74 -11
  14. package/dist/site/public-surface-audit.json +7 -3
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +3 -0
  17. package/dist/site/site-manifest.json +15 -7
  18. package/dist/site/workflow-registry.json +4 -4
  19. package/docs/MAP.md +2 -1
  20. package/docs/aws-us-elastic-runner-burst-plane.md +25 -3
  21. package/docs/dev-alpha-candidate-patrol.md +8 -0
  22. package/docs/next-development-transition.md +119 -0
  23. package/docs/node-api-reference.md +126 -73
  24. package/docs/versioning.md +1 -1
  25. package/package.json +8 -3
  26. package/packages/core/buildchain-agent-manuals.js +1 -0
  27. package/packages/core/channel-candidate.js +8 -0
  28. package/packages/core/channel-promotion-baseline.js +52 -0
  29. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  30. package/packages/core/next-development-candidate-reservation.js +186 -0
  31. package/packages/core/next-development-controller.js +726 -0
  32. package/packages/core/next-development-projection.js +288 -0
  33. package/packages/core/next-development-transition.js +738 -0
  34. package/packages/core/paper-agent-entry.js +7 -4
  35. package/packages/core/paper.js +14 -7
  36. package/packages/core/release-passport-contract.js +1 -1
  37. package/scripts/aws-macos-jit-controller-core.mjs +109 -4
  38. package/scripts/aws-macos-jit-controller.mjs +71 -2
  39. package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
  40. package/scripts/aws-macos-jit-source-rebind.mjs +71 -14
  41. package/scripts/check-inventory.mjs +11 -0
  42. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  43. package/scripts/generate-next-development-guidance.mjs +49 -0
  44. package/scripts/generate-site-bundle.mjs +2 -0
  45. package/scripts/init-repo.mjs +119 -62
  46. package/scripts/next-development-self-dogfood-harness.mjs +409 -0
  47. package/scripts/next-development-self-dogfood.mjs +532 -0
  48. package/scripts/next-development-transition.mjs +47 -0
  49. package/scripts/site-capability-metadata.mjs +3 -0
  50. package/scripts/stable-candidate-qualification.mjs +7 -7
@@ -13,6 +13,7 @@ import {
13
13
  workCheck,
14
14
  } from "./paper-repository.js";
15
15
  import { mergePublicationRehearsalAgentInstructions } from "./publication-rehearsal-projection.js";
16
+ import { mergeNextDevelopmentAgentInstructions } from "./next-development-projection.js";
16
17
 
17
18
  export const PAPER_AGENT_ENTRY_CONTRACT = "kungfu-buildchain-paper-agent-entry";
18
19
  export const PAPER_AGENT_ENTRY_SCHEMA_VERSION = 1;
@@ -168,10 +169,12 @@ export function paperAgentEntryFiles({
168
169
  [PAPER_PATHS.agentEntry, jsonText(entry)],
169
170
  [
170
171
  PAPER_PATHS.agentInstructions,
171
- mergePublicationRehearsalAgentInstructions(
172
- mergePaperAgentEntryInstructions(currentAgents, {
173
- developmentRef: resolvedDevelopmentRef,
174
- }),
172
+ mergeNextDevelopmentAgentInstructions(
173
+ mergePublicationRehearsalAgentInstructions(
174
+ mergePaperAgentEntryInstructions(currentAgents, {
175
+ developmentRef: resolvedDevelopmentRef,
176
+ }),
177
+ ),
175
178
  ),
176
179
  ],
177
180
  ]);
@@ -51,6 +51,11 @@ import {
51
51
  projectPublicationRehearsalToml,
52
52
  publicationRehearsalWorkflow,
53
53
  } from "./publication-rehearsal-projection.js";
54
+ import {
55
+ appendNextDevelopmentToml,
56
+ nextDevelopmentWorkflowHeader,
57
+ projectNextDevelopmentToml,
58
+ } from "./next-development-projection.js";
54
59
 
55
60
  export { PAPER_PATHS, resolvePaperRepository } from "./paper-repository.js";
56
61
  export {
@@ -356,7 +361,7 @@ immutable_base_url = ${tomlString(joinUrl(siteBaseUrl, "archive").replace(/\/$/,
356
361
  registry_path = ".buildchain/publication/publication-registry.json"
357
362
  `
358
363
  : "";
359
- return appendPublicationRehearsalToml(`schema = 1
364
+ return appendNextDevelopmentToml(appendPublicationRehearsalToml(`schema = 1
360
365
 
361
366
  [project]
362
367
  type = "publication-artifact"
@@ -390,14 +395,14 @@ command = "make pdf"
390
395
 
391
396
  [lifecycle.verify]
392
397
  command = "make check"
393
- `);
398
+ `));
394
399
  }
395
400
 
396
401
  function scaffoldBuildWorkflow(
397
402
  buildchainSha,
398
403
  { artifactName = "paper-publication" } = {},
399
404
  ) {
400
- return `name: Build
405
+ return `${nextDevelopmentWorkflowHeader()}name: Build
401
406
 
402
407
  on:
403
408
  workflow_dispatch:
@@ -425,7 +430,7 @@ jobs:
425
430
  }
426
431
 
427
432
  function scaffoldVerifyWorkflow(buildchainSha) {
428
- return `name: Verify
433
+ return `${nextDevelopmentWorkflowHeader()}name: Verify
429
434
 
430
435
  on:
431
436
  pull_request:
@@ -456,7 +461,7 @@ function scaffoldReleaseWorkflow(
456
461
  const passportInput = releasePassportProductName
457
462
  ? ` release-passport-product-name: ${JSON.stringify(releasePassportProductName)}\n`
458
463
  : "";
459
- return `name: Paper Release
464
+ return `${nextDevelopmentWorkflowHeader()}name: Paper Release
460
465
 
461
466
  on:
462
467
  workflow_dispatch:
@@ -1017,8 +1022,10 @@ function migrationFiles({
1017
1022
  const files = new Map([
1018
1023
  [
1019
1024
  PAPER_PATHS.config,
1020
- projectPublicationRehearsalToml(
1021
- fs.readFileSync(path.resolve(cwd, PAPER_PATHS.config), "utf8"),
1025
+ projectNextDevelopmentToml(
1026
+ projectPublicationRehearsalToml(
1027
+ fs.readFileSync(path.resolve(cwd, PAPER_PATHS.config), "utf8"),
1028
+ ),
1022
1029
  ),
1023
1030
  ],
1024
1031
  [PAPER_PATHS.contractLock, contractLockText],
@@ -414,7 +414,7 @@ export function collectKfdAdopterReleaseEvidence({ manifest, gateResults = [], c
414
414
  }
415
415
  const manifestGate = createKfdAdopterManifestGate({
416
416
  manifest, packageArtifactRoot: installedKfdPackageArtifactRoot(), gateResults,
417
- expectedSourceSha: sourceSha, checkedAt,
417
+ authorityPath: "kfd-adopter-manifest.json", expectedSourceSha: sourceSha, checkedAt,
418
418
  });
419
419
  const legacyProjection = createKfdLegacySupportMatrixProjection({ manifest, manifestGate });
420
420
  if (comparisonMatrix) {
@@ -75,6 +75,19 @@ function qualificationId(value) {
75
75
  return exact(value, /^mac-(?:smoke-0[12]|full-01)$/, "qualificationId");
76
76
  }
77
77
 
78
+ function terminalFailureRunIds(value) {
79
+ if (!Array.isArray(value) || value.length === 0) {
80
+ throw new Error("terminalFailureRunIds must be a non-empty array");
81
+ }
82
+ const normalized = value.map((entry) =>
83
+ exact(entry, /^[1-9]\d*$/, "terminalFailureRunId"),
84
+ );
85
+ if (new Set(normalized).size !== normalized.length) {
86
+ throw new Error("terminalFailureRunIds must be unique");
87
+ }
88
+ return [...normalized].sort((left, right) => Number(left) - Number(right));
89
+ }
90
+
78
91
  function commonAws(values) {
79
92
  const region = exact(
80
93
  values.region || MACOS_EC2_JIT.region,
@@ -203,6 +216,87 @@ export function createMacosJitCampaignPlan(values = {}) {
203
216
  return { ...plan, digest: digest(plan) };
204
217
  }
205
218
 
219
+ export function createMacosJitInstanceRehydratePlan(values = {}) {
220
+ const id = campaignId(values.campaignId);
221
+ const boundSource = source(values);
222
+ const aws = commonAws(values);
223
+ const regionConfig = macosJitRegionConfig(aws.region);
224
+ const tags = ownershipTags({ id, sourceSha: boundSource.sha });
225
+ const rehydrationId = exact(
226
+ values.rehydrationId,
227
+ /^r[1-9]\d{0,2}$/,
228
+ "rehydrationId",
229
+ );
230
+ const instanceClientToken = `kungfu-mac-rehydrate-${id}-${rehydrationId}`;
231
+ if (instanceClientToken.length > 64) {
232
+ throw new Error("rehydration client token exceeds the EC2 limit");
233
+ }
234
+ const plan = {
235
+ schemaVersion: 1,
236
+ contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
237
+ kind: "instance-rehydrate-plan",
238
+ repository: repository(values.repository),
239
+ account: {
240
+ id: exact(values.accountId, /^\d{12}$/, "accountId"),
241
+ },
242
+ github: {
243
+ workflowId: MACOS_EC2_JIT.workflowId,
244
+ requiredState: "disabled_manually",
245
+ },
246
+ campaign: { id },
247
+ source: boundSource,
248
+ aws: {
249
+ ...aws,
250
+ hostId: exact(values.hostId, /^h-[0-9a-f]+$/, "hostId"),
251
+ hostAllocatedAt: iso(values.hostAllocatedAt, "hostAllocatedAt"),
252
+ replacesInstanceId: exact(
253
+ values.replacesInstanceId,
254
+ /^i-[0-9a-f]+$/,
255
+ "replacesInstanceId",
256
+ ),
257
+ rehydrationId,
258
+ instanceTags: tags,
259
+ volumeTags: tags,
260
+ instanceClientToken,
261
+ rootVolume: {
262
+ deviceName: "/dev/sda1",
263
+ deleteOnTermination: true,
264
+ encrypted: true,
265
+ volumeSizeGiB: 200,
266
+ volumeType: "gp3",
267
+ },
268
+ metadata: {
269
+ httpEndpoint: "enabled",
270
+ httpTokens: "required",
271
+ httpPutResponseHopLimit: 1,
272
+ instanceMetadataTags: "disabled",
273
+ },
274
+ },
275
+ safety: {
276
+ applyMode: values.execute === true ? "execute" : "dry-run",
277
+ exactSourceRequired: true,
278
+ existingCampaignHostRequired: true,
279
+ noHostAllocation: true,
280
+ zeroActiveInstanceRequired: true,
281
+ zeroRunnerResidueRequired: true,
282
+ awsDryRunRequiredBeforeLaunch: true,
283
+ cleanupOwner: "scheduled-card-scoped-reaper",
284
+ budget: {
285
+ name: regionConfig.budgetName,
286
+ limitUsd: MACOS_EC2_JIT.budgetLimitUsd,
287
+ metrics: ["UnblendedCost"],
288
+ dimensionFilter: {
289
+ usageTypes: regionConfig.budgetUsageTypes,
290
+ operation: MACOS_EC2_JIT.budgetOperation,
291
+ regions: regionConfig.budgetRegions,
292
+ },
293
+ requiredActualThresholds: [80, 95],
294
+ },
295
+ },
296
+ };
297
+ return { ...plan, digest: digest(plan) };
298
+ }
299
+
206
300
  export function createMacosJitSourceRebindPlan(values = {}) {
207
301
  const id = campaignId(values.campaignId);
208
302
  const boundSource = source(values);
@@ -219,6 +313,14 @@ export function createMacosJitSourceRebindPlan(values = {}) {
219
313
  if (workflowId !== MACOS_EC2_JIT.workflowId) {
220
314
  throw new Error(`workflowId must be ${MACOS_EC2_JIT.workflowId}`);
221
315
  }
316
+ const priorRunPolicy = values.priorRunPolicy || "unused";
317
+ if (!["unused", "terminal-failure"].includes(priorRunPolicy)) {
318
+ throw new Error("priorRunPolicy must be unused or terminal-failure");
319
+ }
320
+ const terminalRunIds =
321
+ priorRunPolicy === "terminal-failure"
322
+ ? terminalFailureRunIds(values.terminalFailureRunIds)
323
+ : [];
222
324
  const plan = {
223
325
  schemaVersion: 1,
224
326
  contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
@@ -233,6 +335,8 @@ export function createMacosJitSourceRebindPlan(values = {}) {
233
335
  github: {
234
336
  workflowId,
235
337
  requiredState: "disabled_manually",
338
+ priorRunPolicy,
339
+ terminalFailureRunIds: terminalRunIds,
236
340
  },
237
341
  aws: {
238
342
  ...aws,
@@ -246,8 +350,9 @@ export function createMacosJitSourceRebindPlan(values = {}) {
246
350
  forwardOnlySource: true,
247
351
  sameSourceRefRequired: true,
248
352
  workflowDisabledRequired: true,
249
- zeroPriorJobsRequired: true,
250
- zeroPriorArtifactsRequired: true,
353
+ zeroPriorJobsRequired: priorRunPolicy === "unused",
354
+ zeroPriorArtifactsRequired: priorRunPolicy === "unused",
355
+ terminalFailureEvidencePreserved: priorRunPolicy === "terminal-failure",
251
356
  zeroJitResidueRequired: true,
252
357
  zeroEvidenceRequired: true,
253
358
  exactCampaignResourcesRequired: true,
@@ -301,9 +406,9 @@ export function macosAllocateHostsArgs(plan) {
301
406
  export function macosRunInstancesArgs(plan, { hostId, dryRun = false } = {}) {
302
407
  if (
303
408
  plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
304
- plan.kind !== "campaign-launch-plan"
409
+ !["campaign-launch-plan", "instance-rehydrate-plan"].includes(plan.kind)
305
410
  ) {
306
- throw new Error("macOS JIT campaign launch plan contract is invalid");
411
+ throw new Error("macOS JIT instance launch plan contract is invalid");
307
412
  }
308
413
  const resolvedHostId = exact(hostId, /^h-[0-9a-f]+$/, "hostId");
309
414
  const args = [
@@ -6,6 +6,7 @@ import {
6
6
  AWS_MACOS_JIT_CONTROLLER_CONTRACT,
7
7
  createMacosJitCampaignPlan,
8
8
  createMacosJitClosePlan,
9
+ createMacosJitInstanceRehydratePlan,
9
10
  createMacosJitJobPlan,
10
11
  createMacosJitSourceRebindPlan,
11
12
  macosAllocateHostsArgs,
@@ -13,6 +14,7 @@ import {
13
14
  macosRunInstancesArgs,
14
15
  } from "./aws-macos-jit-controller-core.mjs";
15
16
  import { MACOS_EC2_JIT_REGIONS } from "./aws-macos-jit-core.mjs";
17
+ import { executeMacosJitInstanceRehydrate } from "./aws-macos-jit-instance-rehydrate.mjs";
16
18
  import { executeMacosJitJob } from "./aws-macos-jit-job-controller.mjs";
17
19
  import { executeMacosJitSourceRebind } from "./aws-macos-jit-source-rebind.mjs";
18
20
  import {
@@ -36,6 +38,19 @@ function flag(name) {
36
38
  return process.argv.includes(`--${name}`);
37
39
  }
38
40
 
41
+ function jsonArrayArg(name) {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(arg(name));
45
+ } catch {
46
+ throw new Error(`--${name} must be valid JSON`);
47
+ }
48
+ if (!Array.isArray(parsed)) {
49
+ throw new Error(`--${name} must be a JSON array`);
50
+ }
51
+ return parsed;
52
+ }
53
+
39
54
  function assertCampaignLaunchPreflight(plan, profile) {
40
55
  const launchGate = assertMacosBudgetLaunchGate(plan, profile);
41
56
  const commit = ghJson(
@@ -515,6 +530,32 @@ function confirm(plan) {
515
530
  if (!flag("confirm-zero-allocation")) {
516
531
  throw new Error("--confirm-zero-allocation is required");
517
532
  }
533
+ if (plan.github.priorRunPolicy === "terminal-failure") {
534
+ const confirmed = jsonArrayArg(
535
+ "confirm-terminal-failure-run-ids-json",
536
+ ).map(String);
537
+ if (
538
+ JSON.stringify(confirmed) !==
539
+ JSON.stringify(plan.github.terminalFailureRunIds)
540
+ ) {
541
+ throw new Error(
542
+ "--confirm-terminal-failure-run-ids-json must equal the exact failed run inventory",
543
+ );
544
+ }
545
+ }
546
+ }
547
+ if (plan.kind === "instance-rehydrate-plan") {
548
+ if (arg("confirm-host-id") !== plan.aws.hostId) {
549
+ throw new Error("--confirm-host-id must equal the exact host id");
550
+ }
551
+ if (arg("confirm-replaces-instance-id") !== plan.aws.replacesInstanceId) {
552
+ throw new Error(
553
+ "--confirm-replaces-instance-id must equal replacesInstanceId",
554
+ );
555
+ }
556
+ if (!flag("confirm-no-host-allocation")) {
557
+ throw new Error("--confirm-no-host-allocation is required");
558
+ }
518
559
  }
519
560
  }
520
561
 
@@ -541,7 +582,29 @@ export function main() {
541
582
  executeMacosJitCampaignLaunch(plan, { profile: arg("aws-profile") }),
542
583
  );
543
584
  }
544
- if (["plan-rebind", "rebind-campaign"].includes(mode)) {
585
+ if (["plan-rehydrate", "rehydrate-instance"].includes(mode)) {
586
+ const plan = createMacosJitInstanceRehydratePlan({
587
+ ...commonValues(execute),
588
+ hostId: arg("host-id"),
589
+ hostAllocatedAt: arg("host-allocated-at"),
590
+ replacesInstanceId: arg("replaces-instance-id"),
591
+ rehydrationId: arg("rehydration-id"),
592
+ });
593
+ return emit(plan, mode, execute, () =>
594
+ executeMacosJitInstanceRehydrate(plan, {
595
+ profile: arg("aws-profile"),
596
+ }),
597
+ );
598
+ }
599
+ if (
600
+ [
601
+ "plan-rebind",
602
+ "rebind-campaign",
603
+ "plan-rebind-after-failure",
604
+ "rebind-campaign-after-failure",
605
+ ].includes(mode)
606
+ ) {
607
+ const afterFailure = mode.endsWith("after-failure");
545
608
  const plan = createMacosJitSourceRebindPlan({
546
609
  ...commonValues(execute),
547
610
  previousSourceSha: arg("previous-source-sha"),
@@ -549,8 +612,14 @@ export function main() {
549
612
  workflowId: arg("workflow-id"),
550
613
  hostId: arg("host-id"),
551
614
  instanceId: arg("instance-id"),
615
+ priorRunPolicy: afterFailure ? "terminal-failure" : "unused",
616
+ terminalFailureRunIds: afterFailure
617
+ ? jsonArrayArg("terminal-failure-run-ids-json")
618
+ : [],
552
619
  });
553
- return emit(plan, mode, execute, () => executeMacosJitSourceRebind(plan, { profile: arg("aws-profile") }));
620
+ return emit(plan, mode, execute, () =>
621
+ executeMacosJitSourceRebind(plan, { profile: arg("aws-profile") }),
622
+ );
554
623
  }
555
624
  if (["plan-job", "run-job"].includes(mode)) {
556
625
  const plan = createMacosJitJobPlan({
@@ -0,0 +1,260 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import {
4
+ AWS_MACOS_JIT_CONTROLLER_CONTRACT,
5
+ macosRunInstancesArgs,
6
+ } from "./aws-macos-jit-controller-core.mjs";
7
+ import { MACOS_EC2_JIT_REGIONS } from "./aws-macos-jit-core.mjs";
8
+ import {
9
+ assertDryRun,
10
+ assertMacosBudgetLaunchGate,
11
+ assertOwnership,
12
+ awsArgs,
13
+ awsJson,
14
+ commandResult,
15
+ ghJson,
16
+ requireSuccess,
17
+ } from "./aws-macos-jit-controller-runtime.mjs";
18
+
19
+ function assertInstanceRehydratePreflight(plan, profile) {
20
+ const launchGate = assertMacosBudgetLaunchGate(plan, profile);
21
+ const commit = ghJson(
22
+ ["api", `repos/${plan.repository}/commits/${plan.source.sha}`],
23
+ undefined,
24
+ "GitHub exact-source preflight",
25
+ );
26
+ if (String(commit.sha || "").toLowerCase() !== plan.source.sha) {
27
+ throw new Error("GitHub did not resolve the exact campaign source SHA");
28
+ }
29
+ const host = awsJson(
30
+ plan,
31
+ profile,
32
+ [
33
+ "ec2",
34
+ "describe-hosts",
35
+ "--host-ids",
36
+ plan.aws.hostId,
37
+ "--output",
38
+ "json",
39
+ ],
40
+ "macOS rehydration host preflight",
41
+ ).Hosts?.[0];
42
+ if (
43
+ !host ||
44
+ host.State !== "available" ||
45
+ host.AvailabilityZone !== plan.aws.availabilityZone ||
46
+ host.HostProperties?.InstanceType !== plan.aws.instanceType ||
47
+ (host.Instances || []).length !== 0 ||
48
+ new Date(host.AllocationTime).toISOString() !== plan.aws.hostAllocatedAt
49
+ ) {
50
+ throw new Error("macOS rehydration host identity or capacity mismatch");
51
+ }
52
+ assertOwnership(host.Tags, plan);
53
+
54
+ const regionalPlans = Object.keys(MACOS_EC2_JIT_REGIONS).map((region) => ({
55
+ ...plan,
56
+ aws: { ...plan.aws, region },
57
+ }));
58
+ const otherHosts = regionalPlans.flatMap((regionalPlan) =>
59
+ (
60
+ awsJson(
61
+ regionalPlan,
62
+ profile,
63
+ [
64
+ "ec2",
65
+ "describe-hosts",
66
+ "--filter",
67
+ "Name=tag:kungfu:plane,Values=aws-us-elastic-runner-burst",
68
+ "Name=tag:kungfu:provider,Values=macos-ec2-jit",
69
+ "Name=state,Values=available,pending,under-assessment",
70
+ "--output",
71
+ "json",
72
+ ],
73
+ `macOS rehydration host ceiling in ${regionalPlan.aws.region}`,
74
+ ).Hosts || []
75
+ ).filter((candidate) => candidate.HostId !== plan.aws.hostId),
76
+ );
77
+ if (otherHosts.length !== 0) {
78
+ throw new Error("macOS rehydration found another active Dedicated Host");
79
+ }
80
+ const activeInstances = regionalPlans.flatMap((regionalPlan) => {
81
+ const active = awsJson(
82
+ regionalPlan,
83
+ profile,
84
+ [
85
+ "ec2",
86
+ "describe-instances",
87
+ "--filters",
88
+ "Name=tag:kungfu:plane,Values=aws-us-elastic-runner-burst",
89
+ "Name=tag:kungfu:provider,Values=macos-ec2-jit",
90
+ "Name=instance-state-name,Values=pending,running,stopping,stopped,shutting-down",
91
+ "--output",
92
+ "json",
93
+ ],
94
+ `active macOS JIT instance preflight in ${regionalPlan.aws.region}`,
95
+ );
96
+ return (active.Reservations || []).flatMap(
97
+ (reservation) => reservation.Instances || [],
98
+ );
99
+ });
100
+ if (activeInstances.length !== 0) {
101
+ throw new Error("macOS rehydration requires zero active JIT instances");
102
+ }
103
+ const runners = ghJson(
104
+ ["api", `repos/${plan.repository}/actions/runners?per_page=100`],
105
+ undefined,
106
+ "macOS rehydration runner preflight",
107
+ ).runners;
108
+ if (
109
+ (runners || []).some((runner) =>
110
+ (runner.labels || []).some((label) =>
111
+ String(label.name || label).startsWith("aws-us-ec2-macos-jit-"),
112
+ ),
113
+ )
114
+ ) {
115
+ throw new Error("macOS rehydration requires zero JIT runner residue");
116
+ }
117
+ const image = awsJson(
118
+ plan,
119
+ profile,
120
+ [
121
+ "ec2",
122
+ "describe-images",
123
+ "--image-ids",
124
+ plan.aws.amiId,
125
+ "--output",
126
+ "json",
127
+ ],
128
+ "macOS rehydration AMI preflight",
129
+ ).Images?.[0];
130
+ if (
131
+ !image ||
132
+ image.Name !== plan.aws.amiName ||
133
+ image.Architecture !== "arm64_mac" ||
134
+ image.State !== "available"
135
+ ) {
136
+ throw new Error("macOS AMI identity or availability mismatch");
137
+ }
138
+ const subnet = awsJson(
139
+ plan,
140
+ profile,
141
+ [
142
+ "ec2",
143
+ "describe-subnets",
144
+ "--subnet-ids",
145
+ plan.aws.subnetId,
146
+ "--output",
147
+ "json",
148
+ ],
149
+ "macOS rehydration subnet preflight",
150
+ ).Subnets?.[0];
151
+ if (!subnet || subnet.AvailabilityZone !== plan.aws.availabilityZone) {
152
+ throw new Error(
153
+ "macOS subnet and Dedicated Host availability zone mismatch",
154
+ );
155
+ }
156
+ return {
157
+ ...launchGate,
158
+ exactCommit: plan.source.sha,
159
+ hostId: host.HostId,
160
+ hostState: host.State,
161
+ activeInstances: 0,
162
+ otherActiveHosts: 0,
163
+ amiOwnerId: image.OwnerId,
164
+ subnetAvailabilityZone: subnet.AvailabilityZone,
165
+ };
166
+ }
167
+
168
+ export function executeMacosJitInstanceRehydrate(plan, { profile = "" } = {}) {
169
+ if (
170
+ plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
171
+ plan.kind !== "instance-rehydrate-plan"
172
+ ) {
173
+ throw new Error("macOS JIT instance rehydrate plan contract is invalid");
174
+ }
175
+ const preflight = assertInstanceRehydratePreflight(plan, profile);
176
+ assertDryRun(
177
+ commandResult(
178
+ "aws",
179
+ awsArgs(
180
+ plan,
181
+ profile,
182
+ macosRunInstancesArgs(plan, { hostId: plan.aws.hostId, dryRun: true }),
183
+ ),
184
+ ),
185
+ "EC2 same-host RunInstances DryRun",
186
+ );
187
+ const launched = awsJson(
188
+ plan,
189
+ profile,
190
+ macosRunInstancesArgs(plan, { hostId: plan.aws.hostId }),
191
+ "EC2 same-host RunInstances",
192
+ );
193
+ const instanceId = launched.Instances?.[0]?.InstanceId;
194
+ if (!/^i-[0-9a-f]+$/.test(String(instanceId || ""))) {
195
+ throw new Error("EC2 same-host RunInstances returned no instance identity");
196
+ }
197
+ requireSuccess(
198
+ commandResult(
199
+ "aws",
200
+ awsArgs(plan, profile, [
201
+ "ec2",
202
+ "wait",
203
+ "instance-running",
204
+ "--instance-ids",
205
+ instanceId,
206
+ ]),
207
+ ),
208
+ "macOS rehydrated instance-running waiter",
209
+ );
210
+ const instance = awsJson(
211
+ plan,
212
+ profile,
213
+ [
214
+ "ec2",
215
+ "describe-instances",
216
+ "--instance-ids",
217
+ instanceId,
218
+ "--output",
219
+ "json",
220
+ ],
221
+ "macOS rehydrated instance readback",
222
+ ).Reservations?.[0]?.Instances?.[0];
223
+ if (
224
+ !instance ||
225
+ instance.State?.Name !== "running" ||
226
+ instance.Placement?.HostId !== plan.aws.hostId ||
227
+ instance.ImageId !== plan.aws.amiId ||
228
+ instance.InstanceType !== plan.aws.instanceType
229
+ ) {
230
+ throw new Error("macOS rehydrated instance identity or placement mismatch");
231
+ }
232
+ assertOwnership(instance.Tags, plan);
233
+ return {
234
+ schemaVersion: 1,
235
+ contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
236
+ kind: "instance-rehydrate-result",
237
+ status: "rehydrated",
238
+ repository: plan.repository,
239
+ campaign: plan.campaign,
240
+ source: plan.source,
241
+ replacement: {
242
+ rehydrationId: plan.aws.rehydrationId,
243
+ replacesInstanceId: plan.aws.replacesInstanceId,
244
+ },
245
+ aws: {
246
+ region: plan.aws.region,
247
+ availabilityZone: plan.aws.availabilityZone,
248
+ hostId: plan.aws.hostId,
249
+ hostAllocatedAt: plan.aws.hostAllocatedAt,
250
+ instanceId,
251
+ instanceType: instance.InstanceType,
252
+ imageId: instance.ImageId,
253
+ launchTime: new Date(instance.LaunchTime).toISOString(),
254
+ },
255
+ preflight,
256
+ dryRun: "RunInstances:DryRunOperation",
257
+ hostAllocated: false,
258
+ planDigest: plan.digest,
259
+ };
260
+ }