Files
small-scripts/scripts/wait-for-version

210 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
DEFAULT_SERVER="https://gitea.oreillyit.nz"
DEFAULT_TIMEOUT=300
DEFAULT_INTERVAL=15
DEFAULT_WORKFLOW="version"
usage() {
cat <<'EOF'
Usage: wait-for-version [OPTIONS] REPO
Poll a Gitea repo's Actions runs until the CI workflow for HEAD of main
completes successfully, then print the latest release tag.
Arguments:
REPO owner/name (e.g. skynet/hugo-accelerator)
Options:
-t, --token TOKEN Gitea access token (else $GITEA_TOKEN / $GITHUB_TOKEN)
--workflow NAME Workflow name to wait for (default: version)
--timeout SECONDS Max total wait time (default: 300; 0 = poll once)
--interval SECONDS Polling interval (default: 15; 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 CI succeeded; latest release tag printed to stdout
1 timeout expired, CI failed, or no release tag found
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:-}}"
workflow="$DEFAULT_WORKFLOW"
verbose=false
dryrun=false
repo=""
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 ;;
--workflow)
[[ $# -lt 2 ]] && { echo "Error: --workflow requires a value" >&2; exit 2; }
workflow="$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"
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; }
[[ "$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_base="${server%/}/api/v1/repos/${repo}"
if $dryrun; then
echo "[dryrun] Server: ${server}"
echo "[dryrun] Repo: ${repo}"
echo "[dryrun] Workflow: ${workflow}"
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] Actions API: ${api_base}/actions/runs"
exit 0
fi
curl_args=(-sS -m 30 -w $'\n%{http_code}')
[[ -n "$token" ]] && curl_args+=(-H "Authorization: token ${token}")
sha_response="$(curl "${curl_args[@]}" "${api_base}/branches/main" 2>/dev/null)"
sha_rc=$?
sha_http="${sha_response##*$'\n'}"
sha_body="${sha_response%$'\n'*}"
if (( sha_rc != 0 )); then
echo "Error: curl failed resolving HEAD SHA for ${repo} (rc=${sha_rc})" >&2
exit 3
fi
case "$sha_http" in
200)
sha="$(printf '%s' "$sha_body" | jq -r '.commit.id // empty' 2>/dev/null)"
[[ -z "$sha" ]] && { echo "Error: could not extract HEAD SHA from branch response" >&2; exit 3; }
;;
401|403) echo "Error: Gitea API authentication failed (HTTP ${sha_http}) for ${repo}" >&2; exit 3 ;;
404) echo "Error: repo not found: ${repo} (HTTP 404)" >&2; exit 3 ;;
*) echo "Error: unexpected HTTP ${sha_http} resolving HEAD SHA for ${repo}" >&2; exit 3 ;;
esac
echo "Waiting for '${workflow}' CI on ${repo} @ ${sha:0:8}..." >&2
start=$(date +%s)
deadline=$(( start + timeout ))
poll=0
while true; do
poll=$(( poll + 1 ))
runs_response="$(curl "${curl_args[@]}" \
"${api_base}/actions/runs?head_sha=${sha}&limit=50" 2>/dev/null)"
runs_rc=$?
if (( runs_rc != 0 )); then
$verbose && echo "wait-for-version: poll ${poll}: curl error (rc=${runs_rc})" >&2
else
http_code="${runs_response##*$'\n'}"
body="${runs_response%$'\n'*}"
case "$http_code" in
200)
status="$(printf '%s' "$body" | jq -r \
--arg w "$workflow" \
'[.workflow_runs[] | select(.path | test($w + "\\.ya?ml"))] | first | .status // "pending"' \
2>/dev/null)"
conclusion="$(printf '%s' "$body" | jq -r \
--arg w "$workflow" \
'[.workflow_runs[] | select(.path | test($w + "\\.ya?ml"))] | first | .conclusion // ""' \
2>/dev/null)"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" == "success" ]]; then
# Resolve tag by SHA match — avoids races with release creation
# and works regardless of whether the repo uses Gitea releases.
# When multiple tags share the SHA (e.g. v0.0.8 and v0.0.8.14),
# prefer the 3-component semver root (fewest dot-components).
tag_response="$(curl "${curl_args[@]}" "${api_base}/tags?limit=50" 2>/dev/null)"
tag_http="${tag_response##*$'\n'}"
tag_body="${tag_response%$'\n'*}"
if [[ "$tag_http" != "200" ]]; then
echo "Error: could not fetch tags for ${repo} (HTTP ${tag_http})" >&2
exit 1
fi
tag="$(printf '%s' "$tag_body" | jq -r --arg sha "$sha" \
'[.[] | select(.commit.sha == $sha)] |
sort_by(.name | ltrimstr("v") | split(".") | length) |
first | .name // empty' 2>/dev/null)"
if [[ -n "$tag" ]]; then
$verbose && echo "wait-for-version: poll ${poll}: success, tag=${tag}" >&2
echo "$tag"
exit 0
else
echo "Error: no tag found for ${repo} @ ${sha:0:8}" >&2
exit 1
fi
else
echo "Error: CI failed for ${repo} @ ${sha:0:8} (conclusion: ${conclusion})" >&2
exit 1
fi
fi
$verbose && echo "wait-for-version: poll ${poll}: status=${status:-pending}, retrying" >&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-version: 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-version: poll ${poll}: sleeping ${sleep_time}s (${remaining}s remaining)" >&2
sleep "$sleep_time"
done
echo "Error: timeout after ${timeout}s waiting for '${workflow}' CI on ${repo} @ ${sha:0:8}" >&2
exit 1