Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions reporting/github-copilot-report/github-copilot-report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ fetch_seats() {
}

# ── Microsoft Graph helpers ───────────────────────────────────────────────────
declare -A _GRAPH_CACHE
declare -A _GRAPH_CACHE=()

# graph_user_info EMAIL_OR_UPN
# Returns JSON: {department, jobTitle, displayName, mail}
Expand Down Expand Up @@ -278,16 +278,55 @@ print_status "Found ${SEAT_COUNT} unique licensed user(s) ($(echo "$SEATS_RAW"
# Enterprise seats endpoint has no total_seats field; use unique user count.
TOTAL_ASSIGNED_SEATS=$SEAT_COUNT

# When 0 seats are returned, probe the endpoint directly to distinguish a genuine
# empty result (HTTP 200) from a silent auth/validation failure. gh_api_paginate
# swallows 404/422, so this probe is the only way to surface those to the user.
if [[ "$SEAT_COUNT" -eq 0 ]]; then
_probe_headers=$(mktemp)
_probe_code=$(curl -s -D "$_probe_headers" -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"${API_URL_PREFIX}/enterprises/${GITHUB_ENTERPRISE}/copilot/billing/seats?per_page=1")
# X-OAuth-Scopes is only present for classic PATs; empty for fine-grained PATs / GitHub Apps.
_token_scopes=$(grep -i "^x-oauth-scopes:" "$_probe_headers" | head -1 | cut -d: -f2- | tr -d ' \r' || true)
rm -f "$_probe_headers"

case "$_probe_code" in
200)
print_status "Seats endpoint returned HTTP 200 — enterprise has 0 Copilot seats assigned."
;;
404)
# GitHub returns 404 (not 403) on billing endpoints when manage_billing:enterprise scope is absent.
if [[ -n "$_token_scopes" ]] && echo "$_token_scopes" | tr ',' '\n' | grep -q "manage_billing:enterprise"; then
print_error "HTTP 404 — scopes look correct; verify enterprise slug '${GITHUB_ENTERPRISE}'."
else
print_error "HTTP 404 — likely missing 'manage_billing:enterprise' scope (GitHub returns 404, not 403, for this)."
print_error "Fix: classic PAT with read:enterprise + manage_billing:enterprise, or: gh auth refresh --scopes manage_billing:enterprise"
[[ -n "$_token_scopes" ]] && print_error "Current scopes: ${_token_scopes}"
fi
exit 1
;;
422)
print_error "Seats endpoint returned HTTP 422 — token may lack manage_billing:enterprise scope or the slug is invalid."
exit 1
;;
*)
print_warning "Seats endpoint returned HTTP ${_probe_code} — seat data may be incomplete."
;;
esac
fi

# ── Fetch per-user AI credit consumption (billing API, current month) ──────────
# Calls GET /enterprises/{slug}/settings/billing/ai_credit/usage?user={login}
# for each licensed user and sums grossQuantity across all usageItems.
# Requires manage_billing:enterprise scope (classic/OAuth token; not fine-grained PATs).
_BILLING_YEAR=$(date +%Y)
_BILLING_MONTH=$(( 10#$(date +%m) )) # strip leading zero (macOS compatible)
print_status "Fetching per-user AI credit consumption (billing API, ${_BILLING_YEAR}-$(printf '%02d' $_BILLING_MONTH))..."
declare -A USER_CREDITS_USED
declare -A USER_MODEL_CREDITS # key: "login|model" value: credits
declare -A _ALL_MODELS_SET # keys are unique model names seen
declare -A USER_CREDITS_USED=()
declare -A USER_MODEL_CREDITS=() # key: "login|model" value: credits
declare -A _ALL_MODELS_SET=() # keys are unique model names seen
_billing_ok=true
_billing_fail_statuses=""

Expand Down Expand Up @@ -365,8 +404,8 @@ printf '%s\n' \
> "$OUTPUT_CSV"

# ── Per-department counters ───────────────────────────────────────────────────
declare -A DEPT_USERS
declare -A DEPT_CREDITS
declare -A DEPT_USERS=()
declare -A DEPT_CREDITS=()
TOTAL_CREDITS=0

# ── Process each seat ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -451,6 +490,7 @@ echo ""
printf " %-38s %7s %16s\n" "Department" "Users" "Allocated Credits"
printf " %-38s %7s %16s\n" "──────────────────────────────────────" "───────" "────────────────"
while IFS= read -r dept; do
[[ -z "$dept" ]] && continue # bash 5.3: ${!assoc[@]} on empty array expands to ""
printf " %-38s %7d %16d\n" "$dept" "${DEPT_USERS[$dept]:-0}" "${DEPT_CREDITS[$dept]:-0}"
done < <(printf '%s\n' "${!DEPT_USERS[@]}" | sort)
printf " %-38s %7s %16s\n" "──────────────────────────────────────" "───────" "────────────────"
Expand Down