Fix ARM64 musl node download discovery

This commit is contained in:
mm644706215
2026-03-21 11:54:20 +08:00
parent f3ce598550
commit 46b0b0f571
7 changed files with 162 additions and 17 deletions

View File

@@ -4,6 +4,9 @@ module("luci.controller.openclaw", package.seeall)
local nixio_fs = require "nixio.fs" local nixio_fs = require "nixio.fs"
local util = require "luci.util" local util = require "luci.util"
local oc_paths = require "openclaw.paths" local oc_paths = require "openclaw.paths"
local GITHUB_REPO = "hotwa/luci-app-openclaw"
local GITHUB_RELEASES_URL = "https://github.com/" .. GITHUB_REPO .. "/releases"
local GITHUB_API_RELEASES_URL = "https://api.github.com/repos/" .. GITHUB_REPO .. "/releases"
local function get_install_root_from_uci() local function get_install_root_from_uci()
return require("luci.model.uci").cursor():get("openclaw", "main", "install_root") return require("luci.model.uci").cursor():get("openclaw", "main", "install_root")
@@ -448,7 +451,7 @@ function action_check_update()
local plugin_has_update = false local plugin_has_update = false
-- 使用 GitHub API 获取最新 release (tag + body) -- 使用 GitHub API 获取最新 release (tag + body)
local gh_json = sys.exec("curl -sf --connect-timeout 5 --max-time 10 'https://api.github.com/repos/10000ge10000/luci-app-openclaw/releases/latest' 2>/dev/null") local gh_json = sys.exec("curl -sf --connect-timeout 5 --max-time 10 '" .. GITHUB_API_RELEASES_URL .. "/latest' 2>/dev/null")
if gh_json and gh_json ~= "" then if gh_json and gh_json ~= "" then
-- 提取 tag_name -- 提取 tag_name
local tag = gh_json:match('"tag_name"%s*:%s*"([^"]+)"') local tag = gh_json:match('"tag_name"%s*:%s*"([^"]+)"')
@@ -551,7 +554,7 @@ function action_plugin_upgrade()
sys.exec("rm -f /tmp/openclaw-plugin-upgrade.log /tmp/openclaw-plugin-upgrade.pid /tmp/openclaw-plugin-upgrade.exit") sys.exec("rm -f /tmp/openclaw-plugin-upgrade.log /tmp/openclaw-plugin-upgrade.pid /tmp/openclaw-plugin-upgrade.exit")
-- 后台执行: 下载 .run 并执行安装 -- 后台执行: 下载 .run 并执行安装
local run_url = "https://github.com/10000ge10000/luci-app-openclaw/releases/download/v" .. version .. "/luci-app-openclaw_" .. version .. ".run" local run_url = GITHUB_RELEASES_URL .. "/download/v" .. version .. "/luci-app-openclaw_" .. version .. ".run"
-- 使用 curl 下载 (-L 跟随重定向), 然后 sh 执行 -- 使用 curl 下载 (-L 跟随重定向), 然后 sh 执行
sys.exec(string.format( sys.exec(string.format(
"( echo '正在下载插件 v%s ...' > /tmp/openclaw-plugin-upgrade.log; " .. "( echo '正在下载插件 v%s ...' > /tmp/openclaw-plugin-upgrade.log; " ..

View File

@@ -251,9 +251,13 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'if(log.indexOf("当前已安装在")>=0||log.indexOf("检测目录不存在")>=0||log.indexOf("安装根目录")>=0){' html[#html+1] = 'if(log.indexOf("当前已安装在")>=0||log.indexOf("检测目录不存在")>=0||log.indexOf("安装根目录")>=0){'
html[#html+1] = 'reasons.push("📁 <b>安装目录配置有误</b> — 请选择已挂载的绝对路径;如果当前环境已经安装在其他目录,请先卸载后再切换。");' html[#html+1] = 'reasons.push("📁 <b>安装目录配置有误</b> — 请选择已挂载的绝对路径;如果当前环境已经安装在其他目录,请先卸载后再切换。");'
html[#html+1] = '}' html[#html+1] = '}'
-- 网络问题 -- ARM64 musl 专属下载问题
html[#html+1] = 'if(ll.indexOf("could not resolve")>=0||ll.indexOf("connection timed out")>=0||ll.indexOf("curl")>=0&&ll.indexOf("fail")>=0||ll.indexOf("wget")>=0&&ll.indexOf("fail")>=0||ll.indexOf("所有镜像均下载失败")>=0){' html[#html+1] = 'if(ll.indexOf("arm64 musl")>=0&&(ll.indexOf("release api")>=0||ll.indexOf("node-bins")>=0||ll.indexOf("未找到兼容的 arm64 musl node.js 资产")>=0||ll.indexOf("无法获取 arm64 musl node.js 发布元数据")>=0)){'
html[#html+1] = 'reasons.push("🌐 <b>网络连接失败</b> — 无法下载 Node.js。请检查路由器是否能访问外网。<br/>&nbsp;&nbsp;💡 解决: 检查 DNS 设置和网络连接,或手动指定镜像: <code>NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup</code>");' html[#html+1] = 'reasons.push("🧩 <b>ARM64 musl Node.js 资产不可用</b> — 当前设备依赖仓库发布的 <code>node-bins</code> 资产,而不是通用 Node 镜像。<br/>&nbsp;&nbsp;💡 解决: 检查 <code>hotwa/luci-app-openclaw</code> 的 <code>node-bins</code> release 是否存在满足 <code>>=22.16.0</code> 的 <code>linux-arm64-musl</code> 资产,并确认路由器可访问 GitHub API 与 release 页面。");'
html[#html+1] = '}'
-- 通用网络问题
html[#html+1] = 'if((ll.indexOf("could not resolve")>=0||ll.indexOf("connection timed out")>=0||ll.indexOf("curl")>=0&&ll.indexOf("fail")>=0||ll.indexOf("wget")>=0&&ll.indexOf("fail")>=0||ll.indexOf("所有镜像均下载失败")>=0)&&!(ll.indexOf("arm64 musl")>=0&&(ll.indexOf("release api")>=0||ll.indexOf("node-bins")>=0||ll.indexOf("未找到兼容的 arm64 musl node.js 资产")>=0||ll.indexOf("无法获取 arm64 musl node.js 发布元数据")>=0))){'
html[#html+1] = 'reasons.push("🌐 <b>网络连接失败</b> — 无法下载 Node.js。请检查路由器是否能访问外网。<br/>&nbsp;&nbsp;💡 解决: 检查 DNS 设置、网络连通性以及当前默认下载源是否可访问;如有需要,可通过环境变量覆盖下载源后重试。");'
html[#html+1] = '}' html[#html+1] = '}'
-- 磁盘空间 -- 磁盘空间
html[#html+1] = 'if(ll.indexOf("no space")>=0||ll.indexOf("disk full")>=0||ll.indexOf("enospc")>=0){' html[#html+1] = 'if(ll.indexOf("no space")>=0||ll.indexOf("disk full")>=0||ll.indexOf("enospc")>=0){'
@@ -331,7 +335,7 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}' html[#html+1] = '}'
html[#html+1] = 'act.innerHTML=notesHtml' html[#html+1] = 'act.innerHTML=notesHtml'
html[#html+1] = '+\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocPluginUpgrade()" id="btn-plugin-upgrade">⬆️ 升级插件 v\'+r.plugin_latest+\'</button>\'' html[#html+1] = '+\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocPluginUpgrade()" id="btn-plugin-upgrade">⬆️ 升级插件 v\'+r.plugin_latest+\'</button>\''
html[#html+1] = '+\' <a href="https://github.com/10000ge10000/luci-app-openclaw/releases/latest" target="_blank" rel="noopener" class="btn cbi-button cbi-button-action" style="text-decoration:none;">📥 手动下载</a>\';' html[#html+1] = '+\' <a href="https://github.com/hotwa/luci-app-openclaw/releases/latest" target="_blank" rel="noopener" class="btn cbi-button cbi-button-action" style="text-decoration:none;">📥 手动下载</a>\';'
html[#html+1] = '}' html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}' html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
html[#html+1] = '});}' html[#html+1] = '});}'
@@ -614,8 +618,8 @@ guide.cfgvalue = function()
html[#html+1] = '<a href="https://space.bilibili.com/59438380" target="_blank" rel="noopener" style="color:#00a1d6;font-weight:bold;text-decoration:none;">' html[#html+1] = '<a href="https://space.bilibili.com/59438380" target="_blank" rel="noopener" style="color:#00a1d6;font-weight:bold;text-decoration:none;">'
html[#html+1] = '🔗 space.bilibili.com/59438380</a>' html[#html+1] = '🔗 space.bilibili.com/59438380</a>'
html[#html+1] = '<span style="margin-left:16px;color:#888;">GitHub 项目:</span>' html[#html+1] = '<span style="margin-left:16px;color:#888;">GitHub 项目:</span>'
html[#html+1] = '<a href="https://github.com/10000ge10000/luci-app-openclaw" target="_blank" rel="noopener" style="color:#24292f;font-weight:bold;text-decoration:none;">' html[#html+1] = '<a href="https://github.com/hotwa/luci-app-openclaw" target="_blank" rel="noopener" style="color:#24292f;font-weight:bold;text-decoration:none;">'
html[#html+1] = '🐙 10000ge10000/luci-app-openclaw</a></div></div>' html[#html+1] = '🐙 hotwa/luci-app-openclaw</a></div></div>'
return table.concat(html, "\n") return table.concat(html, "\n")
end end

View File

@@ -53,13 +53,14 @@ NODE_BIN="${NODE_BASE}/bin/node"
NPM_BIN="${NODE_BASE}/bin/npm" NPM_BIN="${NODE_BASE}/bin/npm"
PNPM_BIN="${OC_GLOBAL}/bin/pnpm" PNPM_BIN="${OC_GLOBAL}/bin/pnpm"
# Node.js 官方镜像 + musl 非官方构建 # Node.js 下载源
# Node.js 官方镜像 + musl 非官方构建 OPENCLAW_GITHUB_REPO="${OPENCLAW_GITHUB_REPO:-hotwa/luci-app-openclaw}"
NODE_MIRROR="${NODE_MIRROR:-https://nodejs.org/dist}" NODE_MIRROR="${NODE_MIRROR:-https://nodejs.org/dist}"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node" NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release" NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
# 项目自托管 ARM64 musl Node.js (unofficial-builds 仅提供 x64 musl) NODE_SELF_HOST="${NODE_SELF_HOST:-https://github.com/${OPENCLAW_GITHUB_REPO}/releases/download/node-bins}"
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins" NODE_RELEASE_API="${NODE_RELEASE_API:-https://api.github.com/repos/${OPENCLAW_GITHUB_REPO}/releases/tags/node-bins}"
NODE_RELEASE_PAGE="${NODE_RELEASE_PAGE:-https://github.com/${OPENCLAW_GITHUB_REPO}/releases/tag/node-bins}"
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH" export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH"
export NODE_ICU_DATA="${NODE_BASE}/share/icu" export NODE_ICU_DATA="${NODE_BASE}/share/icu"
@@ -68,6 +69,48 @@ log_info() { echo " [✓] $1"; }
log_warn() { echo " [!] $1"; } log_warn() { echo " [!] $1"; }
log_error() { echo " [✗] $1"; } log_error() { echo " [✗] $1"; }
download_url_to_file() {
local url="$1"
local output="$2"
curl -fSL --connect-timeout 15 --max-time 300 -o "$output" "$url" 2>/dev/null || \
wget -q --timeout=15 -O "$output" "$url" 2>/dev/null
}
resolve_arm64_musl_node_url() {
local node_ver="$1"
local release_json="/tmp/openclaw-node-bins-release.json"
local asset_url=""
echo " 正在从 ${NODE_RELEASE_API} 获取 ARM64 musl 版本列表..." >&2
if ! download_url_to_file "$NODE_RELEASE_API" "$release_json"; then
rm -f "$release_json"
log_error "无法获取 ARM64 musl Node.js 发布元数据" >&2
echo " 仓库: ${OPENCLAW_GITHUB_REPO}" >&2
echo " 最低要求: v${node_ver}" >&2
echo " Release API: ${NODE_RELEASE_API}" >&2
echo " Release 页面: ${NODE_RELEASE_PAGE}" >&2
echo " 请先发布 node-bins 中满足要求的 ARM64 musl 资产,或通过环境变量覆盖下载源" >&2
return 1
fi
asset_url=$(oc_select_node_release_asset_url "$release_json" "linux-arm64" "$node_ver" || true)
rm -f "$release_json"
if [ -n "$asset_url" ]; then
printf '%s\n' "$asset_url"
return 0
fi
log_error "未找到兼容的 ARM64 musl Node.js 资产" >&2
echo " 仓库: ${OPENCLAW_GITHUB_REPO}" >&2
echo " 最低要求: v${node_ver}" >&2
echo " Release API: ${NODE_RELEASE_API}" >&2
echo " Release 页面: ${NODE_RELEASE_PAGE}" >&2
echo " 请先发布 node-bins 中满足要求的 ARM64 musl 资产,或通过环境变量覆盖下载源" >&2
return 1
}
# 安全创建目录 (会在 _oc_fix_opt 修复 /opt 后使用标准路径) # 安全创建目录 (会在 _oc_fix_opt 修复 /opt 后使用标准路径)
ensure_mkdir() { ensure_mkdir() {
local target="$1" local target="$1"
@@ -169,17 +212,16 @@ download_node() {
local mirror_list="" local mirror_list=""
local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz" local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
local glibc_tarball="node-v${node_ver}-${node_arch}.tar.xz" local glibc_tarball="node-v${node_ver}-${node_arch}.tar.xz"
local arm64_musl_url=""
if [ "$libc_type" = "musl" ]; then if [ "$libc_type" = "musl" ]; then
echo "" echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ===" echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ==="
if [ "$node_arch" = "linux-arm64" ]; then if [ "$node_arch" = "linux-arm64" ]; then
# ARM64 musl: unofficial-builds 不提供,从项目自托管下载 # ARM64 musl: 从当前维护仓库的 node-bins release 动态选择兼容资产
# 1) 项目自托管 ARM64 musl 构建 (当前版本) arm64_musl_url=$(resolve_arm64_musl_node_url "$node_ver") || exit 1
mirror_list="${NODE_SELF_HOST}/${musl_tarball}" mirror_list="${arm64_musl_url}"
# 2) unofficial-builds (留作将来可能支持)
mirror_list="$mirror_list ${NODE_MUSL_MIRROR}/v${node_ver}/${musl_tarball}"
else else
# x64 musl: unofficial-builds 提供 # x64 musl: unofficial-builds 提供
# 1) unofficial-builds # 1) unofficial-builds

View File

@@ -66,3 +66,53 @@ oc_read_node_version() {
version=$("$node_bin" --version 2>/dev/null) || return 1 version=$("$node_bin" --version 2>/dev/null) || return 1
oc_normalize_node_version "$version" oc_normalize_node_version "$version"
} }
oc_select_node_release_asset_url() {
local json_file="${1:-}"
local node_arch="${2:-}"
local min_version="${3:-}"
local asset_regex=""
local urls=""
local best_url=""
local best_version=""
local url=""
local asset_name=""
local asset_version=""
[ -n "$json_file" ] || return 1
[ -f "$json_file" ] || return 1
oc_normalize_node_version "$min_version" >/dev/null || return 1
case "$node_arch" in
linux-arm64|linux-x64) ;;
*) return 1 ;;
esac
asset_regex="node-v[0-9][0-9.]*-${node_arch}-musl\\.tar\\.xz"
urls=$(
tr -d '\r' < "$json_file" | \
grep -o "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*${asset_regex}\"" | \
sed 's/^"browser_download_url"[[:space:]]*:[[:space:]]*"//; s/"$//'
) || true
[ -n "$urls" ] || return 1
while IFS= read -r url; do
[ -n "$url" ] || continue
asset_name="${url##*/}"
asset_version=$(printf '%s\n' "$asset_name" | sed -n "s/^node-v\\([0-9][0-9.]*\\)-${node_arch}-musl\\.tar\\.xz$/\\1/p")
asset_version=$(oc_normalize_node_version "$asset_version" 2>/dev/null || true)
[ -n "$asset_version" ] || continue
if oc_node_version_ge "$asset_version" "$min_version"; then
if [ -z "$best_version" ] || oc_node_version_ge "$asset_version" "$best_version"; then
best_version="$asset_version"
best_url="$url"
fi
fi
done <<EOF
$urls
EOF
[ -n "$best_url" ] || return 1
printf '%s\n' "$best_url"
}

View File

@@ -14,6 +14,7 @@
# 使用 patchelf 修改 node 二进制的 ELF interpreter 和 rpath # 使用 patchelf 修改 node 二进制的 ELF interpreter 和 rpath
# 使其使用系统 musl 动态链接器和相对 rpath可从任意安装根目录直接运行。 # 使其使用系统 musl 动态链接器和相对 rpath可从任意安装根目录直接运行。
# 这样 process.execPath 返回正确的 node 路径,子进程 fork 也能正常工作。 # 这样 process.execPath 返回正确的 node 路径,子进程 fork 也能正常工作。
# 注意: 运行时把 NODE_VERSION 视为最低要求,不要求发布资产文件名与最低要求版本完全相同。
# ============================================================================ # ============================================================================
set -e set -e

View File

@@ -10,6 +10,8 @@ MAKEFILE="$REPO_ROOT/Makefile"
BUILD_IPK="$REPO_ROOT/scripts/build_ipk.sh" BUILD_IPK="$REPO_ROOT/scripts/build_ipk.sh"
BUILD_RUN="$REPO_ROOT/scripts/build_run.sh" BUILD_RUN="$REPO_ROOT/scripts/build_run.sh"
ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env" ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env"
CONTROLLER_SCRIPT="$REPO_ROOT/luasrc/controller/openclaw.lua"
BASIC_LUA="$REPO_ROOT/luasrc/model/cbi/openclaw/basic.lua"
fail() { fail() {
echo "FAIL: $1" >&2 echo "FAIL: $1" >&2
@@ -29,6 +31,14 @@ grep -Fq 'oc_node_version_ge "$installed_ver" "$node_ver"' "$ENV_SCRIPT" || fail
if grep -Fq 'mirror_list="$mirror_list ${NODE_SELF_HOST}/${v1_tarball}"' "$ENV_SCRIPT"; then if grep -Fq 'mirror_list="$mirror_list ${NODE_SELF_HOST}/${v1_tarball}"' "$ENV_SCRIPT"; then
fail "installer should not auto-fallback from V2 to V1 tarball" fail "installer should not auto-fallback from V2 to V1 tarball"
fi fi
grep -Fq 'OPENCLAW_GITHUB_REPO="${OPENCLAW_GITHUB_REPO:-hotwa/luci-app-openclaw}"' "$ENV_SCRIPT" || fail "installer should default to hotwa repo"
grep -Fq 'NODE_SELF_HOST="${NODE_SELF_HOST:-https://github.com/${OPENCLAW_GITHUB_REPO}/releases/download/node-bins}"' "$ENV_SCRIPT" || fail "installer should derive node-bins release URL from hotwa repo"
grep -Fq 'NODE_RELEASE_API="${NODE_RELEASE_API:-https://api.github.com/repos/${OPENCLAW_GITHUB_REPO}/releases/tags/node-bins}"' "$ENV_SCRIPT" || fail "installer should derive node-bins release API from hotwa repo"
grep -Fq 'oc_select_node_release_asset_url' "$ENV_SCRIPT" || fail "installer should dynamically select ARM64 musl asset"
grep -Fq 'arm64_musl_url=$(resolve_arm64_musl_node_url "$node_ver") || exit 1' "$ENV_SCRIPT" || fail "installer should resolve ARM64 musl asset dynamically"
if grep -Fq 'mirror_list="${NODE_SELF_HOST}/${musl_tarball}"' "$ENV_SCRIPT"; then
fail "installer should not hardcode exact ARM64 musl asset path"
fi
grep -Fq 'openclaw-paths.sh' "$MAKEFILE" || fail "package makefile should install path helper" grep -Fq 'openclaw-paths.sh' "$MAKEFILE" || fail "package makefile should install path helper"
grep -Fq 'openclaw-node.sh' "$MAKEFILE" || fail "package makefile should install node helper" grep -Fq 'openclaw-node.sh' "$MAKEFILE" || fail "package makefile should install node helper"
@@ -43,5 +53,13 @@ grep -Fq 'openclaw/paths.lua' "$BUILD_IPK" || fail "ipk builder should package L
grep -Fq 'openclaw-paths.sh' "$BUILD_RUN" || fail "run builder should package path helper" grep -Fq 'openclaw-paths.sh' "$BUILD_RUN" || fail "run builder should package path helper"
grep -Fq 'openclaw-node.sh' "$BUILD_RUN" || fail "run builder should package node helper" grep -Fq 'openclaw-node.sh' "$BUILD_RUN" || fail "run builder should package node helper"
grep -Fq 'openclaw/paths.lua' "$BUILD_RUN" || fail "run builder should package Lua path helper" grep -Fq 'openclaw/paths.lua' "$BUILD_RUN" || fail "run builder should package Lua path helper"
grep -Fq 'local GITHUB_REPO = "hotwa/luci-app-openclaw"' "$CONTROLLER_SCRIPT" || fail "controller should default to hotwa repo"
grep -Fq 'local GITHUB_RELEASES_URL = "https://github.com/" .. GITHUB_REPO .. "/releases"' "$CONTROLLER_SCRIPT" || fail "controller should derive release URLs from hotwa repo"
grep -Fq 'local GITHUB_API_RELEASES_URL = "https://api.github.com/repos/" .. GITHUB_REPO .. "/releases"' "$CONTROLLER_SCRIPT" || fail "controller should derive API URLs from hotwa repo"
grep -Fq "https://github.com/hotwa/luci-app-openclaw/releases/latest" "$BASIC_LUA" || fail "UI should link manual download to hotwa repo"
grep -Fq "ARM64 musl" "$BASIC_LUA" || fail "UI should mention ARM64 musl specific guidance"
if grep -Fq 'NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup' "$BASIC_LUA"; then
fail "UI should not recommend NODE_MIRROR for ARM64 musl node download failures"
fi
echo "ok" echo "ok"

View File

@@ -51,4 +51,31 @@ if oc_read_node_version "$tmpdir/node-bad" >/dev/null 2>&1; then
fail "broken node binary should not be accepted" fail "broken node binary should not be accepted"
fi fi
cat > "$tmpdir/node-bins-release.json" <<'EOF'
{
"tag_name": "node-bins",
"assets": [
{
"name": "node-v22.15.1-linux-arm64-musl.tar.xz",
"browser_download_url": "https://github.com/hotwa/luci-app-openclaw/releases/download/node-bins/node-v22.15.1-linux-arm64-musl.tar.xz"
},
{
"name": "node-v23.2.0-linux-arm64-musl.tar.xz",
"browser_download_url": "https://github.com/hotwa/luci-app-openclaw/releases/download/node-bins/node-v23.2.0-linux-arm64-musl.tar.xz"
},
{
"name": "node-v22.16.0-linux-x64-musl.tar.xz",
"browser_download_url": "https://github.com/hotwa/luci-app-openclaw/releases/download/node-bins/node-v22.16.0-linux-x64-musl.tar.xz"
}
]
}
EOF
selected_url=$(oc_select_node_release_asset_url "$tmpdir/node-bins-release.json" "linux-arm64" "22.16.0") || fail "select compatible ARM64 musl asset"
[ "$selected_url" = "https://github.com/hotwa/luci-app-openclaw/releases/download/node-bins/node-v23.2.0-linux-arm64-musl.tar.xz" ] || fail "selected asset should be newest compatible ARM64 musl release"
if oc_select_node_release_asset_url "$tmpdir/node-bins-release.json" "linux-arm64" "24.0.0" >/dev/null 2>&1; then
fail "asset selection should fail when no compatible version exists"
fi
echo "ok" echo "ok"