@kungfu-tech/buildchain 3.0.7 → 3.0.8

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 (45) hide show
  1. package/contracts/engineering-housekeeper-v1.schema.json +11 -1
  2. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +10 -0
  3. package/contracts/release-cut-v1.schema.json +70 -0
  4. package/contracts/release-train-transition-v1.schema.json +85 -0
  5. package/contracts/release-train-v1.schema.json +82 -0
  6. package/dist/site/buildchain-contract.json +302 -16
  7. package/dist/site/buildchain-site.json +83 -27
  8. package/dist/site/capability-registry.json +4 -3
  9. package/dist/site/kfd-claims.json +45 -8
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +22 -6
  12. package/dist/site/node-api-registry.json +1218 -489
  13. package/dist/site/page-registry.json +65 -17
  14. package/dist/site/public-surface-audit.json +13 -8
  15. package/dist/site/publication-authority-registry.json +2 -4
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +1 -0
  18. package/dist/site/site-manifest.json +18 -10
  19. package/dist/site/workflow-registry.json +10 -9
  20. package/docs/MAP.md +1 -1
  21. package/docs/aws-us-elastic-runner-burst-plane.md +20 -7
  22. package/docs/engineering-housekeeper.md +61 -28
  23. package/docs/node-api-reference.md +109 -77
  24. package/docs/release-governance.md +13 -10
  25. package/docs/release-train.md +102 -0
  26. package/docs/reusable-build-surface.md +16 -6
  27. package/docs/site-bundle-contract.md +5 -1
  28. package/docs/versioning.md +1 -0
  29. package/package.json +2 -1
  30. package/packages/core/buildchain-agent-manuals.js +1 -0
  31. package/packages/core/buildchain-compatibility-proof.js +577 -0
  32. package/packages/core/buildchain-contract.js +46 -181
  33. package/packages/core/engineering-housekeeper-github-client.js +18 -1
  34. package/packages/core/engineering-housekeeper-github.js +247 -41
  35. package/packages/core/engineering-housekeeper.js +15 -0
  36. package/packages/core/release-train.js +520 -0
  37. package/scripts/aws-macos-jit-controller-core.mjs +33 -11
  38. package/scripts/aws-macos-jit-controller-runtime.mjs +208 -0
  39. package/scripts/aws-macos-jit-controller.mjs +50 -37
  40. package/scripts/aws-macos-jit-core.mjs +41 -3
  41. package/scripts/buildchain-contract-lock.mjs +6 -0
  42. package/scripts/dispatch-artifact-signing-authority.mjs +4 -7
  43. package/scripts/engineering-housekeeper-workflow.mjs +13 -6
  44. package/scripts/generate-site-bundle.mjs +2 -0
  45. package/scripts/site-capability-metadata.mjs +1 -0
@@ -8,6 +8,7 @@ export const HOUSEKEEPER_REASON_CODES = Object.freeze({
8
8
  ELIGIBLE_MERGED_BRANCH: "eligible.merged-branch",
9
9
  PROTECTED_BRANCH: "branch.protected",
10
10
  RETAINED_BRANCH: "branch.retained",
11
+ NOT_TEMPORARY_DEVELOPMENT: "branch.not-temporary-development",
11
12
  DEFAULT_BRANCH: "branch.default",
12
13
  TARGET_BRANCH: "branch.target",
13
14
  OPEN_PR_HEAD: "branch.open-pr-head",
@@ -28,6 +29,14 @@ export const HOUSEKEEPER_REASON_CODES = Object.freeze({
28
29
  export const DEFAULT_HOUSEKEEPER_POLICY = Object.freeze({
29
30
  protectedPatterns: ["dev/**", "alpha/**", "release/**", "publish-gate/**"],
30
31
  retainedPatterns: ["train/**", "authority/**"],
32
+ temporaryBranchPatterns: [
33
+ "feature/**",
34
+ "fix/**",
35
+ "chore/**",
36
+ "docs/**",
37
+ "ci/**",
38
+ "refactor/**",
39
+ ],
31
40
  pullRequests: Object.freeze({
32
41
  reportStale: true,
33
42
  label: "",
@@ -77,6 +86,10 @@ function normalizePolicy(policy = {}) {
77
86
  ...(policy.retainedPatterns ||
78
87
  DEFAULT_HOUSEKEEPER_POLICY.retainedPatterns),
79
88
  ].sort(),
89
+ temporaryBranchPatterns: [
90
+ ...(policy.temporaryBranchPatterns ||
91
+ DEFAULT_HOUSEKEEPER_POLICY.temporaryBranchPatterns),
92
+ ].sort(),
80
93
  pullRequests: {
81
94
  reportStale: policy.pullRequests?.reportStale !== false,
82
95
  label: String(policy.pullRequests?.label || ""),
@@ -102,6 +115,8 @@ export function classifyHousekeeperBranch(branch, policyInput = {}) {
102
115
  reasons.push(HOUSEKEEPER_REASON_CODES.PROTECTED_BRANCH);
103
116
  if (matchesAny(policy.retainedPatterns, branch.name))
104
117
  reasons.push(HOUSEKEEPER_REASON_CODES.RETAINED_BRANCH);
118
+ if (!matchesAny(policy.temporaryBranchPatterns, branch.name))
119
+ reasons.push(HOUSEKEEPER_REASON_CODES.NOT_TEMPORARY_DEVELOPMENT);
105
120
  if (branch.sourceRepository && branch.sourceRepository !== branch.repository)
106
121
  reasons.push(HOUSEKEEPER_REASON_CODES.CROSS_REPOSITORY);
107
122
  if ((branch.openPullRequestNumbers || []).length > 0)
@@ -0,0 +1,520 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import crypto from "node:crypto";
4
+
5
+ export const RELEASE_CUT_CONTRACT = "kungfu-buildchain-release-cut/v1";
6
+ export const RELEASE_TRAIN_CONTRACT = "kungfu-buildchain-release-train/v1";
7
+ export const RELEASE_TRAIN_TRANSITION_CONTRACT =
8
+ "kungfu-buildchain-release-train-transition/v1";
9
+ export const RELEASE_TRAIN_OBSERVATION_CONTRACT =
10
+ "kungfu-buildchain-release-train-observation/v1";
11
+ export const LEGACY_DEV_ALPHA_CANDIDATE_STATE_SCHEMA =
12
+ "kungfu-buildchain-dev-alpha-candidate-state/v1";
13
+
14
+ export const RELEASE_TRAIN_STATES = Object.freeze([
15
+ "preparing",
16
+ "building",
17
+ "repair-required",
18
+ "publication-blocked",
19
+ "publishable",
20
+ "superseded",
21
+ "terminal",
22
+ ]);
23
+
24
+ export const RELEASE_TRAIN_SUPERSESSION_CAUSES = Object.freeze([
25
+ "incompatible-semantics",
26
+ "alpha-base-incompatibility",
27
+ "invalid-authority",
28
+ "severe-security",
29
+ ]);
30
+
31
+ const SHA = /^[0-9a-f]{40}$/u;
32
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
33
+ const ABSENT_ROOT = `sha256:${"0".repeat(64)}`;
34
+ const SUPERSESSION_CAUSES = new Set(RELEASE_TRAIN_SUPERSESSION_CAUSES);
35
+ const TRANSITIONS = new Map([
36
+ ["preparing", new Set(["building", "repair-required", "superseded"])],
37
+ [
38
+ "building",
39
+ new Set([
40
+ "repair-required",
41
+ "publication-blocked",
42
+ "publishable",
43
+ "superseded",
44
+ ]),
45
+ ],
46
+ [
47
+ "repair-required",
48
+ new Set(["building", "publication-blocked", "superseded"]),
49
+ ],
50
+ [
51
+ "publication-blocked",
52
+ new Set(["repair-required", "publishable", "superseded"]),
53
+ ],
54
+ ["publishable", new Set(["terminal", "repair-required", "superseded"])],
55
+ ["superseded", new Set()],
56
+ ["terminal", new Set()],
57
+ ]);
58
+
59
+ function canonical(value) {
60
+ if (Array.isArray(value)) return value.map(canonical);
61
+ if (value && typeof value === "object") {
62
+ return Object.fromEntries(
63
+ Object.entries(value)
64
+ .sort(([left], [right]) => left.localeCompare(right))
65
+ .map(([key, item]) => [key, canonical(item)]),
66
+ );
67
+ }
68
+ return value;
69
+ }
70
+
71
+ export function releaseTrainRoot(value) {
72
+ return `sha256:${crypto
73
+ .createHash("sha256")
74
+ .update(JSON.stringify(canonical(value)))
75
+ .digest("hex")}`;
76
+ }
77
+
78
+ function clone(value) {
79
+ return structuredClone(value);
80
+ }
81
+
82
+ function text(value, label) {
83
+ const normalized = String(value ?? "").trim();
84
+ if (!normalized) throw new Error(`${label} is required`);
85
+ return normalized;
86
+ }
87
+
88
+ function exactSha(value, label) {
89
+ const normalized = text(value, label).toLowerCase();
90
+ if (!SHA.test(normalized))
91
+ throw new Error(`${label} must be an exact 40-character Git SHA`);
92
+ return normalized;
93
+ }
94
+
95
+ function contentRoot(value, label) {
96
+ const normalized = text(value, label).toLowerCase();
97
+ if (!ROOT.test(normalized))
98
+ throw new Error(`${label} must be a sha256 content root`);
99
+ return normalized;
100
+ }
101
+
102
+ function positiveInteger(value, label) {
103
+ const normalized = Number(value);
104
+ if (!Number.isSafeInteger(normalized) || normalized < 1)
105
+ throw new Error(`${label} must be a positive integer`);
106
+ return normalized;
107
+ }
108
+
109
+ function timestamp(value, label) {
110
+ const normalized = text(value, label);
111
+ const milliseconds = Date.parse(normalized);
112
+ if (!Number.isFinite(milliseconds))
113
+ throw new Error(`${label} must be an ISO-8601 timestamp`);
114
+ const canonicalTimestamp = new Date(milliseconds).toISOString();
115
+ if (normalized !== canonicalTimestamp)
116
+ throw new Error(`${label} must be a canonical ISO-8601 timestamp`);
117
+ return normalized;
118
+ }
119
+
120
+ function repository(value) {
121
+ const normalized = text(value, "repository");
122
+ if (!/^[^/\s]+\/[^/\s]+$/u.test(normalized))
123
+ throw new Error("repository must be owner/repo");
124
+ return normalized;
125
+ }
126
+
127
+ function branch(value, label) {
128
+ const normalized = text(value, label).replace(/^refs\/heads\//u, "");
129
+ if (/\s/u.test(normalized))
130
+ throw new Error(`${label} must not contain spaces`);
131
+ return normalized;
132
+ }
133
+
134
+ function sortedRoots(values, label) {
135
+ if (!Array.isArray(values) || values.length === 0)
136
+ throw new Error(`${label} must be a non-empty array`);
137
+ const normalized = values.map((value, index) =>
138
+ contentRoot(value, `${label}[${index}]`),
139
+ );
140
+ const expected = [...new Set(normalized)].sort();
141
+ if (
142
+ normalized.length !== expected.length ||
143
+ normalized.some((value, index) => value !== expected[index])
144
+ ) {
145
+ throw new Error(`${label} must be sorted and duplicate-free`);
146
+ }
147
+ return normalized;
148
+ }
149
+
150
+ function assertExactFields(value, expected, label) {
151
+ if (!value || typeof value !== "object" || Array.isArray(value))
152
+ throw new Error(`${label} must be an object`);
153
+ const actual = Object.keys(value).sort();
154
+ const keys = [...expected].sort();
155
+ if (
156
+ actual.length !== keys.length ||
157
+ actual.some((key, index) => key !== keys[index])
158
+ ) {
159
+ throw new Error(`${label} has an invalid field set`);
160
+ }
161
+ }
162
+
163
+ function normalizeSupersession(value, generation) {
164
+ if (generation === 1) {
165
+ if (value !== null && value !== undefined)
166
+ throw new Error("generation 1 cannot supersede a prior Release Cut");
167
+ return null;
168
+ }
169
+ assertExactFields(value, ["cause", "priorCutRoot"], "supersession");
170
+ const cause = text(value.cause, "supersession.cause");
171
+ if (!SUPERSESSION_CAUSES.has(cause))
172
+ throw new Error(`unsupported Release Cut supersession cause: ${cause}`);
173
+ return {
174
+ cause,
175
+ priorCutRoot: contentRoot(value.priorCutRoot, "supersession.priorCutRoot"),
176
+ };
177
+ }
178
+
179
+ export function createReleaseCut(input = {}) {
180
+ const generation = positiveInteger(input.generation ?? 1, "generation");
181
+ const body = {
182
+ schemaVersion: 1,
183
+ contract: RELEASE_CUT_CONTRACT,
184
+ repository: repository(input.repository),
185
+ sourceBranch: branch(input.sourceBranch, "sourceBranch"),
186
+ targetBranch: branch(input.targetBranch, "targetBranch"),
187
+ originDevSha: exactSha(input.originDevSha, "originDevSha"),
188
+ candidateSha: exactSha(input.candidateSha, "candidateSha"),
189
+ candidateTreeSha: exactSha(input.candidateTreeSha, "candidateTreeSha"),
190
+ alphaBaseSha: exactSha(input.alphaBaseSha, "alphaBaseSha"),
191
+ buildchainRuntimeSha: exactSha(
192
+ input.buildchainRuntimeSha,
193
+ "buildchainRuntimeSha",
194
+ ),
195
+ generation,
196
+ authorityRoots: sortedRoots(input.authorityRoots, "authorityRoots"),
197
+ supersession: normalizeSupersession(input.supersession, generation),
198
+ createdAt: timestamp(input.createdAt, "createdAt"),
199
+ };
200
+ return { ...body, cutRoot: releaseTrainRoot(body) };
201
+ }
202
+
203
+ function initialState(releaseCut) {
204
+ const body = {
205
+ status: "preparing",
206
+ generation: releaseCut.generation,
207
+ priorStateRoot: ABSENT_ROOT,
208
+ transitionRoot: ABSENT_ROOT,
209
+ };
210
+ return { ...body, stateRoot: releaseTrainRoot(body) };
211
+ }
212
+
213
+ export function createReleaseTrain(input = {}) {
214
+ const releaseCut = createReleaseCut(input.releaseCut || input);
215
+ const identity = {
216
+ schemaVersion: 1,
217
+ contract: RELEASE_TRAIN_CONTRACT,
218
+ releaseCut,
219
+ };
220
+ return {
221
+ ...identity,
222
+ trainRoot: releaseTrainRoot(identity),
223
+ state: initialState(releaseCut),
224
+ transitions: [],
225
+ observations: [],
226
+ };
227
+ }
228
+
229
+ export function validateReleaseCut(cut) {
230
+ assertExactFields(
231
+ cut,
232
+ [
233
+ "schemaVersion",
234
+ "contract",
235
+ "repository",
236
+ "sourceBranch",
237
+ "targetBranch",
238
+ "originDevSha",
239
+ "candidateSha",
240
+ "candidateTreeSha",
241
+ "alphaBaseSha",
242
+ "buildchainRuntimeSha",
243
+ "generation",
244
+ "authorityRoots",
245
+ "supersession",
246
+ "createdAt",
247
+ "cutRoot",
248
+ ],
249
+ "Release Cut",
250
+ );
251
+ const normalized = createReleaseCut(cut);
252
+ if (cut.cutRoot !== normalized.cutRoot)
253
+ throw new Error("Release Cut root does not match its content");
254
+ return normalized;
255
+ }
256
+
257
+ function transitionRequest(input, train) {
258
+ const to = text(input.to, "transition.to");
259
+ if (!RELEASE_TRAIN_STATES.includes(to))
260
+ throw new Error(`unsupported Release Train state: ${to}`);
261
+ const expectedStateRoot = contentRoot(
262
+ input.expectedStateRoot,
263
+ "transition.expectedStateRoot",
264
+ );
265
+ const superseding = to === "superseded";
266
+ const cause = input.supersessionCause
267
+ ? text(input.supersessionCause, "transition.supersessionCause")
268
+ : "";
269
+ if (superseding && !SUPERSESSION_CAUSES.has(cause))
270
+ throw new Error("superseded transitions require an enumerated cause");
271
+ if (
272
+ !superseding &&
273
+ (cause || input.replacementCutRoot || input.replacementCandidateSha)
274
+ )
275
+ throw new Error(
276
+ "supersession fields are only valid for superseded transitions",
277
+ );
278
+ return {
279
+ contract: RELEASE_TRAIN_TRANSITION_CONTRACT,
280
+ trainRoot: train.trainRoot,
281
+ expectedStateRoot,
282
+ to,
283
+ event: text(input.event, "transition.event"),
284
+ reason: text(input.reason, "transition.reason"),
285
+ authorityRoots: sortedRoots(
286
+ input.authorityRoots,
287
+ "transition.authorityRoots",
288
+ ),
289
+ ...(superseding
290
+ ? {
291
+ supersessionCause: cause,
292
+ replacementCutRoot: contentRoot(
293
+ input.replacementCutRoot,
294
+ "transition.replacementCutRoot",
295
+ ),
296
+ replacementCandidateSha: exactSha(
297
+ input.replacementCandidateSha,
298
+ "transition.replacementCandidateSha",
299
+ ),
300
+ }
301
+ : {}),
302
+ recordedAt: timestamp(input.recordedAt, "transition.recordedAt"),
303
+ };
304
+ }
305
+
306
+ function applyTransition(train, input) {
307
+ const request = transitionRequest(input, train);
308
+ const requestRoot = releaseTrainRoot(request);
309
+ const last = train.transitions.at(-1);
310
+ if (last?.requestRoot === requestRoot) return train;
311
+ if (request.expectedStateRoot !== train.state.stateRoot)
312
+ throw new Error("Release Train transition compare-and-swap failed");
313
+ if (!TRANSITIONS.get(train.state.status)?.has(request.to))
314
+ throw new Error(
315
+ `invalid Release Train transition: ${train.state.status} -> ${request.to}`,
316
+ );
317
+ const transition = {
318
+ ...request,
319
+ from: train.state.status,
320
+ requestRoot,
321
+ };
322
+ transition.transitionRoot = releaseTrainRoot(transition);
323
+ const stateBody = {
324
+ status: request.to,
325
+ generation: train.releaseCut.generation,
326
+ priorStateRoot: train.state.stateRoot,
327
+ transitionRoot: transition.transitionRoot,
328
+ };
329
+ return {
330
+ ...train,
331
+ state: { ...stateBody, stateRoot: releaseTrainRoot(stateBody) },
332
+ transitions: [...train.transitions, transition],
333
+ };
334
+ }
335
+
336
+ export function transitionReleaseTrain(trainInput, input = {}) {
337
+ return applyTransition(validateReleaseTrain(trainInput), input);
338
+ }
339
+
340
+ function applyObservation(train, input) {
341
+ const body = {
342
+ contract: RELEASE_TRAIN_OBSERVATION_CONTRACT,
343
+ trainRoot: train.trainRoot,
344
+ observedDevSha: exactSha(input.observedDevSha, "observedDevSha"),
345
+ observedAt: timestamp(input.observedAt, "observedAt"),
346
+ };
347
+ const observation = { ...body, observationRoot: releaseTrainRoot(body) };
348
+ if (
349
+ train.observations.some(
350
+ (entry) => entry.observationRoot === observation.observationRoot,
351
+ )
352
+ ) {
353
+ return train;
354
+ }
355
+ return { ...train, observations: [...train.observations, observation] };
356
+ }
357
+
358
+ export function observeReleaseTrain(trainInput, input = {}) {
359
+ return applyObservation(validateReleaseTrain(trainInput), input);
360
+ }
361
+
362
+ export function validateReleaseTrain(input) {
363
+ if (!input || typeof input !== "object" || Array.isArray(input))
364
+ throw new Error("Release Train must be an object");
365
+ if (
366
+ input.contract !== RELEASE_TRAIN_CONTRACT ||
367
+ Number(input.schemaVersion) !== 1
368
+ ) {
369
+ throw new Error(`Release Train must use ${RELEASE_TRAIN_CONTRACT}`);
370
+ }
371
+ assertExactFields(
372
+ input,
373
+ [
374
+ "schemaVersion",
375
+ "contract",
376
+ "releaseCut",
377
+ "trainRoot",
378
+ "state",
379
+ "transitions",
380
+ "observations",
381
+ ],
382
+ "Release Train",
383
+ );
384
+ const releaseCut = validateReleaseCut(input.releaseCut);
385
+ const identity = {
386
+ schemaVersion: 1,
387
+ contract: RELEASE_TRAIN_CONTRACT,
388
+ releaseCut,
389
+ };
390
+ if (input.trainRoot !== releaseTrainRoot(identity))
391
+ throw new Error("Release Train root does not match its frozen Release Cut");
392
+ if (!RELEASE_TRAIN_STATES.includes(input.state?.status))
393
+ throw new Error("Release Train state is invalid");
394
+ assertExactFields(
395
+ input.state,
396
+ ["status", "generation", "priorStateRoot", "transitionRoot", "stateRoot"],
397
+ "Release Train state",
398
+ );
399
+ if (input.state.generation !== releaseCut.generation)
400
+ throw new Error(
401
+ "Release Train state generation drifted from its Release Cut",
402
+ );
403
+ const stateBody = {
404
+ status: input.state.status,
405
+ generation: input.state.generation,
406
+ priorStateRoot: contentRoot(
407
+ input.state.priorStateRoot,
408
+ "state.priorStateRoot",
409
+ ),
410
+ transitionRoot: contentRoot(
411
+ input.state.transitionRoot,
412
+ "state.transitionRoot",
413
+ ),
414
+ };
415
+ if (input.state.stateRoot !== releaseTrainRoot(stateBody))
416
+ throw new Error("Release Train state root does not match its content");
417
+ if (!Array.isArray(input.transitions) || !Array.isArray(input.observations))
418
+ throw new Error(
419
+ "Release Train transitions and observations must be arrays",
420
+ );
421
+ const replay = createReleaseTrain({ releaseCut });
422
+ let rebuilt = replay;
423
+ for (const [index, transition] of input.transitions.entries()) {
424
+ assertExactFields(
425
+ transition,
426
+ [
427
+ "contract",
428
+ "trainRoot",
429
+ "expectedStateRoot",
430
+ "to",
431
+ "event",
432
+ "reason",
433
+ "authorityRoots",
434
+ ...(transition.to === "superseded"
435
+ ? [
436
+ "supersessionCause",
437
+ "replacementCutRoot",
438
+ "replacementCandidateSha",
439
+ ]
440
+ : []),
441
+ "recordedAt",
442
+ "from",
443
+ "requestRoot",
444
+ "transitionRoot",
445
+ ],
446
+ `Release Train transition ${index}`,
447
+ );
448
+ rebuilt = applyTransition(rebuilt, transition);
449
+ const replayed = rebuilt.transitions.at(-1);
450
+ if (
451
+ rebuilt.transitions.length !== index + 1 ||
452
+ releaseTrainRoot(replayed) !== releaseTrainRoot(transition)
453
+ ) {
454
+ throw new Error(
455
+ "Release Train transition root does not match its content",
456
+ );
457
+ }
458
+ }
459
+ if (rebuilt.state.stateRoot !== input.state.stateRoot)
460
+ throw new Error(
461
+ "Release Train state chain does not replay to the current state",
462
+ );
463
+ for (const [index, observation] of input.observations.entries()) {
464
+ assertExactFields(
465
+ observation,
466
+ [
467
+ "contract",
468
+ "trainRoot",
469
+ "observedDevSha",
470
+ "observedAt",
471
+ "observationRoot",
472
+ ],
473
+ `Release Train observation ${index}`,
474
+ );
475
+ rebuilt = applyObservation(rebuilt, observation);
476
+ const replayed = rebuilt.observations.at(-1);
477
+ if (
478
+ rebuilt.observations.length !== index + 1 ||
479
+ releaseTrainRoot(replayed) !== releaseTrainRoot(observation)
480
+ ) {
481
+ throw new Error(
482
+ "Release Train observation root does not match its content",
483
+ );
484
+ }
485
+ }
486
+ if (rebuilt.observations.length !== input.observations.length)
487
+ throw new Error("Release Train observations must be unique");
488
+ return clone(input);
489
+ }
490
+
491
+ export function readReleaseTrain(input) {
492
+ if (input?.contract === RELEASE_TRAIN_CONTRACT) {
493
+ return {
494
+ format: "release-train-v1",
495
+ authoritative: true,
496
+ train: validateReleaseTrain(input),
497
+ };
498
+ }
499
+ if (input?.schema === LEGACY_DEV_ALPHA_CANDIDATE_STATE_SCHEMA) {
500
+ const generation = positiveInteger(input.generation, "legacy generation");
501
+ const candidate = input.activeCandidate || input.nextCandidate;
502
+ return {
503
+ format: "legacy-dev-alpha-candidate-state-v1",
504
+ authoritative: false,
505
+ train: null,
506
+ legacy: {
507
+ generation,
508
+ candidateSha: candidate
509
+ ? exactSha(candidate.sourceSha, "legacy candidate sourceSha")
510
+ : "",
511
+ stateRoot: ROOT.test(String(input.stateRoot || ""))
512
+ ? String(input.stateRoot)
513
+ : "",
514
+ },
515
+ reason:
516
+ "legacy candidate state can be read but cannot manufacture Release Cut authority roots",
517
+ };
518
+ }
519
+ throw new Error("unsupported Release Train record");
520
+ }
@@ -3,6 +3,7 @@
3
3
  import { digest } from "./aws-runner-burst-core.mjs";
4
4
  import {
5
5
  MACOS_EC2_JIT,
6
+ macosJitRegionConfig,
6
7
  macosJitRunnerLabel,
7
8
  macosJitRunnerLabels,
8
9
  } from "./aws-macos-jit-core.mjs";
@@ -69,9 +70,7 @@ function commonAws(values) {
69
70
  /^us-[a-z]+-\d$/,
70
71
  "region",
71
72
  );
72
- if (region !== MACOS_EC2_JIT.region) {
73
- throw new Error(`region must be ${MACOS_EC2_JIT.region}`);
74
- }
73
+ const regionConfig = macosJitRegionConfig(region);
75
74
  const instanceType = exact(
76
75
  values.instanceType || MACOS_EC2_JIT.instanceType,
77
76
  /^mac2\.metal$/,
@@ -80,13 +79,18 @@ function commonAws(values) {
80
79
  if (instanceType !== MACOS_EC2_JIT.instanceType) {
81
80
  throw new Error(`instanceType must be ${MACOS_EC2_JIT.instanceType}`);
82
81
  }
82
+ const availabilityZone = exact(
83
+ values.availabilityZone,
84
+ /^us-[a-z]+-\d[a-z]$/,
85
+ "availabilityZone",
86
+ );
87
+ if (!availabilityZone.startsWith(region)) {
88
+ throw new Error(`availabilityZone must belong to ${region}`);
89
+ }
83
90
  return {
84
91
  region,
85
- availabilityZone: exact(
86
- values.availabilityZone,
87
- /^us-[a-z]+-\d[a-z]$/,
88
- "availabilityZone",
89
- ),
92
+ availabilityZone,
93
+ controlPlaneStack: regionConfig.stack,
90
94
  instanceType,
91
95
  amiId: exact(values.amiId, /^ami-[0-9a-f]+$/, "amiId"),
92
96
  amiName: exact(values.amiName, /^[A-Za-z0-9._-]+$/, "amiName"),
@@ -123,6 +127,7 @@ export function createMacosJitCampaignPlan(values = {}) {
123
127
  const id = campaignId(values.campaignId);
124
128
  const boundSource = source(values);
125
129
  const aws = commonAws(values);
130
+ const regionConfig = macosJitRegionConfig(aws.region);
126
131
  const tags = ownershipTags({ id, sourceSha: boundSource.sha });
127
132
  const createdAt = iso(values.createdAt, "createdAt");
128
133
  const plan = {
@@ -130,6 +135,13 @@ export function createMacosJitCampaignPlan(values = {}) {
130
135
  contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
131
136
  kind: "campaign-launch-plan",
132
137
  repository: repository(values.repository),
138
+ account: {
139
+ id: exact(values.accountId, /^\d{12}$/, "accountId"),
140
+ },
141
+ github: {
142
+ workflowId: MACOS_EC2_JIT.workflowId,
143
+ requiredState: "disabled_manually",
144
+ },
133
145
  campaign: { id, createdAt },
134
146
  source: boundSource,
135
147
  aws: {
@@ -158,18 +170,29 @@ export function createMacosJitCampaignPlan(values = {}) {
158
170
  exactSourceRequired: true,
159
171
  activeHostCeiling: MACOS_EC2_JIT.maxAcceptedHosts,
160
172
  activeInstanceCeiling: 1,
161
- awsDryRunRequiredBeforeAllocation: true,
173
+ awsPermissionSimulationRequiredBeforeAllocation: true,
162
174
  awsDryRunRequiredBeforeLaunch: true,
163
175
  retainHostOnInstanceLaunchFailure: true,
164
176
  minimumHostAllocationHours: MACOS_EC2_JIT.minimumHostAllocationHours,
165
177
  maximumHostAllocationHours: MACOS_EC2_JIT.maximumHostAllocationHours,
166
178
  cleanupOwner: "scheduled-card-scoped-reaper",
179
+ budget: {
180
+ name: regionConfig.budgetName,
181
+ limitUsd: MACOS_EC2_JIT.budgetLimitUsd,
182
+ metrics: ["UnblendedCost"],
183
+ dimensionFilter: {
184
+ usageTypes: regionConfig.budgetUsageTypes,
185
+ operation: MACOS_EC2_JIT.budgetOperation,
186
+ regions: regionConfig.budgetRegions,
187
+ },
188
+ requiredActualThresholds: [80, 95],
189
+ },
167
190
  },
168
191
  };
169
192
  return { ...plan, digest: digest(plan) };
170
193
  }
171
194
 
172
- export function macosAllocateHostsArgs(plan, { dryRun = false } = {}) {
195
+ export function macosAllocateHostsArgs(plan) {
173
196
  if (
174
197
  plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
175
198
  plan.kind !== "campaign-launch-plan"
@@ -196,7 +219,6 @@ export function macosAllocateHostsArgs(plan, { dryRun = false } = {}) {
196
219
  "--output",
197
220
  "json",
198
221
  ];
199
- if (dryRun) args.splice(2, 0, "--dry-run");
200
222
  return args;
201
223
  }
202
224