不再为 GHCR 存储空间发愁:基于 GitHub Actions 的镜像自动清理方案
在容器化时代的 CI/CD 流程中,GitHub Container Registry (ghcr.io) 是很多开发者的首选。然而,随着镜像频繁构建,你会发现私有仓库中堆积了大量“无主”镜像版本(Untagged)或陈旧版本。GitHub 对私有镜像是有存储额度限制的,一旦超出可能会产生费用或导致构建失败。
不幸的是,GitHub 目前并没有在 UI 界面上提供类似“保留最后 10 个版本”的全局配置选项。今天,我们就用 GitHub Actions 配合 GitHub API,手撸一套自动清理方案。
一、 核心逻辑与 API 选择
要实现自动清理,我们需要经历三个步骤:
- 列出所有版本:调用 GitHub Packages API 获取镜像版本列表。
- 筛选策略:过滤掉带有
latest或特定版本标签(如v1.0.0)的镜像,仅针对过期的sha-xxxx或无标签镜像。 - 执行删除:调用删除接口释放空间。
虽然可以直接写 curl,但在 GitHub Actions 环境下,官方提供的 GitHub CLI (gh) 已经封装好了认证和 API 请求,代码会更简洁。
二、 自动化脚本实现
我们将创建一个专用的 Workflow 文件 .github/workflows/cleanup-ghcr.yml。
1. 定义清理策略
在这个例子中,我们的策略是:保留最近的 5 个版本,其余全部删除,但绝不删除带有 latest 标签的版本。
2. Workflow 配置
name: Cleanup Old GHCR Images
on:
schedule:
- cron: '0 0 * * 0' # 每周日凌晨运行一次
workflow_dispatch: # 支持手动触发
jobs:
delete-old-images:
runs-on: ubuntu-latest
permissions:
packages: write # 必须赋予写入权限才能执行删除
env:
PACKAGE_NAME: "your-app-name" # 你的镜像名称
steps:
- name: Delete old images
run: |
# 获取所有版本的 ID,按创建时间倒序排列
# jq 过滤掉带有 'latest' 标签的版本
VERSIONS=$(gh api /user/packages/container/${{ env.PACKAGE_NAME }}/versions --paginate \
--jq '.[] | select(.metadata.container.tags | contains(["latest"]) | not) | .id')
# 将 ID 转为数组
VERSION_ARRAY=($VERSIONS)
# 计算需要删除的数量(保留前 5 个)
KEEP_COUNT=5
TOTAL_COUNT=${#VERSION_ARRAY[@]}
if [ "$TOTAL_COUNT" -gt "$KEEP_COUNT" ]; then
echo "发现 $TOTAL_COUNT 个版本,准备删除旧的 $((TOTAL_COUNT - KEEP_COUNT)) 个..."
# 截取从第 6 个开始的所有 ID 执行删除
for id in "${VERSION_ARRAY[@]:$KEEP_COUNT}"; do
echo "正在删除版本 ID: $id"
gh api --method DELETE /user/packages/container/${{ env.PACKAGE_NAME }}/versions/$id --silent
done
echo "清理完成!"
else
echo "当前版本数量为 $TOTAL_COUNT,无需清理。"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
三、 关键细节解析
1. 个人 vs 组织 (Org)
上面的代码使用的是 /user/packages/... 路径,这适用于个人账号。如果你是为组织仓库配置,需要将 API 路径修改为:
- 获取版本:
/orgs/{org_name}/packages/container/{package_name}/versions - 删除版本:
/orgs/{org_name}/packages/container/{package_name}/versions/{version_id}
2. 权限陷阱
默认的 GITHUB_TOKEN 可能没有权限删除 Packages。你需要在 YAML 中明确声明 permissions: packages: write。此外,如果该镜像是从另一个仓库关联过来的,确保 Workflow 运行在正确的权限上下文中。
3. 标签保护
脚本中 select(.metadata.container.tags | contains(["latest"]) | not) 这一行非常关键。它通过 jq 确保了那些被标记为 latest 的生产环境镜像不会被误删。如果你还有其他保护标签(如 stable),可以继续增加逻辑。
四、 进阶:使用现成的 Action
如果你不想自己维护 Shell 脚本,社区里有一个非常成熟的封装:actions/delete-package-versions。
使用方式如下:
- name: Delete old versions
uses: actions/delete-package-versions@v4
with:
package-name: 'your-app-name'
package-type: 'container'
min-versions-to-keep: 5
delete-only-untagged-versions: 'true' # 仅删除无标签版本(推荐)
五、 最佳实践建议
- 测试先行:在正式开启
schedule之前,先手动触发workflow_dispatch,并将删除指令改为echo打印,确认逻辑无误后再执行真正的 DELETE 操作。 - 结合 Dockerfile 优化:在 CI 构建镜像时,尽量给镜像打上
sha-${{ github.sha }}的标签,而不是产生大量没有标签的<none>镜像,这样更方便追踪和清理。 - 监控存储:定期检查 GitHub Billing 页面,观察存储占用曲线,确保清理策略生效。
通过这套自动清理机制,你可以彻底告别手动删除镜像的琐碎工作,让 GHCR 始终保持精简高效。