openclaw/scripts/ci-testbox-budget.mjs

130 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
const PROFILES = {
check: { runner: "blacksmith-16vcpu-ubuntu-2404", minutes: 240 },
"check-memory": { runner: "blacksmith-32vcpu-ubuntu-2404", minutes: 240 },
arm: { runner: "blacksmith-16vcpu-ubuntu-2404-arm", minutes: 120 },
build: { runner: "blacksmith-16vcpu-ubuntu-2404", minutes: 35 },
windows: { runner: "blacksmith-16vcpu-windows-2025", minutes: 75 },
};
const WINDOWS_RUNNERS = new Set([
"blacksmith-8vcpu-windows-2025",
"blacksmith-16vcpu-windows-2025",
]);
const ADMISSION_AGE_MS = 10 * 60_000;
const CONCURRENT_LEASES = 32;
const MAX_IDLE_MINUTES = 15;
const DEFAULT_STANDARD_MINUTES = 60;
const HIGH_MEMORY_LEASES = 4;
export function assertFreshTestboxAdmission(expiresAt, now = Date.now()) {
const deadline = Number(expiresAt);
if (!Number.isSafeInteger(deadline) || deadline <= now) {
throw new Error("Testbox admission expired after 10 minutes; request a fresh lease.");
}
}
/**
* @param {{ profile: string, id: string, createdAt: string, runner?: string, minutes?: string | number }} request
*/
export function planTestboxAdmission(
{ profile, id, runner, minutes, createdAt },
now = Date.now(),
) {
const policy = PROFILES[profile];
if (!policy || !/^tbx_[a-zA-Z0-9_-]+$/.test(id ?? "")) {
throw new Error("Testbox admission requires a known profile and a tbx_ lease ID.");
}
const selectedRunner = runner || policy.runner;
if (
profile === "windows" ? !WINDOWS_RUNNERS.has(selectedRunner) : selectedRunner !== policy.runner
) {
throw new Error(`Runner ${selectedRunner} is not allowed for the ${profile} Testbox profile.`);
}
const defaultMinutes = profile === "check" ? DEFAULT_STANDARD_MINUTES : policy.minutes;
const timeout = minutes === undefined || minutes === "" ? defaultMinutes : Number(minutes);
if (!Number.isInteger(timeout) || timeout < 1 || timeout > policy.minutes) {
throw new Error(`Testbox runtime must be an integer from 1 to ${policy.minutes} minutes.`);
}
const created = Date.parse(createdAt);
const expiresAt = created + ADMISSION_AGE_MS;
if (!Number.isFinite(created) || created > now) {
throw new Error("Testbox workflow creation time is invalid.");
}
assertFreshTestboxAdmission(expiresAt, now);
// The shared finite namespace is enforced atomically by GitHub concurrency.
// Do not include a workflow, branch, actor, or runner in this group name.
// High-memory work shares four of the global slots, never a separate pool.
const slots = profile === "check-memory" ? HIGH_MEMORY_LEASES : CONCURRENT_LEASES;
const slot = createHash("sha256").update(id).digest().readUInt32BE(0) % slots;
return {
group: `openclaw-testbox-budget-v1-${slot}`,
runner: selectedRunner,
minutes: timeout,
expires_at: expiresAt,
};
}
export function boundedTestboxIdleMinutes(value) {
const minutes = Number(value.trim());
if (!Number.isInteger(minutes) || minutes < 1) {
throw new Error("Testbox provider returned an invalid idle timeout.");
}
return Math.min(minutes, MAX_IDLE_MINUTES);
}
async function main() {
if (process.argv[2] === "configure") {
// The workflow checks queue age before checkout; admitted work keeps its lease.
const path = "/tmp/.testbox/idle_timeout";
const idle = boundedTestboxIdleMinutes(readFileSync(path, "utf8"));
writeFileSync(path, `${idle}\n`);
console.log(
`Testbox idle timeout capped at ${idle} minutes; active SSH work remains protected.`,
);
return;
}
if (process.argv[2] !== "admit") {
throw new Error("Usage: node scripts/ci-testbox-budget.mjs admit|configure");
}
const response = await fetch(
`${process.env.GITHUB_API_URL}/repos/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
{
headers: {
Authorization: `Bearer ${process.env.GH_TOKEN}`,
Accept: "application/vnd.github+json",
},
signal: AbortSignal.timeout(15_000),
},
);
if (!response.ok) {
throw new Error(`Cannot establish Testbox dispatch age: GitHub returned ${response.status}.`);
}
const run = await response.json();
const plan = planTestboxAdmission({
profile: process.env.TESTBOX_PROFILE,
id: process.env.TESTBOX_ID,
runner: process.env.TESTBOX_RUNNER,
minutes: process.env.TESTBOX_MINUTES,
createdAt: run.created_at,
});
appendFileSync(
process.env.GITHUB_OUTPUT,
Object.entries(plan)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
console.log(
`Testbox ${plan.group}: ${plan.runner}, at most ${plan.minutes} minutes, idle ceiling ${MAX_IDLE_MINUTES} minutes.`,
);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((/** @type {unknown} */ error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}