Skip to content

Instantly share code, notes, and snippets.

@jessehouwing
Last active September 24, 2025 09:26
Show Gist options
  • Select an option

  • Save jessehouwing/75e89531e915033afbb66cda3fd09b38 to your computer and use it in GitHub Desktop.

Select an option

Save jessehouwing/75e89531e915033afbb66cda3fd09b38 to your computer and use it in GitHub Desktop.
Update Repository Cost Centers based on Custom Properties
$ErrorActionPreference = 'Stop'
function Invoke-Gh {
<#
.Synopsis
Wrapper function that deals with Powershell's peculiar error output when gh uses the error stream.
.Example
Invoke-Git ThrowError
$LASTEXITCODE
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[switch]$slurp,
[Parameter(Mandatory = $false)]
[switch]$fromJson,
[parameter(ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)
$output = & {
[CmdletBinding()]
param(
[parameter(ValueFromRemainingArguments = $true)]
[string[]]$InnerArgs
)
$sleep = 0
do {
if ($sleep -gt 0) {
Start-Sleep -Seconds $sleep
}
$allOutput = & gh @InnerArgs 2>&1
$stderr = $allOutput | ? { $_ -is [System.Management.Automation.ErrorRecord] }
$output = $allOutput | ? { $_ -isnot [System.Management.Automation.ErrorRecord] }
$sleep += 5
}
while (
$stderr -like "*GraphQL: was submitted too quickly*" -or
$stderr -like "*API rate limit exceeded*"
)
if ($LASTEXITCODE -ne 0) {
throw "Error executing gh command: $InnerArgs `n" + $stderr
}
if ($slurp) {
$output = $output | & jq -s
}
if ($fromJson) {
$output = $output | ConvertFrom-Json
}
return $output
} -ErrorAction SilentlyContinue -ErrorVariable fail -InnerArgs:$Arguments
if ($fail) {
throw $fail.Exception
}
return $output
}
function Update-CostCenterResources {
param(
[Parameter(Mandatory = $true)]
[string[]]$Handles,
[ValidateSet('User', 'Repo', 'Org')]
[string]$ResourceType = "User",
[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 $ResourceType } | ? { $_.name -eq $handle }).Count -eq 0)
}
}
'Delete' {
$method = 'DELETE'
$Handles = $Handles | Where-Object {
$handle = $_
return (($costCenter.resources | ? { $_.type -eq $ResourceType } | ? { $_.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 30
$count += $batch.Count
if ($batch.Count -gt 0) {
switch ($ResourceType) {
'User' {
$body = @{
users = [string[]]$batch
}
}
'Org' {
$body = @{
organizations = [string[]]$batch
}
}
'Repo' {
$body = @{
repositories = [string[]]$batch
}
}
}
$_ = ($body | ConvertTo-Json) | gh api --method $method /enterprises/$Enterprise/settings/billing/cost-centers/$($CostCenter.id)/resource --input -
}
} while ($batch.Count -gt 0)
}
function get-allorganizations {
param(
[string]$enterprise
)
return (invoke-gh -slurp -- api graphql --paginate -F enterprise=$enterprise -f query='query($endCursor: String, $enterprise: String!) {
enterprise(slug: $enterprise) {
id,
organizations(first: 100, after: $endCursor) {
nodes
{
login,
name
},
pageInfo {
hasNextPage
endCursor
}
}
}
}' --jq '.data.enterprise.organizations.nodes' | ConvertFrom-Json)
}
function update-repocostcenters {
Write-Output "Updating Costcenters for repositories in $enterprise."
foreach ($org in $orgs) {
Write-Output "Processing organization $org"
$repos = invoke-gh -slurp api /orgs/$org/repos --paginate --jq '.[] | { name: .name, full_name: .full_name }' | ConvertFrom-Json
foreach ($repo in $repos) {
$customProperties = invoke-gh api /repos/$($repo.full_name)/properties/values --jq '.' | ConvertFrom-Json
$repoCostCenterProperty = $customProperties | Where-Object { $_.property_name -eq "cost-center" } | Select-Object -ExpandProperty value -ErrorAction Ignore
$repo | Add-Member -NotePropertyName cost_center -NotePropertyValue $repoCostCenterProperty -Force
$currentCostCenter = $costCenters | ? { $_.resources | ? { $_.type -eq "Repo" -and $_.name -eq $repo.full_name } }
if ( $repo.cost_center -eq "inherit" ) {
$repo.cost_center = $null
}
$targetCostCenter = $null
if ($repo.cost_center) {
$targetCostCenter = $costCenters | ? { $_.name -eq $repo.cost_center }
if (-not $targetCostCenter) {
Write-Warning "Costcenter for repository $($repo.full_name) not found."
}
}
if ($null -eq $currentCostCenter) {
if ($null -ne $targetCostCenter) {
Write-Output "Updating costcenter for repository $($repo.full_name) from unassigned to $($targetCostCenter.name)."
Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
}
else {
Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
}
}
else {
if ($null -eq $targetCostCenter) {
Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to unassigned."
Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
}
elseif ($currentCostCenter.id -ne $targetCostCenter.id) {
Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to $($targetCostCenter.name)."
Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
}
}
}
}
}
$enterprise = "xebia"
$orgs = get-allorganizations $enterprise | % { $_.login }
$costCenters = (invoke-gh -fromJson -- api /enterprises/$enterprise/settings/billing/cost-centers).costCenters
update-repocostcenters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment