#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=scripts/github/_gh_env.sh source "$SCRIPT_DIR/_gh_env.sh" METHOD="GET" GRAPHQL=0 REQUEST_PATH="" REQUEST_DATA="" REQUEST_DATA_FILE="" PRINT_HEADERS=0 HOST_OVERRIDE="" declare -a EXTRA_HEADERS=() usage() { cat <<'EOF' Usage: bash scripts/github/github_api.sh [options] bash scripts/github/github_api.sh --graphql --data '{"query":"query { viewer { login } }"}' Options: --method METHOD HTTP method, default GET --graphql Call the GraphQL endpoint instead of REST --host HOST Override GH_HOST / GitHub Enterprise host --data JSON Inline JSON request body --data-file FILE Read JSON request body from file --header VALUE Additional header, repeatable --include-headers Print response headers to stderr -h, --help Show help Environment: GH_TOKEN / GITHUB_TOKEN Required for authentication GH_API_CONNECT_TIMEOUT_SEC Default 10 GH_API_MAX_TIME_SEC Default 60 GH_API_RETRY_MAX_ATTEMPTS Default 5 GH_API_RETRY_BASE_DELAY_SEC Default 2 GH_API_RETRY_MAX_DELAY_SEC Default 20 EOF } fail() { echo "[错误] $1" >&2 exit 1 } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --method) METHOD="${2:?missing value for --method}" shift 2 ;; --graphql) GRAPHQL=1 shift ;; --host) HOST_OVERRIDE="${2:?missing value for --host}" shift 2 ;; --data) REQUEST_DATA="${2:?missing value for --data}" shift 2 ;; --data-file) REQUEST_DATA_FILE="${2:?missing value for --data-file}" shift 2 ;; --header) EXTRA_HEADERS+=("${2:?missing value for --header}") shift 2 ;; --include-headers) PRINT_HEADERS=1 shift ;; -h|--help) usage exit 0 ;; --) shift break ;; -*) fail "未知参数: $1" ;; *) if [[ -n "$REQUEST_PATH" ]]; then fail "REST 路径只能提供一个" fi REQUEST_PATH="$1" shift ;; esac done if [[ -n "$REQUEST_DATA" && -n "$REQUEST_DATA_FILE" ]]; then fail "--data 与 --data-file 只能使用一个" fi if [[ "$METHOD" == "GET" && ( -n "$REQUEST_DATA" || -n "$REQUEST_DATA_FILE" ) ]]; then METHOD="POST" fi if [[ "$GRAPHQL" -eq 1 && -z "$REQUEST_DATA" && -z "$REQUEST_DATA_FILE" ]]; then fail "GraphQL 调用必须提供 --data 或 --data-file" fi if [[ "$GRAPHQL" -eq 0 && -z "$REQUEST_PATH" ]]; then fail "REST 调用必须提供路径,例如 repos/OWNER/REPO/issues" fi } normalize_rest_url() { local path="$1" if [[ "$path" =~ ^https?:// ]]; then printf '%s\n' "$path" return 0 fi path="${path#/}" printf '%s/%s\n' "$(gh_env_rest_api_base_url "$HOST_OVERRIDE")" "$path" } resolve_request_url() { if [[ "$GRAPHQL" -eq 1 ]]; then gh_env_graphql_api_url "$HOST_OVERRIDE" return 0 fi normalize_rest_url "$REQUEST_PATH" } retryable_http_code() { case "${1:-0}" in 408|429|500|502|503|504) return 0 ;; esac return 1 } retryable_curl_rc() { case "${1:-0}" in 6|7|18|28|35|52|55|56) return 0 ;; esac return 1 } extract_retry_after() { local header_file="$1" awk 'BEGIN{IGNORECASE=1} /^retry-after:/ {gsub("\r","",$2); print $2; exit}' "$header_file" } body_summary() { local body_file="$1" local summary summary="$(tr '\n' ' ' <"$body_file" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" if [[ -z "$summary" ]]; then summary="empty response body" fi printf '%s' "$summary" } perform_request() { gh_env_prepare gh_env_require_token local connect_timeout="${GH_API_CONNECT_TIMEOUT_SEC:-10}" local max_time="${GH_API_MAX_TIME_SEC:-60}" local max_attempts="${GH_API_RETRY_MAX_ATTEMPTS:-5}" local base_delay="${GH_API_RETRY_BASE_DELAY_SEC:-2}" local max_delay="${GH_API_RETRY_MAX_DELAY_SEC:-20}" local attempt=1 local delay="$base_delay" local url="" url="$(resolve_request_url)" local header_file="" local body_file="" local stderr_file="" header_file="$(mktemp)" body_file="$(mktemp)" stderr_file="$(mktemp)" local curl_rc=0 local http_code="" local retry_after="" local summary="" local data_args=() local -a curl_header_args=() if [[ -n "$REQUEST_DATA" ]]; then data_args+=(--data "$REQUEST_DATA") elif [[ -n "$REQUEST_DATA_FILE" ]]; then data_args+=(--data "@${REQUEST_DATA_FILE}") fi if (( ${#data_args[@]} > 0 )); then curl_header_args+=(-H 'Content-Type: application/json') fi for header in "${EXTRA_HEADERS[@]}"; do curl_header_args+=(-H "$header") done while (( attempt <= max_attempts )); do : >"$header_file" : >"$body_file" : >"$stderr_file" set +e http_code="$( curl \ --silent \ --show-error \ --output "$body_file" \ --dump-header "$header_file" \ --write-out '%{http_code}' \ --connect-timeout "$connect_timeout" \ --max-time "$max_time" \ --request "$METHOD" \ -H "Authorization: Bearer ${GH_TOKEN:-${GITHUB_TOKEN:-}}" \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2022-11-28' \ "${data_args[@]}" \ "${curl_header_args[@]}" \ "$url" \ 2>"$stderr_file" )" curl_rc=$? set -e if [[ "$curl_rc" -eq 0 && "$http_code" =~ ^2 ]]; then if [[ "$PRINT_HEADERS" -eq 1 ]]; then cat "$header_file" >&2 fi cat "$body_file" rm -f "$header_file" "$body_file" "$stderr_file" return 0 fi if retryable_curl_rc "$curl_rc" || retryable_http_code "$http_code"; then if (( attempt == max_attempts )); then if [[ "$curl_rc" -ne 0 ]]; then summary="$(tr '\n' ' ' <"$stderr_file" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" else summary="$(body_summary "$body_file")" fi echo "[错误] GitHub API 重试耗尽: $summary" >&2 rm -f "$header_file" "$body_file" "$stderr_file" exit 75 fi retry_after="$(extract_retry_after "$header_file")" if [[ -n "$retry_after" && "$retry_after" =~ ^[0-9]+$ ]]; then delay="$retry_after" fi if [[ "$curl_rc" -ne 0 ]]; then summary="$(tr '\n' ' ' <"$stderr_file" | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//')" else summary="$(body_summary "$body_file")" fi echo "[警告] GitHub API 调用失败,第 ${attempt}/${max_attempts} 次,${delay} 秒后重试: ${summary}" >&2 sleep "$delay" attempt=$((attempt + 1)) delay=$((delay * 2)) if (( delay > max_delay )); then delay="$max_delay" fi continue fi if [[ "$PRINT_HEADERS" -eq 1 ]]; then cat "$header_file" >&2 fi if [[ -s "$body_file" ]]; then cat "$body_file" >&2 elif [[ -s "$stderr_file" ]]; then cat "$stderr_file" >&2 else echo "[错误] GitHub API 调用失败,HTTP ${http_code:-unknown}" >&2 fi rm -f "$header_file" "$body_file" "$stderr_file" if [[ "$curl_rc" -eq 0 ]]; then exit 1 fi exit "$curl_rc" done } parse_args "$@" perform_request