GitHub 中如何避免未测试版本进入 higher environments
结论
版本号无法隔离代码依赖。只要 0.0.11 是从已经包含 0.0.10 改动的提交构建出来的,其中就会包含这些改动。无论把版本号改成 0.0.12、增加审批步骤,还是更换部署目标,代码内容都不会因此改变。
建议采用以下流程:
- 让
main始终处于可发布状态。 - 用 feature flag 隔离大型或尚未完成的功能,也可以暂时将其保留在短期 feature branch 中。
- 每个 commit 只构建一次,生成不可变 artifact,同时记录 commit SHA、镜像 digest 和构建编号。
- 环境晋级时提升同一个 artifact,不要在各环境中重复构建。
- 紧急变更从当前生产基线创建 hotfix branch,只加入必要改动,测试通过后单独发布。
- 使用 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,可以按以下步骤处理:
- 暂停将
0.0.11晋级到 higher environments。 - 查明当前已批准环境实际运行的 tag、commit SHA 或镜像 digest。
- 从该基线创建
hotfix/urgent-demo。 - 将紧急改动 cherry-pick 到 hotfix branch,必要时针对该分支重新实现。
- 运行 CI,并在隔离环境中验证 hotfix。
- 构建新的不可变 artifact,使用新的 build ID 或尚未使用的 release version。
- 审批通过后,将该 artifact 依次提升到目标环境。
- 将 hotfix 同步回
main。 - 继续测试原有的大型改动,测试完成后再按正常流程晋级。
如果演示依赖大型改动提供的能力,就说明小改动无法独立运行。此时需要一起测试和发布,或者先拆开依赖关系。
需要避免的做法
不要用长期存在的 dev、qa、preprod 分支表示环境状态。多个环境分支容易造成 cherry-pick 遗漏、merge 顺序不一致和修复无法回流。环境应部署 artifact,代码内容不应由分支名称决定。
以下做法也应避免:
- 覆盖已经发布的 tag 或 package version
- 在每个环境中重新构建
- 从尚未验证的
main顶端制作紧急版本 - 根据版本号大小判断代码是否经过更多测试
- 只记录版本号,不记录 commit SHA 和 artifact digest
- 在没有回滚方案的情况下直接晋级
- 长期保留无人清理的 feature flag
如果团队能够让 main 随时保持可发布状态,可以优先使用短期分支、feature flag 和 artifact promotion。只有确实需要同时维护多个发布系列时,才应引入长期 release branch。分支越多,合并、回补和审计的成本越高。
备注:内容仅供参考。
