老胡茶室
老胡茶室
Beta

团队敏捷实践 —— 使用 semantic-release 自动管理发布版本

冯宇

前言

在之前的分享中,我们团队已经成功运用了 Gitlab CI(参考耗时三天,我将 Gitlab CI 由 shell executor 平滑迁移 Docker 环境),并且已经结构化提交 git commit 记录(参考规范化 git commit 信息)。我们希望更进一步,给用户交付的时候同时整理出一份完整的更新日志,尽管按照之前规范化 git commit 信息的内容,可以手工生成一份 Release Note 交付给用户,但是我们希望结合 CI 更加自动化,自动管理发布版本,自动生成更新日志,因此我们引入了semantic-release进一步自动化管理我们的发布流程。

semantic-release 概述

有关semantic-release的详细介绍可以阅读官方文档,这里只做一些概述性的总结。和之前在规范化 git commit 信息一文介绍的standard-version相比,semantic-release更适合在 CI 环境中运行,它自带支持各种 git server 的认证支持,如 Github,Gitlab,Bitbucket 等等,此外,还支持插件,以便完成其他后续的流程步骤,比如自动生成 git tag 和 release note 之后再 push 回中央仓库,自动发布 npm 包等等。

semantic-release 会根据规范化的 commit 信息生成发布日志,默认使用 angular 规则。其他规则可以配置插件完成。

semantic-release 大致的工作流如下:

  • 提交到特定的分支触发 release 流程
  • 验证 commit 信息,生成 release note,打 git tag
  • 其他后续流程,如生成CHANGELOG.mdnpm publish等等(通过插件完成)

由 CI 自动执行之后的效果就像这样,在 Git tag 页面可以看到 tag 信息,同时包含更新记录:

tag页面

如果启用了@semantic-release/git插件,还会将生成的CHANGELOG.md 反向 push 回中央仓库:

CHANGELOG出现在中央仓库

默认的branches配置如下:

['+([0-9])?(.{+([0-9]),x}).x', 'master', 'next', 'next-major', {name: 'beta', prerelease: true}, {name: 'alpha', prerelease: true}]

代表默认情况下 push 到X.Y.x, master, next, next-major, beta, alpha这几个分支才会触发自动发布流程,并且只有 featfix 提交才会触发版本升级,版本号按照语义化版本规则自动生成。即:

  • 版本号按照x.y.z格式组织(git tag 会加上v前缀,如v1.0.0
  • bug fix 发布会增加修订版本号(如 1.0.0 —> 1.0.1
  • feature 发布会增加次版本号(如1.0.0 —> 1.1.0
  • break change feature 发布会增加主版本号(如1.1.1 —> 2.0.0,官方建议这种不兼容的升级应该推送到 next 分支开发,之后合并到 master

第一次使用 semantic-release 发布会生成1.0.0版本,同时,semantic-release 会兼顾到旧的发布版本,如果之前有过 git tag,则会在旧的 tag 基础之上再做语义化版本控制。

这里演示下实际效果:

  • 第一次使用 semantic-release 发布,生成了1.0.0版本,并在此基础之上,向master分支 push 一条feat: 🎸 测试feat发布记录:

    只有一个feat提交

    只有一条 feat 记录,版本由1.0.0升级到了1.1.0

  • master分支 push 一条fix: 🐛 测试fix记录:

    只有一个fix提交

    只有一条 fix 记录,版本由1.1.0升级到了1.1.1

  • 同时包含多条 feat/fix 记录,push 之后只会升级一次版本,不会跨多版本:

    多条feat提交 多条fix提交

  • 既包含 feat 记录,也包含 fix 记录时,则只会升级次版本号或主板本号(是否有 break changes 决定升级主板本)

    混合feat和fix提交

  • break change feat 提交将会升级主版本号:

    break-change提交

关于 semantic-release 详细的工作流程,建议阅读官方文档: Workflow configuration

我们的敏捷实践

经过评估之后,我们团队对目前的工作流稍微进行了调整,在不大变动团队原本习惯的开发流程的基础上,做了一下调整,并且 semantic-release 的默认配置也能很好的适配我们目前的分支模型:

  • 使用git-cz提交格式化的 commit 记录(沿用之前规范化 git commit 信息的实践)
  • 使用commitlint检测 commit 格式是否符合规范(沿用之前规范化 git commit 信息的实践)
  • 所有的特性功能,以及不紧急的 bug fix 一律提交到 dev 分支(此分支不会触发 release 流程)
  • 所有的 hot fix 直接提交到 master 分支,以便快速发布紧急修复

同时,为了让 commit 信息尽可能的好看,减少不必要的 Merge from 提交记录,我们使用 rebase 的方式合并代码,参考以下步骤:

  • 在 Gitlab 的项目设置中将Merge requests中的Merge method设置为Fast-forward merge,这样 merge request 审核代码的时候就不会产生Merge from xxx

    fast-forward-merge

  • 本地合并远程仓库的代码使用git pull --rebase,而不是git pull,可以通过以下命令配置以后默认 pull 的规则就是 rebase,不用每次都加 --rebase:

    git config --global pull.rebase true

最后,在项目工程中添加.releaserc配置如下:

{
  "plugins": [
    [
      "@semantic-release/commit-analyzer",
      {
        "preset": "conventionalcommits"
      }
    ],
    [
      "@semantic-release/release-notes-generator",
      {
        "preset": "conventionalcommits"
      }
    ],
    "@semantic-release/changelog",
    "@semantic-release/gitlab",
    "@semantic-release/git"
  ]
}

完成 .gitlab-ci.yml 配置如下(仅部分关键的配置片段):

# 添加了 lint 过程用于检测 commitlint 结果
# 添加了 release 过程用于自动化产生 git tag 和 CHANGELOG.md
stages:
  - lint
  - build
  - test
  - release
  - deploy

commitlint:
  stage: lint
  image: node:lts
  script: |
    npm install -g @commitlint/cli @commitlint/config-conventional
    if [ "${CI_COMMIT_BEFORE_SHA}" = "0000000000000000000000000000000000000000" ]; then
      echo "commitlint from HEAD^"
      npx commitlint -x @commitlint/config-conventional -f HEAD^
    else
      echo "commitlint from ${CI_COMMIT_BEFORE_SHA}"
      npx commitlint -x @commitlint/config-conventional -f "${CI_COMMIT_BEFORE_SHA}"
    fi
  dependencies: []
  tags:
    - docker

build: ...

test: ...

release:
  stage: release
  image: node:lts
  script:
    - npm install -g semantic-release @semantic-release/gitlab @semantic-release/changelog conventional-changelog-conventionalcommits @semantic-release/git
    - npx semantic-release
  dependencies: []
  # 仅在中央仓库的分支发生提交时才触发 release 流程
  only:
    - branches@upstream_path/upstream_project
  tags:
    - docker

deploy: ...

小结

至此,我们完成了通过 CI 自动管理版本号和发布日志的需求,以后面向用户交付的时候,直接根据CHANGELOG.md的内容整理下就可以了,大大节省了人力,同时,还留下了发布痕迹,方便追溯历史版本。

另外,需要注意的是上述的配置并不会修改源码部分的版本号配置内容(如build.gradlepackage.json等),如果需要自动管理这些地方的版本,与 git tag 版本保持一致,可以引入@semantic-release/exec插件,自己写脚本,通过脚本自动化修改这些地方的版本号。

还需要注意的是 semantic-release 默认产生的 commit 记录为了避免不必要的 CI 流程,会在 commit 记录加上[skip ci](见上面的截图)来跳过 CI,如果你的流水线需要由 git tag 触发,可以配置@semantic-release/git插件,自定义 commit 记录,去掉[skip ci]