Skip to content

Commit e5a17af

Browse files
locus313Patrick Lewis
andauthored
fix(scripts): BSD head portability and restore billing and Entra ID features (#23)
* fix(lib): replace head -n -1 with sed $'d' for BSD head portability GNU head supports negative line counts (head -n -1 = all but last line) but BSD head on macOS does not, producing 'illegal line count' errors. Replace with sed '$d' which deletes the last line portably on both BSD and GNU sed. Affected: gh_api and _graphql_enterprise_orgs in lib/github-common.sh. * fix(copilot-report): restore billing and Entra ID after dual-auth refactor Three regressions introduced by the d45fe33 dual-auth refactor are fixed: Billing API (credits used per user): - _copilot_api used head -n -1 which fails on macOS BSD head; now uses sed '$d' (fixed at lib level, copied to _copilot_api in this script). - HTTP 404 and 422 responses from the billing endpoint are valid — they mean no usage data this month — treat them as 0 credits instead of marking the user as a failure. - Remove _billing_api() gh-CLI wrapper: now that lib syncs GH_TOKEN from GITHUB_TOKEN automatically, both curl and gh api use the same token, so the wrapper adds complexity without benefit. Entra ID department enrichment: - Auto-resolve the Azure AD tenant GUID from UPN_DOMAIN via OIDC discovery (GET login.microsoftonline.com/{domain}/.well-known/ openid-configuration) so --entra-tenant is rarely needed. - Switch graph_user_info from az rest to curl with the explicit GRAPH_TOKEN so the correct tenant's token is always used. - Add --entra-tenant flag and $ENTRA_TENANT env var as an override when OIDC discovery resolves to the wrong tenant. action.yml: update entra-tenant description to reflect auto-resolution. --------- Co-authored-by: Patrick Lewis <plewis@ontinue.com>
1 parent c73ac35 commit e5a17af

3 files changed

Lines changed: 62 additions & 14 deletions

File tree

lib/github-common.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ gh_api() {
183183
-H "X-GitHub-Api-Version: 2022-11-28" \
184184
"$@" "${url}")
185185
http_code=$(echo "${body}" | tail -1)
186-
body=$(echo "${body}" | head -n -1)
186+
body=$(echo "${body}" | sed '$d')
187187

188188
case "${http_code}" in
189189
200) echo "${body}"; return 0 ;;
@@ -327,7 +327,7 @@ _graphql_enterprise_orgs() {
327327
-d "${query}")
328328

329329
http_code=$(echo "${resp}" | tail -1)
330-
body=$(echo "${resp}" | head -n -1)
330+
body=$(echo "${resp}" | sed '$d')
331331

332332
if [[ "${http_code}" != "200" ]]; then
333333
return 1
@@ -408,3 +408,11 @@ if [[ -z "${GITHUB_TOKEN:-}" ]] && command -v gh &>/dev/null; then
408408
fi
409409
unset _gh_resolved_token
410410
fi
411+
# Keep GH_TOKEN in sync with GITHUB_TOKEN so that any script can call
412+
# 'gh api' directly (e.g. for endpoints that require scopes beyond what
413+
# a raw token carries, or simply to reuse the gh CLI auth session).
414+
# Only set when gh CLI is present and GH_TOKEN is not already provided.
415+
if [[ -n "${GITHUB_TOKEN:-}" ]] && command -v gh &>/dev/null && [[ -z "${GH_TOKEN:-}" ]]; then
416+
GH_TOKEN="$GITHUB_TOKEN"
417+
export GH_TOKEN
418+
fi

reporting/github-copilot-report/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ inputs:
1111
description: 'Email domain for Entra ID lookup when GitHub carries no email address (e.g. example.com)'
1212
required: false
1313
default: ''
14+
entra-tenant:
15+
description: 'Override the Azure AD tenant ID for the Graph API lookup. Auto-resolved from upn-domain via OIDC discovery; only needed when that result is incorrect.'
16+
required: false
17+
default: ''
1418
credits:
1519
description: 'Override credits-per-seat value if your portal shows a different pool size'
1620
required: false
@@ -32,6 +36,7 @@ runs:
3236
GITHUB_TOKEN: ${{ inputs.github-token }}
3337
GITHUB_ENTERPRISE: ${{ inputs.enterprise }}
3438
UPN_DOMAIN: ${{ inputs.upn-domain }}
39+
ENTRA_TENANT: ${{ inputs.entra-tenant }}
3540
CREDITS_PER_SEAT_OVERRIDE: ${{ inputs.credits }}
3641
run: |
3742
ARGS=()

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

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ GITHUB_ENTERPRISE="${GITHUB_ENTERPRISE:-}"
4343
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
4444
API_URL_PREFIX="${API_URL_PREFIX:-https://api.github.com}"
4545
UPN_DOMAIN="${UPN_DOMAIN:-}"
46+
ENTRA_TENANT="${ENTRA_TENANT:-}"
4647
CREDITS_PER_SEAT_OVERRIDE="${CREDITS_PER_SEAT_OVERRIDE:-}"
4748
OUTPUT_CSV="copilot-report-$(date +%Y%m%d).csv"
4849
NO_ENTRA=false
@@ -125,6 +126,11 @@ Options:
125126
-d, --upn-domain DOM Email domain for Entra lookup when GitHub carries no
126127
email address (or $UPN_DOMAIN, e.g. example.com)
127128
Login 'john_example' + domain 'example.com' → john@example.com
129+
The tenant ID is auto-resolved from this domain via
130+
OIDC discovery, so --entra-tenant is rarely needed.
131+
--entra-tenant ID Override the Azure AD tenant ID for the Graph lookup
132+
(or $ENTRA_TENANT). Auto-resolved from --upn-domain
133+
when not set; only needed to override that result.
128134
--credits N Override credits-per-seat value (or $CREDITS_PER_SEAT_OVERRIDE)
129135
Use if your portal shows a different pool size than expected
130136
--output FILE Output CSV (default: copilot-report-YYYYMMDD.csv)
@@ -143,8 +149,9 @@ EOF
143149
# ── Argument parsing ──────────────────────────────────────────────────────────
144150
while [[ $# -gt 0 ]]; do
145151
case "$1" in
146-
-e|--enterprise) GITHUB_ENTERPRISE="$2"; shift 2 ;;
152+
-e|--enterprise) GITHUB_ENTERPRISE="$2"; shift 2 ;;
147153
-d|--upn-domain) UPN_DOMAIN="$2"; shift 2 ;;
154+
--entra-tenant) ENTRA_TENANT="$2"; shift 2 ;;
148155
--credits) CREDITS_PER_SEAT_OVERRIDE="$2"; shift 2 ;;
149156
--output) OUTPUT_CSV="$2"; shift 2 ;;
150157
--no-entra) NO_ENTRA=true; shift ;;
@@ -169,16 +176,36 @@ elif ! command -v az &>/dev/null; then
169176
warn "az CLI is not installed — department/division grouping will be skipped."
170177
warn "Install the Azure CLI and run 'az login' to enable Entra ID enrichment, or pass --no-entra."
171178
elif az account show &>/dev/null 2>&1; then
179+
# Auto-resolve tenant ID from UPN domain via OIDC discovery when not explicitly set.
180+
# GET https://login.microsoftonline.com/{domain}/.well-known/openid-configuration
181+
# returns an issuer like https://sts.windows.net/{tenant-id}/ — extract the GUID.
182+
if [[ -z "$ENTRA_TENANT" && -n "$UPN_DOMAIN" ]]; then
183+
_resolved_tenant=$(curl -sf \
184+
"https://login.microsoftonline.com/${UPN_DOMAIN}/.well-known/openid-configuration" \
185+
2>/dev/null \
186+
| jq -r '.issuer // empty' \
187+
| grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') || true
188+
if [[ -n "$_resolved_tenant" ]]; then
189+
ENTRA_TENANT="$_resolved_tenant"
190+
info "Auto-resolved Entra tenant '${ENTRA_TENANT}' from domain '${UPN_DOMAIN}'."
191+
else
192+
warn "Could not resolve tenant from domain '${UPN_DOMAIN}' — using default az tenant."
193+
fi
194+
fi
172195
info "Acquiring Microsoft Graph token via az CLI..."
173196
GRAPH_TOKEN=$(az account get-access-token \
174197
--resource https://graph.microsoft.com \
198+
${ENTRA_TENANT:+--tenant "$ENTRA_TENANT"} \
175199
--query accessToken -o tsv 2>/dev/null) || true
176200
if [[ -n "$GRAPH_TOKEN" ]]; then
177201
ENTRA_ENABLED=true
178-
info "Graph token acquired."
202+
[[ -n "$ENTRA_TENANT" ]] && info "Graph token acquired (tenant: ${ENTRA_TENANT})." \
203+
|| info "Graph token acquired."
179204
else
180205
warn "az is logged in but could not get a Graph token — department lookup disabled."
181206
warn "Ensure your account has User.Read.All permission in the target tenant."
207+
[[ -z "$ENTRA_TENANT" ]] && \
208+
warn "If you have multiple tenants, try: --entra-tenant <TENANT_ID>"
182209
fi
183210
else
184211
warn "az CLI is not logged in — department/division grouping will be skipped."
@@ -201,7 +228,7 @@ _copilot_api() {
201228
-H "X-GitHub-Api-Version: 2026-03-10" \
202229
"$@" "${url}")
203230
_http_code=$(echo "${_body}" | tail -1)
204-
_body=$(echo "${_body}" | head -n -1)
231+
_body=$(echo "${_body}" | sed '$d')
205232
case "${_http_code}" in
206233
200) echo "${_body}"; return 0 ;;
207234
404) echo "__404__"; return 0 ;;
@@ -262,13 +289,14 @@ graph_user_info() {
262289
return
263290
fi
264291

265-
# az rest reuses the active az session and handles token refresh automatically
292+
# Use curl with the explicit GRAPH_TOKEN for reliable auth and proper URL encoding.
266293
local resp
267-
resp=$(az rest \
268-
--method GET \
269-
--url "https://graph.microsoft.com/v1.0/users" \
270-
--url-parameters "\$filter=mail eq '${query}' or userPrincipalName eq '${query}'" \
271-
"\$select=displayName,department,jobTitle,mail,userPrincipalName" \
294+
resp=$(curl -sfG \
295+
-H "Authorization: Bearer ${GRAPH_TOKEN}" \
296+
-H "ConsistencyLevel: eventual" \
297+
--data-urlencode "\$filter=mail eq '${query}' or userPrincipalName eq '${query}'" \
298+
--data-urlencode "\$select=displayName,department,jobTitle,mail,userPrincipalName" \
299+
"https://graph.microsoft.com/v1.0/users" \
272300
2>/dev/null) || resp='{"value":[]}'
273301

274302
local result
@@ -316,6 +344,7 @@ declare -A USER_CREDITS_USED
316344
declare -A USER_MODEL_CREDITS # key: "login|model" value: credits
317345
declare -A _ALL_MODELS_SET # keys are unique model names seen
318346
_billing_ok=true
347+
_billing_fail_statuses=""
319348

320349
_ALL_LOGINS=$(echo "$SEATS" | jq -r '.[].assignee.login')
321350
_LOGIN_COUNT=$(echo "$_ALL_LOGINS" | wc -l | tr -d ' ')
@@ -325,9 +354,12 @@ while IFS= read -r _login; do
325354
_login_i=$(( _login_i + 1 ))
326355
printf '\r [%d/%d] %s ' "$_login_i" "$_LOGIN_COUNT" "$_login" >&2
327356
_resp=$(_copilot_api \
328-
"/enterprises/${GITHUB_ENTERPRISE}/settings/billing/ai_credit/usage?user=${_login}&year=${_BILLING_YEAR}&month=${_BILLING_MONTH}" \
329-
2>/dev/null) || _resp=""
330-
if [[ -n "$_resp" && "${_resp}" != "__404__" && "${_resp}" != "__422__" ]]; then
357+
"/enterprises/${GITHUB_ENTERPRISE}/settings/billing/ai_credit/usage?user=${_login}&year=${_BILLING_YEAR}&month=${_BILLING_MONTH}") || _resp=""
358+
if [[ "${_resp}" == "__404__" || "${_resp}" == "__422__" ]]; then
359+
# No usage data this month — valid, treat as 0 credits.
360+
_credits="0"
361+
elif [[ -n "$_resp" ]]; then
362+
# Success: parse credit usage
331363
_credits=$(echo "$_resp" | jq '[.usageItems[]?.grossQuantity // 0] | add // 0 | round' 2>/dev/null || echo "0")
332364
# Store per-model breakdown
333365
while IFS=$'\t' read -r _model _qty; do
@@ -338,6 +370,7 @@ while IFS= read -r _login; do
338370
else
339371
_credits="0"
340372
_billing_ok=false
373+
_billing_fail_statuses="${_billing_fail_statuses} ${_login}(empty)"
341374
fi
342375
USER_CREDITS_USED["$_login"]="$_credits"
343376
done <<< "$_ALL_LOGINS"
@@ -348,6 +381,8 @@ if [[ "$_billing_ok" == "true" ]]; then
348381
else
349382
warn "Some billing API calls failed — credits may show 0 for affected users."
350383
warn "Ensure manage_billing:enterprise scope: set GITHUB_TOKEN with the required scopes"
384+
[[ -n "$_billing_fail_statuses" ]] && \
385+
warn "Failed users:${_billing_fail_statuses}"
351386
fi
352387

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

0 commit comments

Comments
 (0)