@kungfu-tech/buildchain 3.0.5-alpha.0 → 3.0.5-alpha.1
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/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +20 -15
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/page-registry.json +13 -8
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +32 -1
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +7 -0
- package/package.json +1 -1
- package/scripts/aws-macos-jit-controller-core.mjs +389 -0
- package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
- package/scripts/aws-macos-jit-controller.mjs +558 -0
- package/scripts/aws-macos-jit-job-controller.mjs +401 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-publication-release-registry",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-02T02:26:15.287Z",
|
|
5
|
+
"publishedAt": "2026-08-02T02:26:15.287Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "8b96fb02e587124bad0f9055f9de6eb7c6b8081c",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"package": {
|
|
34
34
|
"name": "@kungfu-tech/buildchain",
|
|
35
|
-
"version": "3.0.5-alpha.
|
|
35
|
+
"version": "3.0.5-alpha.1",
|
|
36
36
|
"versionSource": "package.json#version"
|
|
37
37
|
},
|
|
38
38
|
"sourceKind": "package-site-bundle",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-manifest",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-02T02:26:15.287Z",
|
|
5
|
+
"publishedAt": "2026-08-02T02:26:15.287Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "8b96fb02e587124bad0f9055f9de6eb7c6b8081c",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "3.0.5-alpha.
|
|
40
|
+
"version": "3.0.5-alpha.1",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
"path": "docs/publish-transaction.md",
|
|
286
286
|
"plane": "verify",
|
|
287
287
|
"exists": true,
|
|
288
|
-
"digest": "sha256:
|
|
288
|
+
"digest": "sha256:3a9ef099d5d93d9b558c7b6a0aa4403dfcce1479ce5d6fc36ecb3e34cdd5bb6d"
|
|
289
289
|
},
|
|
290
290
|
{
|
|
291
291
|
"id": "release-governance",
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
"path": "docs/release-governance.md",
|
|
294
294
|
"plane": "why",
|
|
295
295
|
"exists": true,
|
|
296
|
-
"digest": "sha256:
|
|
296
|
+
"digest": "sha256:96d78f800cd7d43d3a47a11863d4813711e360418ba33a4284688389c7e4b70c"
|
|
297
297
|
},
|
|
298
298
|
{
|
|
299
299
|
"id": "release-flow",
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
"path": "docs/release-flow.md",
|
|
302
302
|
"plane": "verify",
|
|
303
303
|
"exists": true,
|
|
304
|
-
"digest": "sha256:
|
|
304
|
+
"digest": "sha256:dc54b8a264489341984ca6157b07d1956e934366f937b7412a21df7f025d2d8d"
|
|
305
305
|
},
|
|
306
306
|
{
|
|
307
307
|
"id": "runtime-train-validation",
|
|
@@ -8,7 +8,12 @@ confidence: high
|
|
|
8
8
|
sensitivity: public
|
|
9
9
|
evidence_grade: A
|
|
10
10
|
review_state: unreviewed
|
|
11
|
-
last_reviewed: 2026-
|
|
11
|
+
last_reviewed: 2026-08-01
|
|
12
|
+
ai_provenance:
|
|
13
|
+
model_family: GPT-5
|
|
14
|
+
product: Codex
|
|
15
|
+
generated_at: 2026-08-01
|
|
16
|
+
invisible_information: No hidden model checkpoint, parameters, or private training data were available.
|
|
12
17
|
---
|
|
13
18
|
|
|
14
19
|
# AWS US elastic runner burst plane
|
|
@@ -226,6 +231,32 @@ host, including at least one full run, plus proof that:
|
|
|
226
231
|
- AWS has no active campaign instance or allocated campaign host;
|
|
227
232
|
- actual incremental spend remained below USD 25.
|
|
228
233
|
|
|
234
|
+
### Phase 3 lifecycle controller
|
|
235
|
+
|
|
236
|
+
`scripts/aws-macos-jit-controller.mjs` is the operator boundary for the paid
|
|
237
|
+
campaign. It has three explicit mutation modes:
|
|
238
|
+
|
|
239
|
+
- `launch-campaign` binds the exact repository source, AMI, availability zone,
|
|
240
|
+
tagged Dedicated Host, and reusable instance. It rejects pre-existing Mac
|
|
241
|
+
capacity and requires successful `AllocateHosts` and `RunInstances` DryRuns
|
|
242
|
+
before either real call.
|
|
243
|
+
- `run-job` binds one queued exact-source GitHub job to the existing campaign
|
|
244
|
+
host and instance. It writes the repository JIT configuration through a
|
|
245
|
+
mode-0600 temporary file into a distinct SSM SecureString, sends only the
|
|
246
|
+
credential-free bootstrap through SSM, and removes the parameter plus runner
|
|
247
|
+
registration if command delivery fails.
|
|
248
|
+
- `close-campaign` refuses execution before the provider's 24-hour minimum,
|
|
249
|
+
verifies the encrypted delete-on-termination root volume, removes scoped JIT
|
|
250
|
+
residue, terminates the exact instance, and requires a `ReleaseHosts` DryRun
|
|
251
|
+
before release. If Apple host scrubbing is still in progress, it reports
|
|
252
|
+
`release-pending`; the ten-minute card-scoped reaper remains the bounded
|
|
253
|
+
retry path.
|
|
254
|
+
|
|
255
|
+
Every execute mode requires the exact source SHA and campaign id to be repeated
|
|
256
|
+
through `--confirm-source-sha` and `--confirm-campaign-id`. `run-job` also
|
|
257
|
+
requires `--confirm-run-id`. Omitting `--execute` emits a deterministic plan
|
|
258
|
+
without changing AWS or GitHub state.
|
|
259
|
+
|
|
229
260
|
## Provider lifecycle
|
|
230
261
|
|
|
231
262
|
The three infrastructure templates live under
|
|
@@ -415,9 +415,10 @@ first run can stop at `finalizing` after registry publication. If the reviewed
|
|
|
415
415
|
merge commit later contains that exact transaction release material but the
|
|
416
416
|
exact tag is still absent, a retry performs finalization only: it reloads the
|
|
417
417
|
same durable source, release material, tooling, evidence, version, and target
|
|
418
|
-
bindings; creates the exact
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
bindings; creates the exact tag at the transaction source SHA; moves floating
|
|
419
|
+
refs to the transaction release SHA; and completes the passport from the
|
|
420
|
+
transaction source tree. It does not rerun the provider mutation and does not
|
|
421
|
+
authorize the newer composite channel tree
|
|
421
422
|
as published material. A different source tree still requires a new version and
|
|
422
423
|
a fresh release candidate.
|
|
423
424
|
|
|
@@ -536,8 +537,9 @@ inside the mutation boundary.
|
|
|
536
537
|
|
|
537
538
|
If finalization fails after an exact Git tag is created, the next run reads the
|
|
538
539
|
durable `finalizing` state, verifies the exact tag points at the recorded
|
|
539
|
-
|
|
540
|
-
|
|
540
|
+
source SHA (while accepting legacy release/material targets for recovery), and
|
|
541
|
+
retries the remaining floating refs. An exact tag at an unrelated SHA is a
|
|
542
|
+
material conflict and blocks recovery.
|
|
541
543
|
|
|
542
544
|
## CLI Recovery
|
|
543
545
|
|
package/docs/release-flow.md
CHANGED
|
@@ -96,6 +96,10 @@ published artifacts are already durable.
|
|
|
96
96
|
The intended governance split is:
|
|
97
97
|
|
|
98
98
|
- exact tags such as `v3.0.2` and `v3.0.3-alpha.0` are immutable audit refs;
|
|
99
|
+
- for publish transactions, the exact tag points to the transaction
|
|
100
|
+
`source_sha`, matching package-registry source metadata such as npm
|
|
101
|
+
`gitHead`; generated version-state commits remain on protected branches and
|
|
102
|
+
floating channel refs;
|
|
99
103
|
- floating tags such as `v3`, `v3.0`, `v3.0-alpha`, and `v3-alpha` are mutable channel refs
|
|
100
104
|
owned by the Buildchain promotion token;
|
|
101
105
|
- protected branches still require reviewed channel PRs before Buildchain can
|
|
@@ -831,6 +831,13 @@ must be absent or already point at accepted release material. This preserves an
|
|
|
831
831
|
immutable tag that represents a completed transaction while allowing a tag
|
|
832
832
|
collision discovered after registry publication to recover without republishing.
|
|
833
833
|
|
|
834
|
+
For package publish transactions, the immutable public version tag points to
|
|
835
|
+
the transaction `source_sha`, so registry source metadata such as npm `gitHead`
|
|
836
|
+
and the Git tag identify the same source commit. Protected branches and mutable
|
|
837
|
+
channel tags continue to point to the generated `release_sha`. Recovery accepts
|
|
838
|
+
older completed transactions whose exact tags already point to recorded release
|
|
839
|
+
or release-material SHAs, but new tags are source-bound.
|
|
840
|
+
|
|
834
841
|
Every Buildchain publish model that can run registry side effects must bind the
|
|
835
842
|
publish entrypoint to an immutable `publish-gate/*` source lock. The reusable
|
|
836
843
|
`release-candidate-promote.yml@v3` wrapper creates or updates that gate ref and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "3.0.5-alpha.
|
|
3
|
+
"version": "3.0.5-alpha.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { digest } from "./aws-runner-burst-core.mjs";
|
|
4
|
+
import {
|
|
5
|
+
MACOS_EC2_JIT,
|
|
6
|
+
macosJitRunnerLabel,
|
|
7
|
+
macosJitRunnerLabels,
|
|
8
|
+
} from "./aws-macos-jit-core.mjs";
|
|
9
|
+
|
|
10
|
+
export const AWS_MACOS_JIT_CONTROLLER_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-aws-macos-jit-controller/v1";
|
|
12
|
+
|
|
13
|
+
function exact(value, pattern, label) {
|
|
14
|
+
const normalized = String(value || "").trim();
|
|
15
|
+
if (!pattern.test(normalized)) throw new Error(`${label} is invalid`);
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactSha(value) {
|
|
20
|
+
return exact(value, /^[0-9a-f]{40}$/i, "sourceSha").toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function iso(value, label) {
|
|
24
|
+
const parsed = new Date(value || "");
|
|
25
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
26
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
27
|
+
}
|
|
28
|
+
return parsed.toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tag(key, value) {
|
|
32
|
+
return { Key: key, Value: String(value) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function repository(value) {
|
|
36
|
+
const resolved = exact(
|
|
37
|
+
value || MACOS_EC2_JIT.repository,
|
|
38
|
+
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/,
|
|
39
|
+
"repository",
|
|
40
|
+
);
|
|
41
|
+
if (resolved !== MACOS_EC2_JIT.repository) {
|
|
42
|
+
throw new Error(`repository must be ${MACOS_EC2_JIT.repository}`);
|
|
43
|
+
}
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function source(values) {
|
|
48
|
+
return {
|
|
49
|
+
sha: exactSha(values.sourceSha),
|
|
50
|
+
ref: exact(
|
|
51
|
+
values.sourceRef,
|
|
52
|
+
/^refs\/heads\/[A-Za-z0-9._/-]+$/,
|
|
53
|
+
"sourceRef",
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function campaignId(value) {
|
|
59
|
+
return exact(value, /^[a-z0-9][a-z0-9-]{2,47}$/, "campaignId");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function qualificationId(value) {
|
|
63
|
+
return exact(value, /^mac-(?:smoke-0[12]|full-01)$/, "qualificationId");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function commonAws(values) {
|
|
67
|
+
const region = exact(
|
|
68
|
+
values.region || MACOS_EC2_JIT.region,
|
|
69
|
+
/^us-[a-z]+-\d$/,
|
|
70
|
+
"region",
|
|
71
|
+
);
|
|
72
|
+
if (region !== MACOS_EC2_JIT.region) {
|
|
73
|
+
throw new Error(`region must be ${MACOS_EC2_JIT.region}`);
|
|
74
|
+
}
|
|
75
|
+
const instanceType = exact(
|
|
76
|
+
values.instanceType || MACOS_EC2_JIT.instanceType,
|
|
77
|
+
/^mac2\.metal$/,
|
|
78
|
+
"instanceType",
|
|
79
|
+
);
|
|
80
|
+
if (instanceType !== MACOS_EC2_JIT.instanceType) {
|
|
81
|
+
throw new Error(`instanceType must be ${MACOS_EC2_JIT.instanceType}`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
region,
|
|
85
|
+
availabilityZone: exact(
|
|
86
|
+
values.availabilityZone,
|
|
87
|
+
/^us-[a-z]+-\d[a-z]$/,
|
|
88
|
+
"availabilityZone",
|
|
89
|
+
),
|
|
90
|
+
instanceType,
|
|
91
|
+
amiId: exact(values.amiId, /^ami-[0-9a-f]+$/, "amiId"),
|
|
92
|
+
amiName: exact(values.amiName, /^[A-Za-z0-9._-]+$/, "amiName"),
|
|
93
|
+
subnetId: exact(values.subnetId, /^subnet-[0-9a-f]+$/, "subnetId"),
|
|
94
|
+
securityGroupId: exact(
|
|
95
|
+
values.securityGroupId,
|
|
96
|
+
/^sg-[0-9a-f]+$/,
|
|
97
|
+
"securityGroupId",
|
|
98
|
+
),
|
|
99
|
+
instanceProfileName: exact(
|
|
100
|
+
values.instanceProfileName,
|
|
101
|
+
/^[A-Za-z0-9+=,.@_-]{1,128}$/,
|
|
102
|
+
"instanceProfileName",
|
|
103
|
+
),
|
|
104
|
+
evidenceBucket: exact(
|
|
105
|
+
values.evidenceBucket,
|
|
106
|
+
/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
|
|
107
|
+
"evidenceBucket",
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ownershipTags({ id, sourceSha }) {
|
|
113
|
+
return [
|
|
114
|
+
tag("kungfu:owner", "buildchain"),
|
|
115
|
+
tag("kungfu:plane", "aws-us-elastic-runner-burst"),
|
|
116
|
+
tag("kungfu:provider", "macos-ec2-jit"),
|
|
117
|
+
tag("kungfu:campaign-id", id),
|
|
118
|
+
tag("kungfu:source-sha", sourceSha),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createMacosJitCampaignPlan(values = {}) {
|
|
123
|
+
const id = campaignId(values.campaignId);
|
|
124
|
+
const boundSource = source(values);
|
|
125
|
+
const aws = commonAws(values);
|
|
126
|
+
const tags = ownershipTags({ id, sourceSha: boundSource.sha });
|
|
127
|
+
const createdAt = iso(values.createdAt, "createdAt");
|
|
128
|
+
const plan = {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
|
|
131
|
+
kind: "campaign-launch-plan",
|
|
132
|
+
repository: repository(values.repository),
|
|
133
|
+
campaign: { id, createdAt },
|
|
134
|
+
source: boundSource,
|
|
135
|
+
aws: {
|
|
136
|
+
...aws,
|
|
137
|
+
hostTags: tags,
|
|
138
|
+
instanceTags: tags,
|
|
139
|
+
volumeTags: tags,
|
|
140
|
+
hostClientToken: `kungfu-mac-host-${id}`,
|
|
141
|
+
instanceClientToken: `kungfu-mac-instance-${id}`,
|
|
142
|
+
rootVolume: {
|
|
143
|
+
deviceName: "/dev/sda1",
|
|
144
|
+
deleteOnTermination: true,
|
|
145
|
+
encrypted: true,
|
|
146
|
+
volumeSizeGiB: 200,
|
|
147
|
+
volumeType: "gp3",
|
|
148
|
+
},
|
|
149
|
+
metadata: {
|
|
150
|
+
httpEndpoint: "enabled",
|
|
151
|
+
httpTokens: "required",
|
|
152
|
+
httpPutResponseHopLimit: 1,
|
|
153
|
+
instanceMetadataTags: "disabled",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
safety: {
|
|
157
|
+
applyMode: values.execute === true ? "execute" : "dry-run",
|
|
158
|
+
exactSourceRequired: true,
|
|
159
|
+
activeHostCeiling: MACOS_EC2_JIT.maxAcceptedHosts,
|
|
160
|
+
activeInstanceCeiling: 1,
|
|
161
|
+
awsDryRunRequiredBeforeAllocation: true,
|
|
162
|
+
awsDryRunRequiredBeforeLaunch: true,
|
|
163
|
+
retainHostOnInstanceLaunchFailure: true,
|
|
164
|
+
minimumHostAllocationHours: MACOS_EC2_JIT.minimumHostAllocationHours,
|
|
165
|
+
maximumHostAllocationHours: MACOS_EC2_JIT.maximumHostAllocationHours,
|
|
166
|
+
cleanupOwner: "scheduled-card-scoped-reaper",
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
return { ...plan, digest: digest(plan) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function macosAllocateHostsArgs(plan, { dryRun = false } = {}) {
|
|
173
|
+
if (
|
|
174
|
+
plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
|
|
175
|
+
plan.kind !== "campaign-launch-plan"
|
|
176
|
+
) {
|
|
177
|
+
throw new Error("macOS JIT campaign launch plan contract is invalid");
|
|
178
|
+
}
|
|
179
|
+
const args = [
|
|
180
|
+
"ec2",
|
|
181
|
+
"allocate-hosts",
|
|
182
|
+
"--availability-zone",
|
|
183
|
+
plan.aws.availabilityZone,
|
|
184
|
+
"--instance-type",
|
|
185
|
+
plan.aws.instanceType,
|
|
186
|
+
"--quantity",
|
|
187
|
+
"1",
|
|
188
|
+
"--auto-placement",
|
|
189
|
+
"off",
|
|
190
|
+
"--client-token",
|
|
191
|
+
plan.aws.hostClientToken,
|
|
192
|
+
"--tag-specifications",
|
|
193
|
+
JSON.stringify([
|
|
194
|
+
{ ResourceType: "dedicated-host", Tags: plan.aws.hostTags },
|
|
195
|
+
]),
|
|
196
|
+
"--output",
|
|
197
|
+
"json",
|
|
198
|
+
];
|
|
199
|
+
if (dryRun) args.splice(2, 0, "--dry-run");
|
|
200
|
+
return args;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function macosRunInstancesArgs(plan, { hostId, dryRun = false } = {}) {
|
|
204
|
+
if (
|
|
205
|
+
plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
|
|
206
|
+
plan.kind !== "campaign-launch-plan"
|
|
207
|
+
) {
|
|
208
|
+
throw new Error("macOS JIT campaign launch plan contract is invalid");
|
|
209
|
+
}
|
|
210
|
+
const resolvedHostId = exact(hostId, /^h-[0-9a-f]+$/, "hostId");
|
|
211
|
+
const args = [
|
|
212
|
+
"ec2",
|
|
213
|
+
"run-instances",
|
|
214
|
+
"--image-id",
|
|
215
|
+
plan.aws.amiId,
|
|
216
|
+
"--instance-type",
|
|
217
|
+
plan.aws.instanceType,
|
|
218
|
+
"--count",
|
|
219
|
+
"1",
|
|
220
|
+
"--client-token",
|
|
221
|
+
plan.aws.instanceClientToken,
|
|
222
|
+
"--iam-instance-profile",
|
|
223
|
+
JSON.stringify({ Name: plan.aws.instanceProfileName }),
|
|
224
|
+
"--subnet-id",
|
|
225
|
+
plan.aws.subnetId,
|
|
226
|
+
"--security-group-ids",
|
|
227
|
+
plan.aws.securityGroupId,
|
|
228
|
+
"--associate-public-ip-address",
|
|
229
|
+
"--placement",
|
|
230
|
+
JSON.stringify({ HostId: resolvedHostId, Tenancy: "host" }),
|
|
231
|
+
"--block-device-mappings",
|
|
232
|
+
JSON.stringify([
|
|
233
|
+
{
|
|
234
|
+
DeviceName: plan.aws.rootVolume.deviceName,
|
|
235
|
+
Ebs: {
|
|
236
|
+
DeleteOnTermination: plan.aws.rootVolume.deleteOnTermination,
|
|
237
|
+
Encrypted: plan.aws.rootVolume.encrypted,
|
|
238
|
+
VolumeSize: plan.aws.rootVolume.volumeSizeGiB,
|
|
239
|
+
VolumeType: plan.aws.rootVolume.volumeType,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
]),
|
|
243
|
+
"--metadata-options",
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
HttpEndpoint: plan.aws.metadata.httpEndpoint,
|
|
246
|
+
HttpTokens: plan.aws.metadata.httpTokens,
|
|
247
|
+
HttpPutResponseHopLimit: plan.aws.metadata.httpPutResponseHopLimit,
|
|
248
|
+
InstanceMetadataTags: plan.aws.metadata.instanceMetadataTags,
|
|
249
|
+
}),
|
|
250
|
+
"--instance-initiated-shutdown-behavior",
|
|
251
|
+
"stop",
|
|
252
|
+
"--tag-specifications",
|
|
253
|
+
JSON.stringify([
|
|
254
|
+
{ ResourceType: "instance", Tags: plan.aws.instanceTags },
|
|
255
|
+
{ ResourceType: "volume", Tags: plan.aws.volumeTags },
|
|
256
|
+
]),
|
|
257
|
+
"--output",
|
|
258
|
+
"json",
|
|
259
|
+
];
|
|
260
|
+
if (dryRun) args.splice(2, 0, "--dry-run");
|
|
261
|
+
return args;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createMacosJitJobPlan(values = {}) {
|
|
265
|
+
const id = campaignId(values.campaignId);
|
|
266
|
+
const boundSource = source(values);
|
|
267
|
+
const qualification = qualificationId(values.qualificationId);
|
|
268
|
+
const runnerLabel = macosJitRunnerLabel(values.runnerLabel);
|
|
269
|
+
const expectedLabel = `${MACOS_EC2_JIT.labelPrefix}${qualification}`;
|
|
270
|
+
if (runnerLabel !== expectedLabel) {
|
|
271
|
+
throw new Error(`runnerLabel must be ${expectedLabel}`);
|
|
272
|
+
}
|
|
273
|
+
const runId = exact(values.runId, /^\d+$/, "runId");
|
|
274
|
+
const runAttempt = exact(
|
|
275
|
+
values.runAttempt || "1",
|
|
276
|
+
/^[1-9]\d*$/,
|
|
277
|
+
"runAttempt",
|
|
278
|
+
);
|
|
279
|
+
const hostAllocatedAt = iso(values.hostAllocatedAt, "hostAllocatedAt");
|
|
280
|
+
const jitParameterName = `${MACOS_EC2_JIT.jitParameterPrefix}${id}/${runId}/${runAttempt}/${qualification}`;
|
|
281
|
+
const plan = {
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
|
|
284
|
+
kind: "job-run-plan",
|
|
285
|
+
repository: repository(values.repository),
|
|
286
|
+
campaign: { id },
|
|
287
|
+
source: boundSource,
|
|
288
|
+
github: {
|
|
289
|
+
runId,
|
|
290
|
+
runAttempt,
|
|
291
|
+
jobId: exact(values.jobId, /^\d+$/, "jobId"),
|
|
292
|
+
qualificationId: qualification,
|
|
293
|
+
event: "workflow_dispatch",
|
|
294
|
+
},
|
|
295
|
+
runner: {
|
|
296
|
+
name: exact(
|
|
297
|
+
values.runnerName,
|
|
298
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/,
|
|
299
|
+
"runnerName",
|
|
300
|
+
),
|
|
301
|
+
label: runnerLabel,
|
|
302
|
+
labels: macosJitRunnerLabels(runnerLabel),
|
|
303
|
+
oneJobJit: true,
|
|
304
|
+
},
|
|
305
|
+
aws: {
|
|
306
|
+
...commonAws(values),
|
|
307
|
+
hostId: exact(values.hostId, /^h-[0-9a-f]+$/, "hostId"),
|
|
308
|
+
instanceId: exact(values.instanceId, /^i-[0-9a-f]+$/, "instanceId"),
|
|
309
|
+
hostAllocatedAt,
|
|
310
|
+
jitParameterName,
|
|
311
|
+
},
|
|
312
|
+
safety: {
|
|
313
|
+
applyMode: values.execute === true ? "execute" : "dry-run",
|
|
314
|
+
exactSourceRequired: true,
|
|
315
|
+
queuedJobRequired: true,
|
|
316
|
+
sameCampaignHostRequired: true,
|
|
317
|
+
sameCampaignInstanceRequired: true,
|
|
318
|
+
jitConfigTransport: "0600-temporary-file-to-ssm-secure-string",
|
|
319
|
+
bootstrapTransport: "0600-temporary-file-to-ssm-command",
|
|
320
|
+
jitConfigInBootstrap: false,
|
|
321
|
+
jitConfigInArgv: false,
|
|
322
|
+
cleanupOnCommandFailure: ["ssm-parameter", "github-runner-registration"],
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
return { ...plan, digest: digest(plan) };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function createMacosJitClosePlan(values = {}) {
|
|
329
|
+
const id = campaignId(values.campaignId);
|
|
330
|
+
const boundSource = source(values);
|
|
331
|
+
const hostAllocatedAt = iso(values.hostAllocatedAt, "hostAllocatedAt");
|
|
332
|
+
const observedAt = iso(values.observedAt, "observedAt");
|
|
333
|
+
const allocationHours =
|
|
334
|
+
(new Date(observedAt).getTime() - new Date(hostAllocatedAt).getTime()) /
|
|
335
|
+
3_600_000;
|
|
336
|
+
if (allocationHours < MACOS_EC2_JIT.minimumHostAllocationHours) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Dedicated Host cannot be closed before ${MACOS_EC2_JIT.minimumHostAllocationHours} hours`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const plan = {
|
|
342
|
+
schemaVersion: 1,
|
|
343
|
+
contract: AWS_MACOS_JIT_CONTROLLER_CONTRACT,
|
|
344
|
+
kind: "campaign-close-plan",
|
|
345
|
+
repository: repository(values.repository),
|
|
346
|
+
campaign: { id },
|
|
347
|
+
source: boundSource,
|
|
348
|
+
aws: {
|
|
349
|
+
...commonAws(values),
|
|
350
|
+
hostId: exact(values.hostId, /^h-[0-9a-f]+$/, "hostId"),
|
|
351
|
+
instanceId: exact(values.instanceId, /^i-[0-9a-f]+$/, "instanceId"),
|
|
352
|
+
hostAllocatedAt,
|
|
353
|
+
},
|
|
354
|
+
lifecycle: {
|
|
355
|
+
observedAt,
|
|
356
|
+
allocationHours,
|
|
357
|
+
minimumHostAllocationHours: MACOS_EC2_JIT.minimumHostAllocationHours,
|
|
358
|
+
maximumHostAllocationHours: MACOS_EC2_JIT.maximumHostAllocationHours,
|
|
359
|
+
},
|
|
360
|
+
safety: {
|
|
361
|
+
applyMode: values.execute === true ? "execute" : "dry-run",
|
|
362
|
+
exactCampaignOwnershipRequired: true,
|
|
363
|
+
instanceTerminationRequiredBeforeRelease: true,
|
|
364
|
+
encryptedDeleteOnTerminationVolumeRequired: true,
|
|
365
|
+
awsDryRunRequiredBeforeRelease: true,
|
|
366
|
+
reaperFallback: true,
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
return { ...plan, digest: digest(plan) };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function macosReleaseHostsArgs(plan, { dryRun = false } = {}) {
|
|
373
|
+
if (
|
|
374
|
+
plan?.contract !== AWS_MACOS_JIT_CONTROLLER_CONTRACT ||
|
|
375
|
+
plan.kind !== "campaign-close-plan"
|
|
376
|
+
) {
|
|
377
|
+
throw new Error("macOS JIT campaign close plan contract is invalid");
|
|
378
|
+
}
|
|
379
|
+
const args = [
|
|
380
|
+
"ec2",
|
|
381
|
+
"release-hosts",
|
|
382
|
+
"--host-ids",
|
|
383
|
+
plan.aws.hostId,
|
|
384
|
+
"--output",
|
|
385
|
+
"json",
|
|
386
|
+
];
|
|
387
|
+
if (dryRun) args.splice(2, 0, "--dry-run");
|
|
388
|
+
return args;
|
|
389
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export function commandResult(command, args, options = {}) {
|
|
6
|
+
const result = spawnSync(command, args, {
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
9
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
10
|
+
input: options.input,
|
|
11
|
+
env: process.env,
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
status: result.status,
|
|
15
|
+
stdout: String(result.stdout || ""),
|
|
16
|
+
stderr: String(result.stderr || result.error?.message || ""),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function requireSuccess(result, label) {
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
const detail = (result.stderr || result.stdout).trim().slice(0, 2000);
|
|
23
|
+
throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
|
|
24
|
+
}
|
|
25
|
+
return result.stdout;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function jsonResult(result, label) {
|
|
29
|
+
const output = requireSuccess(result, label);
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(output);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`${label} returned invalid JSON`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function awsArgs(plan, profile, serviceArgs) {
|
|
38
|
+
return [
|
|
39
|
+
...(profile ? ["--profile", profile] : []),
|
|
40
|
+
"--region",
|
|
41
|
+
plan.aws.region,
|
|
42
|
+
...serviceArgs,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function awsJson(plan, profile, serviceArgs, label) {
|
|
47
|
+
return jsonResult(
|
|
48
|
+
commandResult("aws", awsArgs(plan, profile, serviceArgs)),
|
|
49
|
+
label,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ghJson(args, input, label) {
|
|
54
|
+
return jsonResult(commandResult("gh", args, { input }), label);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertOwnership(resourceTags, plan) {
|
|
58
|
+
const observed = Object.fromEntries(
|
|
59
|
+
(resourceTags || []).map((entry) => [entry.Key, String(entry.Value)]),
|
|
60
|
+
);
|
|
61
|
+
const expected = {
|
|
62
|
+
"kungfu:owner": "buildchain",
|
|
63
|
+
"kungfu:plane": "aws-us-elastic-runner-burst",
|
|
64
|
+
"kungfu:provider": "macos-ec2-jit",
|
|
65
|
+
"kungfu:campaign-id": plan.campaign.id,
|
|
66
|
+
"kungfu:source-sha": plan.source.sha,
|
|
67
|
+
};
|
|
68
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
69
|
+
if (observed[key] !== value) {
|
|
70
|
+
throw new Error(`AWS resource ownership tag ${key} mismatch`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function assertDryRun(result, label) {
|
|
76
|
+
if (
|
|
77
|
+
result.status === 0 ||
|
|
78
|
+
!/DryRunOperation/.test(`${result.stdout}\n${result.stderr}`)
|
|
79
|
+
) {
|
|
80
|
+
throw new Error(`${label} did not return DryRunOperation`);
|
|
81
|
+
}
|
|
82
|
+
}
|