1. 问题背景:当feature分支变成定时炸弹
2023年初,我们团队从3人扩张到12人,代码库从单一模块拆分为5个服务。混乱随之而来:每周平均15次合并冲突,release分支经常带着未完成的feature上线,紧急hotfix要在三个分支间手动cherry-pick。最崩溃的一次,某同事把本地未推送的提交直接覆盖了远程master,导致全组停工半天。
版本控制不是git push就完事,它需要一套规则。
2. 环境与版本
- Git版本:2.39.1(强制要求,低于2.33的不支持
git switch) - 代码托管:Gitea 1.19.3(内网部署,兼容GitHub API)
- CI/CD:Gitea Actions(兼容GitHub Actions语法,runner版本1.4.0)
- 基础镜像:Node.js 18-alpine + Go 1.21
3. 方案设计:Trunk-Based + 短生命周期分支
我们最终放弃纯Git Flow(太重)和纯GitHub Flow(没有release管理),采用混合模型:
- master分支:始终可部署,只接受merge请求
- feature分支:命名
feature/xxx-描述,存活时间不超过3天 - release分支:命名
release/v1.x.x,从master切出,只允许bugfix - hotfix分支:命名
hotfix/xxx-描述,从master切出,合并回master和当前release
关键规则:
- 任何分支合并必须通过PR(Pull Request)
- PR标题必须包含issue编号
- 禁止直接push到master和release(通过分支保护规则强制)
4. 核心实现:分支策略落地与自动化
4.1 分支保护配置(Gitea)
在Gitea仓库设置中,对master分支启用保护规则:
[protected_branch "master"]
push_whitelist = ci-bot
merge_whitelist = tech-lead
enable_status_check = true
require_signed_commits = false
protect_branch = true
这段配置意味着:只有ci-bot账号能push到master,合并权限只给tech-lead角色。每个PR必须通过CI状态检查才能合并。
4.2 CI/CD流水线配置
.gitea/workflows/ci.yml核心部分:
name: CI Pipeline
run-name: CI for ${{ github.ref_name }} by @${{ github.actor }}
on:
push:
branches: [master, release/*]
pull_request:
types: [opened, synchronize, reopened]
jobs:
lint-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 关键:获取全部提交历史
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install pnpm
run: npm install -g pnpm@8.15.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint:check
- name: Run unit tests
run: pnpm test:coverage -- --reporter=json
- name: Upload coverage
uses: actions/upload-artifact@v3
with:
name: coverage-${{ matrix.node-version }}
path: coverage/
build:
needs: lint-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t registry.internal/app:${{ github.sha }} .
- name: Push to registry
run: docker push registry.internal/app:${{ github.sha }}
流水线关键点:
1. fetch-depth: 0 必须设置,否则git diff无法计算变更范围
2. 测试矩阵覆盖Node 18和20,确保兼容性
3. 每次push到master或release分支都会触发构建,PR则只跑lint和test
4.3 Code Review流程
我们在PR模板中强制要求:
## 变更描述
- 关联issue: #
- 变更类型: [feat/fix/docs/refactor]
- 影响范围: [服务名]
## 自检清单
- [ ] 本地测试通过 (pnpm test)
- [ ] 代码符合ESLint规范
- [ ] 无未处理的TODO/FIXME
- [ ] 更新了API文档
## 测试计划
- 单元测试:
- 集成测试:
- 手动验证步骤:
Review规则:
- 每个PR至少1个approve才能合并
- 技术负责人(tech-lead)对涉及数据库迁移的PR必须二次review
- 24小时内未review的PR,机器人自动@提醒
5. 踩坑与优化
5.1 坑:merge commit污染了master历史
现象:master上出现大量Merge branch 'feature/xxx'提交,历史变成一团乱麻。
解决:PR合并策略改为--squash,强制压缩为单个提交。Gitea中配置:
[repository.pull-request]
INPUT_SQUASH = true
INPUT_MERGE = false
INPUT_REBASE = false
这样master历史保持线性,配合git bisect定位问题效率提升50%。
5.2 坑:CI在PR阶段构建了两次
现象:每个PR触发两次CI,浪费大量算力。
原因:pull_request和push事件重复触发。
修复:在CI中增加条件判断:
jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
这里判断了如果是从fork仓库提交的PR,则跳过构建(因为我们不信任fork的代码),只跑lint和test。
5.3 坑:hotfix分支合回release时冲突
优化:hotfix分支从master切出,但合并时使用git merge --no-ff保留合并提交,并在release分支上定期从master拉取更新:
git checkout release/v1.2.0
git pull origin release/v1.2.0
git merge origin/master --no-ff -m "chore: sync latest fixes"
这个操作由CI自动执行,在release/*分支的流水线中加入同步步骤。
6. 效果数据
实施这套工作流4个月后:
| 指标 | 之前 | 现在 |
|---|---|---|
| 每周合并冲突次数 | 15次 | 2次 |
| 发布周期 | 2天 | 4小时 |
| 线上事故数 | 3次/月 | 0.5次/月 |
| 代码review覆盖率 | 30% | 100% |
| PR平均响应时长 | 8小时 | 2.5小时 |
特别地,紧急hotfix从发现问题到上线,从原来的2小时缩短至25分钟(包含CI构建和自动部署时间)。
7. 总结与建议
这套方案的核心不是工具,而是共识。分支策略再完美,如果团队不遵守就是废纸。建议分三步推进:
- 先立规矩:从分支命名和PR模板开始,强制要求而非建议
- 自动化兜底:用分支保护、CI检查把规则固化到流程里
- 定期复盘:每月回顾一次流程,看数据说话,持续调整
版本控制的终极目标是让团队专注于业务代码,而不是和Git较劲。我们的下一步是引入git-lfs管理大文件,以及试验gitea + act_runner的缓存加速方案,预计能将CI时间再缩短30%。
别让你的代码成为下一个定时炸弹,从今天开始规范Git工作流吧。