@@ -217,7 +217,7 @@ fetch_seats() {
217217}
218218
219219# ── Microsoft Graph helpers ───────────────────────────────────────────────────
220- declare -A _GRAPH_CACHE
220+ declare -A _GRAPH_CACHE=()
221221
222222# graph_user_info EMAIL_OR_UPN
223223# Returns JSON: {department, jobTitle, displayName, mail}
@@ -278,16 +278,55 @@ print_status "Found ${SEAT_COUNT} unique licensed user(s) ($(echo "$SEATS_RAW"
278278# Enterprise seats endpoint has no total_seats field; use unique user count.
279279TOTAL_ASSIGNED_SEATS=$SEAT_COUNT
280280
281+ # When 0 seats are returned, probe the endpoint directly to distinguish a genuine
282+ # empty result (HTTP 200) from a silent auth/validation failure. gh_api_paginate
283+ # swallows 404/422, so this probe is the only way to surface those to the user.
284+ if [[ " $SEAT_COUNT " -eq 0 ]]; then
285+ _probe_headers=$( mktemp)
286+ _probe_code=$( curl -s -D " $_probe_headers " -o /dev/null -w " %{http_code}" \
287+ -H " Authorization: Bearer ${GITHUB_TOKEN} " \
288+ -H " Accept: application/vnd.github+json" \
289+ -H " X-GitHub-Api-Version: 2026-03-10" \
290+ " ${API_URL_PREFIX} /enterprises/${GITHUB_ENTERPRISE} /copilot/billing/seats?per_page=1" )
291+ # X-OAuth-Scopes is only present for classic PATs; empty for fine-grained PATs / GitHub Apps.
292+ _token_scopes=$( grep -i " ^x-oauth-scopes:" " $_probe_headers " | head -1 | cut -d: -f2- | tr -d ' \r' || true)
293+ rm -f " $_probe_headers "
294+
295+ case " $_probe_code " in
296+ 200)
297+ print_status " Seats endpoint returned HTTP 200 — enterprise has 0 Copilot seats assigned."
298+ ;;
299+ 404)
300+ # GitHub returns 404 (not 403) on billing endpoints when manage_billing:enterprise scope is absent.
301+ if [[ -n " $_token_scopes " ]] && echo " $_token_scopes " | tr ' ,' ' \n' | grep -q " manage_billing:enterprise" ; then
302+ print_error " HTTP 404 — scopes look correct; verify enterprise slug '${GITHUB_ENTERPRISE} '."
303+ else
304+ print_error " HTTP 404 — likely missing 'manage_billing:enterprise' scope (GitHub returns 404, not 403, for this)."
305+ print_error " Fix: classic PAT with read:enterprise + manage_billing:enterprise, or: gh auth refresh --scopes manage_billing:enterprise"
306+ [[ -n " $_token_scopes " ]] && print_error " Current scopes: ${_token_scopes} "
307+ fi
308+ exit 1
309+ ;;
310+ 422)
311+ print_error " Seats endpoint returned HTTP 422 — token may lack manage_billing:enterprise scope or the slug is invalid."
312+ exit 1
313+ ;;
314+ * )
315+ print_warning " Seats endpoint returned HTTP ${_probe_code} — seat data may be incomplete."
316+ ;;
317+ esac
318+ fi
319+
281320# ── Fetch per-user AI credit consumption (billing API, current month) ──────────
282321# Calls GET /enterprises/{slug}/settings/billing/ai_credit/usage?user={login}
283322# for each licensed user and sums grossQuantity across all usageItems.
284323# Requires manage_billing:enterprise scope (classic/OAuth token; not fine-grained PATs).
285324_BILLING_YEAR=$( date +%Y)
286325_BILLING_MONTH=$(( 10 #$(date +% m) )) # strip leading zero (macOS compatible)
287326print_status " Fetching per-user AI credit consumption (billing API, ${_BILLING_YEAR} -$( printf ' %02d' $_BILLING_MONTH ) )..."
288- declare -A USER_CREDITS_USED
289- declare -A USER_MODEL_CREDITS # key: "login|model" value: credits
290- declare -A _ALL_MODELS_SET # keys are unique model names seen
327+ declare -A USER_CREDITS_USED=()
328+ declare -A USER_MODEL_CREDITS=() # key: "login|model" value: credits
329+ declare -A _ALL_MODELS_SET=() # keys are unique model names seen
291330_billing_ok=true
292331_billing_fail_statuses=" "
293332
@@ -365,8 +404,8 @@ printf '%s\n' \
365404 > " $OUTPUT_CSV "
366405
367406# ── Per-department counters ───────────────────────────────────────────────────
368- declare -A DEPT_USERS
369- declare -A DEPT_CREDITS
407+ declare -A DEPT_USERS=()
408+ declare -A DEPT_CREDITS=()
370409TOTAL_CREDITS=0
371410
372411# ── Process each seat ────────────────────────────────────────────────────────
@@ -451,6 +490,7 @@ echo ""
451490printf " %-38s %7s %16s\n" " Department" " Users" " Allocated Credits"
452491printf " %-38s %7s %16s\n" " ──────────────────────────────────────" " ───────" " ────────────────"
453492while IFS= read -r dept; do
493+ [[ -z " $dept " ]] && continue # bash 5.3: ${!assoc[@]} on empty array expands to ""
454494 printf " %-38s %7d %16d\n" " $dept " " ${DEPT_USERS[$dept]:- 0} " " ${DEPT_CREDITS[$dept]:- 0} "
455495done < <( printf ' %s\n' " ${! DEPT_USERS[@]} " | sort)
456496printf " %-38s %7s %16s\n" " ──────────────────────────────────────" " ───────" " ────────────────"
0 commit comments