diff --git a/CHANGELOG.md b/CHANGELOG.md index 18fea2977eb1..8dc429407407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - 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. +- Release/CI/E2E: bound Discord smoke API calls in cross-OS release checks so host-side round trips cannot hang on stalled fetches. - 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. diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 765cb26856fb..7a42d26f224a 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -153,6 +153,10 @@ const OMITTED_QA_EXTENSION_PREFIXES = [ ]; export const CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS = 120_000; export const CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS = 10_000; +export const CROSS_OS_DISCORD_FETCH_TIMEOUT_MS = parsePositiveIntegerEnv( + "OPENCLAW_CROSS_OS_DISCORD_FETCH_TIMEOUT_MS", + 10_000, +); export const CROSS_OS_FETCH_BODY_MAX_CHARS = 1024 * 1024; export const CROSS_OS_GATEWAY_STATUS_RPC_TIMEOUT_MS = 30_000; export const CROSS_OS_GATEWAY_STATUS_COMMAND_TIMEOUT_MS = @@ -2561,15 +2565,18 @@ export async function readBoundedCrossOsResponseText( async function waitForDiscordMessage(params) { const deadline = Date.now() + 3 * 60 * 1000; while (Date.now() < deadline) { - const response = await fetch( - `https://discord.com/api/v10/channels/${params.channelId}/messages?limit=20`, - { - headers: { - Authorization: `Bot ${params.token}`, - }, - }, - ); - const text = await readBoundedCrossOsResponseText(response); + let response; + let text; + try { + response = await fetch( + `https://discord.com/api/v10/channels/${params.channelId}/messages?limit=20`, + buildDiscordFetchInit(params.token), + ); + text = await readBoundedCrossOsResponseText(response); + } catch { + await sleep(2_000); + continue; + } if (!response.ok) { await sleep(2_000); continue; @@ -2582,20 +2589,30 @@ async function waitForDiscordMessage(params) { throw new Error(`Discord host-side visibility check timed out for ${params.needle}.`); } +export function buildDiscordFetchInit(token, init = {}) { + return { + ...init, + signal: init.signal ?? AbortSignal.timeout(CROSS_OS_DISCORD_FETCH_TIMEOUT_MS), + headers: { + ...init.headers, + Authorization: `Bot ${token}`, + }, + }; +} + async function postDiscordMessage(params) { const response = await fetch( `https://discord.com/api/v10/channels/${params.channelId}/messages`, - { + buildDiscordFetchInit(params.token, { method: "POST", headers: { - Authorization: `Bot ${params.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ content: params.content, flags: 4096, }), - }, + }), ); const text = await readBoundedCrossOsResponseText(response); if (!response.ok) { @@ -2614,12 +2631,9 @@ async function deleteDiscordMessage(params) { } await fetch( `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}`, - { + buildDiscordFetchInit(params.token, { method: "DELETE", - headers: { - Authorization: `Bot ${params.token}`, - }, - }, + }), ).catch(() => undefined); } diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 4c88b86d2377..ce139e31ca1f 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -20,6 +20,7 @@ import { agentOutputHasExpectedOkMarker, agentTurnUsedEmbeddedFallback, buildCrossOsReleaseSmokePluginAllowlist, + buildDiscordFetchInit, buildPackagedUpgradeUpdateArgs, buildReleaseOnboardArgs, buildWindowsDevUpdateToolchainCheckScript, @@ -41,6 +42,7 @@ import { CROSS_OS_WINDOWS_PACKAGED_UPGRADE_WRAPPER_TIMEOUT_MS, CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS, CROSS_OS_DASHBOARD_SMOKE_TIMEOUT_MS, + CROSS_OS_DISCORD_FETCH_TIMEOUT_MS, CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS, CROSS_OS_COMMAND_HEARTBEAT_SECONDS, isImmutableReleaseRef, @@ -204,9 +206,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => { }); it("rejects malformed cross-OS positive integer environment values", () => { - expect(parsePositiveIntegerEnv("OPENCLAW_CROSS_OS_COMMAND_HEARTBEAT_SECONDS", 60, {})).toBe( - 60, - ); + expect(parsePositiveIntegerEnv("OPENCLAW_CROSS_OS_COMMAND_HEARTBEAT_SECONDS", 60, {})).toBe(60); expect( parsePositiveIntegerEnv("OPENCLAW_CROSS_OS_COMMAND_HEARTBEAT_SECONDS", 60, { OPENCLAW_CROSS_OS_COMMAND_HEARTBEAT_SECONDS: "25", @@ -1215,6 +1215,28 @@ describe("scripts/openclaw-cross-os-release-checks", () => { }); }); + it("bounds Discord API calls with a timeout signal", () => { + expect(CROSS_OS_DISCORD_FETCH_TIMEOUT_MS).toBeGreaterThanOrEqual(10_000); + + const init = buildDiscordFetchInit("discord-token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: "{}", + }); + + expect(init).toMatchObject({ + method: "POST", + body: "{}", + headers: { + Authorization: "Bot discord-token", + "Content-Type": "application/json", + }, + }); + expect(init.signal).toBeInstanceOf(AbortSignal); + }); + it("keeps the dev-update lane for main only", () => { expect(shouldRunMainChannelDevUpdate("main")).toBe(true); expect(shouldRunMainChannelDevUpdate("08753a1d793c040b101c8a26c43445dbbab14995")).toBe(false);