diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50bb8ddcfa6..e6f47b7bb8e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2445,7 +2445,9 @@ jobs: esac swiftformat_archive="$RUNNER_TEMP/swiftformat-$swiftformat_version.zip" swift_tools_dir="$RUNNER_TEMP/openclaw-legacy-swift-tools" - curl --fail --location --silent --show-error --retry 3 \ + curl --fail --location --silent --show-error \ + --connect-timeout 10 --max-time 120 \ + --retry 3 --retry-max-time 120 \ --output "$swiftformat_archive" \ "https://github.com/nicklockwood/SwiftFormat/releases/download/$swiftformat_version/swiftformat.zip" if [[ "$(shasum -a 256 "$swiftformat_archive" | awk '{print $1}')" != "$swiftformat_checksum" ]]; then @@ -2650,7 +2652,9 @@ jobs: esac swiftformat_archive="$RUNNER_TEMP/swiftformat-$swiftformat_version.zip" swift_tools_dir="$RUNNER_TEMP/openclaw-legacy-swift-tools" - curl --fail --location --silent --show-error --retry 3 \ + curl --fail --location --silent --show-error \ + --connect-timeout 10 --max-time 120 \ + --retry 3 --retry-max-time 120 \ --output "$swiftformat_archive" \ "https://github.com/nicklockwood/SwiftFormat/releases/download/$swiftformat_version/swiftformat.zip" if [[ "$(shasum -a 256 "$swiftformat_archive" | awk '{print $1}')" != "$swiftformat_checksum" ]]; then diff --git a/scripts/install-swift-tools.sh b/scripts/install-swift-tools.sh index ad4b2ab8a4d3..5615cc9a7de2 100755 --- a/scripts/install-swift-tools.sh +++ b/scripts/install-swift-tools.sh @@ -18,7 +18,12 @@ install_archive() { local archive="$temp_dir/$name.zip" local extract_dir="$temp_dir/$name" - curl --fail --location --silent --show-error --retry 3 --output "$archive" "$url" + # Bound individual transfers and the retry window. curl resets --max-time for + # each retry, while a started retry can outlive --retry-max-time. + curl --fail --location --silent --show-error \ + --connect-timeout 10 --max-time 120 \ + --retry 3 --retry-max-time 120 \ + --output "$archive" "$url" if [[ "$(shasum -a 256 "$archive" | awk '{print $1}')" != "$checksum" ]]; then echo "$name archive checksum mismatch" >&2 exit 1 diff --git a/scripts/install-xcodegen.sh b/scripts/install-xcodegen.sh index 96229253955a..a6749468c2ba 100755 --- a/scripts/install-xcodegen.sh +++ b/scripts/install-xcodegen.sh @@ -17,7 +17,11 @@ trap 'rm -rf "$temp_dir"' EXIT archive="$temp_dir/xcodegen.zip" extract_dir="$temp_dir/extract" -curl --fail --location --silent --show-error --retry 3 \ +# Bound individual transfers and the retry window. curl resets --max-time for +# each retry, while a started retry can outlive --retry-max-time. +curl --fail --location --silent --show-error \ + --connect-timeout 10 --max-time 120 \ + --retry 3 --retry-max-time 120 \ --output "$archive" \ "https://github.com/yonaskolb/XcodeGen/releases/download/$xcodegen_version/xcodegen.zip" if [[ "$(shasum -a 256 "$archive" | awk '{print $1}')" != "$xcodegen_checksum" ]]; then diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 66c7d4cc5de7..e9c3954176f8 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -2024,6 +2024,8 @@ describe("ci workflow guards", () => { expect(installStep.run).toContain( "https://github.com/nicklockwood/SwiftFormat/releases/download/$swiftformat_version/swiftformat.zip", ); + expect(installStep.run).toContain("--connect-timeout 10 --max-time 120"); + expect(installStep.run).toContain("--retry 3 --retry-max-time 120"); expect(installStep.run).toContain( 'swiftformat_checksum="b990400779aceb7d7020796eb9ba814d4480543f671d38fc0ff48cb72f04c584"', ); diff --git a/test/scripts/install-native-tools.test.ts b/test/scripts/install-native-tools.test.ts new file mode 100644 index 000000000000..a10aca80cd7c --- /dev/null +++ b/test/scripts/install-native-tools.test.ts @@ -0,0 +1,62 @@ +import { spawnSync } from "node:child_process"; +import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; + +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + +const installers = [ + { + script: "scripts/install-xcodegen.sh", + url: "https://github.com/yonaskolb/XcodeGen/releases/download/2.45.4/xcodegen.zip", + }, + { + script: "scripts/install-swift-tools.sh", + url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.62.1/swiftformat.zip", + }, +] as const; + +function expectOption(args: string[], option: string, value: string): void { + const index = args.indexOf(option); + expect(index, `missing curl option ${option}`).toBeGreaterThanOrEqual(0); + expect(args[index + 1]).toBe(value); +} + +describe.runIf(process.platform !== "win32")("native tool installers", () => { + it.each(installers)("bounds stalled downloads in $script", ({ script, url }) => { + const root = tempDirs.make("openclaw-native-tool-installer-"); + const binDir = path.join(root, "bin"); + const argsPath = path.join(root, "curl-args.txt"); + const curlPath = path.join(binDir, "curl"); + mkdirSync(binDir); + writeFileSync( + curlPath, + [ + "#!/usr/bin/env bash", + 'printf "%s\\n" "$@" >"$OPENCLAW_TEST_CURL_ARGS_PATH"', + "exit 28", + "", + ].join("\n"), + ); + chmodSync(curlPath, 0o755); + + const result = spawnSync("bash", [script, path.join(root, "install")], { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_TEST_CURL_ARGS_PATH: argsPath, + PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, + }, + }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(28); + const args = readFileSync(argsPath, "utf8").trimEnd().split("\n"); + expectOption(args, "--connect-timeout", "10"); + expectOption(args, "--max-time", "120"); + expectOption(args, "--retry", "3"); + expectOption(args, "--retry-max-time", "120"); + expect(args).toContain(url); + }); +});