@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/actions/macos-credential-island/README.md +8 -0
- package/bin/buildchain.mjs +1 -1
- package/contracts/auditable-demo-media-profiles-v1.json +61 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
- package/dist/site/buildchain-contract.json +107 -26
- package/dist/site/buildchain-site.json +121 -49
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +9 -3
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +101 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +105 -29
- package/dist/site/public-surface-audit.json +184 -11
- package/dist/site/publication-authority-registry.json +62 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +118 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
- package/docs/cli-reference.md +35 -2
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +123 -72
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +39 -0
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +53 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +36 -1
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-pickup.js +387 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-release.js +61 -18
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +8 -4
- package/packages/core/release-propagation.js +27 -2
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +564 -0
- package/scripts/auditable-demo-renditions.mjs +7 -2
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/auditable-demo.mjs +27 -21
- 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
- package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
- package/scripts/aws-windows-jit-campaign.mjs +202 -0
- package/scripts/aws-windows-jit-controller-core.mjs +20 -2
- package/scripts/aws-windows-jit-controller.mjs +184 -109
- package/scripts/aws-windows-jit-core.mjs +21 -3
- package/scripts/aws-windows-jit.mjs +2 -0
- package/scripts/build-standalone-binary.mjs +6 -0
- package/scripts/buildchain-cli-help.mjs +2 -1
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/check-inventory.mjs +6 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/locked-source-checkout.mjs +17 -3
- package/scripts/release-propagation.mjs +150 -2
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
- package/scripts/workflow-call-contract.mjs +133 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
prepareArtifact,
|
|
13
|
+
validateScenario,
|
|
14
|
+
} from "./auditable-demo-platform.mjs";
|
|
15
|
+
|
|
16
|
+
const NON_AUTHORITIES = [
|
|
17
|
+
"first-party-identity",
|
|
18
|
+
"system-identity",
|
|
19
|
+
"kfd-compliance",
|
|
20
|
+
"product-system-metadata",
|
|
21
|
+
"package-metadata",
|
|
22
|
+
"registry-history",
|
|
23
|
+
"scan-output",
|
|
24
|
+
"standalone-generation",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
throw new Error(`auditable demo transport smoke: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inside(root, relative, label) {
|
|
32
|
+
if (typeof relative !== "string" || !relative || path.isAbsolute(relative))
|
|
33
|
+
fail(`${label} must be relative`);
|
|
34
|
+
const resolvedRoot = path.resolve(root);
|
|
35
|
+
const resolved = path.resolve(resolvedRoot, relative);
|
|
36
|
+
if (
|
|
37
|
+
resolved === resolvedRoot ||
|
|
38
|
+
!resolved.startsWith(`${resolvedRoot}${path.sep}`)
|
|
39
|
+
)
|
|
40
|
+
fail(`${label} escapes its root`);
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeExecuteBits(root) {
|
|
45
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
46
|
+
const file = path.join(root, entry.name);
|
|
47
|
+
if (entry.isDirectory()) removeExecuteBits(file);
|
|
48
|
+
else if (entry.isFile())
|
|
49
|
+
fs.chmodSync(file, fs.statSync(file).mode & ~0o111);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isolatedEnvironment(home, declared) {
|
|
54
|
+
const env = {
|
|
55
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
56
|
+
HOME: home,
|
|
57
|
+
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
58
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
59
|
+
XDG_DATA_HOME: path.join(home, ".local/share"),
|
|
60
|
+
XDG_STATE_HOME: path.join(home, ".local/state"),
|
|
61
|
+
LANG: "C.UTF-8",
|
|
62
|
+
LC_ALL: "C.UTF-8",
|
|
63
|
+
TZ: "UTC",
|
|
64
|
+
CI: "true",
|
|
65
|
+
TERM: "xterm-256color",
|
|
66
|
+
COLORTERM: "truecolor",
|
|
67
|
+
FORCE_COLOR: "3",
|
|
68
|
+
};
|
|
69
|
+
for (const [key, value] of Object.entries(declared)) {
|
|
70
|
+
if (Object.hasOwn(env, key))
|
|
71
|
+
fail(
|
|
72
|
+
`scenario environment may not override the isolated baseline: ${key}`,
|
|
73
|
+
);
|
|
74
|
+
env[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return env;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function runTransportSmoke({ artifactRoot, scenarioPath }) {
|
|
80
|
+
if (process.platform === "win32")
|
|
81
|
+
fail("transport smoke currently requires a POSIX host");
|
|
82
|
+
const root = path.resolve(artifactRoot);
|
|
83
|
+
const scenario = validateScenario(
|
|
84
|
+
JSON.parse(fs.readFileSync(path.resolve(scenarioPath), "utf8")),
|
|
85
|
+
);
|
|
86
|
+
if (!scenario.transportSmoke) fail("scenario.transportSmoke is required");
|
|
87
|
+
const distributionRelative = path.posix.dirname(
|
|
88
|
+
scenario.artifact.metadataPath,
|
|
89
|
+
);
|
|
90
|
+
const sourceDistribution =
|
|
91
|
+
distributionRelative === "."
|
|
92
|
+
? root
|
|
93
|
+
: inside(root, distributionRelative, "distribution root");
|
|
94
|
+
const temporaryRoot = fs.mkdtempSync(
|
|
95
|
+
path.join(os.tmpdir(), "buildchain-demo-transport-"),
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
const transportedRoot = path.join(temporaryRoot, "artifact");
|
|
99
|
+
const transportedDistribution =
|
|
100
|
+
distributionRelative === "."
|
|
101
|
+
? transportedRoot
|
|
102
|
+
: path.join(transportedRoot, ...distributionRelative.split("/"));
|
|
103
|
+
fs.mkdirSync(path.dirname(transportedDistribution), { recursive: true });
|
|
104
|
+
fs.cpSync(sourceDistribution, transportedDistribution, {
|
|
105
|
+
recursive: true,
|
|
106
|
+
dereference: false,
|
|
107
|
+
preserveTimestamps: true,
|
|
108
|
+
verbatimSymlinks: true,
|
|
109
|
+
});
|
|
110
|
+
removeExecuteBits(transportedDistribution);
|
|
111
|
+
const prepared = prepareArtifact({
|
|
112
|
+
artifactRoot: transportedRoot,
|
|
113
|
+
scenarioPath,
|
|
114
|
+
});
|
|
115
|
+
const binary = inside(
|
|
116
|
+
transportedRoot,
|
|
117
|
+
scenario.artifact.binaryPath,
|
|
118
|
+
"transport smoke binary",
|
|
119
|
+
);
|
|
120
|
+
const workspace = path.join(temporaryRoot, "workspace");
|
|
121
|
+
const home = path.join(temporaryRoot, "home");
|
|
122
|
+
fs.mkdirSync(workspace);
|
|
123
|
+
fs.mkdirSync(home);
|
|
124
|
+
const smoke = scenario.transportSmoke;
|
|
125
|
+
const result = spawnSync(binary, smoke.argv, {
|
|
126
|
+
cwd: workspace,
|
|
127
|
+
env: isolatedEnvironment(home, scenario.execution.environment),
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
timeout: smoke.timeoutSeconds * 1000,
|
|
130
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
131
|
+
});
|
|
132
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`;
|
|
133
|
+
if (result.error) fail(`could not execute: ${result.error.message}`);
|
|
134
|
+
if (!smoke.expectedExitCodes.includes(result.status))
|
|
135
|
+
fail(`exited with ${result.status}; output: ${output.slice(-4096)}`);
|
|
136
|
+
for (const expected of smoke.stdoutIncludes)
|
|
137
|
+
if (!output.includes(expected)) fail(`output is missing: ${expected}`);
|
|
138
|
+
return {
|
|
139
|
+
schema: "buildchain.declarative-demo-transport-smoke/v1",
|
|
140
|
+
status: "passed",
|
|
141
|
+
executableFiles: prepared.executableFiles,
|
|
142
|
+
argv: smoke.argv,
|
|
143
|
+
exitCode: result.status,
|
|
144
|
+
outputRoot: `sha256:${crypto.createHash("sha256").update(output).digest("hex")}`,
|
|
145
|
+
authority: {
|
|
146
|
+
classification: "pre-upload-transport-diagnostic",
|
|
147
|
+
grants: [],
|
|
148
|
+
nonAuthorities: NON_AUTHORITIES,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseArgs(argv) {
|
|
157
|
+
const values = {};
|
|
158
|
+
for (let index = 0; index < argv.length; index += 2)
|
|
159
|
+
values[argv[index].replace(/^--/u, "")] = argv[index + 1];
|
|
160
|
+
return values;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
process.argv[1] &&
|
|
165
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
166
|
+
) {
|
|
167
|
+
try {
|
|
168
|
+
const args = parseArgs(process.argv.slice(2));
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`${JSON.stringify({ ok: true, ...runTransportSmoke({ artifactRoot: args["artifact-root"], scenarioPath: args.scenario }) }, null, 2)}\n`,
|
|
171
|
+
);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -36,6 +36,10 @@ const OPTIONAL_ADAPTER_FILES = [
|
|
|
36
36
|
];
|
|
37
37
|
const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
38
38
|
const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
|
|
39
|
+
const STANDARD_MAX_DURATION_MS = 60_000;
|
|
40
|
+
const LONG_FORM_MAX_DURATION_MS = 180_000;
|
|
41
|
+
const LONG_FORM_MAX_FPS = 10;
|
|
42
|
+
const MAX_RENDER_FRAMES = 1_800;
|
|
39
43
|
|
|
40
44
|
function invariant(condition, message) {
|
|
41
45
|
if (!condition) throw new Error(message);
|
|
@@ -186,15 +190,20 @@ function validateScene(value) {
|
|
|
186
190
|
exactKeys(
|
|
187
191
|
value,
|
|
188
192
|
["schema", "id", "width", "height", "fps", "durationMs", "title"],
|
|
189
|
-
["commandLabel", "background", "accent"],
|
|
193
|
+
["durationClass", "commandLabel", "background", "accent"],
|
|
190
194
|
"scene",
|
|
191
195
|
);
|
|
192
196
|
invariant(value.schema === "build-images.demo-scene/v1", "unsupported scene schema");
|
|
193
197
|
invariant(/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value.id), "scene.id is invalid");
|
|
194
198
|
integer(value.width, 640, 1920, "scene.width");
|
|
195
199
|
integer(value.height, 360, 1080, "scene.height");
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
const durationClass = value.durationClass ?? "standard";
|
|
201
|
+
invariant(durationClass === "standard" || durationClass === "long-form", "scene.durationClass is invalid");
|
|
202
|
+
const maximumDurationMs = durationClass === "long-form" ? LONG_FORM_MAX_DURATION_MS : STANDARD_MAX_DURATION_MS;
|
|
203
|
+
const maximumFps = durationClass === "long-form" ? LONG_FORM_MAX_FPS : 30;
|
|
204
|
+
integer(value.fps, 1, maximumFps, "scene.fps");
|
|
205
|
+
integer(value.durationMs, 500, maximumDurationMs, "scene.durationMs");
|
|
206
|
+
invariant(Math.ceil((value.durationMs / 1000) * value.fps) <= MAX_RENDER_FRAMES, "scene exceeds the deterministic source-frame bound");
|
|
198
207
|
text(value.title, 1, 120, "scene.title");
|
|
199
208
|
if (value.commandLabel !== undefined) text(value.commandLabel, 0, 160, "scene.commandLabel");
|
|
200
209
|
for (const key of ["background", "accent"]) {
|
|
@@ -206,6 +215,7 @@ function validateScene(value) {
|
|
|
206
215
|
width: value.width,
|
|
207
216
|
height: value.height,
|
|
208
217
|
fps: value.fps,
|
|
218
|
+
...(value.durationClass === undefined ? {} : { durationClass }),
|
|
209
219
|
durationMs: value.durationMs,
|
|
210
220
|
title: value.title,
|
|
211
221
|
commandLabel: value.commandLabel ?? "",
|
|
@@ -524,7 +534,7 @@ function validateBudgetBasis(entry, label) {
|
|
|
524
534
|
MAX_BUNDLE_MEMBER_BYTES,
|
|
525
535
|
`${label}.budgetBasis.observedBytes`,
|
|
526
536
|
);
|
|
527
|
-
const multiplier = integer(entry.budgetBasis.multiplier, 1,
|
|
537
|
+
const multiplier = integer(entry.budgetBasis.multiplier, 1, 128, `${label}.budgetBasis.multiplier`);
|
|
528
538
|
invariant(entry.budgetBasis.rounding === "next-power-of-two", `${label}.budgetBasis.rounding is unsupported`);
|
|
529
539
|
const expected = 2 ** Math.ceil(Math.log2(observedBytes * multiplier));
|
|
530
540
|
invariant(entry.maximumBytes === expected, `${label}.maximumBytes does not match its measured budget basis`);
|
|
@@ -1017,6 +1027,15 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
|
|
|
1017
1027
|
return { manifest, probe, qualification };
|
|
1018
1028
|
}
|
|
1019
1029
|
|
|
1030
|
+
function renditionInputRoots(root, renditions) {
|
|
1031
|
+
return renditions.map((rendition) => ({
|
|
1032
|
+
id: rendition.id, role: rendition.role, captureRoot: rendition.captureRoot,
|
|
1033
|
+
sceneRoot: sha256(readRegular(path.join(root, rendition.files.scene), `${rendition.id} scene`)),
|
|
1034
|
+
transcriptRoot: sha256(readRegular(path.join(root, rendition.files.transcript), `${rendition.id} transcript`)),
|
|
1035
|
+
projectionRoot: sha256(readRegular(path.join(root, rendition.files.projection), `${rendition.id} projection`)),
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1020
1039
|
function finalizeGate(values) {
|
|
1021
1040
|
const adapterOutput = path.resolve(required(values, "--adapter-output"));
|
|
1022
1041
|
const smokeInput = path.resolve(required(values, "--smoke-input"));
|
|
@@ -1109,14 +1128,7 @@ function finalizeGate(values) {
|
|
|
1109
1128
|
renditionSet: {
|
|
1110
1129
|
schema: normalized.renditionSet.schema,
|
|
1111
1130
|
root: sha256(readRegular(path.join(output, "rendition-set.json"), "rendition set")),
|
|
1112
|
-
renditions: normalized.renditionSet.renditions
|
|
1113
|
-
id: rendition.id,
|
|
1114
|
-
role: rendition.role,
|
|
1115
|
-
captureRoot: rendition.captureRoot,
|
|
1116
|
-
sceneRoot: sha256(readRegular(path.join(output, rendition.scene), `${rendition.id} scene`)),
|
|
1117
|
-
transcriptRoot: sha256(readRegular(path.join(output, rendition.transcript), `${rendition.id} transcript`)),
|
|
1118
|
-
projectionRoot: sha256(readRegular(path.join(output, rendition.projection), `${rendition.id} projection`)),
|
|
1119
|
-
})),
|
|
1131
|
+
renditions: renditionInputRoots(output, normalized.renditionSet.renditions),
|
|
1120
1132
|
},
|
|
1121
1133
|
}
|
|
1122
1134
|
: {}),
|
|
@@ -1179,15 +1191,8 @@ function verifyGate(values) {
|
|
|
1179
1191
|
invariant(
|
|
1180
1192
|
qualifiedRenditionSet.schema === normalized.renditionSet.schema
|
|
1181
1193
|
&& qualifiedRenditionSet.root === sha256(readRegular(path.join(bundle, "rendition-set.json"), "rendition set"))
|
|
1182
|
-
&&
|
|
1183
|
-
===
|
|
1184
|
-
id: rendition.id,
|
|
1185
|
-
role: rendition.role,
|
|
1186
|
-
captureRoot: rendition.captureRoot,
|
|
1187
|
-
sceneRoot: sha256(readRegular(path.join(bundle, rendition.scene), `${rendition.id} scene`)),
|
|
1188
|
-
transcriptRoot: sha256(readRegular(path.join(bundle, rendition.transcript), `${rendition.id} transcript`)),
|
|
1189
|
-
projectionRoot: sha256(readRegular(path.join(bundle, rendition.projection), `${rendition.id} projection`)),
|
|
1190
|
-
}))),
|
|
1194
|
+
&& stableJson(qualifiedRenditionSet.renditions)
|
|
1195
|
+
=== stableJson(renditionInputRoots(bundle, normalized.renditionSet.renditions)),
|
|
1191
1196
|
"gate native rendition roots mismatch",
|
|
1192
1197
|
);
|
|
1193
1198
|
}
|
|
@@ -1311,6 +1316,7 @@ export {
|
|
|
1311
1316
|
inspectRendererMedia,
|
|
1312
1317
|
parseAdapterArguments,
|
|
1313
1318
|
qualifyMediaFixture,
|
|
1319
|
+
renditionInputRoots,
|
|
1314
1320
|
prepareSmoke,
|
|
1315
1321
|
runAdapter,
|
|
1316
1322
|
sha256,
|
|
@@ -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
|
+
}
|