Skip to content

Commit 53d5807

Browse files
locus313Copilot
andcommitted
feat: convert gh-CLI-only scripts to support GITHUB_TOKEN + curl
github-organize-stars: - Now sources lib/github-common.sh for shared helpers - Calls configure_gh_auth to bridge GITHUB_TOKEN→GH_TOKEN; kept as gh-CLI-based since it uses GraphQL mutations not available via REST github-repo-permissions-report: - Removed gh CLI dependency; now uses curl via lib's gh_api and gh_api_paginate for all API calls - Branch protection and ruleset 404s handled via __404__ sentinel github-copilot-report: - Removed gh CLI dependency; added local _copilot_api() using curl with X-GitHub-Api-Version: 2026-03-10 - fetch_seats() now uses gh_api_paginate with the Copilot API version - az CLI is now truly optional: if not installed or not logged in, Entra ID enrichment is silently skipped (--no-entra also works on runners without az installed) - Added missing API_URL_PREFIX default to fix validate_github_token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 36717f9 commit 53d5807

3 files changed

Lines changed: 87 additions & 55 deletions

File tree

personal/github-organize-stars/github-organize-stars.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@
1414
# ./github-organize-stars.sh --no-cache # Force re-fetch stars from GitHub
1515
#
1616
# Environment variables:
17-
# CACHE_FILE Optional. Path to the stars cache file
17+
# GITHUB_TOKEN Optional. PAT or token used to authenticate gh CLI.
18+
# When set, takes precedence over any active gh auth session.
19+
# CACHE_FILE Optional. Path to the stars cache file
1820
# (default: ~/.cache/gh-star-organizer/stars.json)
1921
#
2022
# Requirements:
21-
# - gh (GitHub CLI, authenticated)
23+
# - gh (GitHub CLI, authenticated via GITHUB_TOKEN or gh auth login)
2224
# - jq
2325
# =============================================================================
2426

2527
set -uo pipefail
2628

29+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30+
# shellcheck source=../../lib/github-common.sh
31+
source "${SCRIPT_DIR}/../../lib/github-common.sh"
32+
2733
# ---- Dependency check -------------------------------------------------------
2834
for cmd in gh jq; do
2935
if ! command -v "$cmd" &>/dev/null; then
@@ -55,6 +61,8 @@ for arg in "$@"; do
5561
esac
5662
done
5763

64+
configure_gh_auth "gh auth login"
65+
5866
# =============================================================================
5967
# CATEGORY RULES
6068
# Format of each element: "List Name|LANGUAGES|TOPICS|NAME_KEYWORDS"

reporting/github-copilot-report/github-copilot-report.sh

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
# GitHub Copilot Enterprise licence & usage report, optionally enriched with
66
# Entra ID department data. Output: CSV file + console summary.
77
#
8-
# Authentication — no tokens or secrets required in flags:
9-
# GitHub : gh CLI (run 'gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"'
10-
# as enterprise owner or billing manager)
11-
# Entra : az CLI (run 'az login' once; needs User.Read.All)
8+
# Authentication — no secrets required in flags:
9+
# GitHub : GITHUB_TOKEN env var (PAT with read:enterprise and
10+
# manage_billing:enterprise scopes)
11+
# OR provided automatically from an active gh auth session
12+
# with the required scopes
13+
# Entra : az CLI (optional; run 'az login' once; needs User.Read.All)
14+
# Skipped automatically if az is not installed or not logged in.
15+
# Use --no-entra to suppress Entra lookups explicitly.
1216
#
1317
# What this reports:
1418
# • Every user with a Copilot seat, their plan type, and their pool contribution
@@ -36,6 +40,8 @@ source "${SCRIPT_DIR}/../../lib/github-common.sh"
3640

3741
# ── Defaults ──────────────────────────────────────────────────────────────────
3842
GITHUB_ENTERPRISE="${GITHUB_ENTERPRISE:-}"
43+
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
44+
API_URL_PREFIX="${API_URL_PREFIX:-https://api.github.com}"
3945
UPN_DOMAIN="${UPN_DOMAIN:-}"
4046
CREDITS_PER_SEAT_OVERRIDE="${CREDITS_PER_SEAT_OVERRIDE:-}"
4147
OUTPUT_CSV="copilot-report-$(date +%Y%m%d).csv"
@@ -109,8 +115,9 @@ Usage: github-copilot-report.sh [OPTIONS]
109115
GitHub Copilot Enterprise licence + usage report, optionally enriched with
110116
Entra ID department information.
111117
112-
Authentication (no secrets in flags — both CLIs handle auth):
113-
GitHub → gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"
118+
Authentication (no secrets in flags):
119+
GitHub → export GITHUB_TOKEN=ghp_yourtoken (read:enterprise,manage_billing:enterprise)
120+
OR resolved automatically from an active gh auth session
114121
Entra → az login
115122
116123
Options:
@@ -124,7 +131,7 @@ Options:
124131
--no-entra Skip Entra ID department lookup
125132
-h, --help Show this message
126133
127-
Required gh CLI scopes:
134+
Required GITHUB_TOKEN scopes:
128135
read:enterprise
129136
manage_billing:enterprise (requires enterprise owner or billing manager)
130137
@@ -150,15 +157,17 @@ done
150157
[[ -z "$GITHUB_ENTERPRISE" ]] && \
151158
err "GitHub Enterprise slug is required (-e / \$GITHUB_ENTERPRISE)"
152159

153-
require_command gh
154-
require_command az
155160
require_command jq
156161

157-
gh auth status &>/dev/null || err "gh CLI is not authenticated. Run: gh auth refresh --scopes \"read:enterprise,manage_billing:enterprise\""
162+
require_env_var GITHUB_TOKEN
163+
validate_github_token "bearer"
158164

159165
# ── Acquire Microsoft Graph token via az CLI ──────────────────────────────────
160166
if [[ "$NO_ENTRA" == "true" ]]; then
161167
warn "Entra ID lookup disabled (--no-entra). Department column will be N/A."
168+
elif ! command -v az &>/dev/null; then
169+
warn "az CLI is not installed — department/division grouping will be skipped."
170+
warn "Install the Azure CLI and run 'az login' to enable Entra ID enrichment, or pass --no-entra."
162171
elif az account show &>/dev/null 2>&1; then
163172
info "Acquiring Microsoft Graph token via az CLI..."
164173
GRAPH_TOKEN=$(az account get-access-token \
@@ -176,14 +185,36 @@ else
176185
warn "Run 'az login' to enable Entra ID enrichment, or pass --no-entra."
177186
fi
178187

179-
# ── GitHub API via gh CLI ─────────────────────────────────────────────────────
180-
# Thin wrapper that injects the standard Accept and version headers.
181-
# Pass any extra 'gh api' flags (e.g. --paginate, --jq, --input) as arguments.
182-
gh_api() {
183-
gh api \
184-
-H "Accept: application/vnd.github+json" \
185-
-H "X-GitHub-Api-Version: 2026-03-10" \
186-
"$@"
188+
# ── GitHub API via curl with Copilot API version ──────────────────────────────
189+
# The Copilot usage-metrics endpoints require API version 2026-03-10.
190+
# Uses the same retry/rate-limit logic as lib's gh_api.
191+
_copilot_api() {
192+
local url="$1"
193+
shift
194+
[[ "${url}" == http* ]] || url="${API_URL_PREFIX:-https://api.github.com}${url}"
195+
196+
local _attempt _http_code _body
197+
for _attempt in 1 2 3 4 5; do
198+
_body=$(curl -s -w "\n%{http_code}" \
199+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
200+
-H "Accept: application/vnd.github+json" \
201+
-H "X-GitHub-Api-Version: 2026-03-10" \
202+
"$@" "${url}")
203+
_http_code=$(echo "${_body}" | tail -1)
204+
_body=$(echo "${_body}" | head -n -1)
205+
case "${_http_code}" in
206+
200) echo "${_body}"; return 0 ;;
207+
404) echo "__404__"; return 0 ;;
208+
422) echo "__422__"; return 0 ;;
209+
403|429)
210+
warn "Rate limited (HTTP ${_http_code}). Sleeping 60s before retry ${_attempt}/5..."
211+
sleep 60 ;;
212+
*)
213+
warn "HTTP ${_http_code} from GitHub API (attempt ${_attempt}/5)"
214+
sleep 5 ;;
215+
esac
216+
done
217+
err "Failed to reach ${url} after 5 attempts"
187218
}
188219

189220
# fetch_usage_ndjson REPORT_PATH
@@ -193,7 +224,8 @@ gh_api() {
193224
fetch_usage_ndjson() {
194225
local path="$1"
195226
local resp links
196-
resp=$(gh_api "${path}" 2>/dev/null) || return 0
227+
resp=$(_copilot_api "${path}" 2>/dev/null) || return 0
228+
[[ "${resp}" == "__404__" || "${resp}" == "__422__" ]] && return 0
197229
links=$(echo "$resp" | jq -r '.download_links[]? // empty' 2>/dev/null) || return 0
198230
[[ -z "$links" ]] && return 0
199231
while IFS= read -r url; do
@@ -208,9 +240,7 @@ TOTAL_ASSIGNED_SEATS=0
208240

209241
fetch_seats() {
210242
local slug="$1"
211-
gh_api --paginate \
212-
--jq '.seats[]' \
213-
"/enterprises/${slug}/copilot/billing/seats" \
243+
gh_api_paginate "/enterprises/${slug}/copilot/billing/seats" '.seats[]' '2026-03-10' \
214244
| jq -s '.'
215245
}
216246

@@ -294,10 +324,10 @@ while IFS= read -r _login; do
294324
[[ -z "$_login" ]] && continue
295325
_login_i=$(( _login_i + 1 ))
296326
printf '\r [%d/%d] %s ' "$_login_i" "$_LOGIN_COUNT" "$_login" >&2
297-
_resp=$(gh_api \
327+
_resp=$(_copilot_api \
298328
"/enterprises/${GITHUB_ENTERPRISE}/settings/billing/ai_credit/usage?user=${_login}&year=${_BILLING_YEAR}&month=${_BILLING_MONTH}" \
299329
2>/dev/null) || _resp=""
300-
if [[ -n "$_resp" ]]; then
330+
if [[ -n "$_resp" && "${_resp}" != "__404__" && "${_resp}" != "__422__" ]]; then
301331
_credits=$(echo "$_resp" | jq '[.usageItems[]?.grossQuantity // 0] | add // 0 | round' 2>/dev/null || echo "0")
302332
# Store per-model breakdown
303333
while IFS=$'\t' read -r _model _qty; do
@@ -317,7 +347,7 @@ if [[ "$_billing_ok" == "true" ]]; then
317347
info "Loaded billing data for ${#USER_CREDITS_USED[@]} user(s)."
318348
else
319349
warn "Some billing API calls failed — credits may show 0 for affected users."
320-
warn "Ensure manage_billing:enterprise scope: gh auth refresh --scopes \"read:enterprise,manage_billing:enterprise\""
350+
warn "Ensure manage_billing:enterprise scope: set GITHUB_TOKEN with the required scopes"
321351
fi
322352

323353
# Build sorted list of all model names seen across all users

reporting/github-repo-permissions-report/github-repo-permissions-report.sh

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
# - permission : collaborators/teams and whether they can bypass approvals
1111
# - bypass_actor : explicit bypass actors from branch protection and rulesets
1212
#
13+
# Environment variables:
14+
# GITHUB_TOKEN Required. PAT with repo and read:org scope
15+
# (or provided automatically from an active gh auth session)
16+
# API_URL_PREFIX Optional. GitHub API base URL (default: https://api.github.com)
17+
#
1318
# Requirements:
14-
# - gh CLI authenticated with access to the target repository
19+
# - curl
1520
# - jq
1621
# =============================================================================
1722

@@ -21,7 +26,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2126
# shellcheck source=../../lib/github-common.sh
2227
source "${SCRIPT_DIR}/../../lib/github-common.sh"
2328

24-
API_VERSION="2022-11-28"
29+
API_URL_PREFIX=${API_URL_PREFIX:-'https://api.github.com'}
2530
REPO=""
2631
BRANCH=""
2732
OUTPUT_CSV=""
@@ -75,10 +80,10 @@ done
7580

7681
[[ -z "$REPO" ]] && err "Repository is required. Use -r OWNER/REPO"
7782

78-
require_command gh
7983
require_command jq
8084

81-
gh auth status >/dev/null 2>&1 || err "gh is not authenticated. Run: gh auth login"
85+
require_env_var GITHUB_TOKEN
86+
validate_github_token
8287

8388
TMP_DIR="$(mktemp -d)"
8489
trap 'rm -rf "$TMP_DIR"' EXIT
@@ -90,15 +95,10 @@ BP_JSON="${TMP_DIR}/branch_protection.json"
9095
RULESETS_JSON="${TMP_DIR}/rulesets.json"
9196
BYPASS_JSON="${TMP_DIR}/bypass.json"
9297

93-
gh_api() {
94-
gh api \
95-
-H "Accept: application/vnd.github+json" \
96-
-H "X-GitHub-Api-Version: ${API_VERSION}" \
97-
"$@"
98-
}
99-
10098
info "Fetching repository metadata for ${REPO}..."
101-
gh_api "/repos/${REPO}" > "$REPO_JSON"
99+
_repo_resp=$(gh_api "/repos/${REPO}")
100+
[[ "${_repo_resp}" == "__404__" ]] && err "Repository ${REPO} not found or not accessible"
101+
echo "${_repo_resp}" > "$REPO_JSON"
102102
DEFAULT_BRANCH="$(jq -r '.default_branch // empty' "$REPO_JSON")"
103103
[[ -z "$DEFAULT_BRANCH" ]] && err "Unable to determine default branch for ${REPO}"
104104

@@ -111,30 +111,24 @@ if [[ -z "$OUTPUT_CSV" ]]; then
111111
fi
112112

113113
info "Fetching collaborators..."
114-
gh_api --paginate \
115-
"/repos/${REPO}/collaborators?affiliation=all&per_page=100" \
116-
--jq '.[]' | jq -s '.' > "$COLLAB_JSON"
114+
gh_api_paginate "/repos/${REPO}/collaborators?affiliation=all&per_page=100" '.[]' \
115+
| jq -s '.' > "$COLLAB_JSON"
117116

118117
info "Fetching teams with repository access..."
119-
gh_api --paginate \
120-
"/repos/${REPO}/teams?per_page=100" \
121-
--jq '.[]' | jq -s '.' > "$TEAMS_JSON"
118+
gh_api_paginate "/repos/${REPO}/teams?per_page=100" '.[]' \
119+
| jq -s '.' > "$TEAMS_JSON"
122120

123121
info "Fetching branch protection for ${BRANCH} (if configured)..."
124-
if gh_api "/repos/${REPO}/branches/${BRANCH}/protection" > "$BP_JSON" 2>/dev/null; then
125-
:
126-
else
122+
_bp_resp=$(gh_api "/repos/${REPO}/branches/${BRANCH}/protection")
123+
if [[ "${_bp_resp}" == "__404__" || "${_bp_resp}" == "__422__" ]]; then
127124
echo '{}' > "$BP_JSON"
125+
else
126+
echo "${_bp_resp}" > "$BP_JSON"
128127
fi
129128

130129
info "Fetching repository rulesets (if configured)..."
131-
if gh_api --paginate \
132-
"/repos/${REPO}/rulesets?includes_parents=true&per_page=100" \
133-
--jq '.[]' | jq -s '.' > "$RULESETS_JSON" 2>/dev/null; then
134-
:
135-
else
136-
echo '[]' > "$RULESETS_JSON"
137-
fi
130+
gh_api_paginate "/repos/${REPO}/rulesets?includes_parents=true&per_page=100" '.[]' \
131+
| jq -s '. // []' > "$RULESETS_JSON"
138132

139133
info "Building bypass actor list (branch protection + applicable rulesets)..."
140134
jq -n \

0 commit comments

Comments
 (0)