diff --git a/scripts/ci-docker-pull-retry.sh b/scripts/ci-docker-pull-retry.sh index 0b15071f28f9..5038a1798556 100644 --- a/scripts/ci-docker-pull-retry.sh +++ b/scripts/ci-docker-pull-retry.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="${BASH_SOURCE[0]%/*}" +# shellcheck source=scripts/lib/host-timeout.sh +source "$SCRIPT_DIR/lib/host-timeout.sh" + if [[ "$#" -ne 1 || -z "${1// }" ]]; then echo "usage: $0 " >&2 exit 2 @@ -28,14 +32,15 @@ fi last_status=1 run_docker_pull() { - if ! command -v timeout >/dev/null 2>&1; then - echo "timeout command not found; cannot bound Docker pull after ${timeout_seconds}s" >&2 + local timeout_bin + if ! timeout_bin="$(openclaw_host_timeout_bin)"; then + echo "timeout or gtimeout command not found; cannot bound Docker pull after ${timeout_seconds}s" >&2 return 127 fi - if timeout --kill-after=1s 1s true >/dev/null 2>&1; then - timeout --kill-after=30s "${timeout_seconds}s" docker pull "$image" + if "$timeout_bin" --kill-after=1s 1s true >/dev/null 2>&1; then + "$timeout_bin" --kill-after=30s "${timeout_seconds}s" docker pull "$image" else - timeout "${timeout_seconds}s" docker pull "$image" + "$timeout_bin" "${timeout_seconds}s" docker pull "$image" fi } diff --git a/test/scripts/ci-docker-pull-retry.test.ts b/test/scripts/ci-docker-pull-retry.test.ts index 1a6054cbcd2c..821c846e0a9f 100644 --- a/test/scripts/ci-docker-pull-retry.test.ts +++ b/test/scripts/ci-docker-pull-retry.test.ts @@ -115,7 +115,42 @@ describe("scripts/ci-docker-pull-retry.sh", () => { ); }); - it("fails fast when timeout is unavailable", () => { + it("uses gtimeout when timeout is unavailable", () => { + const binDir = makeTempBin("openclaw-ci-docker-pull-gtimeout-"); + const timeoutArgsPath = path.join(binDir, "gtimeout-args.txt"); + const dockerArgsPath = path.join(binDir, "docker-args.txt"); + + writeExecutable( + path.join(binDir, "gtimeout"), + [ + "#!/bin/bash", + "set -euo pipefail", + 'if [ "${1:-}" = "--kill-after=1s" ]; then exit 0; fi', + `printf "%s\\n" "gtimeout:$*" >${JSON.stringify(timeoutArgsPath)}`, + 'while [ "$#" -gt 0 ] && [ "$1" != "docker" ]; do shift; done', + 'exec "$@"', + "", + ].join("\n"), + ); + writeExecutable( + path.join(binDir, "docker"), + ["#!/bin/sh", "set -eu", `printf "%s\\n" "$*" >${JSON.stringify(dockerArgsPath)}`, ""].join( + "\n", + ), + ); + + const result = runPullHelper(binDir); + + expect(result.status).toBe(0); + expect(execFileSync("cat", [timeoutArgsPath], { encoding: "utf8" }).trim()).toBe( + "gtimeout:--kill-after=30s 42s docker pull registry.example/openclaw:test", + ); + expect(execFileSync("cat", [dockerArgsPath], { encoding: "utf8" }).trim()).toBe( + "pull registry.example/openclaw:test", + ); + }); + + it("fails fast when timeout and gtimeout are unavailable", () => { const binDir = makeTempBin("openclaw-ci-docker-pull-no-timeout-"); const dockerArgsPath = path.join(binDir, "docker-args.txt"); @@ -130,7 +165,7 @@ describe("scripts/ci-docker-pull-retry.sh", () => { expect(result.status).toBe(127); expect(result.stderr).toContain( - "timeout command not found; cannot bound Docker pull after 42s", + "timeout or gtimeout command not found; cannot bound Docker pull after 42s", ); expect(existsSync(dockerArgsPath)).toBe(false); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index a5d93343ca17..feb670e3ec18 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -676,13 +676,15 @@ describe("package artifact reuse", () => { expect(pullHelper).toContain( 'retry_delay_seconds="${OPENCLAW_DOCKER_PULL_RETRY_DELAY_SECONDS:-5}"', ); + expect(pullHelper).toContain('source "$SCRIPT_DIR/lib/host-timeout.sh"'); + expect(pullHelper).toContain("openclaw_host_timeout_bin"); + expect(pullHelper).toContain('"$timeout_bin" --kill-after=1s 1s true'); expect(pullHelper).toContain( - 'timeout --kill-after=30s "${timeout_seconds}s" docker pull "$image"', + '"$timeout_bin" --kill-after=30s "${timeout_seconds}s" docker pull "$image"', ); - expect(pullHelper).toContain("timeout --kill-after=1s 1s true >/dev/null 2>&1"); - expect(pullHelper).toContain('timeout "${timeout_seconds}s" docker pull "$image"'); + expect(pullHelper).toContain('"$timeout_bin" "${timeout_seconds}s" docker pull "$image"'); expect(pullHelper).toContain( - "timeout command not found; cannot bound Docker pull after ${timeout_seconds}s", + "timeout or gtimeout command not found; cannot bound Docker pull after ${timeout_seconds}s", ); expect(dockerE2ePlanAction.match(/bash scripts\/ci-docker-pull-retry\.sh/g)?.length).toBe(2); expect(dockerE2ePlanAction).not.toContain('docker pull "${OPENCLAW_DOCKER_E2E_');