From e0394c2ab43133978b7452e96dea715a6efdbd3b Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Thu, 16 Jul 2026 16:44:30 +0800
Subject: [PATCH] fix(ci): bound native tool downloads (#108685)
* fix(ci): bound native tool downloads
* fix(ci): bound historical SwiftFormat downloads
---------
Co-authored-by: Peter Steinberger
---
.github/workflows/ci.yml | 8 ++-
scripts/install-swift-tools.sh | 7 ++-
scripts/install-xcodegen.sh | 6 ++-
test/scripts/ci-workflow-guards.test.ts | 2 +
test/scripts/install-native-tools.test.ts | 62 +++++++++++++++++++++++
5 files changed, 81 insertions(+), 4 deletions(-)
create mode 100644 test/scripts/install-native-tools.test.ts
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);
+ });
+});