2671 字
13 分钟
阅读量:9 次

GitHub Actions 自动构建 Docker 镜像完全指南:从手动触发到定时检查与版本追踪

🤖AI 摘要
AI

在开源项目镜像分发、企业内部基础镜像维护、多环境构建等场景中,如何让「上游源码变更 → 自动构建 → 推送镜像」这条链路既自动又可控,是一个值得认真设计的问题。本文以一个真实项目 daitcl/Microsoft-Rewards-Script 为例,从最朴素的手动构建出发,逐步演进到「定时检查 + 无状态判断 + 语义化标签」的完整方案,并深入其设计原理与踩坑记录。


1. 最朴素的做法:手动触发构建#

最初的方案非常直接——在仓库里放一个 workflow,配好 workflow_dispatch,需要更新时进 Actions 页面点一下 “Run workflow”:

name: Build and Push Docker Image
on:
workflow_dispatch:
inputs:
tag:
description: '镜像标签'
required: false
default: 'latest'
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: chiihero/Microsoft-Rewards-Script
ref: V4-china
path: source
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: ./source
file: ./source/Dockerfile
push: true
tags: ghcr.io/${{ github.repository_owner }}/microsoft-rewards-script:${{ inputs.tag }}

对于个人项目、更新不频繁的场景,这已经够用了。但运行一段时间后会出现问题:

  • 上游更新了不知道:需要人工去上游仓库看 package.json 的 version 有没有变。
  • 无法追溯源码:镜像上只有 latest,出问题时无法定位到对应哪个 commit。
  • 重复劳动:每次都要手动点,容易忘记或手抖。
  • latest 漂移:容器重建后行为可能变了,排查困难。

2. 改进:让标签携带语义信息#

最直接的改进是一次构建,打多个标签。上游 package.json 里有精确版本号,git 有 commit SHA,把这两个信息都反映到镜像标签上:

标签说明示例
latest最新一次成功构建latest
<version>上游 package.json 精确版本号4.3.2.3
v<version>带 v 前缀,符合语义化习惯v4.3.2.3
sha-<short>对应上游源码提交短 SHA,便于追溯sha-a1b2c3d

读取 package.json 版本号:这一步不需要 jq 或 grep,用 Node 自带的 JSON 解析最稳:

Terminal window
VERSION=$(node -p "require('./source/package.json').version")

写入多个标签:在 build-push-action 的 tags: 里逐行列出即可:

tags: |
${{ env.IMAGE_REF }}:latest
${{ env.IMAGE_REF }}:${{ steps.pkg.outputs.version }}
${{ env.IMAGE_REF }}:v${{ steps.pkg.outputs.version }}
${{ env.IMAGE_REF }}:sha-${{ steps.source_sha.outputs.sha }}

其中 IMAGE_REF 需在 workflow 的 env: 中定义为 ghcr.io/<owner>/microsoft-rewards-script。

3. 定时检查:从”人找版本”到”版本找人”#

第 2 节的标签方案虽然丰富了元信息,但触发方式依然是手动的。

3.1 思路一:维护状态文件(不推荐)#

最直观的做法是:每次构建完,把当前版本号写到一个文件里,下次定时任务对比文件与上游 package.json。

- name: 对比版本
run: |
OLD=$(cat .last_built_version)
NEW=$(node -p "require('./source/package.json').version")
if [ "$OLD" != "$NEW" ]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
fi

问题:状态存储不可靠。

存储方式缺陷
Actions Cache会被驱逐(7 天不访问即删)
Artifact默认 90 天过期
提交回仓库污染提交历史,可能触发递归 workflow
GitHub Variables写权限粒度粗,更新有延迟

3.2 思路二:以镜像存在性为判断依据(推荐)#

换一个角度看问题:“是否需要构建”等价于”目标标签的镜像是否已存在”。

构建工作流本来就往 GHCR 推 v<version> 标签,那就用它做判断:

读取 package.json 版本 → 查 GHCR 上是否已有 v<version>
├── 有 → 跳过构建
└── 无 → 触发构建

这个判断无状态、幂等、可靠。不维护任何额外文件,反复运行结果一致,失败了下次定时任务自然重试。

判断逻辑:should_build = !exists(ghcr.io/owner/image:v${VERSION})

查询镜像是否存在,有两种方式:

方式 A:用 GitHub API(更快)

Terminal window
gh api "/users/${OWNER}/packages/container/microsoft-rewards-script/versions" \
-H "Accept: application/vnd.github+json" \
| grep -q "v${VERSION}"

方式 B:用 docker manifest inspect(更通用)

Terminal window
docker login ghcr.io -u "${{ github.actor }}" --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}"
docker manifest inspect "ghcr.io/${OWNER}/microsoft-rewards-script:v${VERSION}" >/dev/null 2>&1

个人账号用 /users/<owner>/...,组织账号用 /orgs/<owner>/...,两者都试一下最保险。

4. 职责分离:引入 Reusable Workflow#

第 3 节的检查逻辑如果塞进 build.yml,会让它同时承担”检查”和”构建”两个职责, 将来想复用构建就得复制粘贴。更干净的做法是拆成两个:

工作流职责触发方式
check-version.yml读取上游版本,判断是否需要构建schedule + workflow_dispatch
build.yml实际构建并推送镜像workflow_call + workflow_dispatch

build.yml 声明 workflow_call,就变成一个 reusable workflow:

build.yml
on:
workflow_dispatch:
inputs:
tag:
required: false
default: 'latest'
type: string
workflow_call:
inputs:
tag:
required: false
type: string
default: latest

check-version.yml 通过 uses 调用它:

check-version.yml
jobs:
check:
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
steps:
# ... 读取版本、查询镜像 ...
build:
needs: check
if: needs.check.outputs.should_build == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/build.yml
with:
tag: latest

调用方式:uses: ./.github/workflows/build.yml

5. Reusable Workflow 的三条硬性约束#

这三条约束任意一条不满足都会直接报错,且错误信息往往指向不明显的地方。

5.1 被调用的 workflow 必须在默认分支上#

GitHub 硬性要求:uses: ./.github/workflows/xxx.yml 引用的文件必须存在于仓库的默认分支。

即使你在调用方写了 @V4-china,对本地路径引用也无效。后果是:

Unable to find reusable workflow

解决方案:把 build.yml 和 check-version.yml 都合并到 main 分支。

5.2 调用 job 不能有 runs-on / steps#

uses 类型的 job 和普通 job 结构不同:

# ✅ 正确
build:
needs: check
if: needs.check.outputs.should_build == 'true'
permissions:
packages: write
uses: ./.github/workflows/build.yml
with:
tag: latest
# ❌ 错误:不能有 runs-on 和 steps
build:
runs-on: ubuntu-latest # 报错
# steps: ... # 报错
uses: ./.github/workflows/build.yml

5.3 权限不能自动提升#

Reusable workflow 拿到的权限是调用方授予的子集。如果 build.yml 需要 packages: write,调用方 job 必须显式声明:

build:
permissions:
contents: read
packages: write # 必须有,否则 build.yml 推镜像会失败
uses: ./.github/workflows/build.yml

6. 完整实现#

6.1 check-version.yml#

name: Check Version & Trigger Build
on:
schedule:
- cron: '0 3 * * *' # UTC 03:00 = 北京 11:00
workflow_dispatch:
concurrency:
group: check-version
cancel-in-progress: false
jobs:
check:
name: 检查 package.json 版本
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
version: ${{ steps.pkg.outputs.version }}
should_build: ${{ steps.check.outputs.should_build }}
steps:
- name: 仅拉取 package.json
uses: actions/checkout@v4
with:
repository: chiihero/Microsoft-Rewards-Script
ref: V4-china
path: source
sparse-checkout: |
package.json
sparse-checkout-cone-mode: false
- name: 读取版本号
id: pkg
shell: bash
run: |
VERSION=$(node -p "require('./source/package.json').version")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
- name: 检查 GHCR 是否已存在
id: check
shell: bash
env:
VERSION: ${{ steps.pkg.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OWNER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
PKG_ORGS="/orgs/${OWNER}/packages/container/microsoft-rewards-script/versions"
PKG_USERS="/users/${OWNER}/packages/container/microsoft-rewards-script/versions"
# 个人账号走 /users/,组织账号走 /orgs/,两者都试
if gh api "$PKG_ORGS" -H "Accept: application/vnd.github+json" 2>/dev/null \
| grep -q "v${VERSION}" \
|| gh api "$PKG_USERS" -H "Accept: application/vnd.github+json" 2>/dev/null \
| grep -q "v${VERSION}"; then
echo "should_build=false" >> "$GITHUB_OUTPUT"
echo "✅ 镜像 v${VERSION} 已存在,跳过构建"
else
echo "should_build=true" >> "$GITHUB_OUTPUT"
echo "🆕 镜像 v${VERSION} 不存在,触发构建"
fi
build:
name: 调用构建工作流
needs: check
if: needs.check.outputs.should_build == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/build.yml
with:
tag: latest

关键点说明:

  • node -p 直接调用 Node 的 JSON 解析,比 grep/sed 更稳,能正确处理格式变化。
  • 若 gh api 因网络或权限临时失败,两个路径都会返回空,被判定为”镜像不存在”从而触发构建。这是刻意设计——宁可多构建一次,也不漏检版本。

6.2 build.yml#

name: Build and Push Docker Image
on:
workflow_dispatch:
inputs:
tag:
description: '镜像标签'
required: false
default: 'latest'
type: string
workflow_call:
inputs:
tag:
required: false
type: string
default: latest
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout V4-china 分支源码
uses: actions/checkout@v4
with:
repository: chiihero/Microsoft-Rewards-Script
ref: V4-china
path: source
- name: 获取源码提交短 SHA
id: source_sha
run: echo "sha=$(git -C source rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: 读取 package.json 版本号
id: pkg
run: |
VERSION=$(node -p "require('./source/package.json').version")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登录 GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 生成镜像引用
id: image
run: |
OWNER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
echo "ref=ghcr.io/${OWNER}/microsoft-rewards-script" >> "$GITHUB_OUTPUT"
- name: 构建并推送
uses: docker/build-push-action@v6
with:
context: ./source
file: ./source/Dockerfile
push: true
tags: |
${{ steps.image.outputs.ref }}:latest
${{ steps.image.outputs.ref }}:${{ steps.pkg.outputs.version }}
${{ steps.image.outputs.ref }}:v${{ steps.pkg.outputs.version }}
${{ steps.image.outputs.ref }}:sha-${{ steps.source_sha.outputs.sha }}
labels: |
org.opencontainers.image.source=https://github.com/chiihero/Microsoft-Rewards-Script
org.opencontainers.image.revision=${{ steps.source_sha.outputs.sha }}
org.opencontainers.image.version=${{ steps.pkg.outputs.version }}

7. 徽章显示#

reusable workflow 不产生独立运行记录——workflow_call 触发的运行只在调用方的运行里显示为嵌套 job。这意味着:

  • ❌ 无法用 build.yml 自己的状态做徽章,会永远显示 no status
  • ✅ 应该用调用方 check-version.yml 的状态做徽章
<a href="https://github.com/USER/REPO/actions/workflows/check-version.yml">
<img src="https://img.shields.io/github/actions/workflow/status/USER/REPO/check-version.yml?branch=main&label=auto-build" alt="自动构建状态">
</a>

徽章地址:.../check-version.yml?branch=main&label=auto-build

?branch=main 显式指定默认分支,避免 shields.io 找不到运行记录而显示 no status。

8. GPL 合规:分发镜像的注意事项#

如果被构建的上游源码是 GPL 许可(本项目是 GPL-3.0-or-later),分发镜像时需注意合规。

行为GPL 术语是否需要授权
clone 源码到 Actions无❌ 不需要
在 CI 里编译修改/运行❌ 不需要
推送镜像到 GHCRconvey(传播)✅ 需要,GPL 已授予
用户 pull 镜像接收✅ 自动获得授权

GPL v3 §6 要求:分发 object code(镜像)时,必须同时提供对应源码。满足方式:

  • 上游源码公开可访问
  • 镜像标签 sha-<short> 让对应源码可精确定位
  • 镜像 OCI label 写入 org.opencontainers.image.source 和 org.opencontainers.image.revision

必须避免的坑:

  • ❌ 加”禁止商业使用”之类的额外限制(GPL v3 §10 禁止)
  • ❌ 写”仅供教育目的”——容易被误读为使用限制
  • ✅ 保留所有版权声明和 LICENSE 文件
  • ✅ 在 README 明确源码位置

9. 使用示例#

用户只需三步:

Terminal window
# 1. 准备 .env
cp env.example .env
# 编辑 .env 填入账号
# 2. 启动
docker compose up -d
# 3. 更新(自动构建完成后)
docker compose pull
docker compose up -d

生产环境建议固定到具体版本,避免 latest 漂移:

image: ghcr.io/daitcl/microsoft-rewards-script:v4.3.2.3

10. 总结#

本文从最简单的 workflow_dispatch 手动构建出发,逐步演进到「定时检查 + 无状态判断 + 语义化标签 + reusable workflow」的完整方案。

实现要点回顾:

  • 无状态判断:以 GHCR 镜像存在性为判断依据,不维护任何额外状态文件。
  • 职责分离:检查与构建拆成两个 workflow,通过 workflow_call 组合。
  • 语义化标签:同时提供 latest、精确版本、v 前缀、源码 SHA 四个标签。
  • Reusable workflow 三约束:文件必须在默认分支、调用 job 不能有 runs-on/steps、权限需调用方显式授予。
  • 徽章用调用方:workflow_call 不产生独立 run,徽章应指向 check-version.yml。
  • GPL 合规:提供对应源码位置、避免额外限制、保留版权声明。

注意事项:

  • schedule 的 cron 是 UTC 时间,换算成本地时间需注意时差。
  • concurrency 防止定时任务重叠,避免重复构建。
  • sparse-checkout 只拉 package.json 可显著缩短检查耗时。

掌握这套模式后,任何「上游源码 + 自动构建 + 分发镜像」类项目都可以套用,只需替换仓库地址和构建上下文即可。

10.1 完整配置分享#

GitHub Actions 自动构建 Docker 镜像完全指南:从手动触发到定时检查与版本追踪
https://www.daitcc.top/posts/4f46d67c/
作者
Dait
发布于
2026-09-28
许可协议
CC BY-NC-SA 4.0
如果这篇文章对你有帮助或启发,可以请作者喝杯咖啡 ☕️