@kungfu-tech/buildchain 3.0.5-alpha.3 → 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.
@@ -38,6 +38,18 @@ function number(value) {
38
38
  return { N: String(value) };
39
39
  }
40
40
 
41
+ function money(value, label) {
42
+ const normalized = String(value ?? "").trim();
43
+ if (!normalized) {
44
+ throw new Error(`${label} is required`);
45
+ }
46
+ const parsed = Number(normalized);
47
+ if (!Number.isFinite(parsed) || parsed < 0) {
48
+ throw new Error(`${label} must be a non-negative finite number`);
49
+ }
50
+ return Math.round(parsed * 100_000_000) / 100_000_000;
51
+ }
52
+
41
53
  function string(value) {
42
54
  return { S: String(value) };
43
55
  }
@@ -56,6 +68,22 @@ export function createWindowsJitCampaignArmPlan(values = {}) {
56
68
  (WINDOWS_EC2_JIT.pricePerHourUsd *
57
69
  WINDOWS_EC2_JIT.maximumInstanceLifetimeMinutes) /
58
70
  60;
71
+ const phaseSpendBaselineUsd = money(
72
+ values.phaseSpendBaselineUsd,
73
+ "phaseSpendBaselineUsd",
74
+ );
75
+ const campaignReservationCeilingUsd =
76
+ reservationUsd * WINDOWS_EC2_JIT.maxAcceptedInstances;
77
+ const campaignSafetyCeilingUsd =
78
+ campaignReservationCeilingUsd +
79
+ reservationUsd * WINDOWS_EC2_JIT.maxConcurrentInstances;
80
+ const remainingPhaseBudgetUsd =
81
+ WINDOWS_EC2_JIT.budgetLimitUsd - phaseSpendBaselineUsd;
82
+ if (campaignSafetyCeilingUsd >= remainingPhaseBudgetUsd) {
83
+ throw new Error(
84
+ "campaign safety envelope must remain below the remaining Windows phase budget",
85
+ );
86
+ }
59
87
  return {
60
88
  schemaVersion: 1,
61
89
  contract: AWS_WINDOWS_JIT_CAMPAIGN_CONTRACT,
@@ -78,6 +106,11 @@ export function createWindowsJitCampaignArmPlan(values = {}) {
78
106
  maxAcceptedInstances: WINDOWS_EC2_JIT.maxAcceptedInstances,
79
107
  reservationUsd,
80
108
  budgetLimitUsd: WINDOWS_EC2_JIT.budgetLimitUsd,
109
+ phaseSpendBaselineUsd,
110
+ remainingPhaseBudgetUsd,
111
+ campaignReservationCeilingUsd,
112
+ campaignSafetyCeilingUsd,
113
+ reservationLimitUsd: remainingPhaseBudgetUsd - reservationUsd,
81
114
  },
82
115
  };
83
116
  }
@@ -97,6 +130,8 @@ export function windowsCampaignArmItems(plan) {
97
130
  state: string("ARMED"),
98
131
  campaign_id: string(plan.campaign.id),
99
132
  source_sha: string(plan.source.sha),
133
+ phase_spend_baseline_usd: number(plan.limits.phaseSpendBaselineUsd),
134
+ budget_limit_usd: number(plan.limits.budgetLimitUsd),
100
135
  armed_at: number(plan.campaign.armedAt),
101
136
  expires_epoch: number(plan.campaign.expiresAt),
102
137
  },
@@ -115,6 +150,17 @@ export function windowsCampaignArmItems(plan) {
115
150
  max_accepted_instances: number(plan.limits.maxAcceptedInstances),
116
151
  reservation_usd: number(plan.limits.reservationUsd),
117
152
  budget_limit_usd: number(plan.limits.budgetLimitUsd),
153
+ phase_spend_baseline_usd: number(plan.limits.phaseSpendBaselineUsd),
154
+ remaining_phase_budget_usd: number(
155
+ plan.limits.remainingPhaseBudgetUsd,
156
+ ),
157
+ campaign_reservation_ceiling_usd: number(
158
+ plan.limits.campaignReservationCeilingUsd,
159
+ ),
160
+ campaign_safety_ceiling_usd: number(
161
+ plan.limits.campaignSafetyCeilingUsd,
162
+ ),
163
+ reservation_limit_usd: number(plan.limits.reservationLimitUsd),
118
164
  armed_at: number(plan.campaign.armedAt),
119
165
  expires_epoch: number(plan.campaign.expiresAt),
120
166
  },
@@ -128,7 +174,6 @@ export function windowsCampaignReservationItems(plan, observedAt) {
128
174
  const now = epoch(observedAt, "observedAt");
129
175
  const runPk = runKey(plan);
130
176
  const campaignPk = `CAMPAIGN#${plan.campaign.id}`;
131
- const maxAccepted = plan.safety.campaignAcceptedInstanceCeiling;
132
177
  const reservation = plan.safety.campaignReservationUsd;
133
178
  return [
134
179
  {
@@ -171,17 +216,13 @@ export function windowsCampaignReservationItems(plan, observedAt) {
171
216
  UpdateExpression:
172
217
  "ADD accepted_instances :one, reserved_usd :reservation SET updated_at = :now",
173
218
  ConditionExpression:
174
- "#state = :armed AND source_sha = :source AND accepted_instances < :max AND reserved_usd <= :remaining",
219
+ "#state = :armed AND source_sha = :source AND accepted_instances < max_accepted_instances AND reserved_usd <= reservation_limit_usd",
175
220
  ExpressionAttributeNames: { "#state": "state" },
176
221
  ExpressionAttributeValues: {
177
222
  ":armed": string("ARMED"),
178
223
  ":source": string(plan.source.sha),
179
224
  ":one": number(1),
180
225
  ":reservation": number(reservation),
181
- ":max": number(maxAccepted),
182
- ":remaining": number(
183
- plan.safety.campaignBudgetLimitUsd - reservation,
184
- ),
185
226
  ":now": number(now),
186
227
  },
187
228
  },
@@ -230,9 +271,7 @@ export function windowsCampaignKillArgs(stateTable, reason, observedAt) {
230
271
  "--expression-attribute-values",
231
272
  JSON.stringify({
232
273
  ":killed": string("KILLED"),
233
- ":reason": string(
234
- exact(reason, /^[a-z0-9][a-z0-9-]{2,63}$/, "reason"),
235
- ),
274
+ ":reason": string(exact(reason, /^[a-z0-9][a-z0-9-]{2,63}$/, "reason")),
236
275
  ":now": number(epoch(observedAt, "observedAt")),
237
276
  }),
238
277
  "--output",
@@ -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(`AWS campaign mutation failed${detail ? `: ${detail}` : ""}`);
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("--confirm-state-table must equal the campaign state table");
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("--kill-switch-topic must be the dedicated Windows JIT SNS ARN");
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: 2,
14
- maxAcceptedInstances: 6,
15
- budgetLimitUsd: 40,
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 readConfigAtSource(sourceSha, configPath, cwd) {
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
+ }