mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
130 lines
4.9 KiB
JavaScript
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;
|
|
});
|
|
}
|