#!/usr/bin/env bash set -uo pipefail DEFAULT_SERVER="https://gitea.oreillyit.nz" DEFAULT_TIMEOUT=600 DEFAULT_INTERVAL=5 usage() { cat <<'EOF' Usage: wait-for-release [OPTIONS] REPO PATTERN Poll a Gitea repo's releases until one matches PATTERN (shell-style glob) or the timeout expires. Arguments: REPO owner/name (e.g. skynet/myapp) PATTERN shell-style glob matched against each release's tag_name (e.g. 'v1.2.3.*', 'v1.2.*', 'v1.*'). Quote it to prevent your shell from expanding it. Options: -t, --token TOKEN Gitea access token (else $GITEA_TOKEN / $GITHUB_TOKEN) --timeout SECONDS Max total wait time (default: 600; 0 = poll once) --interval SECONDS Polling interval (default: 5; min: 1) --server URL Gitea base URL (else $GITHUB_SERVER_URL, else https://gitea.oreillyit.nz) -v, --verbose Print polling progress to stderr -n, --dryrun Show resolved configuration without making API calls -h, --help Show this help Exit codes: 0 matching release found (tag_name printed to stdout) 1 timeout expired with no match 2 usage error 3 terminal API error (401, 403, 404) EOF } timeout=$DEFAULT_TIMEOUT interval=$DEFAULT_INTERVAL server="${GITHUB_SERVER_URL:-$DEFAULT_SERVER}" token="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}" verbose=false dryrun=false repo="" pattern="" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; -n|--dryrun) dryrun=true; shift ;; -v|--verbose) verbose=true; shift ;; -t|--token) [[ $# -lt 2 ]] && { echo "Error: --token requires a value" >&2; exit 2; } token="$2"; shift 2 ;; --timeout) [[ $# -lt 2 ]] && { echo "Error: --timeout requires a value" >&2; exit 2; } timeout="$2"; shift 2 ;; --interval) [[ $# -lt 2 ]] && { echo "Error: --interval requires a value" >&2; exit 2; } interval="$2"; shift 2 ;; --server) [[ $# -lt 2 ]] && { echo "Error: --server requires a value" >&2; exit 2; } server="$2"; shift 2 ;; --) shift; break ;; -*) echo "Error: Unknown option: $1" >&2; exit 2 ;; *) if [[ -z "$repo" ]]; then repo="$1" elif [[ -z "$pattern" ]]; then pattern="$1" else echo "Error: Unexpected argument: $1" >&2; exit 2 fi shift ;; esac done [[ -z "$repo" ]] && { echo "Error: REPO is required" >&2; echo >&2; usage >&2; exit 2; } [[ -z "$pattern" ]] && { echo "Error: PATTERN is required" >&2; echo >&2; usage >&2; exit 2; } [[ "$repo" != */* ]] && { echo "Error: REPO must be in 'owner/name' form (got: $repo)" >&2; exit 2; } [[ ! "$timeout" =~ ^[0-9]+$ ]] && { echo "Error: --timeout must be a non-negative integer (got: $timeout)" >&2; exit 2; } [[ ! "$interval" =~ ^[0-9]+$ ]] && { echo "Error: --interval must be a positive integer (got: $interval)" >&2; exit 2; } (( interval < 1 )) && { echo "Error: --interval must be >= 1" >&2; exit 2; } api_url="${server%/}/api/v1/repos/${repo}/releases?limit=50" if $dryrun; then echo "[dryrun] Server: ${server}" echo "[dryrun] Repo: ${repo}" echo "[dryrun] Pattern: ${pattern}" echo "[dryrun] Timeout: ${timeout}s" echo "[dryrun] Interval: ${interval}s" if [[ -n "$token" ]]; then echo "[dryrun] Auth: yes" else echo "[dryrun] Auth: no" fi echo "[dryrun] API URL: ${api_url}" exit 0 fi extract_tags() { if command -v jq >/dev/null 2>&1; then jq -r '.[]?.tag_name // empty' 2>/dev/null else grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' \ | sed -E 's/.*"([^"]+)"$/\1/' fi } curl_cmd=(curl -sS -m 30 -w $'\n%{http_code}') [[ -n "$token" ]] && curl_cmd+=(-H "Authorization: token ${token}") start=$(date +%s) deadline=$(( start + timeout )) poll=0 while true; do poll=$(( poll + 1 )) response="$("${curl_cmd[@]}" "$api_url" 2>/dev/null)" curl_rc=$? if (( curl_rc != 0 )); then $verbose && echo "wait-for-release: poll ${poll}: network/curl error (rc=${curl_rc})" >&2 else http_code="${response##*$'\n'}" body="${response%$'\n'*}" case "$http_code" in 200) match="" while IFS= read -r tag; do [[ -z "$tag" ]] && continue # shellcheck disable=SC2053 # intentional glob match if [[ "$tag" == $pattern ]]; then match="$tag" break fi done < <(printf '%s' "$body" | extract_tags) if [[ -n "$match" ]]; then $verbose && echo "wait-for-release: poll ${poll}: matched '${match}'" >&2 echo "$match" exit 0 fi $verbose && echo "wait-for-release: poll ${poll}: HTTP 200, no match yet" >&2 ;; 401|403) echo "Error: Gitea API authentication failed (HTTP ${http_code}) for ${repo}" >&2 exit 3 ;; 404) echo "Error: repo not found: ${repo} (HTTP 404)" >&2 exit 3 ;; *) $verbose && echo "wait-for-release: poll ${poll}: HTTP ${http_code}, retrying" >&2 ;; esac fi now=$(date +%s) if (( now >= deadline )); then break fi remaining=$(( deadline - now )) sleep_time=$interval (( sleep_time > remaining )) && sleep_time=$remaining $verbose && echo "wait-for-release: poll ${poll}: sleeping ${sleep_time}s (${remaining}s remaining)" >&2 sleep "$sleep_time" done echo "Error: timeout after ${timeout}s waiting for '${pattern}' in ${repo}" >&2 exit 1