Files
shater/ci/fetch-sdk.sh
T
omarandClaude Opus 4.8 cb983afee1 perf(ci): stall-proof SDK fetch + caching (SDK/dl/go/npm/apt/usign) + concurrency
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>
2026-07-23 21:24:49 +03:00

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