Read OSS

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 仓库的全生命周期:

  1. 架构:一个约 30 个文件的部署编排器,采用双服务 Docker Compose 拓扑、三层配置体系,以及版本锁定的构建方式
  2. 启动流程:通过轮询循环实现服务启动顺序控制、共识调度器路由,以及 Reth 出色的多阶段初始化机制
  3. 客户端对比:三种构建流程和功能集各异的客户端,通过统一接口整合——以及关键的 base-consensus 非对称性
  4. 依赖管理:一个 Go CLI 工具,处理四种版本格式、执行反降级保护,并自动生成附带 diff 链接的 PR
  5. CI/CD:采用扇出/扇入 manifest 合并模式的多架构构建,以及多层供应链安全加固

从这套设计中可以提炼出一种核心理念:组合优于实现。这个仓库本身不实现区块链逻辑,而是以版本锁定、构建验证和自动更新的方式编排上游实现。每一个 shell 脚本、Dockerfile 和 CI 工作流,都服务于这一编排目标。最终的效果是:只需一条 docker compose up 命令,就能启动一个生产就绪的 Base L2 节点,可以选择任意三种执行客户端,从已验证的源代码构建,运行在任何架构上。

提示: 巩固理解的最好方式就是亲手运行。克隆仓库,设置 CLIENT=reth,在 .env.mainnet 中配置你的 L1 端点,然后执行 docker compose up。观察执行客户端的启动过程、共识入口点的就绪轮询,以及 Engine API 连接的建立。对照日志和入口点脚本一起阅读——代码的一切都会豁然开朗。