Builds were dominated by re-fetching the ImmortalWrt 25.12 SDK tarball
(~300 MB) every run, and a stalled downloads.immortalwrt.org transfer wedged
the apk job for 40+ min (plain `wget -q`, no timeout — same class as the
elfutils hang).
- New ci/fetch-sdk.sh (runner-side): cache -> our durable `sdk-cache` release
mirror -> upstream with a stall-kill (curl --speed-limit 64K --speed-time 60
--max-time 1800) + 3 retries + zstd-magic/size validation; seeds the mirror
best-effort (github.token, non-fatal) so cold runs never touch upstream again.
A 40-min hang is now impossible; the in-container fallback wget also gets
--timeout=60 --tries=3.
- actions/cache@v3.3.2 (last release on the OLD cache API that Gitea act_runner
implements; v4/v3.4.x use the new GitHub cache service) for: SDK tarball, SDK
dl/ sources (hash of package Makefiles; PKG_HASH re-verified so a stale cache
can't leak a wrong source), Go mod+build (go.sum), npm node_modules
(package-lock.json) with build-shaterd.sh --fast, apt archives, built usign.
Degrades safely if the cache server is off — the SDK mirror is independent.
- concurrency group release-${github.ref} cancel-in-progress so a re-dispatch
cancels the stale run instead of piling up (tags stay isolated).
Signing (usign/apk), both keys, per-arch publish, manual triggers, LOCALMIRROR
and the scoped 4-package collection are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
6.3 KiB
Bash
142 lines
6.3 KiB
Bash
#!/bin/sh
|
|
# ci/fetch-sdk.sh — fetch an (ImmortalWrt) SDK tarball WITHOUT ever hanging the
|
|
# build on a flaky upstream. Runs on the CI RUNNER (before the nested container),
|
|
# so the result can live in the workspace and be persisted by actions/cache.
|
|
#
|
|
# Usage: ci/fetch-sdk.sh <SDK_URL> <DEST_FILE>
|
|
#
|
|
# Resolution order (first valid wins):
|
|
# 1. DEST_FILE already present & valid -> reuse (actions/cache restored it).
|
|
# 2. Our own durable mirror: a Gitea release asset under tag $MIRROR_TAG
|
|
# (default `sdk-cache`) on THIS repo — fast + reliable, unlike
|
|
# downloads.immortalwrt.org which flakes/stalls mid-transfer.
|
|
# 3. Upstream $SDK_URL, with a stall-kill (curl --speed-limit/--speed-time:
|
|
# abort when <64 KiB/s for 60 s), a hard --max-time cap, and 3 attempts
|
|
# with backoff — a hung download aborts + retries instead of wedging the
|
|
# job for 40+ minutes. On success the tarball is uploaded to the mirror
|
|
# release (best-effort, needs $MIRROR_TOKEN) so the NEXT cold run never
|
|
# touches upstream again.
|
|
#
|
|
# Validation: size >= 10 MiB + zstd magic (28 b5 2f fd) — rejects truncated
|
|
# transfers and HTML error pages before they reach `tar --zstd`.
|
|
#
|
|
# Env:
|
|
# MIRROR_TAG mirror release tag (default: sdk-cache — deliberately does NOT
|
|
# start with `v`, so publishing it can never re-trigger the
|
|
# workflow's `v*` tag filter)
|
|
# MIRROR_TOKEN Gitea API token for seeding the mirror
|
|
# (default: $GITHUB_TOKEN; empty -> fetch works, seeding skipped)
|
|
# SERVER Gitea base URL (default: $GITHUB_SERVER_URL)
|
|
# REPO_SLUG owner/repo (default: $GITHUB_REPOSITORY)
|
|
set -eu
|
|
|
|
SDK_URL="${1:?SDK tarball URL required}"
|
|
DEST="${2:?destination file required}"
|
|
|
|
MIRROR_TAG="${MIRROR_TAG:-sdk-cache}"
|
|
MIRROR_TOKEN="${MIRROR_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
SERVER="${SERVER:-${GITHUB_SERVER_URL:-}}"
|
|
REPO_SLUG="${REPO_SLUG:-${GITHUB_REPOSITORY:-}}"
|
|
ASSET="$(basename "$SDK_URL")"
|
|
mkdir -p "$(dirname "$DEST")"
|
|
|
|
# valid <file> — non-trivial size + zstd magic.
|
|
valid() {
|
|
[ -s "$1" ] || return 1
|
|
[ "$(wc -c < "$1")" -ge 10485760 ] || return 1
|
|
[ "$(od -An -tx1 -N4 "$1" | tr -d ' \n')" = "28b52ffd" ] || return 1
|
|
}
|
|
|
|
# fetch_once <url> <out> — one bounded curl attempt that cannot hang.
|
|
fetch_once() {
|
|
curl -fSL --connect-timeout 20 --speed-limit 65536 --speed-time 60 \
|
|
--max-time 1800 -o "$2.part" "$1" && mv -f "$2.part" "$2"
|
|
}
|
|
|
|
# fetch_retry <url> <out> — 3 attempts with backoff (5 s, 20 s).
|
|
fetch_retry() {
|
|
_n=1
|
|
for _delay in 5 20 END; do
|
|
if fetch_once "$1" "$2"; then return 0; fi
|
|
rm -f "$2.part"
|
|
[ "$_delay" = END ] && break
|
|
echo "[sdk-fetch] attempt $_n failed; retrying in ${_delay}s"
|
|
sleep "$_delay"; _n=$((_n+1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# --- 1) restored cache -------------------------------------------------------
|
|
if valid "$DEST"; then
|
|
echo "[sdk-fetch] cache hit: $DEST ($(wc -c < "$DEST") bytes)"
|
|
exit 0
|
|
fi
|
|
rm -f "$DEST"
|
|
|
|
# --- 2) our durable mirror (Gitea release asset) -----------------------------
|
|
FROM_UPSTREAM=0
|
|
if [ -n "$SERVER" ] && [ -n "$REPO_SLUG" ]; then
|
|
MURL="$SERVER/$REPO_SLUG/releases/download/$MIRROR_TAG/$ASSET"
|
|
echo "[sdk-fetch] trying mirror: $MURL"
|
|
# single attempt: a 404 (not-yet-seeded) must fail FAST, not retry-loop
|
|
if fetch_once "$MURL" "$DEST" 2>/dev/null && valid "$DEST"; then
|
|
echo "[sdk-fetch] mirror hit ($(wc -c < "$DEST") bytes)"
|
|
exit 0
|
|
fi
|
|
rm -f "$DEST" "$DEST.part"
|
|
echo "[sdk-fetch] mirror miss"
|
|
fi
|
|
|
|
# --- 3) upstream, stall-proof ------------------------------------------------
|
|
echo "[sdk-fetch] downloading upstream: $SDK_URL"
|
|
fetch_retry "$SDK_URL" "$DEST" || { echo "[sdk-fetch] ERROR: upstream download failed" >&2; exit 1; }
|
|
valid "$DEST" || { echo "[sdk-fetch] ERROR: downloaded file fails validation (truncated/HTML?)" >&2; rm -f "$DEST"; exit 1; }
|
|
echo "[sdk-fetch] upstream OK ($(wc -c < "$DEST") bytes)"
|
|
FROM_UPSTREAM=1
|
|
|
|
# --- 4) best-effort: seed the mirror so the next cold run skips upstream -----
|
|
# NON-FATAL by design: a seeding failure must never fail a build that already
|
|
# has a good tarball. Never deletes anything (other arches' assets live in the
|
|
# same release); tolerates races between the two parallel arch jobs.
|
|
seed_mirror() {
|
|
[ "$FROM_UPSTREAM" = 1 ] || return 0
|
|
[ -n "$MIRROR_TOKEN" ] || { echo "[sdk-fetch] no MIRROR_TOKEN — skip mirror seeding"; return 0; }
|
|
[ -n "$SERVER" ] && [ -n "$REPO_SLUG" ] || return 0
|
|
API="$SERVER/api/v1/repos/$REPO_SLUG"
|
|
B="$(mktemp)"
|
|
# FIRST "id" in the body = the release id (nested ids come later) — same
|
|
# parsing contract as ci/gitea-release.sh.
|
|
rel_id() { grep -o '"id"[[:space:]]*:[[:space:]]*[0-9]\{1,\}' "$B" | head -n1 | grep -o '[0-9]\{1,\}'; }
|
|
gapi() { # gapi METHOD PATH [curl-args...] -> echoes HTTP code, body in $B
|
|
_m="$1"; _p="$2"; shift 2
|
|
curl -sS -o "$B" -w '%{http_code}' -X "$_m" -H "Authorization: token $MIRROR_TOKEN" "$@" "$API$_p" || echo 000
|
|
}
|
|
code=$(gapi GET "/releases/tags/$MIRROR_TAG")
|
|
if [ "$code" = 200 ]; then
|
|
rid=$(rel_id)
|
|
else
|
|
code=$(gapi POST "/releases" -H 'Content-Type: application/json' -d "{\"tag_name\":\"$MIRROR_TAG\",\"name\":\"SDK tarball mirror (CI cache)\",\"body\":\"Durable mirror of upstream SDK tarballs, managed by ci/fetch-sdk.sh. Do not delete.\",\"prerelease\":true,\"draft\":false}")
|
|
case "$code" in
|
|
200|201) rid=$(rel_id) ;;
|
|
409) code=$(gapi GET "/releases/tags/$MIRROR_TAG"); [ "$code" = 200 ] && rid=$(rel_id) || rid="" ;;
|
|
*) rid="" ;;
|
|
esac
|
|
fi
|
|
[ -n "${rid:-}" ] || { echo "[sdk-fetch] WARNING: could not resolve mirror release (HTTP $code)"; rm -f "$B"; return 1; }
|
|
# skip if the asset is already there (the other arch job may have won a race)
|
|
code=$(gapi GET "/releases/$rid/assets")
|
|
if [ "$code" = 200 ] && grep -qF "\"name\":\"$ASSET\"" "$B"; then
|
|
echo "[sdk-fetch] mirror already has $ASSET"
|
|
rm -f "$B"; return 0
|
|
fi
|
|
echo "[sdk-fetch] seeding mirror: $ASSET -> release id=$rid (tag $MIRROR_TAG)"
|
|
code=$(gapi POST "/releases/$rid/assets?name=$ASSET" -F "attachment=@$DEST;filename=$ASSET")
|
|
rm -f "$B"
|
|
case "$code" in
|
|
200|201) echo "[sdk-fetch] mirror seeded" ;;
|
|
*) echo "[sdk-fetch] WARNING: mirror upload returned HTTP $code"; return 1 ;;
|
|
esac
|
|
}
|
|
seed_mirror || echo "[sdk-fetch] WARNING: mirror seeding failed (non-fatal — build continues)"
|
|
exit 0
|