Files
ljwx-deploy/scripts/github/github_api.sh
T

303 lines
7.3 KiB
Bash
Executable File

#!/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] <rest-path>
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