TT Lab
开始
学习 学习路径 课程

CI/CD 流水线

合并之后还是绿灯吗

在 TT Lab 中继续学习

一句话总结

在分支上是绿灯,合并之后却变成红灯,这种事并不少见。因为分支上的检查看的是我的改动加上当时的主干,而不是合并之后的主干。缩小这个差距,正是短生命周期分支和服务器端规则要做的事。

为什么需要它

主干开发(trunk-based development)被定义为“开发者在一个叫作主干的分支上协作,并抵制创建其他长期存活的开发分支的压力的源代码管理模型”。短生命周期分支用于代码评审和构建检查,前提是它很快就会消失。发布分支也是在需要时从主干切出,发布后过一段时间就删除。给发布所起的版本名称,也要求具有同样的特性。语义化版本规范明确规定,一旦发布过的版本,其内容就不得修改,要修改就必须发布新版本。不去动已经发出去的东西,才会为回退留下对象。

为什么要避免长期存活的分支?分支越长,与主干的距离就越远,拉开的距离会在合并时一次性结算。而这张账单上最可怕的不是文本冲突。文本冲突,工具会告诉你。可怕的是语义冲突。如果我在分支上使用的函数,其行为被别人在主干上改了,那么这两处改动位于不同的行,会干净地合并,只是结果不对。版本管理工具察觉不到这一点。

从反馈的角度看,答案也是一样的。集成被拖得越晚,发现问题就越晚,发现得越晚,原因的候选就越多。Kent Beck 的那句话——“任何代码都不会在超过几个小时的时间里不被集成”,把目标线写得最简短。

分支保护必须是服务器端规则

如果把规则寄托在人的约定上,就不会被遵守。在忙碌的日子、处理故障的时候、新人入职的第一周,它都会被打破。所以规则要放在接收的一方,而不是推送的一方。

git 本身就有这种区分。钩子默认位于 $GIT_DIR/hooks,不会随克隆一起带过来。 git init 可以复制模板,但我创建的 pre-commit 钩子,不会自动出现在同事的副本里。也就是说,本地钩子是便利工具,而不是强制手段。 强制由服务器端钩子来做。

托管服务的分支保护,也是位于同一个位置的功能。GitHub 的保护规则包括:合并前要求拉取请求、必需状态检查、要求解决对话、要求已签名的提交、要求线性历史、要求合并队列、要求部署成功、锁定分支、限制可推送的人、允许强制推送、允许删除。

这里有一个必须知道的默认值。保护规则默认不适用于管理员。 文档写明,对拥有仓库管理员权限或绕过保护权限的人,这些限制不适用,并提示要打开“不允许绕过上述设置”,才会对管理员也生效。明明把规则打开了,自以为安心,结果最危险的改动恰好从那条规则旁边溜过去,问题就出在这里。

绿灯在合并之后变成红灯的问题

即使打开了必需状态检查,差距依然存在。分支 A 和分支 B 各自都以主干为基准是绿灯,但先合并了 A 之后,B 的检查结果就不再是针对当前主干的了。 如果就这样合并,主干就会被破坏。

解决的方向有两个。

用功能开关取代长分支

“这个功能还没完成,所以不能合并”,是长期存活分支的常见理由。答案是把没有完成的东西放进主干,但不要启用它。 功能开关(feature flag)和抽象分支(branch by abstraction)就是这样做的办法。未完成的代码虽然在主干上,但对用户不可见,所以集成可以每天进行,而公开的时间点则可以另行确定。

不过,开关是有代价的。开关一多,实际运行的组合就增多,会出现测试覆盖不到的路径。创建开关时就一并确定删除它的时间点,是唯一管用的管理办法。

在现场相遇的样子

参考

下一项实验要做什么

在本地创建一个裸仓库,亲手建立接收一方的规则。用 pre-receive 和 update 钩子阻止直接向受保护分支推送,并通过退出码确认只拒绝特定引用与拒绝整个推送的区别。然后创建本地钩子,亲眼看到克隆出来的副本里不会带上那个钩子。必需检查用钩子确认附在提交上的检查结果的方式来模拟,语义冲突则用这样的例子来重现:两个分支各自通过之后,合并时却坏了。最后用脚本做出一个在合并之前纳入主干并重新检查的队列模拟,数一数队伍变长时检查次数会如何变化。