@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.
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/dist/site/buildchain-contract.json +4 -4
- package/dist/site/buildchain-site.json +89 -18
- package/dist/site/capability-registry.json +4 -3
- package/dist/site/kfd-claims.json +65 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +19 -3
- package/dist/site/node-api-registry.json +1017 -37
- package/dist/site/page-registry.json +74 -11
- package/dist/site/public-surface-audit.json +7 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +15 -7
- package/dist/site/workflow-registry.json +4 -4
- package/docs/MAP.md +2 -1
- package/docs/aws-us-elastic-runner-burst-plane.md +25 -3
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/next-development-transition.md +119 -0
- package/docs/node-api-reference.md +126 -73
- package/docs/versioning.md +1 -1
- package/package.json +8 -3
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +52 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +726 -0
- package/packages/core/next-development-projection.js +288 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +7 -4
- package/packages/core/paper.js +14 -7
- package/packages/core/release-passport-contract.js +1 -1
- package/scripts/aws-macos-jit-controller-core.mjs +109 -4
- package/scripts/aws-macos-jit-controller.mjs +71 -2
- package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
- package/scripts/aws-macos-jit-source-rebind.mjs +71 -14
- package/scripts/check-inventory.mjs +11 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-site-bundle.mjs +2 -0
- package/scripts/init-repo.mjs +119 -62
- package/scripts/next-development-self-dogfood-harness.mjs +409 -0
- package/scripts/next-development-self-dogfood.mjs +532 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/site-capability-metadata.mjs +3 -0
- 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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
mergeNextDevelopmentAgentInstructions(
|
|
173
|
+
mergePublicationRehearsalAgentInstructions(
|
|
174
|
+
mergePaperAgentEntryInstructions(currentAgents, {
|
|
175
|
+
developmentRef: resolvedDevelopmentRef,
|
|
176
|
+
}),
|
|
177
|
+
),
|
|
175
178
|
),
|
|
176
179
|
],
|
|
177
180
|
]);
|
package/packages/core/paper.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
1021
|
-
|
|
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:
|
|
250
|
-
zeroPriorArtifactsRequired:
|
|
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
|
-
|
|
409
|
+
!["campaign-launch-plan", "instance-rehydrate-plan"].includes(plan.kind)
|
|
305
410
|
) {
|
|
306
|
-
throw new Error("macOS JIT
|
|
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-
|
|
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, () =>
|
|
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
|
+
}
|