279 lines
7.7 KiB
Bash
Executable File
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 "$@"
|