#!/usr/bin/env bash # build_pack.sh - assemble ExilesPack-.zip (or ExilesPack-.zip with --tag) # # Reads manifest.yaml, fetches the latest release of every `include: true` repo # from git.sub-net.at/Exiles, downloads the chosen zip asset(s), unpacks them # into staging/, and rolls the combined result up into dist/ExilesPack-*.zip. # # We unpack the per-addon release zips (which contain just the addon folder, # nothing else), so .git/.github/.gitea/README.md never enter the pack. # # Deps: bash, curl, jq, unzip, zip. No yq needed - the manifest is parsed # with a tiny awk pass tailored to its simple shape. set -euo pipefail # --------------------------------------------------------------------------- # Paths + args # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MANIFEST="$REPO_ROOT/manifest.yaml" TMP_DIR="$REPO_ROOT/tmp" STAGING_DIR="$REPO_ROOT/staging" DIST_DIR="$REPO_ROOT/dist" API="https://git.sub-net.at/api/v1" ORG="Exiles" TAG="" while [ $# -gt 0 ]; do case "$1" in --tag) TAG="$2"; shift 2 ;; --tag=*) TAG="${1#--tag=}"; shift ;; -h|--help) cat <] Without --tag, the pack is named ExilesPack-.zip. With --tag, the pack is named ExilesPack-.zip. USAGE exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done if [ -z "$TAG" ]; then PACK_NAME="ExilesPack-$(date -u +%Y.%m.%d).zip" else PACK_NAME="ExilesPack-${TAG}.zip" fi # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { printf '[build_pack] %s\n' "$*"; } warn() { printf '[build_pack] WARN: %s\n' "$*" >&2; } die() { printf '[build_pack] ERROR: %s\n' "$*" >&2; exit 1; } # Parse manifest.yaml -> emit "" for every `include: true` entry. # The manifest is a small fixed-shape YAML (no anchors, no nested lists), so a # 20-line awk pass is more honest than pulling in a yq dependency. manifest_includes() { awk ' /^[[:space:]]*-[[:space:]]*repo:[[:space:]]*/ { sub(/^[[:space:]]*-[[:space:]]*repo:[[:space:]]*/, "") gsub(/[[:space:]]+$/, "") repo = $0 include = "" next } /^[[:space:]]+include:[[:space:]]*/ { sub(/^[[:space:]]+include:[[:space:]]*/, "") gsub(/[[:space:]]+$/, "") include = $0 if (repo != "" && include == "true") { print repo } repo = ""; include = "" } ' "$MANIFEST" } # Latest release JSON for a repo, or empty string if none. latest_release_json() { local repo="$1" json json="$(curl -fsS "$API/repos/$ORG/$repo/releases?limit=1" 2>/dev/null || echo '[]')" # `releases?limit=1` returns an array. Strip the wrapper. if [ "$(printf '%s' "$json" | jq -r 'type')" = "array" ]; then printf '%s' "$json" | jq -c '.[0] // empty' else printf '' fi } # Download one URL with one retry. Stream to file. fetch_with_retry() { local url="$1" out="$2" for attempt in 1 2; do if curl -fsSL --retry 0 -o "$out" "$url"; then return 0 fi warn "download attempt $attempt failed for $url" sleep 2 done return 1 } # --------------------------------------------------------------------------- # Reset workspace # --------------------------------------------------------------------------- rm -rf "$TMP_DIR" "$STAGING_DIR" mkdir -p "$TMP_DIR" "$STAGING_DIR" "$DIST_DIR" INCLUDED=0 SKIPPED=0 SKIPPED_REPOS=() # --------------------------------------------------------------------------- # Per-repo: pick assets, download, extract # --------------------------------------------------------------------------- while read -r repo; do [ -z "$repo" ] && continue log "processing $repo" rel="$(latest_release_json "$repo")" if [ -z "$rel" ] || [ "$rel" = "null" ]; then warn "$repo has no releases yet - skipping" SKIPPED=$((SKIPPED + 1)) SKIPPED_REPOS+=("$repo") continue fi tag="$(printf '%s' "$rel" | jq -r '.tag_name')" log " latest release: $tag" # Prefer -all.zip if present, otherwise take every *.zip asset. assets_all="$(printf '%s' "$rel" | jq -r --arg n "${repo}-all.zip" \ '.assets[]? | select(.name == $n) | .browser_download_url')" if [ -n "$assets_all" ]; then download_urls="$assets_all" log " using ${repo}-all.zip" else download_urls="$(printf '%s' "$rel" | jq -r \ '.assets[]? | select(.name | endswith(".zip")) | .browser_download_url')" fi if [ -z "$download_urls" ]; then warn "$repo release $tag has no .zip assets - skipping" SKIPPED=$((SKIPPED + 1)) SKIPPED_REPOS+=("$repo") continue fi mkdir -p "$TMP_DIR/$repo" any_ok=0 while IFS= read -r url; do [ -z "$url" ] && continue name="$(basename "$url")" log " downloading $name" if fetch_with_retry "$url" "$TMP_DIR/$repo/$name"; then if unzip -q -o "$TMP_DIR/$repo/$name" -d "$STAGING_DIR"; then any_ok=1 else warn " unzip failed for $name" fi else warn " could not download $name after retry" fi done <<<"$download_urls" if [ "$any_ok" -eq 1 ]; then INCLUDED=$((INCLUDED + 1)) else SKIPPED=$((SKIPPED + 1)) SKIPPED_REPOS+=("$repo") fi done < <(manifest_includes) # --------------------------------------------------------------------------- # Build the pack # --------------------------------------------------------------------------- if [ ! -d "$STAGING_DIR" ] || [ -z "$(ls -A "$STAGING_DIR" 2>/dev/null)" ]; then die "staging is empty - no addons were successfully fetched" fi OUT="$DIST_DIR/$PACK_NAME" rm -f "$OUT" ( cd "$STAGING_DIR" && zip -qr "$OUT" . ) # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- ADDON_FOLDERS="$(cd "$STAGING_DIR" && find . -mindepth 1 -maxdepth 1 -type d | wc -l)" SIZE="$(stat -c '%s' "$OUT")" SHA="$(sha256sum "$OUT" | awk '{print $1}')" echo echo "============================================================" echo " pack: $PACK_NAME" echo " path: $OUT" echo " addon dirs: $ADDON_FOLDERS" echo " size: $SIZE bytes" echo " sha256: $SHA" echo " repos in: $INCLUDED" echo " repos out: $SKIPPED${SKIPPED_REPOS[*]:+ (${SKIPPED_REPOS[*]})}" echo "============================================================"