@kungfu-tech/buildchain 3.0.5-alpha.4 → 3.0.5-alpha.5
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 +4 -4
- package/dist/site/buildchain-site.json +12 -12
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +9 -9
- package/dist/site/page-registry.json +6 -6
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/docs/aws-us-elastic-runner-burst-plane.md +18 -9
- package/docs/node-api-reference.md +21 -21
- package/docs/release-governance.md +32 -0
- package/package.json +1 -1
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/aws-windows-jit-campaign-core.mjs +48 -9
- package/scripts/aws-windows-jit-campaign.mjs +23 -5
- package/scripts/aws-windows-jit-core.mjs +3 -3
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/workflow-call-contract.mjs +133 -0
|
@@ -30,7 +30,9 @@ function aws(plan, serviceArgs) {
|
|
|
30
30
|
const detail = String(result.stderr || result.stdout || "")
|
|
31
31
|
.trim()
|
|
32
32
|
.slice(0, 2000);
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error(
|
|
34
|
+
`AWS campaign mutation failed${detail ? `: ${detail}` : ""}`,
|
|
35
|
+
);
|
|
34
36
|
}
|
|
35
37
|
return result.stdout ? JSON.parse(result.stdout) : {};
|
|
36
38
|
}
|
|
@@ -43,10 +45,11 @@ function armPlan() {
|
|
|
43
45
|
region: arg("region", "us-east-1"),
|
|
44
46
|
armedAt: arg("armed-at", new Date().toISOString()),
|
|
45
47
|
expiresAt: arg("expires-at"),
|
|
48
|
+
phaseSpendBaselineUsd: arg("phase-spend-baseline-usd"),
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function confirm(plan) {
|
|
52
|
+
function confirm(plan, { phaseSpendBaseline = true } = {}) {
|
|
50
53
|
if (arg("confirm-campaign-id") !== plan.campaign.id) {
|
|
51
54
|
throw new Error("--confirm-campaign-id must equal the campaign id");
|
|
52
55
|
}
|
|
@@ -54,7 +57,19 @@ function confirm(plan) {
|
|
|
54
57
|
throw new Error("--confirm-source-sha must equal the exact source SHA");
|
|
55
58
|
}
|
|
56
59
|
if (arg("confirm-state-table") !== plan.aws.stateTable) {
|
|
57
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
"--confirm-state-table must equal the campaign state table",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
phaseSpendBaseline &&
|
|
66
|
+
(!arg("confirm-phase-spend-baseline-usd").trim() ||
|
|
67
|
+
Number(arg("confirm-phase-spend-baseline-usd")) !==
|
|
68
|
+
plan.limits.phaseSpendBaselineUsd)
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"--confirm-phase-spend-baseline-usd must equal the phase spend baseline",
|
|
72
|
+
);
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
|
|
@@ -65,7 +80,9 @@ function killSwitchTopic() {
|
|
|
65
80
|
topic,
|
|
66
81
|
)
|
|
67
82
|
) {
|
|
68
|
-
throw new Error(
|
|
83
|
+
throw new Error(
|
|
84
|
+
"--kill-switch-topic must be the dedicated Windows JIT SNS ARN",
|
|
85
|
+
);
|
|
69
86
|
}
|
|
70
87
|
if (arg("confirm-kill-switch-topic") !== topic) {
|
|
71
88
|
throw new Error(
|
|
@@ -113,8 +130,9 @@ export function main() {
|
|
|
113
130
|
region: arg("region", "us-east-1"),
|
|
114
131
|
armedAt: now.toISOString(),
|
|
115
132
|
expiresAt: new Date(now.getTime() + 1000).toISOString(),
|
|
133
|
+
phaseSpendBaselineUsd: 0,
|
|
116
134
|
});
|
|
117
|
-
confirm(plan);
|
|
135
|
+
confirm(plan, { phaseSpendBaseline: false });
|
|
118
136
|
const topic = killSwitchTopic();
|
|
119
137
|
aws(
|
|
120
138
|
plan,
|
|
@@ -10,9 +10,9 @@ export const WINDOWS_EC2_JIT = Object.freeze({
|
|
|
10
10
|
instanceType: "c7i.4xlarge",
|
|
11
11
|
pricePerHourUsd: 1.45,
|
|
12
12
|
maximumInstanceLifetimeMinutes: 180,
|
|
13
|
-
maxConcurrentInstances:
|
|
14
|
-
maxAcceptedInstances:
|
|
15
|
-
budgetLimitUsd:
|
|
13
|
+
maxConcurrentInstances: 1,
|
|
14
|
+
maxAcceptedInstances: 5,
|
|
15
|
+
budgetLimitUsd: 80,
|
|
16
16
|
minimumSmokeJobs: 1,
|
|
17
17
|
minimumFullJobs: 3,
|
|
18
18
|
maximumCleanupLatencySeconds: 900,
|
|
@@ -49,7 +49,30 @@ function assertSourcePath(value) {
|
|
|
49
49
|
return sourcePath;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function hasCommit(sourceSha, cwd) {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync("git", ["cat-file", "-e", `${sourceSha}^{commit}`], {
|
|
55
|
+
cwd,
|
|
56
|
+
stdio: "ignore",
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readConfigAtSource(sourceSha, configPath, cwd) {
|
|
65
|
+
if (!hasCommit(sourceSha, cwd)) {
|
|
66
|
+
try {
|
|
67
|
+
execFileSync("git", ["fetch", "--no-tags", "--depth=1", "origin", sourceSha], {
|
|
68
|
+
cwd,
|
|
69
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const detail = String(error.stderr || error.message || "unknown git fetch failure").trim();
|
|
73
|
+
throw new Error(`exact release source ${sourceSha} is unavailable from origin: ${detail}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
53
76
|
const bytes = execFileSync("git", ["show", `${sourceSha}:${configPath}`], {
|
|
54
77
|
cwd,
|
|
55
78
|
encoding: "utf8",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { evaluateWorkflowCallContract } from "../packages/core/workflow-call-contract.js";
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
return `usage: node scripts/workflow-call-contract.mjs check \\
|
|
10
|
+
--caller-workflow <path> --job <id> --caller-repository <owner/repo> \\
|
|
11
|
+
--callee-root <checkout> --callee-workflow <path> --callee-repository <owner/repo> \\
|
|
12
|
+
--trusted-event <event[:type]> [--trusted-event ...] \\
|
|
13
|
+
[--expected-contract-root sha256:...] [--allow-dirty] [--output <path>]`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const options = { trustedEvents: [] };
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const arg = argv[index];
|
|
20
|
+
if (arg === "--allow-dirty") {
|
|
21
|
+
options.allowDirty = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!arg.startsWith("--") || !argv[index + 1])
|
|
25
|
+
throw new Error(`invalid argument: ${arg}`);
|
|
26
|
+
const value = argv[index + 1];
|
|
27
|
+
index += 1;
|
|
28
|
+
if (arg === "--trusted-event") options.trustedEvents.push(value);
|
|
29
|
+
else
|
|
30
|
+
options[
|
|
31
|
+
arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
32
|
+
] = value;
|
|
33
|
+
}
|
|
34
|
+
return options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(root, ...args) {
|
|
38
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
}).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function required(options, names) {
|
|
44
|
+
const missing = names.filter((name) => !options[name]);
|
|
45
|
+
if (missing.length)
|
|
46
|
+
throw new Error(`missing required options: ${missing.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function checkWorkflowCall(options) {
|
|
50
|
+
const callerRoot = path.resolve(options.callerRoot || process.cwd());
|
|
51
|
+
const calleeRoot = path.resolve(options.calleeRoot);
|
|
52
|
+
const callerStatus = git(
|
|
53
|
+
callerRoot,
|
|
54
|
+
"status",
|
|
55
|
+
"--porcelain",
|
|
56
|
+
"--untracked-files=no",
|
|
57
|
+
);
|
|
58
|
+
if (callerStatus && !options.allowDirty) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"caller checkout is dirty; use --allow-dirty only for local diagnostic validation",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const calleeStatus = git(
|
|
64
|
+
calleeRoot,
|
|
65
|
+
"status",
|
|
66
|
+
"--porcelain",
|
|
67
|
+
"--untracked-files=no",
|
|
68
|
+
);
|
|
69
|
+
if (calleeStatus) {
|
|
70
|
+
throw new Error("callee checkout is dirty; exact pinned-ref bytes are required");
|
|
71
|
+
}
|
|
72
|
+
const callerSha = git(callerRoot, "rev-parse", "HEAD");
|
|
73
|
+
const callerTree = git(callerRoot, "rev-parse", "HEAD^{tree}");
|
|
74
|
+
const calleeSha = git(calleeRoot, "rev-parse", "HEAD");
|
|
75
|
+
const report = evaluateWorkflowCallContract({
|
|
76
|
+
callerText: fs.readFileSync(
|
|
77
|
+
path.join(callerRoot, options.callerWorkflow),
|
|
78
|
+
"utf8",
|
|
79
|
+
),
|
|
80
|
+
calleeText: fs.readFileSync(
|
|
81
|
+
path.join(calleeRoot, options.calleeWorkflow),
|
|
82
|
+
"utf8",
|
|
83
|
+
),
|
|
84
|
+
callerRepository: options.callerRepository,
|
|
85
|
+
callerWorkflowPath: options.callerWorkflow,
|
|
86
|
+
callerSha,
|
|
87
|
+
callerTree,
|
|
88
|
+
callerSourceState: callerStatus ? "diagnostic-dirty" : "clean",
|
|
89
|
+
calleeRepository: options.calleeRepository,
|
|
90
|
+
calleeWorkflowPath: options.calleeWorkflow,
|
|
91
|
+
calleeSha,
|
|
92
|
+
jobId: options.job,
|
|
93
|
+
trustedEventClasses: options.trustedEvents,
|
|
94
|
+
expectedContractRoot: options.expectedContractRoot || "",
|
|
95
|
+
});
|
|
96
|
+
if (options.output) {
|
|
97
|
+
const output = path.resolve(callerRoot, options.output);
|
|
98
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
99
|
+
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
return report;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function main(argv = process.argv.slice(2)) {
|
|
105
|
+
const command = argv.shift();
|
|
106
|
+
if (command !== "check") throw new Error(usage());
|
|
107
|
+
const options = parseArgs(argv);
|
|
108
|
+
required(options, [
|
|
109
|
+
"callerWorkflow",
|
|
110
|
+
"job",
|
|
111
|
+
"callerRepository",
|
|
112
|
+
"calleeRoot",
|
|
113
|
+
"calleeWorkflow",
|
|
114
|
+
"calleeRepository",
|
|
115
|
+
]);
|
|
116
|
+
if (!options.trustedEvents.length)
|
|
117
|
+
throw new Error("at least one --trusted-event is required");
|
|
118
|
+
const report = checkWorkflowCall(options);
|
|
119
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
120
|
+
if (!report.ok) process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
process.argv[1] &&
|
|
125
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
126
|
+
) {
|
|
127
|
+
try {
|
|
128
|
+
main();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`workflow call contract: ${error.message}`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
}
|