编程 git tag 推上去,镜像自动进 GHCR:多架构构建、semver 标签,以及 latest 被 prerelease 顶掉的坑

2026-10-17 00:04:38

git tag 推上去,镜像自动进 GHCR:多架构构建、semver 标签,以及 latest 被 prerelease 顶掉的坑

目标很明确:打一个 v2.1.0 的 tag,CI 自动构建 amd64/arm64 镜像、推到 GHCR、打上 2.1.0 / 2.1 / 2 / latest 一整套标签。下面是一条能直接抄的 workflow,以及几个实际踩过的取舍。

触发与 permissions

name: Release
on:
  push:
    tags: ["v*"]
permissions:
  contents: write   # 建 GitHub Release 时需要
  packages: write   # 推 ghcr.io 必需

只有打 tag(或在 GitHub 上发 Release)才真正发布镜像。push 到 main/dev 通常只推 edge/dev 这类 tag 用于验证,不抢 latest。

GHCR 凭据:不需要额外 secret

GHCR 用 workflow 自带的 GITHUB_TOKEN 就能登录推送,只要 permissions 里给了 packages: write:

- uses: docker/login-action@v4
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

首次推完之后,要去 package 设置里把包改成 Public 并 link 到仓库,匿名 docker pull 才拉得动。GHCR 本身不做匿名拉取限速,仓库也在旁边,链路短。

想同时推 Docker Hub,用条件开关:仅当配了 vars.DOCKERHUB_USERNAME 才 login,token 放 secrets.DOCKERHUB_TOKEN,metadata-action 的 images 多行分别写两个 registry 即可。

语义化标签派生

- id: meta
  uses: docker/metadata-action@v5
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=ref,event=tag
      type=semver,pattern={{version}}
      type=semver,pattern={{major}}.{{minor}}
      type=semver,pattern={{major}}
      type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}

结果:v2.0.0 → 2.0.0 / 2.0 / 2 / latest;v2.0.0-rc1(ref 里含 -)→ 只有 2.0.0-rc1,不碰 latest。

这里的坑在于:latest 必须加 predicate。不给 enable 条件的话,prerelease 会静默把 latest 往下指,所有 docker pull :latest 的消费者就被带回旧版或预发布版。另外 metadata-action 默认不生成 :latest,得显式写 type=raw。

多架构:QEMU 模拟还是原生 runner

(a) 单 job + QEMU,简单,一个 job 搞定:

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

代价是 arm64 走 QEMU 模拟,大约 5–10 分钟;amd64 原生大概 2 分钟。镜像越大差距越明显。

(b) 分架构原生 runner + merge,快,但多一个 job。matrix 里 amd64 用 ubuntu-latest,arm64 用 ubuntu-24.04-arm,都不走 QEMU:

build:
  strategy:
    matrix:
      include:
        - { name: amd64, runner: ubuntu-latest,    platform: linux/amd64 }
        - { name: arm64, runner: ubuntu-24.04-arm, platform: linux/arm64 }
  runs-on: ${{ matrix.runner }}

每个架构只推单个 digest:

- id: build
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: ${{ matrix.platform }}
    outputs: type=image,name=ghcr.io/OWNER/IMG,push-by-digest=true,name-canonical=true,push=true
    provenance: false   # 关键
    build-args: |
      APP_VERSION=${{ github.ref_name }}
    cache-from: type=gha,scope=${{ matrix.name }}
    cache-to: type=gha,mode=max,scope=${{ matrix.name }}

provenance: false 不能省。默认开启的 provenance 会额外包一层 attestation index,最终 manifest 里会出现 unknown/unknown 条目,一些老客户端和镜像扫描工具会识别不了。

digest 写文件后上传:

- run: |
    mkdir -p /tmp/digests
    touch "/tmp/digests/${ { steps.build.outputs.digest }#sha256:}"
- uses: actions/upload-artifact@v4
  with:
    name: digests-${{ matrix.name }}
    path: /tmp/digests/*
    if-no-files-found: error

merge job 收齐再拼 manifest:

- uses: actions/download-artifact@v4
  with:
    path: /tmp/digests
    pattern: digests-*
    merge-multiple: true
- run: |
    cd /tmp/digests
    docker buildx imagetools create \
      $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
      $(printf 'ghcr.io/OWNER/IMG@sha256:%s ' *)
- run: docker buildx imagetools inspect ghcr.io/OWNER/IMG:${{ steps.meta.outputs.version }}

merge job 里同样要 setup-buildx-action 和 login-action,以及再跑一遍 metadata-action(标签列表由它输出到 DOCKER_METADATA_OUTPUT_JSON)。

版本号烤进镜像

build-args: |
  APP_VERSION=${{ github.ref_name }}

程序读这个环境/编译期常量,/api/version 或 UI 就能显示出来。手动触发时可从 VERSION 文件读,或退化成 dev-<短SHA>:

if [ "${{ github.ref_type }}" = "tag" ]; then
  echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
else
  echo "version=dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
fi

缓存

cache-from: type=gha,scope=${{ matrix.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.name }}

多镜像、多架构都要用 scope 分开,否则 amd64 和 arm64 的缓存互相覆盖,命中率反而掉下去。

Release 与 notes

softprops/action-gh-release 或 actions/github-script 都可以,generate_release_notes: true 会让 GitHub 根据上一个 tag 到 HEAD 之间的 PR 自动生成 notes;commit 遵循 feat: / fix: 前缀,分组会更可读。这一步需要 permissions.contents: write。该 action 有写权限,建议 SHA pin,blast radius 比较大。

兼容旧引用

有的项目同时打 tag=ref(保留 v 前缀)和 type=semver,pattern={{version}}(不带 v),这样用户既能 docker pull IMG:v1.0.0 也能 IMG:1.0.0。两条都留,代价只是多几个标签。

相关地址

复制全文 生成海报 自动化发版 Docker GHCR GitHub Actions CI CD

推荐文章

程序员茄子在线接单