mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(installer): harden release asset resolution (#1258)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
This commit is contained in:
co-authored by
Jwalin Shah
parent
c4d93f6981
commit
073c46d3c6
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "0.0.0-test",
|
||||
"platforms": {
|
||||
"linux-x86_64": {
|
||||
"url": "https://example.invalid/openhuman_0.0.0-test_amd64.AppImage",
|
||||
"signature": ""
|
||||
},
|
||||
"darwin-aarch64": {
|
||||
"url": "https://example.invalid/openhuman_0.0.0-test_aarch64.dmg",
|
||||
"signature": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
-18
@@ -5,6 +5,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Allow tests to source this file without executing the install flow.
|
||||
SOURCE_ONLY=0
|
||||
for _arg in "$@"; do
|
||||
if [[ "$_arg" == "--source-only" ]]; then
|
||||
SOURCE_ONLY=1
|
||||
fi
|
||||
done
|
||||
|
||||
INSTALLER_VERSION="1.0.0"
|
||||
REPO="tinyhumansai/openhuman"
|
||||
LATEST_JSON_URL="https://github.com/${REPO}/releases/latest/download/latest.json"
|
||||
@@ -74,6 +82,10 @@ while [[ $# -gt 0 ]]; do
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
--source-only)
|
||||
# handled above before argument parsing loop; skip silently
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
log_err "Unknown option: $1"
|
||||
usage
|
||||
@@ -152,6 +164,58 @@ ASSET_URL=""
|
||||
ASSET_NAME=""
|
||||
ASSET_SHA256=""
|
||||
|
||||
# Resolves an asset URL from a latest.json file for a given OS/arch.
|
||||
# Args: $1 = path to latest.json, $2 = os (linux|darwin|windows), $3 = arch (x86_64|aarch64)
|
||||
# Stdout: the URL on success.
|
||||
# Exit code: 0 on success; 2 on parse error (with diagnostic on stderr); 3 on missing platform.
|
||||
resolve_asset_url() {
|
||||
local json_path="$1" os="$2" arch="$3"
|
||||
local key="${os}-${arch}"
|
||||
local url
|
||||
url=$(python3 - "$json_path" "$key" <<'PY'
|
||||
import json, sys
|
||||
path, key = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"ERR_PARSE: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
plat = data.get("platforms", {}).get(key)
|
||||
if not plat:
|
||||
available = ", ".join(sorted(data.get("platforms", {}).keys()))
|
||||
print(f"ERR_PLATFORM: {key} not in [{available}]", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
url = plat.get("url")
|
||||
if not url:
|
||||
print(f"ERR_URL: no url field for {key}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(url)
|
||||
PY
|
||||
)
|
||||
local rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
return $rc
|
||||
fi
|
||||
printf '%s\n' "$url"
|
||||
}
|
||||
|
||||
# Retries an HTTP HEAD on the asset URL, fails loudly with the URL.
|
||||
verify_asset_reachable() {
|
||||
local url="$1" max_attempts=5 delay=2
|
||||
for i in $(seq 1 $max_attempts); do
|
||||
if curl -fsSI --max-time 10 "$url" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [[ $i -lt $max_attempts ]]; then
|
||||
sleep "$delay"
|
||||
delay=$((delay * 2))
|
||||
fi
|
||||
done
|
||||
echo "ERR_UNREACHABLE: $url not reachable after $max_attempts attempts" >&2
|
||||
return 4
|
||||
}
|
||||
|
||||
resolve_from_latest_json() {
|
||||
if ! curl -fsSL "${LATEST_JSON_URL}" -o "${LATEST_JSON_PATH}"; then
|
||||
return 1
|
||||
@@ -162,27 +226,28 @@ resolve_from_latest_json() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
parsed="$(python3 - "${LATEST_JSON_PATH}" "${PLATFORM_KEY}" <<'PY'
|
||||
import json, sys
|
||||
path, key = sys.argv[1], sys.argv[2]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
version = data.get("version", "")
|
||||
platforms = data.get("platforms", {})
|
||||
entry = platforms.get(key) or platforms.get(f"{key}-appimage") or platforms.get(f"{key}-app")
|
||||
url = ""
|
||||
if isinstance(entry, dict):
|
||||
url = entry.get("url", "")
|
||||
print(version)
|
||||
print(url)
|
||||
PY
|
||||
)" || return 1
|
||||
local url
|
||||
url=$(resolve_asset_url "${LATEST_JSON_PATH}" "${OS}" "${ARCH}") || {
|
||||
local rc=$?
|
||||
if [[ $rc -eq 3 ]]; then
|
||||
log_warn "Platform ${OS}-${ARCH} not found in latest.json. Resolved URL will be empty — check if a Linux build has been published."
|
||||
log_warn "$(cat "${LATEST_JSON_PATH}" | python3 -c 'import json,sys; d=json.load(sys.stdin); print("Available platforms: " + ", ".join(sorted(d.get("platforms",{}).keys())))' 2>/dev/null || true)"
|
||||
else
|
||||
log_warn "Failed to parse latest.json (exit $rc)."
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
LATEST_VERSION="$(echo "${parsed}" | sed -n '1p')"
|
||||
ASSET_URL="$(echo "${parsed}" | sed -n '2p')"
|
||||
ASSET_URL="$url"
|
||||
ASSET_NAME="$(basename "${ASSET_URL}")"
|
||||
|
||||
# Extract version from latest.json
|
||||
LATEST_VERSION="$(python3 -c "
|
||||
import json, sys
|
||||
with open('${LATEST_JSON_PATH}') as f: d = json.load(f)
|
||||
print(d.get('version', ''))
|
||||
" 2>/dev/null || true)"
|
||||
|
||||
[ -n "${ASSET_URL}" ]
|
||||
}
|
||||
|
||||
@@ -293,6 +358,10 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${SOURCE_ONLY}" == "1" ]]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
if resolve_from_latest_json; then
|
||||
log_ok "Resolved latest release via latest.json (${LATEST_VERSION})"
|
||||
else
|
||||
@@ -338,6 +407,13 @@ if [ -z "${ASSET_URL}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
echo "DRY RUN: verify asset reachable ${ASSET_URL}"
|
||||
elif ! verify_asset_reachable "${ASSET_URL}"; then
|
||||
log_err "Asset URL is not reachable for ${OS}/${ARCH}: ${ASSET_URL}"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
DOWNLOAD_PATH="${TMP_DIR}/${ASSET_NAME}"
|
||||
log_info "Downloading ${ASSET_NAME}"
|
||||
if [ "${DRY_RUN}" = true ]; then
|
||||
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/test_install.sh — smoke-tests the install.sh resolver in isolation.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# Use a fixture latest.json that mirrors what the real release publishes.
|
||||
FIXTURE="$REPO_ROOT/scripts/fixtures/latest.json"
|
||||
|
||||
# The resolver function should be sourced, not invoked end-to-end (no curl).
|
||||
if ! source "$REPO_ROOT/scripts/install.sh" --source-only 2>/dev/null; then
|
||||
echo "FAIL: scripts/install.sh does not support --source-only mode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolved=$(resolve_asset_url "$FIXTURE" "linux" "x86_64")
|
||||
expected="https://example.invalid/openhuman_0.0.0-test_amd64.AppImage"
|
||||
if [[ "$resolved" != "$expected" ]]; then
|
||||
echo "FAIL: expected $expected, got $resolved"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Also test a missing platform produces exit code 3.
|
||||
set +e
|
||||
resolve_asset_url "$FIXTURE" "linux" "aarch64" >/dev/null 2>&1
|
||||
missing_platform_rc=$?
|
||||
set -e
|
||||
if [[ "$missing_platform_rc" -ne 3 ]]; then
|
||||
echo "FAIL: expected exit code 3 for missing platform linux-aarch64, got $missing_platform_rc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS"
|
||||
Reference in New Issue
Block a user