fix(release): bound candidate GitHub requests

This commit is contained in:
Vincent Koc
2026-06-03 08:19:03 +02:00
parent a462601f05
commit ed4c4afc0f
3 changed files with 86 additions and 10 deletions

View File

@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
- Release/CI/E2E: print heartbeat progress during centralized Docker builds while keeping successful build logs quiet.
- Release/CI/E2E: avoid heartbeat-tail delays in Docker E2E log wrappers while reporting captured log bytes during long runs.
- Release/CI/E2E: keep release user-journey logs and temporary plugin fixtures under per-run scratch roots so parallel runs cannot collide or leak artifacts.
- Release/CI/E2E: bound release candidate GitHub API calls so stalled network requests cannot wedge workflow and artifact polling.
- Control UI: lazy-load the usage view so the initial app bundle stays below the chunk warning threshold.
- Build: keep Baileys optional image backends external so source builds do not warn about missing `jimp` or `sharp`.
- Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.

View File

@@ -12,6 +12,7 @@ const DEFAULT_RELEASE_PROFILE = "beta";
const DEFAULT_NPM_DIST_TAG = "beta";
const DEFAULT_PLUGIN_SCOPE = "all-publishable";
const DEFAULT_TELEGRAM_PROVIDER_MODE = "mock-openai";
const DEFAULT_GITHUB_API_TIMEOUT_MS = 30_000;
function usage() {
return `Usage: pnpm release:candidate -- --tag vYYYY.M.D-beta.N [options]
@@ -182,15 +183,43 @@ function readJson(path, label) {
}
}
async function githubApi(path) {
const token = run("gh", ["auth", "token"], { capture: true }).trim();
const response = await fetch(`https://api.github.com/${path}`, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
function githubApiTimeoutMs() {
const raw = process.env.OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS;
if (!raw) {
return DEFAULT_GITHUB_API_TIMEOUT_MS;
}
const value = Number(raw);
if (!Number.isFinite(value) || value <= 0) {
throw new Error("OPENCLAW_RELEASE_CANDIDATE_GITHUB_API_TIMEOUT_MS must be a positive number");
}
return Math.trunc(value);
}
function githubApiTimedOut(error) {
return (
error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")
);
}
export async function githubApi(path, options = {}) {
const token = options.token ?? run("gh", ["auth", "token"], { capture: true }).trim();
const timeoutMs = options.timeoutMs ?? githubApiTimeoutMs();
let response;
try {
response = await (options.fetchImpl ?? fetch)(`https://api.github.com/${path}`, {
signal: AbortSignal.timeout(timeoutMs),
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
},
});
} catch (error) {
if (githubApiTimedOut(error)) {
throw new Error(`GitHub API ${path} timed out after ${timeoutMs}ms`, { cause: error });
}
throw error;
}
if (!response.ok) {
throw new Error(`GitHub API ${path} failed with ${response.status}: ${await response.text()}`);
}

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
buildPublishCommand,
githubApi,
parseArgs,
parseRunIdFromDispatchOutput,
resolveArtifactName,
@@ -125,4 +126,49 @@ describe("release candidate checklist", () => {
),
).toBe("openclaw-npm-preflight-dba00");
});
it("bounds GitHub API requests with a timeout signal", async () => {
const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
expect(init?.headers).toMatchObject({
Accept: "application/vnd.github+json",
Authorization: "Bearer test-token",
"X-GitHub-Api-Version": "2022-11-28",
});
return {
ok: true,
json: async () => ({ workflow_runs: [] }),
};
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs", {
fetchImpl,
timeoutMs: 1234,
token: "test-token",
}),
).resolves.toEqual({ workflow_runs: [] });
expect(fetchImpl).toHaveBeenCalledWith(
"https://api.github.com/repos/openclaw/openclaw/actions/runs",
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
it("includes the GitHub API path when a request times out", async () => {
const fetchImpl = vi.fn(async () => {
throw new DOMException("request timed out", "TimeoutError");
});
await expect(
githubApi("repos/openclaw/openclaw/actions/runs/123/jobs", {
fetchImpl,
timeoutMs: 5,
token: "test-token",
}),
).rejects.toThrow(
"GitHub API repos/openclaw/openclaw/actions/runs/123/jobs timed out after 5ms",
);
});
});