#!/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 "$@"