最近這幾個月因為寫了不少新的 GitHub Actions workflows,CI 頻繁執行的狀況下,導致這幾天開始出現了 Artifact storage quota has been hit
的錯誤訊息。經查詢後瞭解到,原來我的 GitHub Pro 訂閱,除了 GitHub Actions 每月 3,000 分鐘的執行時間額度外,還有每月 2 GB 的用量限制。但仔細查看 GitHub Actions billing 文件之後,發現還真的非常複雜,這篇文章我打算來順一下計費的脈絡。

以下是我遇到的錯誤訊息:
Error: Failed to CreateArtifact: Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.
More info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending
GitHub Actions 的計費方式
基本上,在使用 GitHub Actions 的時候,以下這兩種情境是完全免費的:
- 使用自我託管的執行器 (self-hosted runners)
- 你的專案是個公開儲存庫 (public repositories),且使用標準 GitHub 託管的執行器 (GitHub-hosted runners)
對於私人儲存庫 (private repositories),每個 GitHub 帳戶將根據其購買的方案,可以獲得一定額度的免費分鐘數和成品儲存空間,用於 GitHub 託管的執行器,用量的計算大致分成以下兩類:
-
執行分鐘數 (每月重置)
分鐘數是根據當月在每種 執行器 (runner) 類型上使用的總處理時間計算。
-
使用儲存空間用量 (每月不會重置)
儲存空間是根據當月成品(artifact)的小時使用量計費。
儲存空間用量的計算方式非常複雜,而且文件也講的很難理解,更沒有後台可以讓你快速查看用量,其實不好懂。GitHub 並不是給你一個容量的上限,而是所謂 GB-Hours
的概念,再除以每月 744 小時。
預設的情況下,超出額度限制的任何使用量,會直接不能使用,不會直接向用戶收費,但你可以到 Budgets and alerts 調整預算限制與警示通知。
分鐘數的計算方式
任何時候 CI / CD 要執行 Pipeline 時,都需要 Runner (執行器) 才能執行程式,當你使用了標準 GitHub 託管的執行器 (GitHub-hosted runners) 在「私有專案」(private repo),就會記錄你每次的「執行分鐘數」。
不過,執行器 (Runner) 執行 1 分鐘,不代表會扣除你的每月免費額度 3,000 分鐘的 1 分鐘。有時候執行 1 分鐘,會直接扣除 2 分鐘額度。有時候執行 1 分鐘,會直接扣除 10 分鐘額度。這樣的機制我們稱為「分鐘數乘數」(minute multipliers)。
-
若選用 Linux runner,乘數為 1 倍
如果用 Linux 的 Runner 跑 10 分鐘,就會以 10 分鐘來計算。
-
若選用 Windows runner,乘數為 2 倍
如果用 Windows 的 Runner 跑 10 分鐘,就會以 20 分鐘來計算。
-
若選用 macOS runner,乘數為 10 倍
如果用 macOS 的 Runner 跑 10 分鐘,就會以 100 分鐘來計算。
如需 GitHub 託管執行器的分鐘數乘數的完整詳細資訊,請參閱 Actions 分鐘數乘數參考。
儲存空間用量的計算方式
我用以下例子來說明儲存空間的成本計算方式。
假設你在這個月使用 2 GB 的儲存空間 20 天,並使用 1 GB 的儲存空間 10 天,則您的儲存空間使用量將為:
- 2GB x 20 天 x (每天 24 小時) = 960 GB-Hours
- 3GB x 10 天 x (每天 24 小時) = 720 GB-Hours
當月總 GB-Hours
為 960 + 720 = 1680 GB-Hours
當月總 GB-Month
為 1680 GB-Hours / 744 Hours-per-month = 2.25 GB
如果你不小心還沒到月底就用超過限制了,要等下個月初才會重新開始計算,我覺得非常不便!
以我的帳號的 Action storage 上限為 2GB 為例,不用到月底,你的 Action storage 就無法再寫入了:

如果你有允許超量使用的話,月底時,GitHub 會將您的儲存空間四捨五入到最接近的 MB,並自動扣款。
這裡必須要注意的是,GitHub 會在每 6 到 12 小時的視窗內更新您的儲存空間用量,但你並不知道何時會計算你當前的用量,這部分是個大黑箱。
假設你的計算用量的時間是晚上 00:00 的話,你在 01:00 寫入了 1GB 的檔案到 artifact storage,但是在 03:00 的時候把 artifact storage 刪除,那麼到 06:00 的時候,就不會計算到你的 GB-Hours
之內。
如何刪除 Action storage 佔用的空間
很可惜的,在 GitHub 上面完全沒有任何 UI 可以看到 Action storage 佔用的空間,而且你的 Action storage 用量每個 Private Repo 都有可能會用到,所以只能透過 GitHub REST API 取得用量資訊,然後逐一刪除過期的 workflow runs 或 artifacts 才有機會釋出空間。
因此,我用 PowerShell 寫了一份腳本,可以快速刪除 GitHub Actions 的 artifacts 與 workflow runs!
只要建立 $HOME\Documents\PowerShell\Scripts\Cleanup-GitHubActions.ps1
檔案:
<#
.SYNOPSIS
清除指定 GitHub Repo 中早於指定天數的 GitHub Actions artifacts 與 workflow runs。
.DESCRIPTION
以 GitHub CLI gh 調用 REST API。
先刪 artifacts 再刪 workflow runs。支援 -WhatIf 與 -Confirm。
.PARAMETER Owner
擁有者帳號。例如: doggy8088
.PARAMETER Repo
倉庫名稱。例如: playwright.tw
.PARAMETER DaysToKeep
保留天數。早於此天數的資料會被刪除。預設 14。
.PARAMETER PerPage
單頁抓取數量。預設 100。
.PARAMETER ArtifactsOnly
只處理 artifacts。
.PARAMETER RunsOnly
只處理 workflow runs。
.EXAMPLE
.\Cleanup-GHA.ps1 -Owner doggy8088 -Repo playwright.tw -DaysToKeep 14 -WhatIf
.EXAMPLE
.\Cleanup-GHA.ps1 -Owner org -Repo repo -Confirm
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param(
[Parameter(Mandatory = $true)][string]$Owner,
[Parameter(Mandatory = $true)][string]$Repo,
[int]$DaysToKeep = 14,
[int]$PerPage = 100,
[switch]$ArtifactsOnly,
[switch]$RunsOnly
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Test-Gh {
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
throw "找不到 GitHub CLI gh。請安裝後重試。"
}
try {
gh auth status | Out-Null
} catch {
throw "gh 尚未登入或權限不足。請執行 gh auth login 後重試。"
}
}
function Get-Cutoff {
param([int]$KeepDays)
return [DateTimeOffset]::UtcNow.AddDays(-$KeepDays)
}
function Remove-OldArtifacts {
param(
[string]$BasePath,
[DateTimeOffset]$Cutoff,
[int]$PerPage
)
$page = 1
$deleted = 0
do {
try {
$resp = gh api "$BasePath/artifacts?per_page=$PerPage&page=$page" | ConvertFrom-Json
} catch {
Write-Warning "取得 artifacts 清單失敗,page=$page。錯誤: $($_.Exception.Message)"
break
}
$items = @()
if ($resp -and $resp.artifacts) { $items = @($resp.artifacts) }
foreach ($a in $items) {
$created = [DateTimeOffset]::Parse($a.created_at)
if ($created -lt $Cutoff) {
$target = "artifact id=$($a.id) name=$($a.name) created_at=$($a.created_at) run_id=$($a.workflow_run.id)"
if ($PSCmdlet.ShouldProcess($target, "Delete")) {
try {
gh api --method DELETE "$BasePath/artifacts/$($a.id)" | Out-Null
$deleted++
Write-Host "Deleted $target"
} catch {
Write-Warning "刪除 artifact 失敗 id=$($a.id) name=$($a.name) 錯誤: $($_.Exception.Message)"
}
} else {
Write-Host "Would delete $target"
}
}
}
$page++
} while ($items.Count -gt 0)
return $deleted
}
function Remove-OldRuns {
param(
[string]$BasePath,
[DateTimeOffset]$Cutoff,
[int]$PerPage
)
$page = 1
$deleted = 0
do {
try {
$resp = gh api "$BasePath/runs?per_page=$PerPage&page=$page" | ConvertFrom-Json
} catch {
Write-Warning "取得 workflow runs 清單失敗,page=$page。錯誤: $($_.Exception.Message)"
break
}
$items = @()
if ($resp -and $resp.workflow_runs) { $items = @($resp.workflow_runs) }
foreach ($r in $items) {
$created = [DateTimeOffset]::Parse($r.created_at)
if ($created -lt $Cutoff) {
$target = "run id=$($r.id) name=$($r.name) created_at=$($r.created_at)"
if ($PSCmdlet.ShouldProcess($target, "Delete")) {
try {
gh api --method DELETE "$BasePath/runs/$($r.id)" | Out-Null
$deleted++
Write-Host "Deleted $target"
} catch {
Write-Warning "刪除 workflow run 失敗 id=$($r.id) name=$($r.name) 錯誤: $($_.Exception.Message)"
}
} else {
Write-Host "Would delete $target"
}
}
}
$page++
} while ($items.Count -gt 0)
return $deleted
}
# 主流程
if ($ArtifactsOnly -and $RunsOnly) {
throw "-ArtifactsOnly 與 -RunsOnly 不可同時使用。"
}
Test-Gh
$base = "repos/$Owner/$Repo/actions"
$cutoff = Get-Cutoff -KeepDays $DaysToKeep
Write-Host "將刪除建立於 $DaysToKeep 天之前的 artifacts 與 workflow runs"
Write-Host "時間門檻 UTC: $($cutoff.UtcDateTime.ToString('u'))"
Write-Host ""
$totalArtifacts = 0
$totalRuns = 0
if (-not $RunsOnly) {
$totalArtifacts = Remove-OldArtifacts -BasePath $base -Cutoff $cutoff -PerPage $PerPage
Write-Host ""
Write-Host "共刪除 artifacts: $totalArtifacts"
Write-Host ""
}
if (-not $ArtifactsOnly) {
$totalRuns = Remove-OldRuns -BasePath $base -Cutoff $cutoff -PerPage $PerPage
Write-Host ""
Write-Host "共刪除 workflow runs: $totalRuns"
}
# 結束碼: 有刪除則 0,否則 0(不視為錯誤)
exit 0
使用方式如下,記得將 USER
與 REPO
換成你自己的 GitHub 帳號與 Repo 名稱:
-
刪除超過 14 天的所有 artifacts 與 workflow runs
Cleanup-GitHubActions -Owner USER -Repo REPO
你也可以明確加入 -DaysToKeep
指定保留的天數 (預設為 14 天)
Cleanup-GitHubActions -Owner USER -Repo REPO -DaysToKeep 14
-
刪除所有 artifacts 與 workflow runs
Cleanup-GitHubActions -Owner USER -Repo REPO -DaysToKeep 0
-
僅刪除 artifacts 且保留 workflow runs 所有的 Summary 與 Logs
Cleanup-GitHubActions -Owner USER -Repo REPO -DaysToKeep 0 -ArtifactsOnly
-
僅刪除 workflow runs (此舉將會連同 artifacts 一起全數刪除)
Cleanup-GitHubActions -Owner USER -Repo REPO -DaysToKeep 0 -RunsOnly
相關連結