210 lines
8.1 KiB
Bash
Executable File
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
|