GitHub's new Billing - Assigning Cost Centers in Bulk
I've recently set up the internal cost centers to split our GitHub enterprise bill according to cost centers defined in GitHub. Cost Centers are part of GitHub's new Billing experience which is currently rolling out to GitHub Enterprise customers and which is also coming to GitHub for Teams.
You can assign resources to cost centers, which will in turn charge any costs associated with that resource to that cost center.
Examples of ressource categories are:
- Repositories - All costs associated with a specific repository. Action Minutes, Codespaces, LFS Storage etc.
- Users - All costs associated with a specific user. Enterprise Seat, Copilot, Advanced Security.
- Organizations - All costs associated with a specific organization. Costs generated by any unassigned repositories in the organization. GitHub Packages storage and network traffic.
- Enterprise - All costs associated with the whole enterprise. Costs associated with users who are not explicitly assigned to a cost center.
By linking organizations, users and repositories to a cost center, you can cause their specific costs to be redirected from the Enterprise to the specific Cost Center.
Linking Organizations and Repositories can be done through the Cost Center UI in the GitHub Enterprise Admin portal:
If you're lucky there are only a few Organizations that need to be assigned, but in our case, Members were a whole other story. We already have 500+ members and external collaborators in our enterprise and these needed to be assigned to specific cost centers. As you can see, this can't be done in the UI, but requires API calls.
It turns out the API is a bit tricky, there are certain things about the API that aren't (yet) nicely documented:
- You can only assign users to a cost center in batches of 50 (confirmed with GitHub Support)
- You can only assign a user if it isn't already assigned to another cost center. Changing cost centers requires 2 operations.
- The API is eventually consistent. Changes made may take a couple of seconds to show up in subsequent API calls.
- While you could assign cost centers on a per-user basis, this is a great way to burn through your API Rate Limits quickly.
To work around these issues, I've written a small wrapper around the API in PowerShell which uses the existing resource assignments stored in the Cost Centers to reduce the number of required calls to the API:
function Update-CostCenterResources {
param(
[Parameter(Mandatory=$true)]
[string[]]$Handles,
[Parameter(Mandatory=$true)]
[ValidateSet('Add','Delete')]
[string]$Action,
[Parameter(Mandatory=$true)]
$CostCenter,
[Parameter(Mandatory=$true)]
[string]$Enterprise
)
switch ($Action)
{
'Add' {
$method = 'POST'
$Handles = $Handles | Where-Object {
$handle = $_
return (($costCenter.resources | ?{ $_.type -eq "User" } | ?{$_.name -eq $handle }).Count -eq 0)
}
}
'Delete' {
$method = 'DELETE'
$Handles = $Handles | Where-Object {
$handle = $_
return (($costCenter.resources | ?{ $_.type -eq "User" } | ?{$_.name -eq $handle }).Count -gt 0)
}
}
}
# Call fails when processing too many users at once. Thus batching the calls...
$count = 0
do {
$batch = $Handles | Select-Object -Skip $count -First 50
$count += $batch.Count
if ($batch.Count -gt 0) {
$body = @{
users = [string[]]$batch
}
$_ = ($body | ConvertTo-Json) | gh api --method $method /enterprises/$Enterprise/settings/billing/cost-centers/$($CostCenter.id)/resource --input -
}
} while ($batch.Count -gt 0)
}
$enterprise = "xebia"
$costCenters = (invoke-gh -fromJson -- api /enterprises/$enterprise/settings/billing/cost-centers).costCenters
$costCenterNL = $costCenters | ?{ $_.name -eq "Netherlands" }
$handles = @("jessehouwing", "jessehouwing-demo")
# First remove the users from their currently assigned cost centers (if any)
$costCenters |
?{ $_.id -ne $costCenterNL.id } |
?{ $_ | ?{ $.resources | ?{ $_.type -eq "User" -and $_.name -in $handles } } |
%{
Update-CostCenterResources -handles $handles -action "Delete" -CostCenter $_ -Enterprise $enterprise
}
# Then assign the users to their new cost center
Update-CostCenterResources -handles $handles -action "Add" -CostCenter $costCenterNL -Enterprise $enterprise
You can find the current cost center for a user in the Cost Center's Resources array, or from the GitHub Enterprise assigned-seats REST API:
$handle = "jessehouwing"
$enterprise = "xebia"
# retrieve from Cost Center API
$costCenters = (gh api /enterprises/$enterprise/settings/billing/cost-centers | ConvertFrom-Json).costCenters
$currentCostCenter = $costCenters | ?{ $_.resources | ?{ $_.type -eq "User" -and $_.name -eq $handle } }
# retrieve from Assigned Seats API:
$enterpriseUsers = gh api https://api.github.com/enterprises/$enterprise/consumed-licenses --jq '.users[]' --paginate | ConvertFrom-Json
$currentCostCenter = ($enterpriseUsers | ?{ $_.github_com_login -eq $handle }).github_com_cost_center
By combining the above logic with some more proprietary magic, we were able to quickly assign all our members to Cost Centers based on their Azure EntraID metadata.
As you can see in the chart below, when we assigned out cost centers on January 9th, all future costs were no longer associated to the enterprise (in green), but to the respective cost centers.
Note: It is currently not possible to retroactively assign a resource to a cost center. Otherwise, we'd have assigned all costs as of the 1st of the month.Leave a comment.