WEBKT

不再为 GHCR 存储空间发愁:基于 GitHub Actions 的镜像自动清理方案

4 0 0 0

在容器化时代的 CI/CD 流程中,GitHub Container Registry (ghcr.io) 是很多开发者的首选。然而,随着镜像频繁构建,你会发现私有仓库中堆积了大量“无主”镜像版本(Untagged)或陈旧版本。GitHub 对私有镜像是有存储额度限制的,一旦超出可能会产生费用或导致构建失败。

不幸的是,GitHub 目前并没有在 UI 界面上提供类似“保留最后 10 个版本”的全局配置选项。今天,我们就用 GitHub Actions 配合 GitHub API,手撸一套自动清理方案。

一、 核心逻辑与 API 选择

要实现自动清理,我们需要经历三个步骤:

  1. 列出所有版本:调用 GitHub Packages API 获取镜像版本列表。
  2. 筛选策略:过滤掉带有 latest 或特定版本标签(如 v1.0.0)的镜像,仅针对过期的 sha-xxxx 或无标签镜像。
  3. 执行删除:调用删除接口释放空间。

虽然可以直接写 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' # 仅删除无标签版本(推荐)

五、 最佳实践建议

  1. 测试先行:在正式开启 schedule 之前,先手动触发 workflow_dispatch,并将删除指令改为 echo 打印,确认逻辑无误后再执行真正的 DELETE 操作。
  2. 结合 Dockerfile 优化:在 CI 构建镜像时,尽量给镜像打上 sha-${{ github.sha }} 的标签,而不是产生大量没有标签的 <none> 镜像,这样更方便追踪和清理。
  3. 监控存储:定期检查 GitHub Billing 页面,观察存储占用曲线,确保清理策略生效。

通过这套自动清理机制,你可以彻底告别手动删除镜像的琐碎工作,让 GHCR 始终保持精简高效。

DevOps进阶指南 GHCR容器镜像清理

评论点评