#!/bin/bash # pr-ci-wait.sh - Wait for PR CI status to reach terminal state (GitHub/Gitea) # Usage: pr-ci-wait.sh -n [-t timeout_sec] [-i interval_sec] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" TIMEOUT_SEC=1800 INTERVAL_SEC=15 usage() { cat < [-t timeout_sec] [-i interval_sec] Options: -n, --number NUMBER PR number (required) -t, --timeout SECONDS Max wait time in seconds (default: 1800) -i, --interval SECONDS Poll interval in seconds (default: 15) -h, --help Show this help Examples: $(basename "$0") -n 643 $(basename "$0") -n 643 -t 900 -i 10 EOF } # get_remote_host and get_gitea_token are provided by detect-platform.sh extract_state_from_status_json() { python3 - <<'PY' import json import sys try: payload = json.load(sys.stdin) except Exception: print("unknown") raise SystemExit(0) state = (payload.get("state") or "").lower() if state in {"success", "pending", "failure", "error"}: print(state) raise SystemExit(0) statuses = payload.get("statuses") or [] values = [] for item in statuses: if not isinstance(item, dict): continue value = (item.get("status") or item.get("state") or "").lower() if value: values.append(value) if any(v in {"failure", "error"} for v in values): print("failure") elif values and all(v == "success" for v in values): print("success") elif any(v in {"pending", "running", "queued", "waiting"} for v in values): print("pending") else: print("unknown") PY } print_status_summary() { python3 - <<'PY' import json import sys try: payload = json.load(sys.stdin) except Exception: print("[pr-ci-wait] status payload unavailable") raise SystemExit(0) statuses = payload.get("statuses") or [] if not statuses: print("[pr-ci-wait] no status contexts reported yet") raise SystemExit(0) for item in statuses: if not isinstance(item, dict): continue name = item.get("context") or item.get("name") or "unknown-context" state = item.get("status") or item.get("state") or "unknown-state" target = item.get("target_url") or item.get("url") or "" if target: print(f"[pr-ci-wait] {name}: {state} ({target})") else: print(f"[pr-ci-wait] {name}: {state}") PY } github_get_pr_head_sha() { gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid' } github_get_commit_status_json() { local owner="$1" local repo="$2" local sha="$3" gh api "repos/${owner}/${repo}/commits/${sha}/status" } gitea_get_pr_head_sha() { local host="$1" local repo="$2" local token="$3" local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}" curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) print((data.get("head") or {}).get("sha", "")) ' } gitea_get_commit_status_json() { local host="$1" local repo="$2" local token="$3" local sha="$4" local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status" curl -fsS -H "Authorization: token ${token}" "$url" } while [[ $# -gt 0 ]]; do case "$1" in -n|--number) PR_NUMBER="$2" shift 2 ;; -t|--timeout) TIMEOUT_SEC="$2" shift 2 ;; -i|--interval) INTERVAL_SEC="$2" shift 2 ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done if [[ -z "$PR_NUMBER" ]]; then echo "Error: PR number is required (-n)." >&2 usage >&2 exit 1 fi if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then echo "Error: timeout and interval must be integer seconds." >&2 exit 1 fi detect_platform > /dev/null OWNER=$(get_repo_owner) REPO=$(get_repo_name) START_TS=$(date +%s) DEADLINE_TS=$((START_TS + TIMEOUT_SEC)) if [[ "$PLATFORM" == "github" ]]; then if ! command -v gh >/dev/null 2>&1; then echo "Error: gh CLI is required for GitHub CI status polling." >&2 exit 1 fi HEAD_SHA=$(github_get_pr_head_sha) if [[ -z "$HEAD_SHA" ]]; then echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2 exit 1 fi echo "[pr-ci-wait] Platform=github PR=#${PR_NUMBER} head_sha=${HEAD_SHA}" elif [[ "$PLATFORM" == "gitea" ]]; then HOST=$(get_remote_host) || { echo "Error: Could not determine remote host." >&2 exit 1 } TOKEN=$(get_gitea_token "$HOST") || { echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2 exit 1 } HEAD_SHA=$(gitea_get_pr_head_sha "$HOST" "$OWNER/$REPO" "$TOKEN") if [[ -z "$HEAD_SHA" ]]; then echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2 exit 1 fi echo "[pr-ci-wait] Platform=gitea host=${HOST} PR=#${PR_NUMBER} head_sha=${HEAD_SHA}" else echo "Error: Unsupported platform '${PLATFORM}'." >&2 exit 1 fi while true; do NOW_TS=$(date +%s) if (( NOW_TS > DEADLINE_TS )); then echo "Error: Timed out waiting for CI status on PR #$PR_NUMBER after ${TIMEOUT_SEC}s." >&2 exit 124 fi if [[ "$PLATFORM" == "github" ]]; then STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA") else STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$HEAD_SHA") fi STATE=$(printf '%s' "$STATUS_JSON" | extract_state_from_status_json) echo "[pr-ci-wait] state=${STATE} pr=#${PR_NUMBER} sha=${HEAD_SHA}" case "$STATE" in success) printf '%s' "$STATUS_JSON" | print_status_summary echo "[pr-ci-wait] CI is green for PR #$PR_NUMBER." exit 0 ;; failure|error) printf '%s' "$STATUS_JSON" | print_status_summary echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2 exit 1 ;; pending|unknown) sleep "$INTERVAL_SEC" ;; *) echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..." sleep "$INTERVAL_SEC" ;; esac done