303 lines
7.3 KiB
Bash
Executable File
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
|