mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
782d52c8b8
Blacksmith's image tracks an older runner-images snapshot. Measured on a leased box 2026-08-16, its toolcache holds Node 20.20.0, 22.22.0 and 24.13.0 while this repo's engines floor is >=22.22.3 and >=24.15.0 -- short by three and two patches. Every candidate is rejected, so all 306 of 306 sampled jobs fell through to a nodejs.org download. GitHub-hosted runners carry 24.19.0 and resolve from the toolcache in about a second, which is why only Blacksmith pays. Normally that download is 2.6s (p99 3.3s), but ~46 jobs fetch the same 50 MB simultaneously and the mirror throttles: three of 53 sampled runs had setup-node medians of 44-93s with maxes to 139s, and because every job pays at once it lands whole on the wall -- those runs went ~210s to 325s. Keep the payload in the Actions cache, which Blacksmith serves from its colocated backend. Measured on Blacksmith: cold 1605ms, warm 77ms. Restores are prefix-keyed and the save carries the resolved patch. An exact key would be worse than nothing: cache entries are immutable and an exact hit suppresses the post-job save, so a floating `24.x` key would pin the first Node it ever saw and, once the floor advanced past it, every job would restore the rejected payload and re-download forever. Keying the save on the installed version lets a newer resolve publish a new entry that later prefix restores pick up. A rejected payload is pruned before the replacement installs, because the entry is saved wholesale and a leftover would ride along in every future save. Windows keeps its existing path. Proven on Blacksmith across cold, warm, stale and truncated-binary cases; both guards are mutation-checked. This stays useful even if Blacksmith refreshes their image: the floor moves independently of the snapshot, so the gap recurs. The image refresh is still the better fix and is worth asking them for.
253 lines
9.1 KiB
Bash
253 lines
9.1 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
openclaw_node_version_matches() {
|
|
local actual="$1"
|
|
local requested="$2"
|
|
if [[ -z "$requested" ]]; then
|
|
return 0
|
|
fi
|
|
case "$requested" in
|
|
*x)
|
|
[[ "${actual%%.*}" == "${requested%%.*}" ]] || return 1
|
|
if [[ "${requested%%.*}" == "22" ]]; then
|
|
openclaw_node_version_at_least "$actual" "22.22.3"
|
|
elif [[ "${requested%%.*}" == "24" ]]; then
|
|
openclaw_node_version_at_least "$actual" "24.15.0"
|
|
elif [[ "${requested%%.*}" == "25" ]]; then
|
|
openclaw_node_version_at_least "$actual" "25.9.0"
|
|
fi
|
|
;;
|
|
*.*.*)
|
|
[[ "$actual" == "$requested" ]]
|
|
;;
|
|
*.*)
|
|
[[ "$actual" == "$requested".* ]]
|
|
;;
|
|
*)
|
|
[[ "${actual%%.*}" == "$requested" ]]
|
|
;;
|
|
esac
|
|
}
|
|
|
|
openclaw_node_version_at_least() {
|
|
local actual="$1"
|
|
local minimum="$2"
|
|
local actual_major actual_minor actual_patch minimum_major minimum_minor minimum_patch
|
|
IFS=. read -r actual_major actual_minor actual_patch <<< "$actual"
|
|
IFS=. read -r minimum_major minimum_minor minimum_patch <<< "$minimum"
|
|
actual_minor="${actual_minor:-0}"
|
|
actual_patch="${actual_patch:-0}"
|
|
minimum_minor="${minimum_minor:-0}"
|
|
minimum_patch="${minimum_patch:-0}"
|
|
|
|
if (( actual_major != minimum_major )); then
|
|
(( actual_major > minimum_major ))
|
|
return
|
|
fi
|
|
if (( actual_minor != minimum_minor )); then
|
|
(( actual_minor > minimum_minor ))
|
|
return
|
|
fi
|
|
(( actual_patch >= minimum_patch ))
|
|
}
|
|
|
|
openclaw_active_node_version() {
|
|
node -p 'process.versions.node' 2>/dev/null || true
|
|
}
|
|
|
|
openclaw_prepend_node_bin() {
|
|
local node_bin_dir="$1"
|
|
local github_path_dir="${2:-$node_bin_dir}"
|
|
local shell_node_bin_dir="$node_bin_dir"
|
|
if command -v cygpath >/dev/null 2>&1; then
|
|
shell_node_bin_dir="$(cygpath -u "$node_bin_dir" 2>/dev/null || printf '%s' "$node_bin_dir")"
|
|
fi
|
|
export PATH="$shell_node_bin_dir:$PATH"
|
|
if [[ -n "${GITHUB_PATH:-}" ]]; then
|
|
local github_node_bin_dir="$github_path_dir"
|
|
if [[ $# -lt 2 ]] && command -v cygpath >/dev/null 2>&1; then
|
|
github_node_bin_dir="$shell_node_bin_dir"
|
|
github_node_bin_dir="$(cygpath -w "$shell_node_bin_dir" 2>/dev/null || printf '%s' "$shell_node_bin_dir")"
|
|
fi
|
|
echo "$github_node_bin_dir" >> "$GITHUB_PATH"
|
|
fi
|
|
hash -r
|
|
}
|
|
|
|
openclaw_find_toolcache_node() {
|
|
local requested_node="$1"
|
|
local roots=()
|
|
local root
|
|
# The repo-owned cached toolchain comes first: on runners whose image toolcache
|
|
# predates our engines floor it is the only candidate that can satisfy the
|
|
# request without a network round trip.
|
|
for root in \
|
|
"${OPENCLAW_NODE_TOOLCHAIN_ROOT:-}" \
|
|
"${RUNNER_TOOL_CACHE:-}" \
|
|
"${AGENT_TOOLSDIRECTORY:-}" \
|
|
"${ACTIONS_RUNNER_TOOL_CACHE:-}" \
|
|
"${OPENCLAW_CONTAINER_TOOL_CACHE:-/__t}" \
|
|
"/opt/hostedtoolcache" \
|
|
"/home/runner/_work/_tool" \
|
|
"/Users/runner/hostedtoolcache" \
|
|
"/c/hostedtoolcache/windows"
|
|
do
|
|
if [[ ! -d "$root" && "$root" == *\\* ]] && command -v cygpath >/dev/null 2>&1; then
|
|
root="$(cygpath -u "$root" 2>/dev/null || printf '%s' "$root")"
|
|
fi
|
|
if [[ -d "$root/node" ]]; then
|
|
roots+=("$root/node")
|
|
elif [[ "$(basename "$root")" == "node" && -d "$root" ]]; then
|
|
roots+=("$root")
|
|
fi
|
|
done
|
|
|
|
local node_root candidate candidate_version
|
|
for node_root in ${roots[@]+"${roots[@]}"}; do
|
|
while IFS= read -r candidate; do
|
|
candidate_version="$("$candidate" -p 'process.versions.node' 2>/dev/null || true)"
|
|
if openclaw_node_version_matches "$candidate_version" "$requested_node"; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
done < <(find "$node_root" \( -name node -o -name node.exe \) -type f 2>/dev/null | sort -r)
|
|
done
|
|
return 1
|
|
}
|
|
|
|
openclaw_resolve_node_download_version() {
|
|
local requested_node="$1"
|
|
if [[ "$requested_node" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
[[ "$requested_node" == v* ]] && printf '%s\n' "$requested_node" || printf 'v%s\n' "$requested_node"
|
|
return 0
|
|
fi
|
|
|
|
local prefix="${requested_node#v}"
|
|
prefix="${prefix%%[xX]*}"
|
|
prefix="v${prefix}"
|
|
[[ "$prefix" == *. ]] || prefix="${prefix}."
|
|
curl -fsSL --connect-timeout 10 --max-time 120 --retry 2 --retry-delay 2 \
|
|
https://nodejs.org/dist/index.json |
|
|
OPENCLAW_NODE_PREFIX="$prefix" python3 -c 'import json, os, sys
|
|
prefix = os.environ["OPENCLAW_NODE_PREFIX"]
|
|
for item in json.load(sys.stdin):
|
|
version = item.get("version", "")
|
|
if version.startswith(prefix):
|
|
print(version)
|
|
break
|
|
'
|
|
}
|
|
|
|
openclaw_node_download_platform() {
|
|
local os_name arch_name
|
|
os_name="$(uname -s)"
|
|
arch_name="$(uname -m)"
|
|
case "$os_name:$arch_name" in
|
|
Linux:x86_64) printf 'linux-x64\n' ;;
|
|
Linux:aarch64 | Linux:arm64) printf 'linux-arm64\n' ;;
|
|
Darwin:x86_64) printf 'darwin-x64\n' ;;
|
|
Darwin:arm64) printf 'darwin-arm64\n' ;;
|
|
MINGW*:x86_64 | MSYS*:x86_64 | CYGWIN*:x86_64 | MINGW*:AMD64 | MSYS*:AMD64 | CYGWIN*:AMD64)
|
|
printf 'win-x64\n'
|
|
;;
|
|
MINGW*:aarch64 | MINGW*:arm64 | MSYS*:aarch64 | MSYS*:arm64 | CYGWIN*:aarch64 | CYGWIN*:arm64) printf 'win-arm64\n' ;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
openclaw_download_node() {
|
|
local requested_node="$1"
|
|
local version platform archive_url archive_path install_root temp_root
|
|
version="$(openclaw_resolve_node_download_version "$requested_node")"
|
|
platform="$(openclaw_node_download_platform)" || return 1
|
|
temp_root="${RUNNER_TEMP:-/tmp}"
|
|
if command -v cygpath >/dev/null 2>&1; then
|
|
temp_root="$(cygpath -u "$temp_root" 2>/dev/null || printf '%s\n' "$temp_root")"
|
|
fi
|
|
install_root="${temp_root}/openclaw-node-${version}-${platform}"
|
|
# Land inside the cached root when one is configured so the post-job save
|
|
# publishes this download for the next run instead of repeating it. Clear the
|
|
# root first: reaching here means any restored payload was rejected, and
|
|
# keeping it would ride along in every future save and grow without bound.
|
|
if [[ -n "${OPENCLAW_NODE_TOOLCHAIN_ROOT:-}" ]]; then
|
|
rm -rf "${OPENCLAW_NODE_TOOLCHAIN_ROOT:?}"
|
|
install_root="${OPENCLAW_NODE_TOOLCHAIN_ROOT}/${version}-${platform}"
|
|
fi
|
|
if [[ "$platform" == win-* ]]; then
|
|
local ps_archive_path ps_install_root ps_bin_dir node_bin_dir
|
|
archive_path="${temp_root}/node-${version}-${platform}.zip"
|
|
archive_url="https://nodejs.org/dist/${version}/node-${version}-${platform}.zip"
|
|
rm -rf "$install_root"
|
|
mkdir -p "$install_root"
|
|
echo "Downloading Node ${version} from ${archive_url}"
|
|
curl -fsSL --connect-timeout 10 --max-time 120 --retry 2 --retry-delay 2 \
|
|
-o "$archive_path" "$archive_url"
|
|
ps_archive_path="$archive_path"
|
|
ps_install_root="$install_root"
|
|
if command -v cygpath >/dev/null 2>&1; then
|
|
ps_archive_path="$(cygpath -w "$archive_path")"
|
|
ps_install_root="$(cygpath -w "$install_root")"
|
|
fi
|
|
ps_bin_dir="$ps_install_root\\node-${version}-${platform}"
|
|
node_bin_dir="$install_root/node-${version}-${platform}"
|
|
if command -v pwsh >/dev/null 2>&1; then
|
|
pwsh -NoLogo -NoProfile -Command "Expand-Archive -LiteralPath '${ps_archive_path}' -DestinationPath '${ps_install_root}' -Force"
|
|
openclaw_prepend_node_bin "$node_bin_dir" "$ps_bin_dir"
|
|
elif command -v powershell.exe >/dev/null 2>&1; then
|
|
powershell.exe -NoLogo -NoProfile -Command "Expand-Archive -LiteralPath '${ps_archive_path}' -DestinationPath '${ps_install_root}' -Force"
|
|
openclaw_prepend_node_bin "$node_bin_dir" "$ps_bin_dir"
|
|
else
|
|
unzip -q "$archive_path" -d "$install_root"
|
|
openclaw_prepend_node_bin "$node_bin_dir"
|
|
fi
|
|
else
|
|
archive_url="https://nodejs.org/dist/${version}/node-${version}-${platform}.tar.xz"
|
|
archive_path="${temp_root}/node-${version}-${platform}.tar.xz"
|
|
mkdir -p "$install_root"
|
|
echo "Downloading Node ${version} from ${archive_url}"
|
|
rm -f "$archive_path"
|
|
if ! curl -fsSL --connect-timeout 10 --max-time 120 --retry 2 --retry-delay 2 \
|
|
-o "$archive_path" "$archive_url"; then
|
|
rm -f "$archive_path"
|
|
return 1
|
|
fi
|
|
if ! tar -xJf "$archive_path" -C "$install_root" --strip-components=1; then
|
|
rm -f "$archive_path"
|
|
return 1
|
|
fi
|
|
rm -f "$archive_path"
|
|
openclaw_prepend_node_bin "$install_root/bin"
|
|
fi
|
|
}
|
|
|
|
openclaw_ensure_node() {
|
|
local requested_node="${1:-}"
|
|
requested_node="${requested_node#v}"
|
|
if [[ -z "$requested_node" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local active_node_version node_bin
|
|
active_node_version="$(openclaw_active_node_version)"
|
|
if openclaw_node_version_matches "$active_node_version" "$requested_node"; then
|
|
echo "Using active Node ${active_node_version} at $(command -v node)"
|
|
return 0
|
|
fi
|
|
|
|
node_bin="$(openclaw_find_toolcache_node "$requested_node" || true)"
|
|
if [[ -n "$node_bin" ]]; then
|
|
echo "Using Node $("$node_bin" -p 'process.versions.node') from $node_bin"
|
|
openclaw_prepend_node_bin "$(dirname "$node_bin")"
|
|
else
|
|
openclaw_download_node "$requested_node" || true
|
|
fi
|
|
|
|
active_node_version="$(openclaw_active_node_version)"
|
|
if ! openclaw_node_version_matches "$active_node_version" "$requested_node"; then
|
|
echo "::error::Expected Node '${requested_node}', but active node is '${active_node_version:-missing}' at $(command -v node || true)"
|
|
return 1
|
|
fi
|
|
}
|