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 ──────────────────────────────────────────────────────────────────
3842GITHUB_ENTERPRISE=" ${GITHUB_ENTERPRISE:- } "
43+ GITHUB_TOKEN=" ${GITHUB_TOKEN:- } "
44+ API_URL_PREFIX=" ${API_URL_PREFIX:- https:// api.github.com} "
3945UPN_DOMAIN=" ${UPN_DOMAIN:- } "
4046CREDITS_PER_SEAT_OVERRIDE=" ${CREDITS_PER_SEAT_OVERRIDE:- } "
4147OUTPUT_CSV=" copilot-report-$( date +%Y%m%d) .csv"
@@ -109,8 +115,9 @@ Usage: github-copilot-report.sh [OPTIONS]
109115GitHub Copilot Enterprise licence + usage report, optionally enriched with
110116Entra 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
116123Options:
@@ -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
155160require_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 ──────────────────────────────────
160166if [[ " $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."
162171elif 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."
177186fi
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() {
193224fetch_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
209241fetch_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)."
318348else
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 "
321351fi
322352
323353# Build sorted list of all model names seen across all users
0 commit comments