Files

279 lines
7.7 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"
# shellcheck source=scripts/github/_gh_target.sh
source "$SCRIPT_DIR/_gh_target.sh"
TARGET_REPO=""
PROJECT_OWNER=""
PROJECT_NUMBER=""
PROJECT_URL=""
JSON_OUTPUT=0
usage() {
cat <<'EOF'
Usage:
bash scripts/github/preflight.sh [options]
Options:
--repo OWNER/REPO Verify repository access
--project-owner OWNER Verify GitHub Project owner
--project-number N Verify GitHub Project number
--project-url URL Parse owner/number from Project URL
--json Print machine-readable JSON
-h, --help Show help
EOF
}
fail() {
echo "[错误] $1" >&2
exit 1
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
fail "缺少命令: ${command_name}"
fi
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--repo)
TARGET_REPO="${2:?missing value for --repo}"
shift 2
;;
--project-owner)
PROJECT_OWNER="${2:?missing value for --project-owner}"
shift 2
;;
--project-number)
PROJECT_NUMBER="${2:?missing value for --project-number}"
shift 2
;;
--project-url)
PROJECT_URL="${2:?missing value for --project-url}"
shift 2
;;
--json)
JSON_OUTPUT=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "未知参数: $1"
;;
esac
done
if [[ -n "$PROJECT_URL" ]]; then
local resolved=""
resolved="$(gh_target_resolve_project_from_url "$PROJECT_URL")"
PROJECT_OWNER="$(printf '%s' "$resolved" | cut -f1)"
PROJECT_NUMBER="$(printf '%s' "$resolved" | cut -f2)"
fi
if [[ -n "$PROJECT_OWNER" && -z "$PROJECT_NUMBER" ]]; then
fail "使用 GitHub Project 时必须同时提供 --project-number"
fi
if [[ -z "$PROJECT_OWNER" && -n "$PROJECT_NUMBER" ]]; then
fail "使用 GitHub Project 时必须同时提供 --project-owner"
fi
}
github_rest_host() {
local host="${GH_HOST:-github.com}"
if [[ "$host" == "github.com" ]]; then
printf '%s\n' "api.github.com"
return 0
fi
printf '%s\n' "$host"
}
dns_probe_json() {
local host="$1"
local addresses_csv=""
if command -v getent >/dev/null 2>&1; then
addresses_csv="$(
getent ahosts "$host" 2>/dev/null \
| awk '{print $1}' \
| sort -u \
| paste -sd, -
)"
if [[ -n "$addresses_csv" ]]; then
jq -nc \
--arg host "$host" \
--arg addresses_csv "$addresses_csv" \
'{host: $host, addresses: ($addresses_csv | split(",") | map(select(length > 0)))}'
return 0
fi
fi
python3 - "$host" <<'PY'
import json
import time
import socket
import sys
host = sys.argv[1]
addresses = []
last_error = None
for _ in range(3):
try:
for family, _, _, _, sockaddr in socket.getaddrinfo(host, 443, proto=socket.IPPROTO_TCP):
addr = sockaddr[0]
if addr not in addresses:
addresses.append(addr)
break
except OSError as exc:
last_error = exc
time.sleep(1)
if not addresses and last_error is not None:
print(json.dumps({"host": host, "addresses": [], "warning": str(last_error)}, ensure_ascii=False))
raise SystemExit(0)
print(json.dumps({"host": host, "addresses": addresses}, ensure_ascii=False))
PY
}
project_probe_payload() {
jq -nc \
--arg owner "$PROJECT_OWNER" \
--argjson number "$PROJECT_NUMBER" \
'{
query: "query($owner:String!,$number:Int!){ organization(login:$owner){ projectV2(number:$number){ id title viewerCanUpdate closed } } user(login:$owner){ projectV2(number:$number){ id title viewerCanUpdate closed } } }",
variables: { owner: $owner, number: $number }
}'
}
main() {
local dns_json=""
local viewer_json=""
local repo_json="null"
local project_raw="null"
local project_json="null"
local host=""
local viewer_login=""
local rate_remaining=""
local rate_reset=""
local scopes=""
parse_args "$@"
require_command "jq"
require_command "curl"
require_command "python3"
gh_env_prepare
gh_env_require_token
host="$(github_rest_host)"
dns_json="$(dns_probe_json "$host")"
viewer_json="$(
bash "$SCRIPT_DIR/github_api.sh" \
--graphql \
--data '{"query":"query { viewer { login } rateLimit { remaining resetAt } }"}'
)"
viewer_login="$(printf '%s' "$viewer_json" | jq -r '.data.viewer.login // empty')"
rate_remaining="$(printf '%s' "$viewer_json" | jq -r '.data.rateLimit.remaining // empty')"
rate_reset="$(printf '%s' "$viewer_json" | jq -r '.data.rateLimit.resetAt // empty')"
scopes="$(
curl \
--silent \
--show-error \
--head \
--connect-timeout "${GH_API_CONNECT_TIMEOUT_SEC:-10}" \
--max-time "${GH_API_MAX_TIME_SEC:-60}" \
-H "Authorization: Bearer ${GH_TOKEN:-${GITHUB_TOKEN:-}}" \
-H 'Accept: application/vnd.github+json' \
"$(gh_env_rest_api_base_url)/" \
| awk 'BEGIN{IGNORECASE=1} /^x-oauth-scopes:/ {sub(/\r$/,""); sub(/^x-oauth-scopes:[[:space:]]*/,""); print; exit}'
)"
if [[ -n "$TARGET_REPO" ]]; then
repo_json="$(
bash "$SCRIPT_DIR/github_api.sh" "repos/${TARGET_REPO}" \
| jq '{full_name, private, default_branch, html_url}'
)"
fi
if [[ -n "$PROJECT_OWNER" && -n "$PROJECT_NUMBER" ]]; then
project_raw="$(
bash "$SCRIPT_DIR/github_api.sh" \
--graphql \
--data "$(project_probe_payload)"
)"
project_json="$(
printf '%s' "$project_raw" \
| jq '(.data.organization.projectV2 // .data.user.projectV2) as $project
| if $project == null then null else $project end'
)"
if [[ "$project_json" == "null" ]]; then
fail "GitHub Project 不可访问: ${PROJECT_OWNER}/${PROJECT_NUMBER}"
fi
fi
if [[ "$JSON_OUTPUT" == "1" ]]; then
jq -nc \
--arg viewer_login "$viewer_login" \
--arg rate_remaining "$rate_remaining" \
--arg rate_reset "$rate_reset" \
--arg scopes "$scopes" \
--arg repo "$TARGET_REPO" \
--arg project_owner "$PROJECT_OWNER" \
--arg project_number "$PROJECT_NUMBER" \
--argjson dns "$dns_json" \
--argjson repo_data "$repo_json" \
--argjson project_data "$project_json" \
'{
dns: $dns,
viewer: {
login: $viewer_login,
rate_remaining: $rate_remaining,
rate_reset_at: $rate_reset,
scopes: $scopes
},
repo: (if $repo == "" then null else ($repo_data + { target: $repo }) end),
project: (if $project_owner == "" then null else ($project_data + { owner: $project_owner, number: ($project_number|tonumber) }) end)
}'
return 0
fi
if [[ "$(printf '%s' "$dns_json" | jq -r '.addresses | length')" -gt 0 ]]; then
echo "[OK] DNS: $(printf '%s' "$dns_json" | jq -r '.host') -> $(printf '%s' "$dns_json" | jq -r '.addresses | join(", ")')"
else
echo "[警告] DNS: $(printf '%s' "$dns_json" | jq -r '.host') 解析结果为空,继续用 GitHub API 实测连通性"
fi
echo "[OK] Token: ${viewer_login}"
echo "[OK] Rate limit: remaining=${rate_remaining}, reset_at=${rate_reset}"
if [[ -n "$scopes" ]]; then
echo "[OK] Scopes: ${scopes}"
fi
if [[ -n "$TARGET_REPO" ]]; then
echo "[OK] Repo: $(printf '%s' "$repo_json" | jq -r '.full_name') (default_branch=$(printf '%s' "$repo_json" | jq -r '.default_branch'))"
fi
if [[ -n "$PROJECT_OWNER" && -n "$PROJECT_NUMBER" ]]; then
echo "[OK] Project: ${PROJECT_OWNER}/${PROJECT_NUMBER} -> $(printf '%s' "$project_json" | jq -r '.title')"
echo "[OK] Project update access: $(printf '%s' "$project_json" | jq -r '.viewerCanUpdate')"
fi
}
main "$@"