工程实践

一个跑在 GitHub Actions 里的体验日志检查

阅读约 1 分钟

每一个手动维护体验日志的团队,都在同一种事故之后经历过同一种对话:一个发布版本没有记录就上线了,有人问为什么,诚实的答案是那个本该写记录的人当时在赶进度,而体验日志这个步骤只存在于记忆里。体验日志自动化讲的是流水线能安全自动化哪些部分、哪些还是离不开人;而在 CI 里做一次体验日志检查,是这个问题的另外一半,因为把写作这件事自动化,并不能解决根本没人有义务先去触发它的问题。大多数团队本来就已经在 GitHub Actions 里跑他们 pull request 的各项检查,所以这个检查也顺理成章地放在这里。

为什么”我们要求大家自己添加记录”会按照一种可以预测的模式失败

因为它要在一个 pull request 里和其他所有事情争夺注意力,而它又是唯一一个跳过了也没有立即后果的部分。测试失败会响亮地拦住合并。缺一条体验日志记录什么都拦不住,所以只要有人在赶时间,它就会输掉,而现实里大部分时候人都在赶时间。一条靠记性维持的规则,会恰好以你能预料到的速度衰败:大家刚达成一致的头几周还不错,一旦真正在意这件事的人休假或者换了团队,就会悄悄没人管了。

一个针对体验日志记录的 CI 检查,到底在验证什么

不是文字的质量,只是这条记录存不存在、格式对不对,而这恰好是一个跑在 CI 里、而不是跑在人脑子里的体验日志检查该管的范围。一种常见的形态是:检查会看这个 PR 的 diff,要求要么在 changeset 目录下有一个新文件(Changesets 和类似工具用的就是这个模式),要么体验日志文件里有一行改动,两者都没有就让 build 失败。至于这条记录到底写得怎么样,这个审查始终发生在它一直发生的地方,也就是 code review 里,因为那种判断本来就不该交给一个脚本。

CI 检查验证的东西它不验证的东西
diff 里存在一个 changeset 或体验日志的改动行措辞是否清楚
在 monorepo 里,这条记录引用的包对不对这次改动到底值不值得写一条记录
文件语法上是否合法(frontmatter、JSON 形态)这条记录对影响范围是否说了实话

是不是每个 PR 都需要这个,还是有些改动可以豁免

有一些是可以豁免的,而豁免清单恰恰是这类系统真正被搭建起来还是被放弃的分水岭。一次看不出可见影响的依赖升级、一次只改测试的改动、一次不改变行为的内部重构:这些都不该逼着贡献者为一件读体验日志的人根本不关心的事情硬编一条记录出来。有效的模式是让贡献者可以打上(no-changelog-needed)这样一个标签或标志,不需要文件就能满足 CI 检查,由批准这个 PR 的人来审查,这样豁免本身也要经过一条记录本该经过的同一种审视。

像紧急热修复这样正当的例外该怎么处理

Gate 应该卡在合并这一步,而不是部署那一步:一个真正处在时间压力下的热修复,只要 CI 检查能被”意图”满足而不是非要一段写完的文字,就可以带着一条占位记录或者一个后续 ticket 合并;有些团队会接受一条一行的 stub,让维护者在下一次发布切分之前再打磨。Gate 绝对不该允许的,是悄悄跳过这一步,因为一条被遗忘的 stub 总归比一条从来不存在的记录小一号的失败,而 stub 至少留下了一条以后有人能找到的痕迹。

# .github/workflows/changelog-check.yml
on:
  pull_request:
    types: [opened, synchronize, reopened, labeled, unlabeled]
jobs:
  changelog:
    if: >-
      !contains(github.event.pull_request.labels.*.name,
      'no-changelog-needed')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # the diff needs the base branch
      - name: Require changelog entry
        run: |
          base="origin/${{ github.base_ref }}"
          if ! git diff --name-only "$base"...HEAD \
              | grep -q '^\.changeset/'; then
            echo "No changeset. Add one, or have a maintainer"
            echo "apply the no-changelog-needed label."
            exit 1
          fi

在这个检查真正开始拦住别人的 PR 之前,怎么知道它本身是对的

先对着一个用完就扔的分支开一个练手 PR:一个带 changeset,一个不带,一个带着豁免标签,在这个检查 真的要去管别人的工作之前,先确认这三种情况各自都得到了你预期的结果。一个因为某个条件写反了、结果每个 PR 都放行的失败开放式体验日志检查,比完全没有这个检查还要糟 糕,因为它看起来像是有覆盖,实际上什么都没挡住。对着同一个文件手动跑一次 workflow_dispatch,拿最近几个已经合并的 PR 试一遍,不需要真的开一个 pull request,就能抓住大部分这类错误。

同样的思路,在 GitHub Actions 之外也一样成立吗

形状是一样的,变的只是语法。GitLab CI 用一个检查 $CI_MERGE_REQUEST_LABELS 的 job rules 块, 来表达和 GitHub Actions 里的 if 一样的规则,一个强制要求的合并请求批准,也可以替代豁免审查这 一步。这篇文章讲的是 GitHub Actions,因为这是大多数读者本来就已经在用的平台,但底层的要求,也 就是一个由机器检查的 gate,而不是一条只是口头约定的规范,在任何一个在合并之前跑 CI 的地方都是 一样的。

这在 monorepo 里也是一样的工作方式吗

还需要多一样东西:这条记录到底是给哪个包写的。Monorepo 的体验日志讲的是,一旦各个包开始独立发布,整个仓库共用一份文件的方式为什么就不管用了;CI 检查继承的是同一个要求;一个没有指名具体包的 changeset,并不能有效证明正确的那份体验日志真的会更新,只能证明 diff 里某个地方有个文件动过。为这件事专门打造的工具(Changesets 是 JavaScript 生态里常见的那一个)会要求贡献者在创建 changeset 的那一刻,就选好受影响的包和一个 semver 的升级幅度,这样 CI 检查就能免费拿到这两个信息,而不用事后再去推断。

FAQ

CI 检查应该拦住合并,还是只发出警告就够了? 应该拦住。警告在功能上和客客气气地请求没什么两样,而那种方式已经失败过了。豁免标签存在的意义,恰恰就是让一个真正只需要警告的情况,也能有一条走过同一道严格 gate 的正当路径。

谁来审查一个豁免标签是不是被正确使用了? 批准这个 pull request 的人,作为他反正已经在做的那次审查的一部分。这个标签绝不应该由贡献者自己打上就算数、不经审查,否则它就变成了 gate 原本要堵住的那个悄悄的漏洞。

在 CI 里强制要求这个,能取代体验日志自动化流水线吗? 不能,它是在给那条流水线喂料。体验日志自动化讲的是怎么把结构化的记录变成一个页面、一条信息流、一封邮件;而 CI 检查保证的,正是这些结构化的记录本身首先得存在,才谈得上自动化。

值得最先搭建的、这件事最小的版本是什么? 一个单独的检查,只要在指定的体验日志目录下没有任何文件改动就失败,配上一个豁免标签。按包路由和给 monorepo 做 semver 推断,都可以留到以后再做;最核心的那个习惯,一条记录存在,或者有人明确说了不需要,才是从第一天起就值得拥有的东西。


本文的技术内容未经独立审核。如有错误,请告诉我们,我们会更正。

changeloop 相关页面: changelog 工具对比, changelog 生成器

changeloop
打造闭环 changelog 的团队。用户提出需求,你的团队交付,提出需求的人得知结果。