和 CNB 流水线、GitLab CI 对称的一篇:讲 GitHub 侧的目录、触发方式、镜像地址、变量契约与已知差异。
GitHub 侧不 source CNB / GitLab 的脚本,也不与它们共用 Dockerfile:
| 职责 | CNB 侧 | GitHub 侧 |
|---|---|---|
| Dockerfile | .cnb/docker/Dockerfile.golang、Dockerfile.fnpack、Dockerfile.wails3、Dockerfile.wails2、Dockerfile.fyne、Dockerfile.mygo、Dockerfile.gio |
.github/docker/Dockerfile.golang、Dockerfile.fnpack、Dockerfile.wails3、Dockerfile.wails2、Dockerfile.fyne、Dockerfile.mygo、Dockerfile.gio |
| 构建脚本 | .cnb/scripts/build-golang.sh、build-fnpack.sh、build-wails3.sh、build-wails2.sh、build-fyne.sh、build-mygo.sh、build-gio.sh |
.github/scripts/build-golang.sh、build-fnpack.sh、build-wails3.sh、build-wails2.sh、build-fyne.sh、build-mygo.sh、build-gio.sh |
| 流水线 | .cnb.yml + .cnb/workflows/ |
.github/workflows/ |
| 元数据 | .cnb/annotation{,-fnpack,-wails3,-wails2,-fyne,-mygo,-gio}.json |
.github/annotation{,-fnpack,-wails3,-wails2,-fyne,-mygo,-gio}.json |
GitLab 侧的同构清单(以及三侧完整对照表)见 gitlab.md,本页不再重复。
各自独立而不是共用的原因:三侧的标签规则、冒烟断言是同构的,但平台胶水完全不同——CNB 读 CNB_* 变量、用 ##[set-output]、写 commit annotation;GitHub 读 Actions 上下文、写 Step Summary/artifact、用 QEMU + buildx;GitLab 读 CI_* 变量、用 job artifacts、同样用 QEMU + buildx。共用意味着任何一侧改平台细节都要考虑另外两侧。
代价只有一条,且是硬性规则:改镜像内容、标签规则、冒烟断言时,三份都要改(见 CONTRIBUTING)。只改平台胶水时各改各的。
| 镜像 | GHCR 地址 | 对应 CNB 地址 |
|---|---|---|
| Golang 构建环境 | ghcr.io/dtapps/golang |
docker.cnb.cool/dtapp/golang |
| Golang 构建环境 + fnpack | ghcr.io/dtapps/golang/fnpack |
docker.cnb.cool/dtapp/golang/fnpack |
| Golang 构建环境 + wails3 | ghcr.io/dtapps/golang/wails3 |
docker.cnb.cool/dtapp/golang/wails3 |
| Golang 构建环境 + wails2 | ghcr.io/dtapps/golang/wails2 |
docker.cnb.cool/dtapp/golang/wails2 |
| Golang 构建环境 + fyne | ghcr.io/dtapps/golang/fyne |
docker.cnb.cool/dtapp/golang/fyne |
| Golang 构建环境 + mygo | ghcr.io/dtapps/golang/mygo |
docker.cnb.cool/dtapp/golang/mygo |
| Golang 构建环境 + gio | ghcr.io/dtapps/golang/gio |
docker.cnb.cool/dtapp/golang/gio |
脚本从 Actions 内置变量派生,不写死:
| 变量 | 来源 | 作用 |
|---|---|---|
IMAGE |
ghcr.io + GITHUB_REPOSITORY(转小写) |
golang 镜像全名 |
FNPACK_IMAGE |
IMAGE + /fnpack |
fnpack 镜像全名 |
WAILS3_IMAGE |
IMAGE + /wails3 |
wails3 镜像全名 |
WAILS2_IMAGE |
IMAGE + /wails2 |
wails2 镜像全名 |
FYNE_IMAGE |
IMAGE + /fyne |
fyne 镜像全名 |
MYGO_IMAGE |
IMAGE + /mygo |
mygo 镜像全名 |
GIO_IMAGE |
IMAGE + /gio |
gio 镜像全名 |
REGISTRY_USER |
GITHUB_ACTOR |
docker login 用户名 |
REVISION |
GITHUB_SHA |
OCI revision |
IMAGE_SOURCE |
https://github.com/<GITHUB_REPOSITORY> |
OCI source / url |
fork 后自动推到 ghcr.io/<你的账号>/golang,rename 仓库也不需要改代码。
每个镜像一个 workflow,分别对应一个 GHCR 地址。
- 触发:
schedule/workflow_dispatch build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-golang.sh→ Step Summary → artifact- 超时:180 分钟
- 触发:
schedule:每天 19:05 UTC(北京 03:05),比build-golang晚 3 小时workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/fnpack_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-fnpack.sh→ Step Summary → artifact- 超时:90 分钟
- 触发:
schedule:每天 22:05 UTC(北京 06:05),比build-fnpack再晚 3 小时workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/wails3_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-wails3.sh→ Step Summary → artifact- 超时:120 分钟(
go install要按 amd64 + arm64 各编译一遍 wails3 CLI,arm64 那趟走 QEMU)
wails3_version 留空时脚本先 go list -m github.com/wailsapp/wails/v3@latest 解析最新稳定 tag(如 v3.1.0)再传进 --build-arg;解析失败回退 latest,由 Dockerfile 在构建时解析(层缓存随之失效)。策略细节见 wails3/README.md。
build-fnpack / build-wails3 / build-wails2 / build-fyne / build-mygo / build-gio 启动后都会最多等 5 分钟,让 <go 版本> 标签在 registry 里可读;仍然不存在就报错退出,不会自己重复安装一遍工具链。
- 触发:
schedule:每天 01:05 UTC(北京 09:05)workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/wails2_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-wails2.sh→ Step Summary → artifact- 超时:120 分钟(
go install要按 amd64 + arm64 各编译一遍 wails CLI,arm64 那趟走 QEMU)
wails2_version 留空时脚本先 go list -m github.com/wailsapp/wails/v2@latest 解析最新稳定 tag(如 v2.17.1)再传进 --build-arg;解析失败回退 latest,由 Dockerfile 在构建时解析。镜像装 GTK3 / WebKit2GTK 4.1,构建应用时要加 -tags webkit2_41。
- 触发:
schedule:每天 04:05 UTC(北京 12:05),比build-wails2再晚 3 小时workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/fyne_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-fyne.sh→ Step Summary → artifact- 超时:120 分钟(
go install要按 amd64 + arm64 各编译一遍 fyne CLI,arm64 那趟走 QEMU)
fyne_version 留空时脚本先 go list -m fyne.io/tools@latest 解析最新稳定 tag(如 v1.7.4)再传进 --build-arg;解析失败回退 latest,由 Dockerfile 在构建时解析。镜像装 OpenGL / X11 / Wayland 开发包,构建 Fyne 应用不需要额外 build tag。注意 fyne.io/tools 把 v1.26.0 / v1.26.1 标了 retract(上游误发),不要把它们当成新版本。
- 触发:
schedule:每天 07:05 UTC(北京 15:05),比build-fyne再晚 3 小时workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/mygo_version/bun_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-mygo.sh→ Step Summary → artifact- 超时:120 分钟(
go install要按 amd64 + arm64 各编译一遍 mygo CLI,arm64 那趟走 QEMU;还要装 bun)
mygo_version 留空时脚本先 go list -m github.com/egoist/mygo@latest 解析最新稳定 tag(如 v0.8.1)再传进 --build-arg,bun_version 留空时用 npm view bun version 解析(如 1.4.4);解析失败都回退 latest,由 Dockerfile 在构建时解析。镜像装 GTK3 / WebKitGTK 4.1 开发包与无头运行库,mygo 的 Linux 后端纯 Go、运行期 dlopen,编译应用不需要额外 build tag。
- 触发:
schedule:每天 10:05 UTC(北京 18:05),比build-mygo再晚 3 小时workflow_dispatch:手动,要求对应版本的基础镜像已存在;可填version/gio_version/gio_lib_version
build作业:检出 → 释放磁盘 → QEMU → buildx →source .github/scripts/build-gio.sh→ Step Summary → artifact- 超时:120 分钟(
go install要按 amd64 + arm64 各编译一遍gogio,arm64 那趟走 QEMU;还要在构建期真实编译一个最小 Gio 程序校验 cgo 依赖)
gio_version 留空时脚本先 go list -m gioui.org/cmd@latest 解析最新稳定 tag(如 v0.10.3)再传进 --build-arg,gio_lib_version 留空时用 go list -m gioui.org@latest 解析;解析失败都回退 latest,由 Dockerfile 在构建时解析。变体叫 gio,CLI 叫 gogio(模块 gioui.org/cmd),不是 gio;gogio 是打包工具(android / ios / tvos / js / windows / macos),没有 linux 目标也没有 version 子命令,所以冒烟测试不跑 gogio version,而是用 go version -m "$(command -v gogio)" 取 gioui.org/cmd 的模块版本比对。镜像装 EGL/GLES、X11、Wayland、Vulkan 开发包与无头运行库,Linux 桌面程序就是普通 go build + cgo,构建时不需要额外 build tag。
各 workflow 的 concurrency 同构(以 build-golang 为例):
concurrency:
group: build-golang-${{ github.ref }}
cancel-in-progress: false
queue: maxgroup:build-<镜像>-${{ github.ref }},按分支隔离;各 workflow 的 group 各不相同,彼此不互斥。cancel-in-progress: false:新触发的 run 进入pending等待,不取消正在运行的旧 run。queue: max:同一 group 最多 100 个pending,按进入等待的先后 FIFO 依次执行;只有队列满时溢出的新 run 才会被取消。
queue 默认是 single(只留 1 个 pending,后来的 run 顶掉早先的 pending),这正是「连续触发时旧运行被取消」的来源;max 是 GitHub 2026-05 起支持的值。两条约束:
queue: max不能与cancel-in-progress: true组合,否则 workflow 校验失败。- 排队只发生在「同一个 workflow、同一个分支」的多次触发之间。各变体镜像的先后顺序仍只靠定时任务错开 3 小时保证;手动同时触发各 workflow 时会并行,fnpack / wails3 / wails2 / fyne / mygo / gio 依旧靠脚本里那 5 分钟等基础镜像标签。
参考:GitHub Docs — Control the concurrency of workflows and jobs。
workflow 只显式传各自需要的变量:
# build-fnpack.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
fnpack_version: ${{ inputs.fnpack_version }}
# build-wails3.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
wails3_version: ${{ inputs.wails3_version }}
# build-wails2.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
wails2_version: ${{ inputs.wails2_version }}
# build-fyne.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
fyne_version: ${{ inputs.fyne_version }}
# build-mygo.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
mygo_version: ${{ inputs.mygo_version }}
bun_version: ${{ inputs.bun_version }}
# build-gio.yml
env:
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
version: ${{ inputs.version }}
gio_version: ${{ inputs.gio_version }}
gio_lib_version: ${{ inputs.gio_lib_version }}其余由脚本从 Actions 内置变量派生。GITHUB_TOKEN 不会自动进环境变量,所以必须显式从 secrets 传。fnpack_version 留空时,脚本回退到 Dockerfile 的 FNPACK_VERSION(当前 1.2.3,上游没有 latest 查询接口,暂作为最新版本);wails3_version 留空时脚本解析上游最新稳定 tag,解析不到才回退 Dockerfile 的 WAILS3_VERSION(默认 latest)。wails2_version 同理,解析的是 github.com/wailsapp/wails/v2@latest,回退 Dockerfile 的 WAILS2_VERSION;fyne_version 解析的是 fyne.io/tools@latest,回退 Dockerfile 的 FYNE_VERSION(默认 latest);mygo_version 解析的是 github.com/egoist/mygo@latest,bun_version 解析的是 npm 上的 bun@latest,回退 Dockerfile 的 MYGO_VERSION / BUN_VERSION(默认都是 latest);gio_version 解析的是 gioui.org/cmd@latest(gogio),gio_lib_version 解析的是 gioui.org@latest(Gio 库),回退 Dockerfile 的 GIO_VERSION / GIO_LIB_VERSION(默认都是 latest)。
权限:
各 workflow 的 permissions 相同:
permissions:
contents: read
packages: write显式声明 permissions: 后,未列出的权限一律不授予。secrets.GITHUB_TOKEN 经 REGISTRY_TOKEN 传给构建脚本,用于 docker login 和 GHCR 读写。
-
contents: read:actions/checkout检出源码。 -
packages: write:脚本对 GHCR 的三类写操作:docker buildx build --push推多架构 manifest list;--cache-to type=registry写<镜像>:buildcache缓存清单;docker buildx imagetools create派生<版本>-linux-{amd64,arm64},冒烟通过后提升<major.minor>/latest。
write隐含read:fnpack / wails3 / wails2 / fyne / mygo / gio 还要imagetools inspect读基座 manifest digest。
不需要 id-token: write / attestations: write(--provenance=mode=min 由 buildx 本地生成),也不需要 actions: read(queue: max 是平台层能力,upload-artifact 走 ACTIONS_RUNTIME_TOKEN)。
降成
packages: read会在首次--push时被拒,排查见 构建失败排查。
| 维度 | CNB | GitHub |
|---|---|---|
| 镜像名来源 | CNB_DOCKER_REGISTRY + CNB_REPO_SLUG_LOWERCASE |
ghcr.io + GITHUB_REPOSITORY |
| 登录 | CNB_TOKEN_USER_NAME / CNB_TOKEN |
GITHUB_ACTOR / GITHUB_TOKEN |
| 定时任务 | golang 00:05、fnpack 03:05、wails3 06:05、wails2 09:05、fyne 12:05、mygo 15:05、gio 18:05 Asia/Shanghai | golang 16:05、fnpack 19:05、wails3 22:05、wails2 01:05、fyne 04:05、mygo 07:05、gio 10:05 UTC |
| 多架构 | docker 服务自带 | setup-qemu-action + setup-buildx-action |
| GitHub 下载源 | https://files.m.daocloud.io |
直连 github.com / nodejs.org |
| 元数据 | commit annotation | Step Summary + artifact |
| 磁盘准备 | 无 | 删除预装 SDK,释放空间 |
GitLab 与 GitHub 的差异(镜像名来源、登录、定时任务、并发、元数据)见 gitlab.md。
与 CNB / GitLab 完全一致:
<go 版本> 1.27.3
<major.minor> 1.27
latest latest
<go 版本>-linux-amd64 1.27.3-linux-amd64
<go 版本>-linux-arm64 1.27.3-linux-arm64
各变体镜像使用同一组标签。golang 的 latest / <major.minor> 在自身冒烟通过后提升;fnpack / wails3 / wails2 / fyne / mygo / gio 只在基础镜像对应标签也指向本次版本时才提升。
与 CNB 侧同一套做法:buildx 层缓存写到 GHCR 的 <镜像>:buildcache,mode=min。
- 没有用
type=gha:它需要ACTIONS_CACHE_URL/ACTIONS_RUNTIME_TOKEN(脚本裸调docker buildx build时得自己暴露),且有 10GB 上限,这个镜像的缓存很容易顶到;registry 缓存和 CNB 侧对称,脚本结构不用改。 mode=min只导出最终镜像里已有的层,缓存清单引用的 blob 与镜像层是同一批 digest,所以存储增量很小。GHCR 不会自动回收无人引用的 blob:工具升级后旧层会留在包里,长期需要偶尔清理。CACHE=off关闭缓存并强制全量重建;缓存导出失败会自动去掉缓存重试一次。
env:
CACHE: off # 可选:需要一次干净的全量构建时加细节见 golang/build.md。
docker pull ghcr.io/dtapps/golang@sha256:<golang_digest>
docker pull ghcr.io/dtapps/golang/fnpack@sha256:<fnpack_digest>
docker pull ghcr.io/dtapps/golang/wails3@sha256:<wails3_digest>
docker pull ghcr.io/dtapps/golang/wails2@sha256:<wails2_digest>
docker pull ghcr.io/dtapps/golang/fyne@sha256:<fyne_digest>
docker pull ghcr.io/dtapps/golang/mygo@sha256:<mygo_digest>
docker pull ghcr.io/dtapps/golang/gio@sha256:<gio_digest>golang 的 digest 在 golang-annotation artifact / Step Summary 里;fnpack / wails3 / wails2 / fyne / mygo / gio 分别在 fnpack-annotation / wails3-annotation / wails2-annotation / fyne-annotation / mygo-annotation / gio-annotation 里。
- GHCR 推送 403:检查
packages: write与仓库 Settings → Actions 权限。 - 大写仓库名:GHCR 要求包名小写,脚本会提前拦截。
- arm64 构建报 exec format error:检查
setup-qemu-action是否在 buildx 之前。 - 磁盘不足:确认「释放磁盘空间」步骤存在;不要在同一 runner 上并行跑其他重构建。