Skip to the content.

Powershell script

#Requires -Version 5.1
<#
.SYNOPSIS
    Reports GitHub Actions usage (minutes and cost) consumed within a given period.

.DESCRIPTION
    Uses the GitHub CLI (`gh api`) to call the enhanced billing platform's
    "usage report" endpoint:

        /organizations/{org}/settings/billing/usage
        /users/{username}/settings/billing/usage
        /enterprises/{enterprise}/settings/billing/usage

    That endpoint only filters by year / month / day, so this script queries every
    month spanned by -StartDate..-EndDate, then filters the returned line items by
    their per-day `date` field to the exact range and sums them up. The breakdown
    is reported per repository and SKU (the usage endpoint exposes `repositoryName`
    and `sku` per line item, but no workflow/action name).

    Authentication is handled by `gh` (run `gh auth login` first). The calling user
    must be an org/enterprise admin or billing manager. Only the last 24 months of
    data are available, and the endpoint requires an account on the enhanced billing
    platform. Note: the billing usage endpoints do NOT work with fine-grained PATs.

.PARAMETER Org
    Organization login to report on.

.PARAMETER User
    User login to report on.

.PARAMETER Enterprise
    Enterprise slug to report on.

.PARAMETER StartDate
    Inclusive start of the period (any parseable date, e.g. 2026-05-01).

.PARAMETER EndDate
    Inclusive end of the period (e.g. 2026-05-31).

.PARAMETER Product
    Product to report on. Defaults to 'Actions'. Use '*' to include all products.

.PARAMETER CostCenterId
    (Enterprise only) Restrict to a single cost center.

.EXAMPLE
    .\Get-GitHubActionsUsage.ps1 -Org my-org -StartDate 2026-05-01 -EndDate 2026-05-31

.EXAMPLE
    .\Get-GitHubActionsUsage.ps1 -Enterprise my-ent -StartDate 2026-04-15 -EndDate 2026-06-14 -CostCenterId 42
#>
[CmdletBinding(DefaultParameterSetName = 'Org')]
param(
    [Parameter(Mandatory, ParameterSetName = 'Org')]
    [string]$Org,

    [Parameter(Mandatory, ParameterSetName = 'User')]
    [string]$User,

    [Parameter(Mandatory, ParameterSetName = 'Enterprise')]
    [string]$Enterprise,

    [Parameter(Mandatory)]
    [datetime]$StartDate,

    [Parameter(Mandatory)]
    [datetime]$EndDate,

    [string]$Product = 'Actions',

    [Parameter(ParameterSetName = 'Enterprise')]
    [int]$CostCenterId
)

$ErrorActionPreference = 'Stop'

# --- Preflight: gh present and authenticated ------------------------------------
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
    throw "GitHub CLI ('gh') was not found on PATH. Install it from https://cli.github.com/ and run 'gh auth login'."
}
gh auth status 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
    throw "You are not logged in to GitHub CLI. Run 'gh auth login' (the account must have billing/admin access)."
}

if ($EndDate -lt $StartDate) {
    throw "-EndDate ($($EndDate.ToString('yyyy-MM-dd'))) is earlier than -StartDate ($($StartDate.ToString('yyyy-MM-dd')))."
}

# --- Resolve the base API path for the chosen account type ----------------------
switch ($PSCmdlet.ParameterSetName) {
    'Org'        { $basePath = "/organizations/$Org";   $scopeLabel = "organization '$Org'" }
    'User'       { $basePath = "/users/$User";          $scopeLabel = "user '$User'" }
    'Enterprise' { $basePath = "/enterprises/$Enterprise"; $scopeLabel = "enterprise '$Enterprise'" }
}

# --- Build the list of (year, month) pairs the period spans ---------------------
$cursor = Get-Date -Year $StartDate.Year  -Month $StartDate.Month -Day 1
$last   = Get-Date -Year $EndDate.Year    -Month $EndDate.Month   -Day 1
$months = while ($cursor -le $last) { $cursor; $cursor = $cursor.AddMonths(1) }

# --- Fetch usage for each month, collect line items -----------------------------
$allItems = New-Object System.Collections.Generic.List[object]

foreach ($m in $months) {
    $endpoint = "$basePath/settings/billing/usage?year=$($m.Year)&month=$($m.Month)"
    if ($PSCmdlet.ParameterSetName -eq 'Enterprise' -and $PSBoundParameters.ContainsKey('CostCenterId')) {
        $endpoint += "&cost_center_id=$CostCenterId"
    }

    Write-Verbose "GET $endpoint"
    $raw = gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" $endpoint 2>&1
    if ($LASTEXITCODE -ne 0) {
        throw "gh api call failed for $($m.ToString('yyyy-MM')): $raw"
    }

    $data = $raw | ConvertFrom-Json
    if ($data.usageItems) { $data.usageItems | ForEach-Object { $allItems.Add($_) } }
}

# --- Filter to the exact date window and chosen product -------------------------
$start = $StartDate.Date
$end   = $EndDate.Date
$filtered = $allItems | Where-Object {
    $d = [datetime]::Parse($_.date)
    ($d.Date -ge $start) -and ($d.Date -le $end) -and (($Product -eq '*') -or ($_.product -eq $Product))
}

if (-not $filtered) {
    Write-Host "No $Product usage found for $scopeLabel between $($start.ToString('yyyy-MM-dd')) and $($end.ToString('yyyy-MM-dd'))." -ForegroundColor Yellow
    return
}

# --- Aggregate by repository + SKU ----------------------------------------------
$breakdown = $filtered | Group-Object repositoryName, sku | ForEach-Object {
    $first = $_.Group | Select-Object -First 1
    [pscustomobject]@{
        Repository = if ($first.repositoryName) { $first.repositoryName } else { '(none)' }
        SKU        = $first.sku
        UnitType   = $first.unitType
        Quantity   = [math]::Round((($_.Group | Measure-Object quantity      -Sum).Sum), 2)
        Gross      = [math]::Round((($_.Group | Measure-Object grossAmount   -Sum).Sum), 2)
        Discount   = [math]::Round((($_.Group | Measure-Object discountAmount -Sum).Sum), 2)
        Net        = [math]::Round((($_.Group | Measure-Object netAmount     -Sum).Sum), 2)
    }
} | Sort-Object -Property Net -Descending

# --- Report ---------------------------------------------------------------------
Write-Host ""
Write-Host "GitHub $Product usage for $scopeLabel" -ForegroundColor Cyan
Write-Host "Period: $($start.ToString('yyyy-MM-dd')) to $($end.ToString('yyyy-MM-dd'))" -ForegroundColor Cyan
Write-Host ""

$breakdown | Format-Table -AutoSize

$totalMinutes = ($filtered | Where-Object { $_.unitType -eq 'minutes' } | Measure-Object quantity -Sum).Sum
$totalGross   = ($filtered | Measure-Object grossAmount   -Sum).Sum
$totalDiscount= ($filtered | Measure-Object discountAmount -Sum).Sum
$totalNet     = ($filtered | Measure-Object netAmount     -Sum).Sum

Write-Host ("Total minutes : {0:N2}" -f ($totalMinutes | ForEach-Object { if ($_) { $_ } else { 0 } }))
Write-Host ("Gross cost    : {0:N2}" -f $totalGross)
Write-Host ("Discounts     : {0:N2}" -f $totalDiscount)
Write-Host ("Net cost      : {0:N2}" -f $totalNet) -ForegroundColor Green
Write-Host "(Amounts are in the account's billing currency, typically USD.)" -ForegroundColor DarkGray

# Emit the breakdown to the pipeline so it can be exported / processed further.
$breakdown