This document describes how to attribute and charge back all GitHub Enterprise Cloud (EMU) costs — licenses, GitHub Copilot, GitHub Actions, Codespaces, Packages, Git LFS, GitHub Advanced Security, premium requests, and self-hosted runner infrastructure — to internal lines of business (LOBs).
The design is grounded in the GitHub-native Cost Centers capability on the enhanced billing platform and is tuned for the constraints captured during requirements gathering: an EMU enterprise, a single monolithic organization, IdP-managed group membership, repository custom properties, and the goal of true chargeback to per-LOB Azure subscriptions.
Document status
- Last reviewed: 2026-05-19
- Authorship: Drafted with AI assistance (GitHub Copilot, multi-model review) and reviewed by a human maintainer before publication.
- Sources: Based on public documentation — primarily docs.github.com, learn.microsoft.com, and official vendor blogs cited inline.
- Verify before acting: GitHub and Microsoft update product documentation continuously. Re-confirm against the live source pages before relying on this content for production decisions.
Owner: Platform / GitHub Admin team Status: Design — pending stakeholder review
This design routes every in-scope GitHub-billed cost — Enterprise and Copilot licenses, Copilot premium requests, Actions, Codespaces, Packages, LFS, Advanced Security, and self-hosted runner infrastructure — to exactly one line of business (LOB), then bills each LOB on its own Azure subscription through GitHub's native Cost Centers capability. GitHub Marketplace apps and other spend that GitHub does not allow on a cost center are handled separately (see Risks and Edge Cases).
Core idea in one sentence: identity-driven user assignment plus repository-property-driven repo assignment populate a per-LOB cost center, which is bound to a per-LOB Azure subscription for direct chargeback, with a thin warehouse layer providing multi-year reporting, audit, and budget governance.
Approach at a glance:
- Two assignment levers, one source-of-truth per lever. IdP group membership (mirrored into GitHub teams) drives user assignment for license-based products (Enterprise seats, Copilot, GHAS, premium requests). Repository custom properties drive repo assignment for usage-based products (Actions, Codespaces, Packages, LFS).
- One cost center per LOB. Sub-LOB granularity stays in finance, not in GitHub. Three reserved catch-alls track different operational states:
00 - Shared Platform,98 - Pending Onboarding,99 - Attribution Defect. - True chargeback via Azure subscription linkage. Each LOB cost center is attached to that LOB's Azure subscription so GitHub-metered usage invoices the LOB directly. The attach step is UI-only and remains a manual onboarding gate.
- Summarized REST data for the chargeback report; restricted per-user audit table for waste and anomaly detection. Detailed UI-only reports stay out of the automated pipeline.
- Reuse before build. Adopt github/cost-center-automation for sync, budgets, and PRU tracking; wrap it with a thin internal layer for reconciliation, runner cost merging, and approval workflow.
- Warehouse choice deferred but constrained. Four landing-zone options (Fabric + FinOps Toolkit, ADLS + Synapse/Databricks, Power BI direct, existing internal FinOps platform) all feed from the same daily exports; whichever is chosen must meet a minimum binding contract (raw immutable exports, replayable close, 7-year retention).
flowchart LR
subgraph Sources["Sources of truth"]
IDP["Entra ID<br/>LOB groups"]
REPO["Repository<br/>custom property <code>lob</code>"]
META["config/lobs.yaml<br/>LOB metadata"]
AZSUB["Per-LOB Azure<br/>subscription"]
end
subgraph Sync["Sync layer"]
TEAM["GitHub teams<br/>chargeback-lob-<slug>"]
AUTO["github/cost-center-automation<br/>+ thin internal wrapper"]
end
subgraph GitHub["GitHub Enterprise Cloud (EMU)"]
CC["Cost Centers<br/>1 per LOB + 3 catch-alls"]
BUDGET["Budgets<br/>per cost center"]
INVOICE["GitHub invoice<br/>routed per CC"]
end
subgraph Outputs["Reporting & outputs"]
WH["Warehouse<br/>(Fabric / ADLS / PBI / FinOps platform)"]
REPORT["Monthly chargeback report<br/>LOB granularity"]
AUDIT["Restricted audit table<br/>per-user premium requests"]
DASH["Per-LOB dashboard + FAQ"]
end
IDP --> TEAM
TEAM --> AUTO
REPO --> AUTO
META --> AUTO
AUTO --> CC
AZSUB -. UI-only attach .-> CC
CC --> BUDGET
CC --> INVOICE
INVOICE -. direct chargeback .-> AZSUB
CC -- daily REST pull --> WH
WH --> REPORT
WH --> AUDIT
WH --> DASH
| Phase | Focus | Mode |
|---|---|---|
| 0 — Prerequisites | Service account, automation repo, LOB list, lob property |
None |
| 1 — Showback pilot | Two pilot LOBs, plan-only, report against history | Showback |
| 2 — Apply + budgets | Apply-mode sync, alert-only budgets, edge-case validation | Showback |
| 3 — True chargeback | Attach Azure billing identities, sign off with finance | Chargeback |
| 4 — Full rollout | All LOBs in waves, drive catch-all usage to zero, on-call handoff | Chargeback |
- Sections 1–4 are scope, assumptions, and platform limits.
- Sections 5–10 cover the design itself (allocation, mapping, naming, Azure linkage, runner reconciliation).
- Sections 11–12 cover reporting and the warehouse.
- Sections 13–16 cover operations (budgets, automation, APIs, runbook).
- Sections 17–18 cover dispute resolution and end-user documentation — the human-process layer.
- Sections 19–20 cover risks and the phased rollout plan.
- Sections 21–22 close with open questions and references.
- Executive Summary
- Goals and Non-Goals
- Assumptions and Constraints
- GitHub Cost Centers — Capability Summary
- Documented Limits
- Allocation Rules per Product
- LOB-to-Cost-Center Mapping Strategy
- Source-of-Truth Pipeline
- Naming Conventions
- True Chargeback via Azure Subscription Linkage
- Self-Hosted Runner Reconciliation
- Reporting, Retention, and Invoice Reconciliation
- Long-Term Warehouse Strategy
- Budgets and Guardrails
- Build vs. Buy — Automation
- APIs, Permissions, and Token Strategy
- Operational Runbook
- Dispute Resolution Process
- End-User Documentation
- Risks and Edge Cases
- Phased Rollout Plan
- Open Questions
- References
- Attribute every billable GitHub product to exactly one LOB on every invoice line.
- Achieve true chargeback by routing each LOB's GitHub usage to that LOB's own Azure subscription (where Azure billing identity is supported).
- Keep LOB-to-resource mappings driven by the existing identity and repo metadata systems, so the chargeback model stays in sync with organizational reality without manual reassignment.
- Provide budgets and alerts per LOB to give cost owners agency, not just visibility.
- Produce a monthly chargeback report that reconciles to the GitHub invoice within rounding tolerance.
- Replacing finance ledger systems. The system produces reconciled inputs; it does not post journal entries.
- Tracking marketplace apps or third-party integrations not billed by GitHub.
- Allocating partner/contractor seats outside the IdP. Those are handled as a single "Shared / Unattributed" cost center until the IdP catches up.
- Real-time cost prevention beyond what GitHub budget enforcement already provides.
The design assumes the following confirmed inputs:
Note: These are prerequisites this design depends on, not universal requirements of GitHub or Azure. They were confirmed during requirements gathering. If any change — different IdP, multiple Entra tenants, classic PATs banned by security policy, no per-LOB Azure subscription, multi-currency billing, no change-management system — the corresponding section of the design must be revisited before implementation.
| Area | Assumption |
|---|---|
| Plan | GitHub Enterprise Cloud with EMU, on the enhanced billing platform |
| Org topology | One large monolithic organization spanning all LOBs |
| Identity | EMU users provisioned by SCIM from Entra ID (single tenant) |
| Group truth | IdP groups synced to GitHub teams; this is the LOB source for people |
| Repo truth | Repository custom properties (for example, lob, cost-center) are the LOB source for repos |
| Azure | Each LOB owns a distinct Azure subscription that is to be charged directly. Subscriptions are tagged chargeback-lob=<slug> under a tag policy the platform team can audit |
| Currency and legal entity | All in-scope LOB Azure subscriptions billed in USD with a single legal entity. Non-USD or cross-entity rollout is out of scope for v1 (see Risks) |
| Tooling | A change-management ticketing system (or equivalent auditable approval record) is available for the manual Azure-attach gate |
| Tokens | Classic PATs scoped to billing endpoints can be issued to a designated EMU service account, subject to security review (see APIs, Permissions, and Token Strategy) |
| Products in scope | GitHub Enterprise seats, Copilot seats, Copilot premium requests, Actions hosted minutes, self-hosted runner infra, Git LFS, GitHub Advanced Security (Code Security, Secret Protection) |
| Admin authority | A central platform team owns the enterprise, billing, and the chargeback automation |
Important: Cost Centers apply only to metered usage under the enhanced billing platform. Volume or pre-paid subscription billing is not affected. Confirm the enterprise has fully transitioned before relying on this design.
A cost center is an enterprise-scoped grouping of resources used to allocate GitHub spending. Each cost center can contain any combination of:
- Users (EMU
_short_loginvalues) - Organizations (relevant for multi-org enterprises; less so here)
- Repositories (the primary lever in a monolithic-org setup)
Each cost center can optionally be linked to a distinct Azure subscription for direct billing — the basis for the true chargeback design in this document.
Cost centers are managed in the GitHub UI under Enterprise → Billing & Licensing → Cost centers, and through REST endpoints under /enterprises/{enterprise}/settings/billing/cost-centers. Once a resource is assigned, future usage attributes to the cost center; past usage is not retroactively re-allocated.
Important: GitHub teams are not a cost center resource type. Only users, organizations, and repositories can be attached. To allocate by team, the automation must expand each team to its user members and attach the users individually. The github/cost-center-automation tool's Teams Mode performs this expansion for you.
Note: Outside collaborators and other unaffiliated users can be added to a cost center only via the API, not the UI. Pure-EMU enterprises do not have outside collaborators, so this is rarely relevant here, but worth noting if any partner identities exist.
These are the published platform limits as of May 2026; verify against the Cost centers limitations doc before any large-scale rollout.
| Limit | Value | Implication for this design |
|---|---|---|
| Active cost centers per enterprise | 250 | Comfortably above any realistic LOB count. Archived cost centers do not count toward this cap. |
| Resources per cost center | 25,000 | Per cost center, not total. With 250 cost centers the theoretical ceiling is 6.25M resources — the practical limit is operational, not API-imposed. |
| Resources added or removed per API call | 50 | The sync automation must batch in groups of 50 with retry/backoff. |
| Azure subscription assignment | UI-only | Cannot be automated via REST today. Plan a manual step in LOB onboarding. See True Chargeback. |
| Detailed usage report (UI) | Max 31-day window per request | Multi-month history must be persisted to a warehouse. Detailed reports are not available via REST. See Long-Term Warehouse Strategy. |
| Summarized usage report (REST) | Past 24 months accessible | The system of record for periods older than 24 months must live in your warehouse. |
| Premium request usage report | Begins October 1, 2025 | No premium request data exists for periods earlier than this date. |
Note: Dedicated SKU-level tracking for Spark and Copilot cloud agent premium requests began on November 1, 2025, per GitHub billing docs. October 2025 records use a single aggregated premium-request SKU without the Copilot cloud agent / Spark sub-SKU breakdown.
This is the most important table in the design. Two assignment levers — user and org/repo — are both required because GitHub allocates products differently.
| Product | Charged based on | Lever in this design |
|---|---|---|
| GitHub Enterprise (seats) | User who holds the license; org if user is unassigned | User assignment |
| GitHub Copilot (seats) | User who holds the license; org if user is unassigned | User assignment |
| Copilot premium requests / cloud agent | User who triggered the request; org of the Copilot license if user is unassigned | User assignment |
| GitHub Advanced Security / Secret Protection / Code Security | Active committer (user); org if user is unassigned | User assignment |
| GitHub Actions (hosted runners) | Repository or org where the workflow ran | Repository assignment |
| GitHub Actions cache and custom-image storage | Repository or org owning the cache / image | Repository assignment |
| GitHub Codespaces (compute) | Repository or org where the codespace was created | Repository assignment |
| GitHub Codespaces prebuild storage | Repository owning the prebuild | Repository assignment |
| GitHub Packages | Repository or org that owns the package | Repository assignment |
| Git LFS | Repository or org where LFS storage / bandwidth is consumed | Repository assignment |
| GitHub Models inference (when enabled) | User who invoked the model; falls back to org of the calling repo | User assignment, repo fallback |
| Self-hosted runner compute | Not on the GitHub invoice (paid in your own infra) | Out-of-band reconciliation (see Self-Hosted Runner Reconciliation) |
Key implication: every LOB needs both its users and its repositories assigned to its cost center. Assigning only one or the other will leave a category of cost falling through to "Enterprise Only" (unallocated) on the report.
Note: When a user is directly assigned to a cost center, that takes priority over indirect assignment via organization membership. Use direct user assignment as the rule, not the exception, for license-based products.
The table above is the happy path. These cases need an explicit policy, otherwise spend silently lands in "Enterprise Only" or the wrong LOB:
- User with no cost center membership. In a monolithic-org topology where the org itself is not assigned to any cost center, license usage and premium requests for an unassigned user fall to Enterprise Only, not to
99 - Attribution Defect. Two acceptable policies: (a) attach the monolithic org to99 - Attribution Defectso the fallback is captured, or (b) gate license assignment in the IdP so a user cannot receive a Copilot or Enterprise seat without an LOB group. This design recommends option (b); option (a) is a safety net during cutover. - Forks and first-time contributor PRs. GitHub Actions minutes bill to the repository owner, not the actor. PRs from forks consume the base repo's Actions minutes, so the cost lands on the base repo's LOB. Document this so LOBs hosting open source or shared internal repos understand that they pay for community contributions.
- Copilot cloud-agent invocations. When the cloud agent runs a workflow, the premium requests bill to the assigned user of the agent invocation, and the Actions minutes bill to the repository the workflow runs in. These can land in different LOBs. Surface both lines in the report; do not attempt to merge them.
- Mid-month cost-center moves. Usage timestamps before the move stay with the prior cost center; usage after the move attributes to the new one. There is no proration. Schedule moves at month boundaries when the LOB change is administrative; accept the in-month split when the change is operational.
- Multi-LOB users. GitHub allows only one cost center per user. The chargeback automation logs a warning when a user is in two
chargeback-lob-*teams; the IdP must enforce single-LOB membership upstream. The automation picks one (last-write-wins) and surfaces the conflict. - Codespaces secrets, Migrations, Code Search. These are not separately metered today. If GitHub introduces meters, slot them into the existing user-vs-repo split using the same logic as the closest analog above.
A single monolithic organization with multiple LOBs has two complications: users are mixed in one org, and repos are mixed in one org. A clean chargeback model needs both axes mapped.
Create exactly one cost center per LOB. Avoid splitting a single LOB across multiple cost centers — sub-LOB granularity belongs in downstream finance reporting, not in GitHub, because:
- Users can only belong to one cost center at a time.
- Repos can only belong to one cost center at a time.
- Multi-cost-center sprawl makes budget and reporting math harder without improving accuracy.
If a sub-LOB breakdown is mandatory, encode it in repository custom properties (for example, business-unit, team) and produce sub-LOB rollups from the detailed usage report. The cost center stays at LOB granularity.
Drive cost center membership from two synchronized sources:
| Resource type | Source | Mapping rule |
|---|---|---|
| Users | IdP group membership (mirrored into GitHub teams) | One designated "LOB" team (or top-level IdP group) per LOB. Membership in that team = cost center membership. |
| Repositories | Repository custom property (for example, lob) |
The property value (case-normalized) selects the cost center. |
This separation is intentional. People move between LOBs via HRIS / IdP changes; repositories move between LOBs through deliberate property edits, code review, and CODEOWNERS — separate lifecycles, separate authoritative systems.
Reserve three operational cost centers in addition to the LOB cost centers. Each one signals a different operational state:
00 - Shared Platform— for shared infrastructure repos (DevEx, internal tooling, golden paths) and for platform-team users. Expected to have steady, planned spend that is charged back through an internal allocation key, not direct LOB.98 - Pending Onboarding— for users and repos belonging to LOBs that are mid-onboarding (IdP group exists, GitHub team being populated, Azure subscription not yet attached). Expected to trend down as onboarding waves complete.99 - Attribution Defect— for users without an IdP LOB group, repos without alobproperty, or anything else that should not be in the catch-all by design. Expected to trend to zero. Anything in this bucket triggers a cleanup ticket.
The operational signals are different: 00 is steady-state platform overhead; 98 measures onboarding velocity; 99 measures attribution defect rate. Do not collapse them.
+---------------------+ +---------------------+ +-------------------------+
| Entra ID | ---> | GitHub teams (EMU) | ---> | Cost center: users |
| LOB groups | | one team per LOB | | (license + premium req) |
+---------------------+ +---------------------+ +-------------------------+
+---------------------+ +---------------------+ +-------------------------+
| Repository custom | ---> | Cost-center sync | ---> | Cost center: repos |
| property `lob` | | automation | | (Actions, LFS, etc.) |
+---------------------+ +---------------------+ +-------------------------+
+---------------------+ +---------------------+ +-------------------------+
| Per-LOB Azure sub | ---> | Cost center Azure | ---> | Direct chargeback to |
| (one per LOB) | | billing identity | | LOB Azure invoice |
+---------------------+ +---------------------+ +-------------------------+
EMU SCIM from the IdP creates and deactivates GitHub users. IdP group membership is mirrored into GitHub teams. This stage already exists in the EMU baseline and is not changed by this design.
For each LOB, designate one GitHub team as the chargeback team. Naming convention: chargeback-lob-<lob-slug>. Membership in this team is the single authority for assigning users to a cost center. Other functional teams (review groups, application teams) continue to operate without billing semantics.
Define a tenant-wide repository custom property lob (or cost-center) with an allowed-values list matching the LOB slugs. Enforce population through:
- Repository creation templates and golden-path workflows
- A scheduled audit that lists repos with empty or invalid
lobvalues - CODEOWNERS plus repo admin responsibility for keeping the value current
A scheduled job reads the team membership and the repo property values, then makes the cost center match through the GitHub REST API. Plan-then-apply, with an audit log per run. See Build vs. Buy — Automation.
For each LOB cost center, attach the LOB's Azure subscription as the billing identity. From that point GitHub charges the LOB subscription directly for that cost center's usage. This is configured in the cost center settings UI and verified against Azure during creation.
Consistent slugs are critical because the same LOB identifier is used in IdP groups, GitHub team names, repo property values, and cost center names.
| Item | Pattern | Example |
|---|---|---|
| LOB slug | kebab-case, ASCII, no spaces |
retail-banking |
| IdP group | gh-lob-<slug> |
gh-lob-retail-banking |
| GitHub team | chargeback-lob-<slug> |
chargeback-lob-retail-banking |
Repo custom property lob |
<slug> |
retail-banking |
| Cost center name | LOB - <Display Name> |
LOB - Retail Banking |
| Shared platform cost center | 00 - Shared Platform |
(literal) |
| Pending onboarding cost center | 98 - Pending Onboarding |
(literal) |
| Attribution defect cost center | 99 - Attribution Defect |
(literal) |
Tip: If finance maintains stable LOB codes (for example,
LOB-042), include the code in the cost center display name:LOB-042 - Retail Banking. Stable codes survive display-name renames and make finance reconciliation deterministic. Add the same code as a column inconfig/lobs.yaml.
Maintain a single source-of-truth file — config/lobs.yaml in the internal thin-wrapper repo (not in upstream github/cost-center-automation, which has its own config/config.yaml for tool runtime settings) — listing every LOB with its slug, display name, finance code, IdP group, owner email, and budget defaults. The wrapper reads this file to derive the team mappings, repository custom-property mappings, and budget definitions that it then feeds into the upstream tool.
Important: Azure subscription IDs are deliberately not stored in
lobs.yaml. The Azure-to-cost-center binding is a UI-only operation in GitHub, so a Git copy can never be automatically reconciled with the live state and would drift silently. The binding is governed instead by an Azure subscription tagging convention plus a monthly audit query (see True Chargeback via Azure Subscription Linkage).
Note: Two distinct config files exist and they are not interchangeable:
- Upstream
github/cost-center-automation/config/config.yaml— the tool's own runtime configuration (mode, scope, naming patterns, budget amounts). One file per environment.- Internal
<our-wrapper-repo>/config/lobs.yaml— our LOB registry, owned by the platform team. The wrapper transforms entries here into the upstream tool'steam_mappings/repository_config.explicit_mappings/budgets.productsstructures at run time.
A minimal but complete example showing two active LOBs, one pending LOB, and the three reserved catch-alls. Only the fields the wrapper actually consumes are shown; extend with finance metadata as needed.
# config/lobs.yaml
# Internal LOB registry consumed by the chargeback wrapper.
# One entry per line of business + the three reserved catch-alls.
# Source of truth: platform team. Reviewed monthly with finance.
#
# DELIBERATELY OMITTED: Azure subscription IDs and billing-identity
# attachment metadata. The Azure-to-cost-center binding is a UI-only
# operation in GitHub, so duplicating it in Git creates a shadow copy
# that drifts silently. Instead:
# - Each LOB Azure subscription carries the tag 'chargeback-lob=<slug>'.
# - The cost-center display name on GitHub encodes the same <slug>.
# - A monthly audit query joins GitHub cost centers + tagged Azure subs
# and reports any mismatch. See Section 9.
schema_version: 1
updated: 2026-05-14
defaults:
budget_currency: USD
budget_alert_thresholds: [50, 75, 90, 100] # percent of budget
cost_center_name_prefix: "LOB - "
team_name_prefix: "chargeback-lob-"
repo_property_name: lob
azure_subscription_tag_key: chargeback-lob # value must equal the LOB slug
lobs:
- slug: retail-banking
finance_code: LOB-042 # stable code from finance ledger
display_name: Retail Banking
status: active # active | pending | retired
owner_email: retail-banking-platform-leads@contoso.com
entra_group_id: 8f3c5f80-1234-4abc-9def-1111aaaa2222
budgets:
copilot: 18000 # USD per month
actions: 4500
packages: 800
codespaces: 600
lfs: 200
notes: "Includes mortgage origination sub-team; do not split."
- slug: capital-markets
finance_code: LOB-077
display_name: Capital Markets
status: active
owner_email: capmkts-eng-leads@contoso.com
entra_group_id: 9a4d6090-2345-4bcd-8eaf-2222bbbb3333
budgets:
copilot: 9000
actions: 7500
packages: 400
codespaces: 1500
lfs: 100
- slug: data-platform
finance_code: LOB-103
display_name: Data Platform
status: pending # awaiting Azure subscription attach in GitHub UI
owner_email: data-platform-leads@contoso.com
entra_group_id: 7b2e4070-3456-4cde-7fbf-3333cccc4444
budgets:
copilot: 6000
actions: 3000
notes: "Resources currently park in '98 - Pending Onboarding'."
# Reserved catch-all cost centers.
# These are NOT line-of-business entries; they are operational buckets that
# stay billed to the central enterprise subscription.
catch_alls:
- slug: shared-platform
cost_center_name: "00 - Shared Platform"
purpose: "Steady-state shared infrastructure (e.g., shared runners idle capacity, platform tooling)."
- slug: pending-onboarding
cost_center_name: "98 - Pending Onboarding"
purpose: "Resources for LOBs whose cost center / Azure subscription is not yet linked. Should trend down each month."
- slug: attribution-defect
cost_center_name: "99 - Attribution Defect"
purpose: "Resources that failed the assignment rules (e.g., repo missing 'lob' custom property, user missing IdP group). Should trend to zero."Key conventions the wrapper enforces when reading this file:
slugis the immutable identifier; renaming adisplay_namedoes not break attribution. The same slug appears on the GitHub cost center display name (LOB - <Display Name>orLOB-<finance_code> - <Display Name>) and on the LOB's Azure subscription as the value of thechargeback-lobtag.finance_codeis what gets prepended to the cost center display name when present (LOB-042 - Retail Banking).status: pendingblocks the wrapper from moving resources out of98 - Pending Onboarding. Promotion toactiveis allowed only after the Azure subscription has been attached to the cost center in the GitHub UI and the monthly audit query (Section 9) has confirmed the binding.- No subscription IDs, billing accounts, or attach timestamps live in this file. The
chargeback-lobAzure tag is the source of truth for the binding; the cost center display name is the cross-reference; the change-management ticket is the human audit trail. - Adding a new LOB requires a PR with platform-team and finance approval; the wrapper's plan-mode run is attached to the PR for review before merge.
Each LOB cost center is bound to its LOB's Azure subscription so GitHub usage allocated to the cost center bills directly to that subscription. The mechanics:
- The LOB provides the Azure subscription that will receive the chargeback (identified by the LOB slug in the change-management ticket, not by raw subscription ID in any platform-repo file).
- The platform team adds the Azure billing identity to the cost center in the GitHub UI (this step is not available via the REST API today). GitHub validates the identity against Azure during configuration.
- Going forward, GitHub-metered usage attributed to that cost center is invoiced against the LOB's Azure subscription rather than the central enterprise subscription.
- License-based products allocated to the cost center (Copilot seats, Enterprise seats, GHAS) bill the same way.
Warning: The Azure subscription assignment is UI-only. The chargeback automation cannot attach or rotate Azure billing identities programmatically. Treat this as a manual gate in the LOB onboarding runbook.
Because the attach is UI-only and the wrapper cannot enforce it, the binding is governed by convention plus audit, not by a Git-stored mirror of the linkage:
- Cost center display name encodes the LOB slug. Every LOB cost center is named with the convention
LOB - <Display Name>(orLOB-<finance_code> - <Display Name>when finance codes are in use). This makes the LOB identity machine-readable from the GitHub side without any external lookup. - Azure subscription tag encodes the same LOB slug. Each LOB Azure subscription carries the tag
chargeback-lob=<slug>(the key is configured indefaults.azure_subscription_tag_keyinlobs.yaml). Tag policy on the LOB management group enforces presence of the tag; missing-tag findings open a P2 ticket on the LOB owner. - Monthly audit query reconciles both sides. A scheduled job runs after the monthly close and produces a three-column reconciliation:
- the GitHub cost center (name and ID, fetched via REST),
- the Azure subscription currently attached to it in the GitHub UI — read from the GitHub cost center API if exposed, otherwise inferred from the actual Azure-side billed entity for the period,
- the Azure subscription whose
chargeback-lobtag matches the LOB slug embedded in the cost center name. Rows where the second and third values disagree are governance violations and block the next month's report sign-off until cleared.
- Change-management ticket is the human audit trail. Each attach, detach, or re-attach is recorded in the platform team's change-management system with the operator, the date, the cost center ID, the Azure subscription ID, and a link to the LOB onboarding ticket. This is where ID-level evidence lives — in the change system, not in the platform repo.
This design accepts that the platform repo will never know the live Azure subscription ID for any cost center. That is the trade-off for not maintaining a shadow copy that silently drifts. The audit query, the Azure tag, and the change ticket together substitute for the Git mirror, and each of them is independently verifiable.
Edge cases to plan for:
- An LOB does not yet have a usable Azure subscription. Park its cost center against the central enterprise subscription and tag the cost center with a temporary marker (for example, prepend
PENDING-to the display name). Re-link when the LOB onboarding completes. - Mid-month subscription change. GitHub charges based on the configuration at the time of usage; switching the Azure billing identity does not retroactively rebill prior usage. Schedule changes at month boundaries when possible.
- Resource moves between cost centers mid-month. Usage before the move stays with the old cost center; usage after the move attributes to the new one. The detailed usage report timestamps allow accurate proration if needed.
chargeback-lobtag missing or wrong on the Azure side. The monthly audit query catches this. Until the tag is corrected, the LOB cannot move out ofpendingstatus and the cost center remains a candidate for99 - Attribution Defectreview.
Self-hosted runner compute does not appear in the GitHub invoice. The chargeback record for that compute lives in the runner's hosting environment and must be merged with the GitHub-billed view to give LOBs a complete picture.
Different runner topologies need different attribution math. Choose per pool.
Pattern A — Static pool per LOB (preferred when feasible):
Tag every Azure resource that supports the runner — VM scale sets, container apps, AKS node pools, Azure Container Instances — with at minimum:
lob— same value as the GitHub repo custom propertypurpose=github-actions-runnerrunner-pool— pool name owned by that LOB
Reconciliation is then a direct sum of Azure-tagged spend grouped by lob.
Pattern B — Shared pool (Actions Runner Controller / AKS or shared VMSS):
The pool is a single Azure resource serving many LOBs, so direct tagging cannot attribute. Split the pool's Azure cost downstream using a job wall-clock occupancy weighting key (workflow-runs alone is the wrong key — it ignores job duration, runner shape, retries, and matrix fan-out):
- Pull workflow run inventory per repository from
GET /repos/{owner}/{repo}/actions/runs, then expand each run to its jobs viaGET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs. - For each job, capture:
started_at,completed_at,runner_name,labels,conclusion. Compute wall-clock seconds per job. - Month-boundary splitting: when a job spans a month boundary, split its wall-clock time into the two months at the boundary. Bill each month independently.
- Failed and cancelled jobs: include them in the weighting key. The runner was occupied; the LOB owns the cost regardless of conclusion. Surface a separate "failure-rate" KPI per LOB if waste reduction is a goal.
- Runner shape weighting: if the shared pool runs heterogeneous runner SKUs (for example, 2-vCPU and 8-vCPU nodes), weight by
wall_clock_seconds × vCPU_count(or vCPU-seconds) rather than raw seconds. Otherwise small jobs on big runners under-pay. - Roll job-time up to LOB by joining on the
lobrepo custom property of the job's repository. - Apportion the pool's Azure monthly cost across LOBs in proportion to LOB-share of total weighted occupancy.
- Idle capacity and cold-start overhead: the difference between
sum(occupied vCPU-seconds)andpool capacity vCPU-secondsis shared platform overhead. Charge it to00 - Shared Platform, not pro-rata to LOBs. Track this idle ratio as a platform KPI — high idle ratio means the pool is over-provisioned; high occupancy with queue time means under-provisioned. - Publish the split rule and version it. Every chargeback dispute on shared infra traces to this rule, so it must be reviewable.
- Pull Azure cost data filtered by
purpose=github-actions-runner, grouped bylob(Pattern A) or byrunner-pool(Pattern B). - Pull the GitHub usage summary for the same period, grouped by cost center.
- Join on the LOB slug; produce a single row per LOB per month with both columns:
github_invoiced_costandself_hosted_runner_cost. - Surface both in the chargeback report and ledger feed.
Tip: Where workflows mix hosted and self-hosted runners, allow LOBs to see both numbers in the same report. This is the most-asked-for capability from finance and engineering leads when chargeback launches.
Note: GitHub does not currently bill for self-hosted runner minutes; only Actions storage (artifacts and logs) is metered. A previously announced per-minute fee for self-hosted runners has been deferred and has no published GA date as of the report date. If GitHub introduces a meter, it will surface in the GitHub usage report keyed by org/repo and will flow through the existing cost center allocation without code changes.
| Report | Available via REST? | Granularity | Retention |
|---|---|---|---|
Summarized usage (/usage/summary) |
Yes | Aggregated by product, SKU, cost center, day | 24 months via API |
Per-cost-center usage (/usage) |
Yes | Per resource per day, includes cost_center_name, organization_name, repository_name (no username / workflow_path) |
24 months via API |
Premium request usage (/premium_request/usage) |
Yes | Per user, per model, per day | 24 months via API; data starts October 1, 2025 |
Detailed usage report (with username and workflow_path) |
No | Per resource per day with user and workflow detail | Max 31-day window per request, UI download only, emailed as CSV, one report per account at a time |
Warning: The detailed usage report — the one that includes
usernameandworkflow_path— is not exposed by the REST API. It must be requested in the GitHub web UI and is delivered by email. This design does not consume the detailed report in the automated pipeline — see Chargeback granularity — design decision. The detailed report remains available in the UI for ad-hoc finance investigations only.
The chargeback pipeline is the system of record, but day-to-day cost questions are answered in the GitHub UI. Sign in as enterprise owner or billing manager, then Your enterprises → select the enterprise → Settings → Billing & licensing. The table below maps the common questions to the surface that answers them.
| Question | UI surface | What it shows | Export path |
|---|---|---|---|
| What did each LOB cost this month? | Usage page → Group by Cost center | Stacked bar and time-series of net spend per cost center, broken down by product (Actions, Copilot, Codespaces, Packages, LFS, GHAS, premium requests) | Get report → Summarized CSV; same data as REST /usage/summary |
| Who and what is in a given cost center? | Cost centers → click the cost center name | Members (users), repositories, organizations, attached Azure subscription, current-period usage by SKU, attached budgets and consumption | History tab for member / sub binding events |
| Where do I attach the Azure subscription to a cost center? | Cost centers → [name] → Attach subscription | The manual Azure-attach gate referenced throughout this design (Section 9). Requires Azure billing-account admin in the same Microsoft tenant | Audited via the Cost center event history |
| Which user consumed what (Actions, Codespaces, premium requests, …)? | Usage page → Get report → Detailed | Per-line-item rows including username, workflow_path, cost_center_name, repository_name |
CSV by email, 31-day window, UI only — ad-hoc, not part of the automated pipeline |
| Which user consumed how many Copilot premium requests? | Usage page filtered to Copilot premium requests, or the per-user audit table in the warehouse | Per-user requests, model, quota, exceedance flag | REST /premium_request/usage (also feeds the Per-user premium-request audit table) |
| Which seats are assigned to whom? | Licensing → Enterprise seats and Copilot → Seat assignments | One row per assigned seat with the user identity | REST + UI |
| Did an LOB cross its budget? | Budgets → filter by cost center | Current consumption against each defined budget threshold (50 / 75 / 90 / 100 percent) | REST /enterprises/{enterprise}/settings/billing/budgets (preview) |
| Has a cost center binding changed mid-period? | Cost centers → [name] → History | Member, repo, sub-attach, and detach events with timestamp and actor | Feeds the Section 9 audit query |
Important: The Detailed report is the only first-party surface that breaks cost down to the individual user. It is UI-only, email-delivered, 31-day max window, and one report per account at a time. Requests for per-user cost should be triaged by the platform team rather than self-served by every LOB cost owner — both for access control and to avoid stepping on each other's outstanding requests.
Per-user cost is offered at two fidelities and one of them is restricted:
- Per-user license cost (Enterprise seat, Copilot seat) — directly attributable from the seat assignment lists. Each user is in exactly one LOB cost center, so each seat rolls up to one LOB and one user simultaneously. Available via REST and UI.
- Per-user Copilot premium-request cost — published as the Per-user premium-request audit table. Restricted access (platform FinOps + LOB cost owner scoped to their LOB only). Sourced from REST.
- Per-user Actions / Codespaces / Packages / LFS cost — only available through the UI Detailed report (31-day window). Used ad-hoc on platform-team request; not persisted in the chargeback warehouse and not in the column contract. If a finance question needs this, the platform team requests the Detailed report, joins it to the LOB roster offline, and returns the answer to the LOB cost owner without persisting the per-user rows.
This split is intentional. The chargeback report stays at LOB granularity (the contract finance signed off on); per-user breakdown is a drill-down for the LOB cost owner's internal showback to teams, not a chargeback line item.
This design uses summarized REST data only. The detailed usage report (which adds username and workflow_path) is UI- and email-only and is not consumed by the automated chargeback pipeline.
What this means in practice:
- Chargeback is allocated at cost center × product × SKU × organization × repository × day granularity.
- The chargeback report does not break costs down per user or per workflow file. License-based products are still attributed to the LOB through the user's cost center membership; the per-user line item is just not present in the warehouse.
- The detailed report remains available in the GitHub web UI for ad-hoc investigations — platform engineers can request it for a 31-day window if a finance question requires per-user or per-workflow detail.
- If at a later date GitHub exposes the detailed report through the REST API, the warehouse schema can absorb the new fields without breaking existing rows.
This trade-off is acceptable because: (a) cost center membership already encodes the user-to-LOB mapping for license-based products, (b) repository custom properties already encode the repo-to-LOB mapping for usage-based products, and (c) finance reconciliation works at LOB granularity, not at user granularity.
| Source | Endpoint or system | Cadence |
|---|---|---|
| Enterprise usage summary (REST) | GET /enterprises/{enterprise}/settings/billing/usage/summary?year=&month=&cost_center_id= |
Daily pull, monthly close |
| Per-cost-center usage (REST) | GET /enterprises/{enterprise}/settings/billing/usage?cost_center_id= |
Daily |
| Premium request usage (REST) | GET /enterprises/{enterprise}/settings/billing/premium_request/usage |
Daily |
| Detailed usage report (UI / email) | Web UI request → CSV by email | Ad-hoc only — not in the daily pipeline. Used for finance investigations needing user or workflow detail. |
| Cost center inventory (REST) | GET /enterprises/{enterprise}/settings/billing/cost-centers |
Hourly cache |
| Self-hosted runner cost | Azure Cost Management export filtered by tag | Daily |
| LOB metadata | config/lobs.yaml in the chargeback repo |
On change |
The monthly report is the artifact finance consumes. Lock the schema early and version it.
| Column | Source | Description |
|---|---|---|
period_year_month |
Calculated | YYYY-MM |
lob_slug |
LOB metadata | Stable identifier |
lob_display_name |
LOB metadata | Human-readable name |
cost_center_id |
GitHub | UUID |
cost_center_name |
GitHub | Cost center name |
azure_subscription_id |
GitHub cost center API / Azure tag lookup | Azure target subscription. Resolved at report build time from the GitHub cost center binding (or, if not exposed via REST, from the Azure subscription whose chargeback-lob tag matches the LOB slug). Never sourced from lobs.yaml. |
product |
GitHub usage | for example, actions, copilot, git_lfs |
sku |
GitHub usage | SKU name (for example, actions_linux, copilot_premium_request) |
unit_type |
GitHub usage | for example, minutes, gb_month, requests |
organization_name |
GitHub usage | Owning organization (when applicable) |
repository_name |
GitHub usage | Owning repository (when applicable) |
quantity |
GitHub usage | Raw units consumed |
gross_amount_usd |
GitHub usage | Pre-discount amount |
discount_amount_usd |
GitHub usage | Included quota / contract discounts |
net_amount_usd |
GitHub usage | Billed amount |
self_hosted_runner_cost_usd |
Azure Cost Mgmt | Only on rows where applicable |
notes |
Calculated | Reconciliation flags, for example RECONCILED, MISSING_LOB, REASSIGNED_MID_MONTH |
Note: The
usernameandworkflow_pathcolumns from the detailed report are intentionally not in the contract. See Chargeback granularity — design decision.
The /premium_request/usage REST endpoint already returns per-user data including username, model, requests, exceeds_quota, and total_monthly_quota. The standard chargeback report deliberately rolls this up to LOB granularity. A separate restricted audit table persists the raw per-user rows to enable a small set of operational use cases that LOB granularity cannot serve:
- Top-N premium-request users per LOB (waste detection, training need identification).
- Anomaly investigation when an LOB's overall premium-request spend spikes.
- Seat utilization review (Copilot seats allocated but with zero or near-zero premium request usage).
- Quota-exceedance triage (
exceeds_quota=true+total_monthly_quotacolumns surface users hitting limits before finance close).
Access controls:
- The platform FinOps team has full read access for cross-LOB investigations.
- Each LOB cost owner has read access scoped to their own LOB only through row-level security in the warehouse.
- HR / People Operations is consulted before any individual-level report is shared outside the LOB cost owner.
- The table is excluded from any general-purpose chargeback dataset published to engineering.
Schema (in addition to the standard chargeback columns):
| Column | Source | Description |
|---|---|---|
username |
/premium_request/usage |
EMU _short_login |
model |
/premium_request/usage |
Model name (for example, claude-opus-4, gpt-5) |
requests |
/premium_request/usage |
Number of premium requests in the period |
gross_amount_usd |
/premium_request/usage |
Pre-quota cost |
net_amount_usd |
/premium_request/usage |
Billed cost after quota |
exceeds_quota |
/premium_request/usage |
true if the user passed their monthly quota |
total_monthly_quota |
/premium_request/usage |
The user's quota for the period |
This preserves operational visibility without breaking the LOB-granularity contract finance signed off on. If GitHub later exposes detailed report data via REST, the same audit-table pattern extends to per-user Actions and Codespaces usage.
- Sum of
net_amount_usdacross all cost centers for a period must equal the GitHub invoice total within ±1 USD (rounding tolerance). Mismatches block report sign-off. - Any rows assigned to
99 - Attribution Defectopen a P2 cleanup ticket. The goal is zero rows in this bucket each month; track defect rate as a platform KPI. - Rows assigned to
98 - Pending Onboardingare expected during onboarding waves; they should trend down to zero as each wave completes. Track as an onboarding velocity KPI, not a defect. - Rows assigned to
00 - Shared Platformare charged back through the internal allocation key documented separately in the platform team budget. - Self-hosted runner reconciliation runs separately; mismatches between Azure-tagged spend and runner inventory open infrastructure tickets.
GitHub's REST endpoints expose the past 24 months of summarized usage. For multi-year trend analysis, audit, and finance retention beyond two years, every usage record retrieved must be persisted to your own store. The same store also enables:
- Joining GitHub usage with Azure Cost Management exports for self-hosted runner reconciliation.
- Joining with HRIS / IdP master data for canonical LOB hierarchy and sub-LOB rollups that GitHub does not model.
- Producing FOCUS-conformed cost rows so GitHub spend appears in the same FinOps dashboards as Azure spend.
Four landing-zone options are realistic for this design. The choice is downstream of the chargeback automation itself — the same daily REST exports feed any of them. Lock the choice with finance and the platform data team before Phase 2 of the rollout.
| Option | Description | Strengths | Trade-offs |
|---|---|---|---|
| 1. Microsoft Fabric / ADX with FinOps Toolkit | Land GitHub and Azure Cost Management exports in the same Fabric lakehouse or ADX cluster, conform both to the FOCUS specification, publish to Power BI with the FinOps Toolkit templates. | FOCUS-conformed; reuses existing FinOps dashboards; one place for GitHub + Azure spend; well-documented patterns. | Heavier lift if Fabric / ADX is not already in use; capacity costs; FinOps Toolkit schema must be extended for non-Azure rows. |
| 2. Azure Data Lake Storage Gen2 + Synapse or Databricks | Land raw exports in ADLS, transform with Synapse pipelines or Databricks notebooks, publish to Power BI. No FinOps Toolkit dependency. | Maximum flexibility; works with any non-FOCUS internal schema; good fit if the data team already runs Databricks or Synapse. | More custom engineering; you build the equivalent of FinOps Toolkit transforms yourself; no out-of-the-box FOCUS conformance. |
| 3. Power BI direct from CSV exports | Daily exports written to a SharePoint / OneDrive / blob folder, ingested by Power BI dataflows or imported datasets. No lakehouse. | Lowest infra cost; fastest to stand up; finance and platform team can own end-to-end. | Limited multi-year history; no easy join with high-volume Azure cost exports; refresh and dataset size limits in Power BI Pro / Premium. |
| 4. Existing internal FinOps platform | Push GitHub exports as a new source into whatever the organization already runs (Apptio Cloudability, Vantage, CloudZero, Finout, internal showback). | Reuses existing finance integrations and chargeback workflow; no parallel BI surface to maintain. | Vendor-specific connectors and FOCUS support vary; depends on whether the platform supports cost center as a dimension; some products map GitHub via "Virtual Tags" rather than native cost centers. |
Important: The chargeback automation — cost center sync, budget creation, monthly export — is independent of the warehouse choice. Land that work first against simple file exports, then route the same exports into whichever option finance ratifies.
Minimum schema to persist per usage row:
report_date,period_year_monthcost_center_id,cost_center_name,lob_slug(joined from LOB metadata)product,sku,unit_type,quantitygross_amount_usd,discount_amount_usd,net_amount_usdorganization_name,repository_nameazure_subscription_id(resolved at build time — see True Chargeback; not stored inlobs.yaml)source_report_type— one ofsummary | per_cc | premium_requestingested_at— load timestamp for audit
- GitHub remains the canonical allocator. The warehouse copies what GitHub already computed; it does not invent attributions.
- Tolerate one-source-of-truth tension. The GitHub billing UI and the warehouse must reconcile to the invoice each month. If they disagree, the warehouse is wrong.
- Daily incremental loads, monthly close. Daily for trend visibility, monthly close once the GitHub invoice is final.
- Backfill is bounded. You can backfill at most 24 months from the API at any time, so the warehouse must capture data forward from go-live.
"Decide warehouse later" is acceptable only if the chosen option — whichever it is — meets these requirements. Lock these before Phase 2 of the rollout:
- Raw immutable exports. Every API response is persisted unmodified with a per-call hash, before any transformation. Never overwrite; new pulls land as new partitions.
- Transformed chargeback rows. A reproducible transform produces the monthly chargeback rows from the raw exports. The transform code is versioned in git; each row carries the transform version that produced it.
- Reconciliation snapshot per close. Each month-close produces a frozen snapshot — raw payloads, transformed rows, the GitHub invoice PDF / CSV, and the reconciliation result — stored as an immutable bundle for audit.
- Replay procedure. Given any month's snapshot, the close can be regenerated identically without depending on the live API (which has only 24 months of history). This is the only defense against month-18 drift.
- Access model. Read access for finance and LOB cost owners; restricted access for the per-user audit table (see the Reporting section); write access only to the chargeback automation service principal.
- Retention. Minimum 7 years for closed-month snapshots (typical finance retention); longer if regulated.
- Ledger interface contract. A documented push or pull interface to the finance ledger system, with idempotent semantics so re-runs do not double-post.
GitHub budgets attach at four scopes — enterprise, organization, repository, and cost center. For chargeback, the cost center scope is the primary control surface.
Create these on every LOB cost center as part of onboarding:
| Budget | Scope | Type | Purpose |
|---|---|---|---|
| Copilot premium request | Cost center | SkuPricing (copilot_premium_request) |
Cap PRU overage spend |
| Actions compute (Linux) | Cost center | SkuPricing (actions_linux) |
Cap hosted-runner spend |
| Actions compute (Windows) | Cost center | SkuPricing (actions_windows) |
Optional — only for LOBs with Windows pipelines |
| Git LFS | Cost center | ProductPricing (git_lfs) |
Cap LFS storage growth |
| Codespaces | Cost center | ProductPricing (codespaces) |
Optional |
| Packages | Cost center | ProductPricing (packages) |
Optional |
Each budget should:
- Use
prevent_further_usage=trueonly with explicit LOB owner sign-off, since blocking usage will halt CI/CD. - Always set
will_alert=truewith the LOB cost owner and the platform team on the recipient list. - Default alerting at 75 / 90 / 100 percent thresholds (GitHub-built-in).
Set very high enterprise-scope budgets that act as a circuit breaker — they should never trip in normal operation, but they catch a runaway budget misconfiguration in a cost center.
Warning: Avoid low enterprise-wide budgets that overlap with cost center budgets. Enterprise limits are evaluated independently and a low enterprise budget can stop usage in an LOB that has not yet hit its own cost center budget.
GitHub publishes github/cost-center-automation, an open-source Python tool maintained by GitHub that supports the exact patterns this design needs.
| Need in this design | github/cost-center-automation support |
|---|---|
| Sync users to cost centers from GitHub teams | Teams Mode — direct fit |
| Sync repos to cost centers from custom properties | Repository Mode — direct fit |
| Per-user premium-request quota tracking | PRU Mode — direct fit (used for the audit table data feed) |
| Auto-create budgets per cost center per product | Built-in budget creation for Copilot premium requests and Actions — use this rather than rebuilding |
| Plan-then-apply with diff preview | Built-in (--mode plan / --mode apply) |
| Run on a schedule | Included GitHub Actions workflow (every 6 hours) |
| EMU / GHE.com endpoint support | Supported via GITHUB_API_BASE_URL |
| Azure billing identity attachment per cost center | Not supported — must be done via UI |
| Multi-LOB user disambiguation | Documented behavior (last team wins, with warnings) |
Adopt github/cost-center-automation for stages 4a (user sync) and 4b (repo sync), and add a thin internal wrapper for:
- Reconciliation reporting (the column contract above)
- Self-hosted runner cost merging
- Approval workflow before applying changes that affect more than N users or M repos in one run
Important: The wrapper cannot automate Azure billing identity attachment per cost center. That step is UI-only today and remains a manual gate in the LOB onboarding runbook.
Document the internal wrapper in this repo. Treat the upstream tool as a managed dependency, pinned to a known release.
| Operation | Token type | Required scopes |
|---|---|---|
| Manage cost centers (create, list, add resources) | Classic PAT | manage_billing:enterprise |
| Read teams for sync | Classic PAT or GitHub App | read:org |
| Read enterprise usage summary, premium requests | Classic PAT | manage_billing:enterprise |
| Manage budgets | Classic PAT | manage_billing:enterprise |
| Read repository custom properties at scale | Classic PAT or GitHub App | repo (read) |
Important: Fine-grained PATs are not supported for the billing usage and budget endpoints at the time of writing. Use a classic PAT created by an enterprise owner or billing manager service account, stored as a GitHub Actions secret with restricted environment access.
Caution: Classic PATs are a security prerequisite, not just an implementation detail. Many enterprises restrict or ban classic PATs by policy. Before Phase 1, confirm with the security team that:
- Issuing a classic PAT to a designated EMU service account is permitted (or a documented exception is granted).
- Compensating controls are in place: PAT scoped to the minimum (
manage_billing:enterprise,read:org,reporead), stored as a GitHub Actions environment secret with restricted environment access, every API call audited via the chargeback workflow run ID, and rotation on a published cadence.- GitHub Apps are tracked as a future replacement once the billing and budgets endpoints support fine-grained authentication.
If classic PATs are not permitted and no exception is granted, this design cannot proceed — the upstream
github/cost-center-automationtool requires this scope today.
Note: Cost centers can also be created by organization owners for their own org, not only by enterprise owners. In a monolithic-org topology this distinction is moot — the platform team holds both roles — but worth noting if the topology evolves to multi-org or if a sub-org delegation model is introduced later.
| Purpose | Method | Path |
|---|---|---|
| List cost centers | GET | /enterprises/{enterprise}/settings/billing/cost-centers |
| Create cost center | POST | /enterprises/{enterprise}/settings/billing/cost-centers |
| Add resources to cost center | POST | /enterprises/{enterprise}/settings/billing/cost-centers/{id}/resource |
| Usage summary | GET | /enterprises/{enterprise}/settings/billing/usage/summary?year=&month=&cost_center_id= |
| Premium request usage | GET | /enterprises/{enterprise}/settings/billing/premium_request/usage |
| List budgets | GET | /enterprises/{enterprise}/settings/billing/budgets |
| Create budget | POST | /enterprises/{enterprise}/settings/billing/budgets |
| Update / delete budget | PATCH / DELETE | /enterprises/{enterprise}/settings/billing/budgets/{budget_id} |
| Org teams (members) | GET | /orgs/{org}/teams/{team_slug}/members |
| Repo custom properties | GET | /orgs/{org}/properties/values |
Note: The Budgets endpoints are in public preview and require the API version header
X-GitHub-Api-Version: 2026-03-10. Pin the version in the automation and review the changelog before upgrading. The version header value should be re-verified against the github/cost-center-automation source code at upgrade time, since the public docs page for the budgets endpoint has been intermittently unavailable.
- Create one EMU service account designated as enterprise billing manager.
- Issue one classic PAT with the minimum scopes above; rotate every 90 days.
- Store the PAT as a GitHub Actions environment secret accessible only to the chargeback workflow.
- All API calls log the run ID in the audit trail (see operational runbook).
- If the PAT expires, is revoked, or rotation fails during a monthly close, the close pauses (no partial pulls, no partial budget edits). Reissue the PAT under the rotation procedure, then resume from the last successful step. Raw exports for that period remain reproducible from the GitHub REST API for 24 months.
- LOB lead provides: display name, slug, finance code (if any), primary cost owner email, secondary alert recipients, and confirmation that the LOB Azure subscription is tagged
chargeback-lob=<slug>and is ready to be attached. The Azure subscription ID is captured in the change-management ticket, not inlobs.yaml. - Add the LOB entry to
config/lobs.yaml. Initial state ispending. - Create the IdP group and the corresponding
chargeback-lob-<slug>GitHub team (provisioned by IdP sync). - Add the LOB slug to the allowed-values list of the
lobrepository custom property. - Run the chargeback automation in
--mode planand review the diff. Withstatus: pending, the plan creates the LOB cost center as an empty container; the LOB's user and repo resources are routed to98 - Pending Onboardingrather than to the new cost center. Resources only move out of98after step 8. - Apply the change. The empty LOB cost center now exists in GitHub; LOB resources are still attached to
98 - Pending Onboarding. - Manual UI step (gated): an enterprise owner or billing manager attaches the LOB's Azure subscription to the new cost center through the GitHub UI (this is not available via REST). Verify the binding in the cost center settings; record the verification in the change-management ticket (operator, timestamp, cost center ID, subscription ID). The platform repo is intentionally not updated with the subscription ID.
- Run the binding audit query (Section 9) and confirm the LOB row is clean. Move LOB resources from
98 - Pending Onboardingto the new LOB cost center. Flip the LOB entry inconfig/lobs.yamlfrompendingtoactive. - Create the default budget set with the LOB-provided amounts.
- Send the LOB cost owner the link to the cost center page, the per-LOB FAQ (see End-User Documentation), and a sample report.
- The HRIS / IdP change updates the user's group membership. GitHub team membership follows automatically.
- The next chargeback automation run reassigns the user to the destination cost center. Past usage stays with the original cost center. Future usage attributes to the new one.
- A repo admin updates the
lobrepository custom property to the new value (typically through a PR template or governance workflow). - The next chargeback automation run reassigns the repo. Same proration semantics as users.
- Remove or merge the IdP group; users either move to another LOB or are deactivated.
- Reassign or archive the LOB's repositories.
- Wait one full billing period after the last activity for the cost center to stop accruing.
- Mark the cost center as deleted via the UI or API. Historical usage remains visible under the Deleted tab.
The close is a multi-team workflow. Each step names the responsible role and the permission required, because no single role has all the access needed.
| Step | Responsible role | Required access | Output |
|---|---|---|---|
| 1. Pull final usage summary for the closed period | Platform team | GitHub classic PAT with manage_billing:enterprise |
Raw payload bundle in warehouse |
| 2. Pull final premium-request usage (per-user, for audit table) | Platform team | Same PAT | Restricted audit table updated |
| 3. Run reconciliation against the GitHub invoice | Platform team + Finance ops | Platform: PAT; Finance: read access to warehouse + invoice PDF | Reconciliation snapshot, ±1 USD tolerance |
4. Resolve any rows in 99 - Attribution Defect (open cleanup tickets) |
Platform team | GitHub admin to reassign | Defect tickets filed; defect rate KPI updated |
5. Resolve any rows in 98 - Pending Onboarding that should now be active |
Platform team + LOB lead | GitHub admin + LOB sign-off | LOBs promoted in config/lobs.yaml |
| 6. Reconcile self-hosted runner costs from Azure tags | Platform team + Azure subscription owners | Azure Cost Management Reader on each LOB sub | self_hosted_runner_cost_usd populated per row |
| 7. Verify Azure subscription chargeback flowed correctly per LOB | Azure subscription owners + Finance | Azure Cost Management on LOB sub | Per-LOB confirmation logged |
| 8. Publish the monthly chargeback report to the finance share | Finance ops | Warehouse read + finance share write | Report file with snapshot ID |
| 9. Push the per-LOB ledger feed to the finance system | Finance ops | Ledger system write | Idempotent ledger post |
| 10. Close the snapshot bundle as immutable | Platform team | Warehouse admin | Snapshot frozen, hash recorded |
Important: Steps 1–2 and 4–5 require GitHub billing manager / admin rights and cannot be executed by finance ops alone. Steps 6–7 require Azure access on each LOB subscription; if the platform team cannot obtain Cost Management Reader on a given LOB sub, the LOB's Azure subscription owner must export their own Cost Management view for that period and hand it off, otherwise the close is blocked for that LOB until access is granted. The platform team owns the orchestration; finance owns the verification and the ledger push. Document named role groups (not individuals) in
config/lobs.yamlso handoffs survive turnover.
The chargeback automation is a scheduled, side-effecting job; treat its failures the way any other production data pipeline is treated.
| Failure | Detection | Recovery |
|---|---|---|
| Daily REST export missed | Monitoring alert on the workflow run | Re-run the workflow for the missed day. Backfill is bounded to the 24-month REST window; beyond that, the warehouse snapshot is the only record. |
| Classic PAT expired or revoked mid-close | Authentication failures in the workflow log | Pause the close. Reissue the PAT under the documented rotation procedure, then resume from the last successful step. No partial budgets are edited. |
github/cost-center-automation upgrade breaks the apply step |
--mode plan diff diverges from the previous run |
Pin the upstream version; do not auto-upgrade in production. Test upgrades in a non-prod enterprise first. |
| Section 9 audit query reports a binding mismatch | Audit job result | Block the affected LOB row in the report; open a P1 to re-attach in the GitHub UI; do not retroactively re-bill. |
| Warehouse snapshot bundle write fails | Snapshot integrity check | Retry the write; if the snapshot is irrecoverable, replay the close from raw exports. The snapshot is the system of record for the period; an unwritten snapshot is a P1. |
The first time an LOB disputes a chargeback line, the system either has a defensible process or it loses finance trust. Define the process before the first invoice cycle.
- Disputes are filed as GitHub issues in the chargeback automation repository using a
Disputeissue template. The template captures: LOB slug, period, disputed line(s), expected attribution, supporting evidence (link to repo, link to user activity, link to runner pool), and dispute category. - SLA target: triage within 2 business days; resolution within 10 business days.
| Category | Owner | Typical resolution |
|---|---|---|
Tagging error (repo lob property wrong) |
Platform team | Correct the property; confirm next sync attributes correctly. Past usage stays as-is. |
| Membership error (user in wrong LOB team) | Platform team + IdP team | Correct in IdP; same proration semantics. |
| Allocation rule disagreement (for example, fork PR billed to base repo) | Platform team + LOB lead | Validate against the Allocation Rules table. If the rule is correct, deny the dispute with documentation. If the rule is unclear, escalate to the FinOps council. |
| Shared-runner attribution dispute | Platform team | Trace to the versioned split rule (Pattern B in Self-Hosted Runner Reconciliation). Show the LOB their weighted occupancy. |
| Invoice / Azure billing mismatch | Finance ops + Azure subscription owner | Reconcile the GitHub invoice line against the Azure Cost Management entry; if they truly disagree, open a GitHub support ticket. |
- Disputes that cannot be resolved at the platform / LOB level escalate to a monthly FinOps council (platform lead, finance lead, one rotating LOB representative).
- Council decisions are recorded as ADRs (architecture decision records) in this repo so future disputes have precedent.
- Never alter historical chargeback rows. Adjustments are posted as separate credit / debit lines in the next month's report with a back-reference to the dispute issue. The warehouse audit trail stays clean.
- If the disputed amount is below an agreed threshold (for example, 50 USD), accept the dispute without investigation to avoid spending more on resolution than the disputed amount.
LOB engineers and cost owners cannot act on a chargeback they do not understand. Maintain a small set of user-facing docs alongside the design.
Maintain a single FAQ page on the internal developer portal. Required questions:
- How is my LOB charged for GitHub usage?
- Where do I see my LOB's spending? (Direct link to the cost center page and the per-LOB dashboard.)
- Why did my Copilot premium-request bill go up this month? (Pointer to the audit table for LOB cost owners; pointer to the model multipliers doc.)
- How do I move a repository to my LOB? (Pointer to the repo
lobproperty procedure.) - How do I dispute a charge? (Pointer to the dispute issue template.)
- Who do I contact? (Platform team distribution list and FinOps council rotation.)
When an LOB completes onboarding (step 10 of Onboard a new LOB), send the LOB cost owner a templated email containing:
- The cost center URL
- The per-LOB dashboard URL (filtered view in the chosen warehouse / Power BI surface)
- Link to the FAQ
- Link to the dispute process
- Names of the platform team contacts and the FinOps council
| Artifact | Owner | Location |
|---|---|---|
| This design doc | Platform team | gh-abcs-admin/docs/22-github-chargeback-system-design.md |
| Per-LOB FAQ | Platform team | Internal developer portal |
| Dispute issue template | Platform team | Chargeback automation repo .github/ISSUE_TEMPLATE/ |
| Allocation rule ADRs | FinOps council | This repo, in a new docs/adr/ folder |
| Per-LOB dashboard | Data / FinOps team | Warehouse / Power BI |
| Risk | Impact | Mitigation |
|---|---|---|
| User belongs to multiple LOB teams | Allocation goes to one LOB only; the other LOBs do not see the cost | Enforce single-LOB membership in the IdP. Treat warnings from the sync tool as P2 tickets. |
Repo lob property unset |
Repo-level usage falls into 99 - Attribution Defect |
Repo-creation templates require the property; weekly audit ticket for empty values. |
| User leaves the enterprise mid-month | Per GitHub allocation rules, residual usage stays attributed to the cost center the user belonged to | Document the behavior; do not remove deactivated users from cost centers. |
| Cost center deleted while still accruing | Future usage rebills to the central enterprise subscription | Decommission only after a full quiet period; verify in the usage summary. |
| Azure billing identity verification fails | Cost center created without LOB chargeback active | Retry workflow; escalate to LOB Azure admin. Park as PENDING- until resolved. |
Budget set to prevent_further_usage blocks production CI |
Engineering outage | Default to alert-only. Require LOB owner sign-off in writing before enabling enforcement. |
| Public preview API change (Budgets) | Automation breaks | Pin the X-GitHub-Api-Version header; subscribe to the GitHub changelog; smoke test in plan mode after each release. |
| Premium-request multipliers change | Forecast accuracy degrades | Surface multipliers in the report; re-baseline forecasts after any pricing change. |
| Mixed-ownership repo (multiple LOBs share code) | Single cost center cannot reflect joint ownership | Define a primary owner LOB in policy; optionally split via internal cross-charge in finance, not in GitHub. |
Detailed usage report (with username, workflow_path) is UI-only |
Not used by the automated pipeline by design | See Chargeback granularity — design decision. Detailed report stays available in the UI for ad-hoc investigations. |
| Azure subscription attachment per cost center is UI-only | LOB onboarding requires a manual gate that cannot be in CI | Govern via cost-center display-name convention + chargeback-lob Azure subscription tag + monthly binding audit query (see Section 9). Block go-live for the LOB until the audit row is clean. The change-management ticket holds the ID-level evidence; the platform repo does not. |
| Missing daily export beyond the API window | Permanent gap in long-term warehouse | Operate daily exports as a monitored job with paging; backfill within the 24-month API window; no recovery beyond that window. |
| No audit trail of cost-center membership changes | Disputes cannot prove who moved User X to LOB Y on what date | Persist daily snapshots of cost-center membership and the IdP group-change correlation ID in the warehouse. Link to the chargeback automation run ID. |
| Cross-country LOBs with different billing currency, tax treatment, or legal entity | Simple USD net-amount reporting hides legal-entity exposure; non-USD attach is a finance, tax, and legal review prerequisite, not an edge case | Gate non-USD LOBs behind explicit finance + tax + legal sign-off before attaching the Azure billing identity. Capture billing_currency, tax_jurisdiction, and legal entity per LOB in config/lobs.yaml. The chargeback report's USD-centric columns are defaults; multi-currency rollout requires schema extension. |
| GitHub Marketplace apps billed per org | Spend appears on the GitHub invoice but cannot be attached to a cost center | Treat as a separate "non-attributable GitHub spend" lane in the chargeback report; charge to 00 - Shared Platform or to the requesting LOB by manual policy. Out of scope for cost center automation. |
| Codespaces prebuild storage and Actions cache storage runaway | Sustained storage growth without a single owner | Add product-specific budgets per LOB cost center for these SKUs (see Budgets and Guardrails). |
| GitHub Models inference (when generally available) not yet in budget set | Unbudgeted spend after the meter ships | Add Models inference budget to the default set when the SKU is GA in the enterprise. |
- Confirm enhanced billing platform is live and cost centers are visible in the UI.
- Create the chargeback service account, PAT, and a private repo for the automation.
- Define LOB list, naming conventions, and the Azure subscription tag key (
chargeback-lob) inconfig/lobs.yaml. Subscription IDs themselves are not stored — see True Chargeback. - Create the
lobrepository custom property and seed values for the top 20 percent of repos by activity.
- Deploy
github/cost-center-automationin plan-only mode for two pilot LOBs. - Create the cost centers and assign resources for the pilots.
- Generate the monthly chargeback report against historical data; validate the column contract with finance.
- Do not attach Azure billing identities yet. The pilot operates in showback mode.
- Switch automation to apply mode for the pilot LOBs on a 6-hour schedule.
- Create the default budget set per pilot LOB with alert-only thresholds.
- Validate user, repo, and budget edge cases against the runbook.
- Attach Azure billing identities to pilot LOB cost centers.
- Verify the next GitHub invoice charges the LOB Azure subscriptions correctly.
- Sign off the report format with finance.
- Onboard remaining LOBs in waves of three to five per week.
- Backfill the
lobproperty across all repositories. Each wave should drive98 - Pending Onboardingand99 - Attribution Defectusage closer to zero. - Hand the operational runbook to the platform on-call rotation.
- Establish a quarterly review of LOB list, budget defaults, and naming conventions.
These are deliberately unresolved and need stakeholder input before implementation begins:
- Sub-LOB granularity. Does finance need a sub-LOB breakdown in the GitHub-side report, or is a flat LOB roll-up sufficient with sub-LOB derived from repo metadata?
- Catch-all chargeback policy. How is
00 - Shared Platformcharged back? Equal split across LOBs, weighted by headcount, or absorbed by the platform team's own budget? - Contractor and partner seats. Are contractor accounts in their own IdP group with a designated cost center, or do they roll up to the engaging LOB?
- Data residency premium-request multiplier. If the enterprise uses GitHub Enterprise Cloud with data residency, the 1.1x multiplier on premium requests should be surfaced in the report — confirm this is in scope.
- Approval workflow. Should bulk reassignments above a threshold require a manual approval step in the automation, or is plan-and-audit sufficient?
- Self-hosted runner attribution detail. Should runner cost be attributed at the LOB level only, or also broken down by repository? Repo-level requires runner pool tagging at finer granularity.
- Long-term archive. How many months of detailed usage reports does the organization need to retain in the chargeback system itself, separate from GitHub's own retention?
- Anomaly detection. Beyond the budget alert thresholds, should the warehouse layer add automated anomaly detection (statistical thresholds on per-LOB daily spend deltas)? Deferred to v2; budget alerts cover the most common cases.
- Forecasting and variance. Should the monthly report include forward forecasts and month-over-month variance per LOB? Requires at least 3 months of warehouse history; deferred to v2 unless finance wants it earlier.
- Commitment utilization. If the enterprise has a Copilot Business / Enterprise contract with committed seats, should the report show per-LOB commitment utilization (LOB share of committed vs. consumed)? Out of scope for v1; revisit when contract terms are confirmed.
- FOCUS schema commitment. Once a warehouse option is locked, should the schema use FOCUS specification column names (
BilledCost,SubAccountName, etc.) for forward compatibility with multi-cloud FinOps tooling? Recommend yes for Option 1 (Fabric + FinOps Toolkit), TBD for Options 2–4.
All URLs verified during the research and review cycles for this design (May 2026). Items marked verified were re-read live during review and the cited quote was confirmed against the source page.
- About cost centers — limitations, resource types, Azure subscription attachment — verified
- Cost center allocation rules per product
- Controlling and tracking costs at scale (cost center tutorial)
- About the enhanced billing platform
- Billing reports reference (summarized, detailed, premium-request) — verified
- GitHub product and SKU names
- REST API: Billing usage (
/usage,/usage/summary) — verified - REST API: Premium request usage
- REST API: Enhanced billing budgets (public preview,
2026-03-10) - REST API: Repository custom properties
- REST API: Teams and team membership
- REST API: GitHub Actions workflow runs
- Automating usage reporting with the REST API
- GitHub API versioning
- About Enterprise Managed Users
- SCIM provisioning for EMU with Entra ID
- About custom repository properties
- Personal access tokens — classic vs fine-grained
- Roles in an enterprise account
- About billing for GitHub Copilot
- Premium request behavior, quotas, and multipliers
- Managing Copilot policies for the enterprise
- About billing for GitHub Actions
- About self-hosted runners
- Actions Runner Controller (ARC) for Kubernetes
- github/cost-center-automation — official GitHub Python tool with Teams Mode, Repository Mode, PRU Mode, and budget creation — verified
- Azure Cost Management documentation
- Azure cost allocation rules
- Microsoft FinOps Toolkit — referenced as Option 1 in the warehouse strategy
- Microsoft Fabric overview
- Azure Data Explorer (ADX) overview
- FinOps Framework — lifecycle, capabilities, maturity
- FOCUS specification — open standard for cloud cost data
- FinOps showback vs chargeback maturity
- 12-github-copilot-governance.md — enterprise Copilot policies and content exclusions
- GitHub-Copilot-Usage-Based-Billing-Research.md — current premium-request multipliers (canonical source over external tables)
- 21-github-enterprise-adoption-plan.md — ADO→GitHub migration plan that this chargeback design slots into
- 20-governed-ai-sdlc-plan.md — governance context for AI-assisted SDLC
- Parallel research authored by a coworker (May 2026) was reviewed as part of the cross-check pass. The factual corrections and architectural additions accepted from that input are documented in the change history of this file; the underlying source document is not committed to this repository.
Source corpus: Official GitHub documentation, the
github/cost-center-automationrepository, Microsoft Learn (Azure Cost Management, Fabric, FinOps Toolkit), the FinOps Foundation framework, and the FOCUS specification. Multiple AI models cross-checked the design against the live documentation before publication.Document version date: 2026-05-14