From ccf55a43cd5b895669f46c2a7391068cf8e50494 Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Sat, 18 Jul 2026 16:29:34 +0800 Subject: [PATCH] fix(ci): bound npm resume GitHub lookups (#109982) --- scripts/openclaw-npm-resume-run.d.mts | 16 +++++++++ scripts/openclaw-npm-resume-run.mjs | 12 +++++-- test/scripts/openclaw-npm-resume-run.test.ts | 35 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/scripts/openclaw-npm-resume-run.d.mts b/scripts/openclaw-npm-resume-run.d.mts index 6dba05ca9129..ec0fc3077f39 100644 --- a/scripts/openclaw-npm-resume-run.d.mts +++ b/scripts/openclaw-npm-resume-run.d.mts @@ -43,6 +43,22 @@ export function validateOpenClawNpmResumeRun( input: OpenClawNpmResumeValidationInput, ): OpenClawNpmResumeIdentity; +export function runOpenClawNpmResumeGh( + args: string[], + params?: { + execFileSyncImpl?: ( + command: string, + args: string[], + options: { + encoding: "utf8"; + killSignal: "SIGKILL"; + maxBuffer: number; + timeout: number; + }, + ) => string; + }, +): string; + export function resolveOpenClawNpmResumeRun(options: { repo: string; runId: string; diff --git a/scripts/openclaw-npm-resume-run.mjs b/scripts/openclaw-npm-resume-run.mjs index f5c87ca905ef..5e13a241d47b 100644 --- a/scripts/openclaw-npm-resume-run.mjs +++ b/scripts/openclaw-npm-resume-run.mjs @@ -5,6 +5,9 @@ import { fileURLToPath } from "node:url"; const SHA_PATTERN = /^[a-f0-9]{40}$/u; const RELEASE_PUBLISH_REF_PATTERN = /^release-publish\/([a-f0-9]{12})-([1-9][0-9]*)$/u; const WORKFLOW_PATH = ".github/workflows/openclaw-npm-release.yml"; +// Resume checks run during release recovery, so keep enough headroom for GitHub +// latency while preventing one stalled read from consuming the workflow budget. +const GH_COMMAND_TIMEOUT_MS = 60_000; function fail(message) { throw new Error(message); @@ -100,14 +103,17 @@ export function validateOpenClawNpmResumeRun({ }; } -function defaultRunGh(args) { - return execFileSync("gh", args, { +export function runOpenClawNpmResumeGh(args, params = {}) { + const execFileSyncImpl = params.execFileSyncImpl ?? execFileSync; + return execFileSyncImpl("gh", args, { encoding: "utf8", + killSignal: "SIGKILL", maxBuffer: 32 * 1024 * 1024, + timeout: GH_COMMAND_TIMEOUT_MS, }); } -export function resolveOpenClawNpmResumeRun({ repo, runId, runGh = defaultRunGh }) { +export function resolveOpenClawNpmResumeRun({ repo, runId, runGh = runOpenClawNpmResumeGh }) { if (!/^[1-9][0-9]*$/u.test(runId)) { fail("OpenClaw npm resume run id must be a positive integer."); } diff --git a/test/scripts/openclaw-npm-resume-run.test.ts b/test/scripts/openclaw-npm-resume-run.test.ts index 98805e66c93b..a032ff80fbae 100644 --- a/test/scripts/openclaw-npm-resume-run.test.ts +++ b/test/scripts/openclaw-npm-resume-run.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { resolveOpenClawNpmResumeRun, + runOpenClawNpmResumeGh, validateOpenClawNpmResumeRun, } from "../../scripts/openclaw-npm-resume-run.mjs"; import type { OpenClawNpmResumeValidationInput } from "../../scripts/openclaw-npm-resume-run.mjs"; @@ -36,6 +37,40 @@ function fixture( } describe("openclaw npm resume run identity", () => { + it("bounds each GitHub lookup", () => { + const execFileSyncImpl = vi.fn(() => "result"); + + expect( + runOpenClawNpmResumeGh(["api", "repos/openclaw/openclaw/actions/runs/456"], { + execFileSyncImpl, + }), + ).toBe("result"); + expect(execFileSyncImpl).toHaveBeenCalledWith( + "gh", + ["api", "repos/openclaw/openclaw/actions/runs/456"], + { + encoding: "utf8", + killSignal: "SIGKILL", + maxBuffer: 32 * 1024 * 1024, + timeout: 60_000, + }, + ); + }); + + it("propagates GitHub lookup timeouts", () => { + const timeoutError = Object.assign(new Error("spawnSync gh ETIMEDOUT"), { + code: "ETIMEDOUT", + }); + + expect(() => + runOpenClawNpmResumeGh(["api", "repos/openclaw/openclaw/actions/runs/456"], { + execFileSyncImpl: () => { + throw timeoutError; + }, + }), + ).toThrow(timeoutError); + }); + it("accepts a successful run bound to a signed main-reachable tooling tag", () => { expect(validateOpenClawNpmResumeRun(fixture())).toEqual({ tagObjectSha: TAG_OBJECT_SHA,