PAYATHON 2026

GitHub 中如何避免未测试版本进入 higher environments

排障赵师傅

结论

版本号无法隔离代码依赖。只要 0.0.11 是从已经包含 0.0.10 改动的提交构建出来的,其中就会包含这些改动。无论把版本号改成 0.0.12、增加审批步骤,还是更换部署目标,代码内容都不会因此改变。

建议采用以下流程:

  1. 让 main 始终处于可发布状态。
  2. 用 feature flag 隔离大型或尚未完成的功能,也可以暂时将其保留在短期 feature branch 中。
  3. 每个 commit 只构建一次,生成不可变 artifact,同时记录 commit SHA、镜像 digest 和构建编号。
  4. 环境晋级时提升同一个 artifact,不要在各环境中重复构建。
  5. 紧急变更从当前生产基线创建 hotfix branch,只加入必要改动,测试通过后单独发布。
  6. 使用 GitHub branch protection、GitHub Environments 和 required reviewers 管理合并及部署。

如果大型改动已经进入 main,并且无法通过 feature flag 关闭,就不能直接从最新的 main 发布紧急变更。应从最后一个已批准版本创建 hotfix branch,再应用紧急改动。

现有流程为什么会出问题

假设提交关系如下:

A ---- B ---- C
       |      |
    0.0.10  0.0.11

其中:

  • A 是当前已经验证的版本。
  • B 是需要测试 2 至 3 天的大型改动。
  • C 是需要尽快演示的小改动。

C 的父提交是 B,因此从 C 构建的 artifact 必然同时包含 B 和 C。部署系统只能部署某个提交对应的完整结果,不能根据版本号自动移除父提交中的代码。

如果要单独发布 C,需要建立一条不经过 B 的提交链:

A ---- B ---- C
 \
  D

D 是将紧急改动应用到 A 后形成的新提交,可以单独构建、测试和发布。

CI 通常指持续集成过程,不一定是环境名称。如果组织将 ci 用作环境名,建议在文档中明确它代表 integration、QA 还是 test,以免混淆流水线阶段和运行环境。

推荐的 branching 模型

日常开发使用短期分支

每项改动都从 main 创建短期分支:

git switch main
git pull --ff-only
git switch -c feature/order-import

通过 Pull Request 合并前,至少要完成以下工作:

  • 单元测试
  • 静态检查
  • 构建验证
  • 必要的集成测试
  • 代码审查

在 GitHub 中保护 main,禁止直接 push,并把必要的检查设为 required status checks。

大型功能使用 feature flag

如果大型功能需要提前合入 main,应默认关闭:

if (featureFlags.enableOrderImport) {
  registerOrderImportRoutes();
}

这样既可以持续集成代码,也不会在 higher environments 中启用尚未通过业务验证的行为。feature flag 的默认值、环境配置、启用权限和删除时间都应纳入变更管理。

feature flag 适用于能够安全关闭的功能。数据库破坏性变更、公共 API 不兼容变更和无法关闭的基础设施调整,仍然需要拆分发布或使用独立 release branch。

紧急发布使用 hotfix branch

假设最后一个获批的生产 tag 是 v0.0.9,紧急改动对应的提交是 abc1234:

git switch --detach v0.0.9
git switch -c hotfix/urgent-demo
git cherry-pick abc1234

随后提交 Pull Request,运行完整的 CI 检查,并构建独立 artifact。

如果 abc1234 依赖大型改动中的接口、数据库结构或共享代码,cherry-pick 可能发生冲突,也可能应用成功却无法正常运行。遇到这种情况,应针对稳定基线重新实现最小修复,不能因为提交成功就直接发布。

hotfix 发布后,还需将修复同步回 main,避免后续版本遗漏这项改动:

git switch main
git pull --ff-only
git cherry-pick <hotfix-commit-sha>

同步也可以通过 Pull Request 完成。不要依靠人工记忆长期维护两套修改。

将 versioning 与构建身份分开

当前流程把每次 dev 部署都称为”新版本”,容易混淆开发构建、候选版本和正式版本。可以将标识分为三类:

  • commit SHA:表示代码来源,例如 3f41c2a
  • build ID:表示 CI 构建记录,例如 build-1842
  • release version:表示经过发布流程确认的版本,例如 v1.4.2

容器镜像可以同时保存易读的 tag 和不可变 digest:

registry.example.com/payment:build-1842
registry.example.com/payment@sha256:<digest>

部署时应优先使用 digest。普通 tag 可能被重新指向,digest 才能准确标识经过验证的 artifact。

已经发布的版本号不能覆盖或重复使用。如果 0.0.10 和 0.0.11 已经进入 package registry,应生成下一个未使用的版本,并在发布记录中注明对应的 commit。版本号大小只表示发布顺序或兼容性约定,无法证明新版本包含所有较小版本的提交。

推荐的 deployment 流程

流水线可以按以下顺序运行:

Pull Request
    ↓
required checks
    ↓
merge to main
    ↓
build once
    ↓
immutable artifact
    ↓
deploy to dev
    ↓
automated tests / manual verification
    ↓
approval
    ↓
promote the same artifact to preprod
    ↓
approval
    ↓
promote the same artifact to prod

应采用 “build once, promote many”。如果 dev、preprod 和 prod 分别重新构建,即使使用同一个 commit,也可能因依赖解析、基础镜像或构建参数发生变化而产生不同结果。

环境差异应通过部署配置、secret 和运行时参数处理,不要通过重新编译代码生成不同的 artifact。

GitHub Actions 示例

构建 workflow 可以在代码合并到 main 后生成 artifact,并记录来源 SHA:

name: Build

on:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Test
        run: ./scripts/test.sh

      - name: Build
        run: ./scripts/build.sh

      - name: Record source revision
        run: printf '%s\n' "$GITHUB_SHA" > dist/SOURCE_COMMIT

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-${{ github.sha }}
          path: dist/

晋级 workflow 应接收已经构建好的 artifact 标识,不再重新 checkout 和 build:

name: Promote

on:
  workflow_dispatch:
    inputs:
      artifact_ref:
        description: Immutable artifact reference
        required: true
        type: string
      target_environment:
        description: Target environment
        required: true
        type: choice
        options:
          - integration
          - preprod
          - prod

permissions:
  contents: read

jobs:
  deploy:
    environment: ${{ inputs.target_environment }}
    runs-on: ubuntu-latest

    steps:
      - name: Deploy approved artifact
        env:
          ARTIFACT_REF: ${{ inputs.artifact_ref }}
          TARGET_ENVIRONMENT: ${{ inputs.target_environment }}
        run: ./scripts/deploy.sh "$ARTIFACT_REF" "$TARGET_ENVIRONMENT"

以上代码只展示流程结构。deploy.sh、artifact 下载方式、云平台认证和所需的 permissions 必须按实际平台配置,不能直接照搬其中的占位命令。

在 GitHub Environments 中,应为 preprod 和 prod 配置:

  • required reviewers
  • environment-specific secrets
  • deployment branch 或 tag 限制
  • 必要的等待时间和并发控制

审批人需要看到 artifact digest、来源 commit、测试结果和变更记录。仅凭 0.0.11 这样的版本号,无法判断其中具体包含哪些代码。

当前情况的处理方式

对于已经构建的 0.0.10 和 0.0.11,可以按以下步骤处理:

  1. 暂停将 0.0.11 晋级到 higher environments。
  2. 查明当前已批准环境实际运行的 tag、commit SHA 或镜像 digest。
  3. 从该基线创建 hotfix/urgent-demo。
  4. 将紧急改动 cherry-pick 到 hotfix branch,必要时针对该分支重新实现。
  5. 运行 CI,并在隔离环境中验证 hotfix。
  6. 构建新的不可变 artifact,使用新的 build ID 或尚未使用的 release version。
  7. 审批通过后,将该 artifact 依次提升到目标环境。
  8. 将 hotfix 同步回 main。
  9. 继续测试原有的大型改动,测试完成后再按正常流程晋级。

如果演示依赖大型改动提供的能力,就说明小改动无法独立运行。此时需要一起测试和发布,或者先拆开依赖关系。

需要避免的做法

不要用长期存在的 dev、qa、preprod 分支表示环境状态。多个环境分支容易造成 cherry-pick 遗漏、merge 顺序不一致和修复无法回流。环境应部署 artifact,代码内容不应由分支名称决定。

以下做法也应避免:

  • 覆盖已经发布的 tag 或 package version
  • 在每个环境中重新构建
  • 从尚未验证的 main 顶端制作紧急版本
  • 根据版本号大小判断代码是否经过更多测试
  • 只记录版本号,不记录 commit SHA 和 artifact digest
  • 在没有回滚方案的情况下直接晋级
  • 长期保留无人清理的 feature flag

如果团队能够让 main 随时保持可发布状态,可以优先使用短期分支、feature flag 和 artifact promotion。只有确实需要同时维护多个发布系列时,才应引入长期 release branch。分支越多,合并、回补和审计的成本越高。

备注:内容仅供参考。