Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ output.txt
# Python bytecode/cache
**/__pycache__/
**/*.pyc

# macOS
.DS_Store
**/.DS_Store
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ cd reporting/github-repo-permissions-report

**Script:** `reporting/github-copilot-report/github-copilot-report.sh`

Generates a GitHub Copilot Enterprise licence and usage report. Shows every licensed user, their plan type, pool credit contribution, and actual AI credit consumption for the current billing month. Optionally enriches data with Entra ID department information.
Generates a GitHub Copilot Enterprise licence and usage report. Shows every licensed user, their plan type, pool credit contribution, and actual AI credit consumption for the current billing month, plus their effective AI credit budget (Individual override, else Universal) and how much of it has been consumed. Optionally enriches data with Entra ID department information.

**Prerequisites:**
- **[curl](https://curl.se)** — HTTP client
Expand Down Expand Up @@ -606,6 +606,7 @@ az login # optional; needed only for Entra ID department enrichment
./github-copilot-report.sh -e YOUR-ENTERPRISE
./github-copilot-report.sh -e YOUR-ENTERPRISE -d example.com
./github-copilot-report.sh -e YOUR-ENTERPRISE --no-entra
./github-copilot-report.sh -e YOUR-ENTERPRISE --no-budgets
./github-copilot-report.sh -e YOUR-ENTERPRISE --credits 1900 --output report.csv
```

Expand All @@ -618,10 +619,12 @@ az login # optional; needed only for Entra ID department enrichment
| `--credits N` | Override credits-per-seat value (or `$CREDITS_PER_SEAT_OVERRIDE`) | Auto-detected |
| `--output FILE` | Output CSV filename | `copilot-report-YYYYMMDD.csv` |
| `--no-entra` | Skip Entra ID department lookup | — |
| `--no-budgets` | Skip per-user AI credit budget lookup (Universal/Individual) | — |

**What it does:**
- Fetches all Copilot seats across the enterprise (deduplicated by user)
- Fetches per-user AI credit consumption for the current billing month
- Fetches enterprise-level AI credit budgets (Universal and Individual overrides) and each user's consumed amount against them
- Fetches enterprise-level model usage metrics (last 28 days)
- Optionally enriches each user with Entra ID department and job title via `az rest`
- Outputs a CSV and a formatted console summary with department breakdown and model usage tables
Expand All @@ -630,7 +633,7 @@ az login # optional; needed only for Entra ID department enrichment
> Requires a PAT with `read:enterprise` and `manage_billing:enterprise` scopes — the built-in `GITHUB_TOKEN` cannot grant enterprise-level access. Set `GITHUB_TOKEN` before executing (or have an active `gh` auth session with those scopes so the lib can auto-resolve the token).

> [!NOTE]
> The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`.
> The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`. Budget lookups require the token owner to be an enterprise admin or billing manager, and can be disabled with `--no-budgets`.

---

Expand Down
5 changes: 5 additions & 0 deletions reporting/github-copilot-report/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ inputs:
description: 'Skip Entra ID department lookup'
required: false
default: 'false'
no-budgets:
description: 'Skip per-user AI credit budget lookup (Universal/Individual)'
required: false
default: 'false'
runs:
using: composite
steps:
Expand All @@ -42,4 +46,5 @@ runs:
ARGS=()
[[ -n "${{ inputs.output }}" ]] && ARGS+=(--output "${{ inputs.output }}")
[[ "${{ inputs.no-entra }}" == "true" ]] && ARGS+=(--no-entra)
[[ "${{ inputs.no-budgets }}" == "true" ]] && ARGS+=(--no-budgets)
"${{ github.action_path }}/github-copilot-report.sh" "${ARGS[@]}"
109 changes: 107 additions & 2 deletions reporting/github-copilot-report/github-copilot-report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
# • Every user with a Copilot seat, their plan type, and their pool contribution
# (credits per assigned seat — credits are pooled at the enterprise level)
# • Actual per-user AI credit consumption this month (from billing API)
# • Each user's effective AI credit budget (Individual override, else Universal)
# and how much of it has been consumed (from the Budgets API)
# • Users grouped by Entra ID department
# • Enterprise-level model usage breakdown
#
Expand All @@ -30,6 +32,19 @@
#
# Requires API version 2026-03-10 and the new Copilot usage metrics endpoints.
# The legacy /copilot/metrics and /copilot/usage endpoints were closed Apr 2, 2026.
#
# User-level AI credit budgets (GitHub Budgets API, enterprise-scoped):
# GET /enterprises/{enterprise}/settings/billing/budgets
# GET /enterprises/{enterprise}/settings/billing/budgets/{budget_id}/user-states
# Universal budget (budget_scope=multi_user_customer) applies to every user
# unless overridden.
# Individual budget (budget_scope=user) overrides the Universal budget for
# one specific user.
# Both are configured via Enterprise → Policies → Copilot → Budgets → Users.
# The user-states endpoint returns each user's effective target/consumed
# amount plus an override_budget_id when an Individual budget applies.
# Requires the token owner to be an enterprise admin or billing manager.
# Use --no-budgets to skip.
# =============================================================================

set -euo pipefail
Expand All @@ -47,6 +62,7 @@ ENTRA_TENANT="${ENTRA_TENANT:-}"
CREDITS_PER_SEAT_OVERRIDE="${CREDITS_PER_SEAT_OVERRIDE:-}"
OUTPUT_CSV="copilot-report-$(date +%Y%m%d).csv"
NO_ENTRA=false
NO_BUDGETS=false
GRAPH_TOKEN=""
ENTRA_ENABLED=false

Expand Down Expand Up @@ -113,6 +129,7 @@ Options:
Use if your portal shows a different pool size than expected
--output FILE Output CSV (default: copilot-report-YYYYMMDD.csv)
--no-entra Skip Entra ID department lookup
--no-budgets Skip per-user AI credit budget lookup (Universal/Individual)
-h, --help Show this message

Required GITHUB_TOKEN scopes:
Expand All @@ -133,6 +150,7 @@ while [[ $# -gt 0 ]]; do
--credits) CREDITS_PER_SEAT_OVERRIDE="$2"; shift 2 ;;
--output) OUTPUT_CSV="$2"; shift 2 ;;
--no-entra) NO_ENTRA=true; shift ;;
--no-budgets) NO_BUDGETS=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
Expand Down Expand Up @@ -368,6 +386,82 @@ else
print_warning "Failed users:${_billing_fail_statuses}"
fi

# ── Fetch enterprise AI credit budgets (Budgets API: Universal / Individual) ─
# Two calls total (each paginated), not one per user:
# 1. GET /enterprises/{enterprise}/settings/billing/budgets — locates the
# Universal (multi_user_customer) budget and any direct Individual
# (user-scope) budgets.
# 2. GET .../budgets/{universal_id}/user-states — per-user consumed/target
# amounts against the Universal budget; override_budget_id marks users
# who have an Individual override.
declare -A USER_BUDGET_SCOPE=() # "Individual" | "Universal" | "None"
declare -A USER_BUDGET_AMOUNT=() # USD
declare -A USER_BUDGET_USED=() # USD

if [[ "$NO_BUDGETS" == "true" ]]; then
print_warning "AI credit budget lookup disabled (--no-budgets)."
else
print_status "Fetching enterprise AI credit budgets (Universal / Individual)..."

_all_budgets="[]"
_page=1
while :; do
_bresp=$(gh_api "/enterprises/${GITHUB_ENTERPRISE}/settings/billing/budgets?per_page=100&page=${_page}" --api-version 2026-03-10) || _bresp=""
[[ "${_bresp}" == "__404__" || "${_bresp}" == "__422__" || -z "$_bresp" ]] && break
_all_budgets=$(jq -cn --argjson a "$_all_budgets" --argjson b "$(echo "$_bresp" | jq -c '.budgets // []')" '$a + $b')
[[ "$(echo "$_bresp" | jq -r '.has_next_page // false')" == "true" ]] || break
_page=$(( _page + 1 ))
done

_ai_budgets=$(echo "$_all_budgets" | jq -c '[.[] | select(.budget_product_sku=="ai_credits")]')

# Direct Individual (user-scope) budgets — covers the case where no Universal budget exists.
while IFS=$'\t' read -r _u _amt _used; do
[[ -z "$_u" ]] && continue
USER_BUDGET_SCOPE["$_u"]="Individual"
USER_BUDGET_AMOUNT["$_u"]="$_amt"
USER_BUDGET_USED["$_u"]="$_used"
done < <(echo "$_ai_budgets" | jq -r '.[] | select(.budget_scope=="user") | [.user, .budget_amount, (.effective_budget.consumed_amount // .consumed_amount // 0)] | @tsv')

_universal_id=$(echo "$_ai_budgets" | jq -r '[.[] | select(.budget_scope=="multi_user_customer")][0].id // empty')
_universal_amount=$(echo "$_ai_budgets" | jq -r '[.[] | select(.budget_scope=="multi_user_customer")][0].budget_amount // empty')

if [[ -n "$_universal_id" ]]; then
_page=1
while :; do
_sresp=$(gh_api "/enterprises/${GITHUB_ENTERPRISE}/settings/billing/budgets/${_universal_id}/user-states?per_page=100&page=${_page}" --api-version 2026-03-10) || _sresp=""
[[ "${_sresp}" == "__404__" || "${_sresp}" == "__422__" || -z "$_sresp" ]] && break
while IFS=$'\t' read -r _u _consumed _target _override; do
[[ -z "$_u" ]] && continue
if [[ -n "$_override" && "$_override" != "null" ]]; then
USER_BUDGET_SCOPE["$_u"]="Individual"
else
USER_BUDGET_SCOPE["$_u"]="Universal"
fi
USER_BUDGET_AMOUNT["$_u"]="$_target"
USER_BUDGET_USED["$_u"]="$_consumed"
done < <(echo "$_sresp" | jq -r '.user_states[]? | [.user, .consumed_amount, .target_amount, (.override_budget_id // "")] | @tsv')
[[ "$(echo "$_sresp" | jq -r '.has_next_page // false')" == "true" ]] || break
_page=$(( _page + 1 ))
done

# Licensed users covered by the Universal budget but absent from user-states (no usage yet)
while IFS= read -r _login; do
[[ -z "$_login" ]] && continue
[[ -n "${USER_BUDGET_SCOPE[$_login]+_}" ]] && continue
USER_BUDGET_SCOPE["$_login"]="Universal"
USER_BUDGET_AMOUNT["$_login"]="$_universal_amount"
USER_BUDGET_USED["$_login"]="0"
done <<< "$_ALL_LOGINS"
fi

if [[ "${#USER_BUDGET_SCOPE[@]}" -gt 0 ]]; then
print_status "Loaded budget data for ${#USER_BUDGET_SCOPE[@]} user(s)."
else
print_warning "No enterprise AI credit budgets are configured (Enterprise → Policies → Copilot → Budgets), or the token lacks enterprise admin/billing manager access."
fi
fi

# Build sorted list of all model names seen across all users
ALL_MODELS=()
while IFS= read -r _m; do
Expand Down Expand Up @@ -400,7 +494,7 @@ fi
# ── Write CSV header ──────────────────────────────────────────────────────────
_model_header_cols=$(printf ',"Credits: %s"' "${ALL_MODELS[@]}")
printf '%s\n' \
"GitHub Login,Display Name,Email / UPN,Organisation,Plan Type,Pool Contribution (Credits/Seat),AI Credits Used (Month)${_model_header_cols},Department,Job Title,Last Active,Last Editor" \
"GitHub Login,Display Name,Email / UPN,Organisation,Plan Type,Pool Contribution (Credits/Seat),AI Credits Used (Month),Budget Scope,Budget Limit (USD),Budget Used (USD),Budget Remaining (USD)${_model_header_cols},Department,Job Title,Last Active,Last Editor" \
> "$OUTPUT_CSV"

# ── Per-department counters ───────────────────────────────────────────────────
Expand All @@ -423,6 +517,13 @@ while IFS= read -r seat; do
credits=$(plan_credits "$plan")
credits_used="${USER_CREDITS_USED[$login]:-}"

budget_scope="${USER_BUDGET_SCOPE[$login]:-None}"
budget_amount="${USER_BUDGET_AMOUNT[$login]:-}"
budget_used="${USER_BUDGET_USED[$login]:-}"
budget_remaining=""
[[ "$budget_scope" != "None" ]] && \
budget_remaining=$(awk -v a="${budget_amount:-0}" -v u="${budget_used:-0}" 'BEGIN{printf "%.2f", a-u}')

# Prefer email; fall back to UPN derived from login + --upn-domain
if [[ -n "$email" ]]; then
lookup="$email"
Expand All @@ -439,14 +540,18 @@ while IFS= read -r seat; do
[[ -z "$disp" ]] && disp="$gh_name"

# Append to CSV — double-quote all fields, escape embedded quotes
printf '"%s","%s","%s","%s","%s","%s","%s"' \
printf '"%s","%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"' \
"${login//\"/\"\"}" \
"${disp//\"/\"\"}" \
"${lookup//\"/\"\"}" \
"${org//\"/\"\"}" \
"${plan//\"/\"\"}" \
"$credits" \
"${credits_used}" \
"${budget_scope}" \
"${budget_amount}" \
"${budget_used}" \
"${budget_remaining}" \
>> "$OUTPUT_CSV"
# Per-model credit columns (0 if user had no usage on that model)
for _m in "${ALL_MODELS[@]}"; do
Expand Down
6 changes: 6 additions & 0 deletions tests/test_script_validation.bats
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,12 @@ _run_script() {
[ "$status" -eq 0 ]
}

@test "github-copilot-report: --no-budgets is recognised (fails at token check)" {
_run_script "${REPO_ROOT}/reporting/github-copilot-report/github-copilot-report.sh" "unset GITHUB_TOKEN;" "--no-budgets"
[ "$status" -eq 1 ]
[[ "$output" != *"Unknown option"* ]]
}

# ═══════════════════════════════════════════════════════════════════════════════
# personal/github-organize-stars
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down