CI/CD 流水线:多架构 Docker 构建与发布流程
前置知识
- ›第 1 篇和第 3 篇:架构概览与客户端差异
- ›GitHub Actions 工作流语法
- ›Docker 多架构镜像基础概念(manifest、digest)
CI/CD 流水线:多架构 Docker 构建与发布流程
在第 4 篇中,我们了解了依赖更新器如何创建包含版本升级的 PR。这些 PR 会触发验证构建,合并后则触发生产构建,并将多架构镜像推送到 GitHub Container Registry。整条流水线会并行产出六个 Docker 镜像(三种客户端 × 两种架构),最终由三个 manifest 合并任务汇聚收口。
本文是系列的最后一篇,将深入解析 GitHub Actions 工作流的实现细节:扇出/扇入构建模式、平台特定功能标志、多架构 manifest 合并,以及贯穿整个系列的供应链安全加固措施。
流水线总览:三种客户端 × 两种架构
CI 策略由两个工作流文件分工承担:
| 工作流 | 触发条件 | 用途 |
|---|---|---|
docker.yml |
推送至 main 分支、版本标签(v*) |
构建、推送并合并多架构镜像 |
pr.yml |
Pull Request | 仅构建验证(不推送) |
docker.yml 中的生产流水线共创建九个任务:
flowchart TD
subgraph "Fan Out: 6 parallel build jobs"
G1["geth<br/>linux/amd64"]
G2["geth<br/>linux/arm64"]
R1["reth<br/>linux/amd64<br/>+asm-keccak"]
R2["reth<br/>linux/arm64"]
N1["nethermind<br/>linux/amd64"]
N2["nethermind<br/>linux/arm64"]
end
subgraph "Fan In: 3 merge jobs"
MG["merge-geth"]
MR["merge-reth"]
MN["merge-nethermind"]
end
G1 -->|digest artifact| MG
G2 -->|digest artifact| MG
R1 -->|digest artifact| MR
R2 -->|digest artifact| MR
N1 -->|digest artifact| MN
N2 -->|digest artifact| MN
MG --> IMG1["ghcr.io/base/node-geth<br/>+ ghcr.io/base/node"]
MR --> IMG2["ghcr.io/base/node-reth"]
MN --> IMG3["ghcr.io/base/node-nethermind"]
矩阵策略让各架构的构建真正并行运行。每种架构使用各自专属的 runner 类型——amd64 对应 ubuntu-24.04,arm64 对应 ubuntu-24.04-arm。这是原生编译,而非通过 QEMU 进行交叉编译,对于 Reth 的 maxperf 编译配置这类性能敏感型构建至关重要。
构建任务的通用模式
以 Geth 的构建任务为例,来看一个构建任务的完整结构。任务定义从 docker.yml 第 24 行开始:
sequenceDiagram
participant GH as GitHub Runner
participant GHCR as Container Registry
participant ART as Artifact Storage
GH->>GH: 1. Harden runner
GH->>GH: 2. Checkout code
GH->>GHCR: 3. Docker login
GH->>GH: 4. Extract metadata (tags, labels)
GH->>GH: 5. Setup buildx
GH->>GHCR: 6. Build & push by digest
GH->>ART: 7. Upload digest artifact
其中最关键的是第 6 步:构建过程推送的并不是带标签的镜像,而是一个仅通过内容摘要(SHA256 哈希)标识的无标签镜像,通过 push-by-digest=true 实现:
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
随后,digest 被导出并作为 GitHub Actions artifact 上传(第 71–88 行):
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: digests-geth-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
digest 被存储为一个以哈希值命名的空文件。这个设计很巧妙——不需要任何文件内容,只需要文件名本身,在 artifact 上传/下载的过程中也能保持干净的传递。
平台特定的构建功能
矩阵配置中最值得关注的差异体现在 Reth 的构建上(第 89–99 行):
reth:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
features: jemalloc,asm-keccak,optimism
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
features: jemalloc,optimism
asm-keccak 功能仅在 amd64 上启用。该功能使用手工调优的 x86-64 汇编指令来实现 Keccak-256 哈希函数——以太坊大量使用的核心密码学原语。由于 ARM CPU 不支持所需的汇编指令,arm64 构建中排除了这一功能。
这些功能标志通过构建参数传入(第 134–135 行):
build-args: |
FEATURES=${{ matrix.settings.features }}
Geth 和 Nethermind 在两种架构上的矩阵配置完全相同,没有平台特定的功能差异。它们之间的区别体现在 Dockerfile 层面(构建工具链),而非 CI 配置层面。
提示:
jemalloc内存分配器(两种架构均启用)替换了系统默认的 malloc,能显著提升 Reth 在区块链状态管理中典型的高碎片化内存分配场景下的性能表现。
pr.yml 中的 PR 验证工作流与生产环境的矩阵配置完全镜像,但设置了 push: false:
- name: Build the Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83
with:
push: false
platforms: ${{ matrix.settings.arch }}
这确保每个 PR 都会经过相同构建矩阵的验证,同时不会将开发镜像污染到镜像仓库中。
Manifest 合并:整合各架构的 Digest
合并任务是多架构魔法真正发生的地方。以 merge-geth 为例来说明:
flowchart TD
A["Download amd64 digest artifact"] --> C
B["Download arm64 digest artifact"] --> C
C["Both digests in /tmp/digests/"] --> D["docker buildx imagetools create"]
D --> E["Multi-arch manifest"]
E --> F["ghcr.io/base/node-geth:main"]
E --> G["ghcr.io/base/node-geth:sha-9480ec..."]
E --> H["ghcr.io/base/node:main (deprecated)"]
合并任务通过模式匹配下载两种架构构建产生的 digest artifact:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: ${{ runner.temp }}/digests
pattern: digests-geth-*
merge-multiple: true
接着,metadata action 生成对应的镜像标签(分支名、Git 标签(如有)、完整 SHA):
tags: |
type=ref,event=branch # "main" for pushes to main
type=ref,event=tag # "v1.2.3" for version tags
type=sha,format=long # "sha-9480ec16531c2f222ea18eba6efed235e7210381"
最后,docker buildx imagetools create 将各架构镜像拼合为一个多架构 manifest(第 264–268 行):
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }}@sha256:%s ' *)
printf ... * 中的 glob 展开为目录下所有 digest 文件名——即之前创建的那些空文件。每个文件名本身就是一个 digest 哈希值,加上 @sha256: 前缀即构成合法的镜像引用。
Geth 的合并任务有一点特殊:它会同时推送到两个镜像名称——node-geth(规范名称)和 node(用于向后兼容的废弃别名)。Reth 和 Nethermind 则只推送到各自对应的镜像名称。
CI 中的安全加固
CI 流水线实现了多层供应链安全措施,与 Dockerfile 中的 commit hash 校验相互补充。
固定 Action SHA
所有 GitHub Action 引用均使用完整的 commit SHA,而非版本标签:
# Instead of: uses: actions/checkout@v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
GitHub Actions 上的版本标签可以被移动(与上游仓库的 Git 标签一样)。使用 commit SHA 能确保工作流始终运行经过审查的代码,不受未来标签变更的影响。SHA 后面的注释标注了对应版本号,方便人工识别。
Harden Runner
每个任务都以 step-security/harden-runner 作为第一步:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863
with:
egress-policy: audit
它会监控 runner 发起的所有出站网络连接,为构建过程中访问的每个外部服务留下审计记录。虽然当前设置为 audit(审计模式,而非 block 拦截模式),但已能有效发现通过异常出站连接实施的潜在供应链攻击。
最小权限原则
工作流只申请必要的权限(第 19–21 行):
permissions:
contents: read
packages: write
PR 工作流的权限要求更为严格——仅需 contents: read,因为它不需要推送镜像。
| 安全层 | 作用位置 | 防护目标 |
|---|---|---|
| 固定 Action SHA | 所有工作流 | Action 标签被移动的攻击 |
| harden-runner | 所有任务 | 意外的出站网络连接 |
| 最小权限 | 工作流级别 | Token 权限过大的风险 |
| Commit hash 校验 | Dockerfile | 上游标签被移动的攻击 |
| versions.json 版本锁定 | 仓库 | 版本不受控漂移 |
| 反降级校验 | 依赖更新器 | 恶意版本回滚 |
PR 验证与 Issue 自动管理
除主构建流水线外,还有两个辅助工作流维护仓库的健康状态。
pr.yml 在每个 PR 上运行完整的构建矩阵。六个并行任务(三种客户端 × 两种架构)必须全部通过才允许合并。这能在早期发现 Dockerfile 问题、版本不匹配以及上游构建故障。
flowchart LR
PR["Pull Request"] --> V["6 parallel builds<br/>(3 clients × 2 archs)"]
V -->|All pass| M["Merge allowed"]
V -->|Any fail| B["Merge blocked"]
stale.yml 每天 UTC 时间 00:30 运行,自动处理长期不活跃的 issue 和 PR:
days-before-stale: 14
days-before-close: 5
超过 14 天没有活动的 issue 和 PR 会被打上"stale"标签并收到警告消息。再过 5 天仍无活动,则自动关闭。这防止了 issue 追踪器中积累大量僵尸工单。
完整的 CI/CD 流水线
让我们通过依赖更新这一最常见的触发场景,来梳理四个工作流的完整协作链路:
flowchart TD
A["Daily cron (1 PM UTC)"] -->|"update-dependencies.yml"| B["Dependency updater runs"]
B -->|"New versions found"| C["Auto-create PR"]
C -->|"pr.yml triggers"| D["6 validation builds<br/>(no push)"]
D -->|"All pass"| E["Reviewer approves & merges"]
E -->|"docker.yml triggers"| F["6 production builds<br/>(push digests)"]
F --> G["3 manifest merge jobs"]
G --> H["ghcr.io/base/node-geth<br/>ghcr.io/base/node-reth<br/>ghcr.io/base/node-nethermind"]
B -->|"No updates"| I["Workflow exits"]
style A fill:#e6f3ff
style H fill:#e6ffe6
从每日定时触发到多架构镜像发布,整条流水线全程自动化,人工审核是唯一的手动环节。依赖更新器发现新版本、创建附带 diff 链接的 PR、CI 验证构建、审核者批准,最终合并触发生产镜像构建。
系列总结
在这五篇文章中,我们完整梳理了 base/node 仓库的全生命周期:
- 架构:一个约 30 个文件的部署编排器,采用双服务 Docker Compose 拓扑、三层配置体系,以及版本锁定的构建方式
- 启动流程:通过轮询循环实现服务启动顺序控制、共识调度器路由,以及 Reth 出色的多阶段初始化机制
- 客户端对比:三种构建流程和功能集各异的客户端,通过统一接口整合——以及关键的 base-consensus 非对称性
- 依赖管理:一个 Go CLI 工具,处理四种版本格式、执行反降级保护,并自动生成附带 diff 链接的 PR
- CI/CD:采用扇出/扇入 manifest 合并模式的多架构构建,以及多层供应链安全加固
从这套设计中可以提炼出一种核心理念:组合优于实现。这个仓库本身不实现区块链逻辑,而是以版本锁定、构建验证和自动更新的方式编排上游实现。每一个 shell 脚本、Dockerfile 和 CI 工作流,都服务于这一编排目标。最终的效果是:只需一条 docker compose up 命令,就能启动一个生产就绪的 Base L2 节点,可以选择任意三种执行客户端,从已验证的源代码构建,运行在任何架构上。
提示: 巩固理解的最好方式就是亲手运行。克隆仓库,设置
CLIENT=reth,在.env.mainnet中配置你的 L1 端点,然后执行docker compose up。观察执行客户端的启动过程、共识入口点的就绪轮询,以及 Engine API 连接的建立。对照日志和入口点脚本一起阅读——代码的一切都会豁然开朗。