mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 08:04:55 +00:00
fix(release): bound beta smoke waits
This commit is contained in:
@@ -14,6 +14,26 @@ interface Options {
|
||||
skipTelegram: boolean;
|
||||
}
|
||||
|
||||
export type RunOptions = {
|
||||
capture?: boolean;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type WorkflowRunInfo = {
|
||||
conclusion: string | null;
|
||||
html_url: string;
|
||||
status: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type PollRunOptions = {
|
||||
pollIntervalMs?: number;
|
||||
readRun?: (repo: string, runId: string) => WorkflowRunInfo;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
timeoutMs?: number;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
function usage(): string {
|
||||
return `Usage: pnpm release:beta-smoke -- --beta beta4 [options]
|
||||
|
||||
@@ -88,15 +108,43 @@ function requireValue(argv: string[], index: number, flag: string): string {
|
||||
}
|
||||
|
||||
const CAPTURE_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
||||
const DEFAULT_COMMAND_TIMEOUT_MS = readPositiveInt(
|
||||
process.env.OPENCLAW_RELEASE_BETA_SMOKE_COMMAND_MS,
|
||||
10 * 60_000,
|
||||
);
|
||||
const TELEGRAM_POLL_INTERVAL_MS = readPositiveInt(
|
||||
process.env.OPENCLAW_RELEASE_BETA_SMOKE_POLL_INTERVAL_MS,
|
||||
30_000,
|
||||
);
|
||||
const TELEGRAM_POLL_TIMEOUT_MS = readPositiveInt(
|
||||
process.env.OPENCLAW_RELEASE_BETA_SMOKE_POLL_TIMEOUT_MS,
|
||||
4 * 60 * 60_000,
|
||||
);
|
||||
|
||||
function run(command: string, args: string[], input?: { capture?: boolean }): string {
|
||||
function readPositiveInt(raw: string | undefined, fallback: number): number {
|
||||
const text = (raw ?? "").trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(text);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
export function run(command: string, args: string[], input?: RunOptions): string {
|
||||
const timeoutMs = input?.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: CAPTURE_MAX_BUFFER_BYTES,
|
||||
stdio: input?.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
const reason = result.status ?? result.signal ?? result.error?.message ?? "unknown";
|
||||
if (result.error || result.status !== 0) {
|
||||
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
||||
const reason =
|
||||
errorCode === "ETIMEDOUT"
|
||||
? `timed out after ${timeoutMs}ms`
|
||||
: (result.status ?? result.signal ?? result.error?.message ?? "unknown");
|
||||
const stderr = result.stderr ? `\n${result.stderr}` : "";
|
||||
throw new Error(`${command} ${args.join(" ")} failed with ${reason}${stderr}`);
|
||||
}
|
||||
@@ -161,7 +209,7 @@ function runParallels(beta: string, model: string): void {
|
||||
"150m",
|
||||
...forwarded.map(shellQuote),
|
||||
].join(" ");
|
||||
run("bash", ["-lc", command]);
|
||||
run("bash", ["-lc", command], { timeoutMs: 155 * 60_000 });
|
||||
}
|
||||
|
||||
function ghJson(repo: string, pathSuffix: string): unknown {
|
||||
@@ -268,14 +316,22 @@ async function dispatchTelegram(options: Options, packageSpec: string): Promise<
|
||||
});
|
||||
}
|
||||
|
||||
async function pollRun(repo: string, runId: string): Promise<void> {
|
||||
export async function pollRun(
|
||||
repo: string,
|
||||
runId: string,
|
||||
options: PollRunOptions = {},
|
||||
): Promise<void> {
|
||||
const started = (options.now ?? Date.now)();
|
||||
const timeoutMs = Math.max(1, options.timeoutMs ?? TELEGRAM_POLL_TIMEOUT_MS);
|
||||
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? TELEGRAM_POLL_INTERVAL_MS);
|
||||
const sleep =
|
||||
options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
||||
const readRun =
|
||||
options.readRun ??
|
||||
((currentRepo: string, currentRunId: string) =>
|
||||
ghJson(currentRepo, `actions/runs/${currentRunId}`) as WorkflowRunInfo);
|
||||
for (;;) {
|
||||
const info = ghJson(repo, `actions/runs/${runId}`) as {
|
||||
conclusion: string | null;
|
||||
html_url: string;
|
||||
status: string;
|
||||
updated_at: string;
|
||||
};
|
||||
const info = readRun(repo, runId);
|
||||
console.log(
|
||||
`Telegram workflow ${runId}: ${info.status}${info.conclusion ? `/${info.conclusion}` : ""} updated=${info.updated_at}`,
|
||||
);
|
||||
@@ -288,7 +344,11 @@ async function pollRun(repo: string, runId: string): Promise<void> {
|
||||
console.log(info.html_url);
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 30_000));
|
||||
const elapsedMs = (options.now ?? Date.now)() - started;
|
||||
if (elapsedMs >= timeoutMs) {
|
||||
throw new Error(`Telegram workflow ${runId} did not complete within ${timeoutMs}ms`);
|
||||
}
|
||||
await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
mergeTelegramProofIntoReleaseBody,
|
||||
parseArgs,
|
||||
parseWorkflowRunIdFromOutput,
|
||||
pollRun,
|
||||
run,
|
||||
selectNewestDispatchedRunId,
|
||||
} from "../../scripts/release-beta-smoke.ts";
|
||||
|
||||
@@ -97,4 +99,66 @@ describe("release-beta-smoke", () => {
|
||||
|
||||
expect(merged.indexOf("actions/runs/123")).toBeLessThan(merged.indexOf("### Assets"));
|
||||
});
|
||||
|
||||
it("bounds child command hangs", () => {
|
||||
expect(() =>
|
||||
run(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
|
||||
capture: true,
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
).toThrow(/timed out after 50ms/u);
|
||||
});
|
||||
|
||||
it("uses a non-ignorable timeout signal for trapped children", () => {
|
||||
expect(() =>
|
||||
run(
|
||||
process.execPath,
|
||||
["-e", "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000)"],
|
||||
{
|
||||
capture: true,
|
||||
timeoutMs: 50,
|
||||
},
|
||||
),
|
||||
).toThrow(/timed out after 50ms/u);
|
||||
});
|
||||
|
||||
it("stops polling Telegram workflow runs after the timeout budget", async () => {
|
||||
let now = 0;
|
||||
const sleeps: number[] = [];
|
||||
|
||||
await expect(
|
||||
pollRun("openclaw/openclaw", "123", {
|
||||
now: () => now,
|
||||
pollIntervalMs: 400,
|
||||
readRun: () => ({
|
||||
conclusion: null,
|
||||
html_url: "https://github.com/openclaw/openclaw/actions/runs/123",
|
||||
status: "queued",
|
||||
updated_at: "2026-05-28T12:00:00Z",
|
||||
}),
|
||||
sleep: async (ms) => {
|
||||
sleeps.push(ms);
|
||||
now += ms;
|
||||
},
|
||||
timeoutMs: 1000,
|
||||
}),
|
||||
).rejects.toThrow("Telegram workflow 123 did not complete within 1000ms");
|
||||
expect(sleeps).toEqual([400, 400, 200]);
|
||||
});
|
||||
|
||||
it("returns when the Telegram workflow succeeds", async () => {
|
||||
await expect(
|
||||
pollRun("openclaw/openclaw", "123", {
|
||||
readRun: () => ({
|
||||
conclusion: "success",
|
||||
html_url: "https://github.com/openclaw/openclaw/actions/runs/123",
|
||||
status: "completed",
|
||||
updated_at: "2026-05-28T12:00:00Z",
|
||||
}),
|
||||
sleep: async () => {
|
||||
throw new Error("sleep should not run after completion");
|
||||
},
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user