Rename the `rails/` directory to `tools/` for agent discoverability — agents frequently failed to locate helper scripts due to the non-intuitive directory name. Add backward-compat symlink `rails/ → tools/`. New tool suites: - Authentik: auth-token, user-list, user-create, group-list, app-list, flow-list, admin-status (8 scripts) - Coolify: team-list, project-list, service-list, service-status, deploy, env-set (7 scripts) - Woodpecker: pipeline-list, pipeline-status, pipeline-trigger (3 stubs) - GLPI: session-init, computer-list, ticket-list, ticket-create, user-list (6 scripts) - Health: stack-health.sh — stack-wide connectivity check Infrastructure: - Shared credential loader at tools/_lib/credentials.sh - install.sh creates symlink + chmod on tool scripts - All ~253 rails/ path references updated across 68+ files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
274 lines
7.1 KiB
Bash
Executable File
274 lines
7.1 KiB
Bash
Executable File
#!/bin/bash
|
|
# pr-ci-wait.sh - Wait for PR CI status to reach terminal state (GitHub/Gitea)
|
|
# Usage: pr-ci-wait.sh -n <pr_number> [-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 <<EOF
|
|
Usage: $(basename "$0") -n <pr_number> [-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() {
|
|
local remote_url
|
|
remote_url=$(git remote get-url origin 2>/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
|
|
}
|
|
|
|
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
|