#!/bin/bash # ci-queue-wait.sh - Wait until project CI queue is clear (no running/queued pipeline on branch head) # Usage: ci-queue-wait.sh [-B branch] [-t timeout_sec] [-i interval_sec] [--purpose push|merge] [--require-status] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" BRANCH="main" TIMEOUT_SEC=900 INTERVAL_SEC=15 PURPOSE="merge" REQUIRE_STATUS=0 usage() { cat </dev/null || true) if [[ -z "$remote_url" ]]; then return 1 fi if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then echo "${BASH_REMATCH[1]}" return 0 fi if [[ "$remote_url" =~ ^git@([^:]+): ]]; then echo "${BASH_REMATCH[1]}" return 0 fi return 1 } get_gitea_token() { local host="$1" if [[ -n "${GITEA_TOKEN:-}" ]]; then echo "$GITEA_TOKEN" return 0 fi local creds="$HOME/.git-credentials" if [[ -f "$creds" ]]; then local token token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1) if [[ -n "$token" ]]; then echo "$token" return 0 fi fi return 1 } get_state_from_status_json() { python3 - <<'PY' import json import sys try: payload = json.load(sys.stdin) except Exception: print("unknown") raise SystemExit(0) statuses = payload.get("statuses") or [] state = (payload.get("state") or "").lower() pending_values = {"pending", "queued", "running", "waiting"} failure_values = {"failure", "error", "failed"} success_values = {"success"} if state in pending_values: print("pending") raise SystemExit(0) if state in failure_values: print("terminal-failure") raise SystemExit(0) if state in success_values: print("terminal-success") raise SystemExit(0) 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 not values and not state: print("no-status") elif any(v in pending_values for v in values): print("pending") elif any(v in failure_values for v in values): print("terminal-failure") elif values and all(v in success_values for v in values): print("terminal-success") else: print("unknown") PY } print_pending_contexts() { python3 - <<'PY' import json import sys try: payload = json.load(sys.stdin) except Exception: print("[ci-queue-wait] unable to decode status payload") raise SystemExit(0) statuses = payload.get("statuses") or [] if not statuses: print("[ci-queue-wait] no status contexts reported") raise SystemExit(0) pending_values = {"pending", "queued", "running", "waiting"} found = False for item in statuses: if not isinstance(item, dict): continue name = item.get("context") or item.get("name") or "unknown-context" value = (item.get("status") or item.get("state") or "unknown").lower() target = item.get("target_url") or item.get("url") or "" if value in pending_values: found = True if target: print(f"[ci-queue-wait] pending: {name}={value} ({target})") else: print(f"[ci-queue-wait] pending: {name}={value}") if not found: print("[ci-queue-wait] no pending contexts") PY } github_get_branch_head_sha() { local owner="$1" local repo="$2" local branch="$3" gh api "repos/${owner}/${repo}/branches/${branch}" --jq '.commit.sha' } github_get_commit_status_json() { local owner="$1" local repo="$2" local sha="$3" gh api "repos/${owner}/${repo}/commits/${sha}/status" } gitea_get_branch_head_sha() { local host="$1" local repo="$2" local branch="$3" local token="$4" local url="https://${host}/api/v1/repos/${repo}/branches/${branch}" curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) commit = data.get("commit") or {} print((commit.get("id") or "").strip()) ' } gitea_get_commit_status_json() { local host="$1" local repo="$2" local sha="$3" local token="$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 -B|--branch) BRANCH="$2" shift 2 ;; -t|--timeout) TIMEOUT_SEC="$2" shift 2 ;; -i|--interval) INTERVAL_SEC="$2" shift 2 ;; --purpose) PURPOSE="$2" shift 2 ;; --require-status) REQUIRE_STATUS=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then echo "Error: timeout and interval must be integer seconds." >&2 exit 1 fi OWNER=$(get_repo_owner) REPO=$(get_repo_name) detect_platform > /dev/null PLATFORM="${PLATFORM:-unknown}" if [[ "$PLATFORM" == "github" ]]; then if ! command -v gh >/dev/null 2>&1; then echo "Error: gh CLI is required for GitHub CI queue guard." >&2 exit 1 fi HEAD_SHA=$(github_get_branch_head_sha "$OWNER" "$REPO" "$BRANCH") if [[ -z "$HEAD_SHA" ]]; then echo "Error: Could not resolve ${BRANCH} head SHA." >&2 exit 1 fi echo "[ci-queue-wait] platform=github purpose=${PURPOSE} branch=${BRANCH} 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_branch_head_sha "$HOST" "$OWNER/$REPO" "$BRANCH" "$TOKEN") if [[ -z "$HEAD_SHA" ]]; then echo "Error: Could not resolve ${BRANCH} head SHA." >&2 exit 1 fi echo "[ci-queue-wait] platform=gitea purpose=${PURPOSE} branch=${BRANCH} sha=${HEAD_SHA}" else echo "Error: Unsupported platform '${PLATFORM}'." >&2 exit 1 fi START_TS=$(date +%s) DEADLINE_TS=$((START_TS + TIMEOUT_SEC)) while true; do NOW_TS=$(date +%s) if (( NOW_TS > DEADLINE_TS )); then echo "Error: Timed out waiting for CI queue to clear on ${BRANCH} 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" "$HEAD_SHA" "$TOKEN") fi STATE=$(printf '%s' "$STATUS_JSON" | get_state_from_status_json) echo "[ci-queue-wait] state=${STATE} purpose=${PURPOSE} branch=${BRANCH}" case "$STATE" in pending) printf '%s' "$STATUS_JSON" | print_pending_contexts sleep "$INTERVAL_SEC" ;; no-status) if [[ "$REQUIRE_STATUS" -eq 1 ]]; then echo "Error: No CI status contexts found for ${BRANCH} while --require-status is set." >&2 exit 1 fi echo "[ci-queue-wait] no status contexts present; proceeding." exit 0 ;; terminal-success|terminal-failure|unknown) # Queue guard only blocks on pending/running/queued states. exit 0 ;; *) echo "[ci-queue-wait] unrecognized state '${STATE}', proceeding conservatively." exit 0 ;; esac done