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,