【工程专题】将问题规避在发生前 -- APP 组推行的工作流程

这篇主要回顾过去两年 APP 组运行的流程&制度, 得益于前 APP 负责人 张晓旭 提供的良好基础. 和杨老师带来的良好讨论氛围; 在我开始接手时, 能方便的从以前的讨论和流程中借鉴, 改进总结成文. 即使杭州 APP 组几乎换了一批人, 还能推动执行的比较顺利.

而得益于这套运行流程及组员的协作.
我们组不但从没拖过软件进度后腿, 同时也是软件组出错较少的. 当其他组还在纠正流程性问题时, APP 组已经较好的运行了一年多. 两任软件组 Leader 对 APP 组的评价均是 “可以完全放手不管”.
(ps.我们是家无人机公司, 软件组包含了 APP, WEB, CE, CS, Tracker, Gimbal, Captain, FC ,OA 等)

两年的经验回顾来看, 流程和制度带来的收益, 不能用来与”开发”带来的实际产出对比. 其实际的达到的效果是
提高下限, 用流程和自动化规避人为因素带来”额外”问题

整个流程包括 Git Workflow, Code Review, CI, 测试 互相配合组成的系统, 每个环节互相弥补不足保证整体开发能以较流畅的方式进行.
其中前两个流程我整理在下面两篇文章中, 整体流程描述在这篇文章末尾: (我更想聊一下的是做这个给我们带来了什么)


最初的状态

可能大公司有比较成熟的制度(据我了解有些也不太注重). 但若在小公司开发, 野蛮发展的情况下, 经常会是这样的场景:

  • 粗犷一些的, 只有一个 develop, 所有人基于 develop 开发, 增加功能后代码直接推入 develop
    (据我这两年面试的经验来看, 这样的情况不少见, 并且关注的人不多)
  • 好一些的, 会区分 个人分支主线 (develop). 平时在自己的分支开发, 有需要就推入主线. 测试的验证也基于主线,最后发布时,可能会留个分支(一般是master),或者 Tag. 作为发布版本留存.


在我刚加入时, 整体是这样的情况:

  • 每个人有自己的独立分支, 并以自己的名字命名: mjx/xxx,mjx_xx
  • 我们有一个主线 develop, 开发, 测试, 代码同步都基于这个主线
  • CI 有一个对应分支 develop
  • 对各分支之间没有明确约束
  • 在发版前才会让测试接入集中测试我们的功能
  • 没有其他的流程, 大家开发测试发布,靠的是约定俗称与默契


直接看似乎没法直观的看出问题, 但当上面这个流程, 放在 一两年, 三四年的跨度上(我已经在零零呆了快 5 年)
会发现, 很多团队协作中出现的问题, 其原因都是开发时的流程, 没有对行为作出约束导致的.

优秀的人本身凭借优秀的意识, 能在很多情况给出好的结果
但当时间被放大, 外部环境可能从容, 可能急迫, 可能很多人,很多事同时集中在一块时

只要流程上有某种行为发生的可能性, 就不可避免的会碰到问题

制度&流程, 是在堵住逻辑上的可能性

当我回过头来审视以前我们的问题的时候, 突然冒出一个想法
制定流程的过程, 实质是一个逻辑补漏的过程

我们反反复复的在做 同一件事 : 开发 -> 提测 -> 发布版本
而制定制度和流程, 其实是在遇到问题后, 把这件事情可能存在的逻辑漏洞补上, 并且优化执行的过程. (就像写一段算法一样)

抛开个人行为上的因素
我们在 “完成一个任务” (开发 -> 提测 -> 发布版本), 可以采取不同的策略
只要理论上, 这个过程存在实施某种行为的可能, 那么逻辑上讲, 未来必然在某种情况下, 有遭遇这种行为带来副作用的可能
这无关 能力, 无关 态度. 是一种 逻辑上的存在足够长的时间 上带来的 发生必然性
(就跟写一段算法, 要是判断条件,边界情况写的不严谨, 只要测试集足够大, 总会出问题)


所谓制定制度, 就是发现一个 实现过程 的逻辑漏洞, 然后堵住他

一个很直观的例子: 若不增加 “不允许从 develop 以外的分支创建新分支” 的约束, 那么逻辑上, 就存在两分支为父子关系,同时合入导致的冲突情况. 就跟写代码一样条件没写全, 被钻了漏洞,很有意思

我下面用实际遇到的问题, 其原因及导致的后果来阐述这个想法


我们遇到过的问题

分支关系无约束, 出现了代码冲突:

同事 A 开发的功能, 依赖同事 B 的实现. 但 B 可能还没做完, 也可能刚好在修改. 总之依赖的代码不在主线.
这时, 为了顺利开发, A 从 B 的开发分支中切了一条新的分支, 基于这个分支做了修改. 最后两个分支都合入 develop

这几乎是我们遇到大部分 难以解决的 合并冲突 的原因

从逻辑上看, 所有代码来源都应该只有一个 根节点 既 develop 分支, 正常情况下, 合入 develop 的都是 develop 的子节点.
这时各节点之间是兄弟关系, 不会有新增代码上的继承关系.
而若某个 ”子节点” 中延伸出又一层子节点(develop 的孙节点). 不可避免的新一层子节点会包含大量和上一层节点 相同的代码.
这两个分支若都往 develop 合并, 出现冲突便是情理之中的.

分支策略与实际开发流程不匹配, 影响代码产出:

在只有一个主线 develop 的时代, 如果我们要发布一个版本, 测试, bugfix 工作都在 develop 进行.
早期, 一次发版, 会投入我们整个 APP. 这时大家都从主线上拉出分支修改, 修改后合入提交测试. 不会有问题

但随着组内承担的业务增多, 部分功能需要本次上线, 另外部分功能不在本次发布, 就带来很大困扰. 为了主线不引入本次上线要求以外的变量
其他分支的合并都会被截停, 先留在自己分支中不做合并

开发之间的代码会出现一段无法同步时期, 这时只有一个分支的情况已经影响到了开发效率.

我把当时的讨论线上部分收集在这: [旧] 有关分支策略的讨论

代码提交无约束, 导致了主线不稳定:

主线交汇着所有同事代码, 没有约束主线的很容易出现一个情况, 某个同事向主线提交了异常代码, 其他组员在没留意的情况下做了同步. 然后大家的代码都受到了污染. 这里问题分两个部分:

  • 直接导致编译出错: 少见, 但是一旦出现对所有人影响都很大. 通常见于:
    • 开发中修改文件引用 (比如要调试一个远端库, podfile 地址改成了本地);
    • 本地修改环境,没同步修改远端, 导致其他环境下代码无法执行;
    • 自己调试后又修改了一两行代码, 然后直接提交合并
    • 赶时间, 常见于发版前, 改了代码立刻提交
  • 不影响编译, 但提交的代码中隐藏严重的逻辑问题:
    这个实在太多了, 我在 Code Review 专栏中专门收集了常见问题 Code Review 回顾.
    代码直接的 逻辑问题设计问题 占 review 中问题的大多数. 这些问题就是程序 bug 的直接因素.

不管哪种原因引起的, 一旦主线不稳定, 组内组外都会被影响

完成的代码, 没有达到需求:

上面提到了 Code Review, 在我早期接手 APP 组时, 已经开始 Review 大家提交的代码, 当时遇到另一个问题.
单纯从代码逻辑上, 已经提出了问题并修改完毕. 但对于业务实现, 代码是看不出来的.
经常会有 Review 完了别人的代码, 感觉不对劲, 然后切到对应分支跑起来, 发现功能完全没有做完. 也许从代码实现上看, 已经改的差不多. 但从需求上看, 实际提交的是质量很差的东西.


Code Review 不但无法阻止这样的代码进入主线, 甚至会造成一种错觉 “我们主线中已经带了某个功能” (实际, 若允许半成品的代码进入, 对整个项目进度和之后的开发都会带来影响)

原因是, 开发的产出在于完成某个功能. 所以早期,我们以合入主线为节点, 作为任务规划和任务完成目标. 这时就会出现为了任务”达成”, 而先将代码提交的情况

人工控制的流程

这也是放在长时间跨度上很容易出现的问题, 不论一个人多么严谨, 只要操作是由人进行的. 在时间紧迫事情紧急时总容易出现乱子.
比如在我们有了两主线(develop,to_test)后, 手动推分支和合并处理错的.
比如在发布前,因为出现问题, 紧急改内容发版, 操作出错的.
比如合并分支时,由于协作信息同步问题, 导致已经修改的 commit 丢失
所以在流程中, 我们就尽量借助 gitlab , jenkins 自身提供的功能,帮助我们实现一些操作.


最后, 我们是怎么做的

最终推行到现在的方式由以下几个部分组成:
Git WorkFlow + Code Review + CI + 测试

从上面的问题中可以看到, 我们总共有这样的使用需求:

  • 要有一个分支: 能经常更新保持整组同步 同时 代码要稳定
  • 要有一个分支: 脱离于当前开发环境, 没有额外功能合入 同时 保持经常更新, 保持整组同步
  • 要有一个分支/Tag: 与线上代码完全一致
  • 进入主线的代码 必须不能出现编译错误
  • 进入主线的代码 要被每个人审核过
  • 进入主线的代码 必须是功能完善的
  • 能自动化的部分, 尽量自动化

需求确保:

首先, 为了保证代码是满足需求,并且经过验证的. 我们找 测试组 接管了部分我们的权利:

  • 合并代码的权利
  • 指定哪个版本用于提测的权利
  • 指定哪个版本可以发布的权利

说起来比较简单, 原先测试职责只负责上线前的代码验收, 功能测试.

现在我们将测试提前, 在平时开发过程中就让测试参与进来, 不论开发的是不是要上线的功能.
只要是合进主线的代码, 除了开发 review 以外. 最终合并的权利掌握在测试手中.
一旦测试觉得这个分支功能质量不行, 不满足需求, 都可以直接将合并关掉
(在我 Code Review 一文中可以看到, 1000 个 Merge Request, 其中关掉了 90 多个)

这一步的目的在于解决, 开发提交代码时, 只顾完成任务, 而不对功能质量负责的问题.

Git Pipeline + CI

自动化部分, 借助的是 GitLab 的 Pipeline 功能以及 Jenkins 的自动打包功能, 实现三点

  1. 代码自动化校验: 所有提交的 Merge Request, 会触发 Pipeline 然后由编译机编译. 只有编译通过的代码才允许合并
    这避免了上面提到少数情况下会出现的错误代码合入主线
    同时, 测试使用这个编译出来包, 针对这个分支做测试
  2. 分支操作交给脚本: 由 CI 管理分支, 我们最终 “需要测试的版本”, “可以发布的版本”. 都是由测试点击网页上的按钮实现.
    这一步保证 develop -> to_test -> master 的 git 操作是 fast push (意为后面的分支完全与前面分支一致), 避免代码丢失和合并该逻辑混乱.
  3. 版本自动发布: 包括以下类型
    • master 包: 直接对应发布到 Android 应用商店, APP Store, 官网
    • to_test 包: 测试分支, 用于测试验收本次发版的代码用
    • develop 包: develop 主线, 一般不对外
    • merge 包: 第一条提到的 由 pipeline 触发的打包, 一方面检查编译问题, 保证代码在远端环境能编译. 另一方面, 打包给测试
    • dynamic 包: 提供分支选择, 在有需要的情况下对出对应包

Git Workflow

(详情说明在这 :Git 工作流程)
而为了满足上面的分支要求, 最终定义了常用的 5 中类型分支:
master, to_test, develop, feature/xx, fix/xx

Code Review


最后放上以前的讨论过程, 受益于这个讨论氛围良多, 很多问题我们都会抛出来, 在线上或者当面直接聊出结果
(此处只包含 2018 以前部分, 主要是对旧项目的总结)