跳到主要内容

使用约定式提交进行语义化版本控制

· 阅读需 21 分钟

全文机翻,有小幅语法调整,不会或无法翻译的部分保留原文。

原文: Semantic Versioning with Conventional Commits

使用约定式提交进行语义化版本控制

版本控制很重要。我不必告诉你这个。然而,我看到它一遍又一遍地做得不好。

我经常遇到的缺陷是缺乏明确的开发和发布流程以及较差的工具支持。如果你的开发人员坐在那里想知道如何完成某些任务,那么这个过程就被打破了。他们应该知道如何发布新版本或如何在主干移动时修复生产。您的 CI/CD 流程也应该支持这些场景!

许多人依靠他们的 CI/CD 工具来确定他们应用程序的下一个版本。我也为此感到内疚,但在某些时候,我意识到这是只有开发人员才能做出的决定。我们的工具还不够智能,无法查看代码更改并告诉我它是功能、修复还是其他什么。

这让我产生了改变我们做事方式的强烈愿望。我的目标是拥有一个涵盖所有这些用例的明确流程,对开发人员友好,并在我们倾向于跨项目使用的各种 CI/CD 工具中得到支持。

这篇文章记录了这种方法。

语义版本控制

我们将在这里使用语义版本控制(semver)。这可能是当今软件中使用的最突出的版本控制方案。

我们构建的大多数应用程序和库都倾向于公开一个 API,无论是 REST API、接口等。Semver 就是对这个 API 进行版本控制。

语法是众所周知的:

<MAJOR>.<MINOR>.<PATCH>

MAJORMINORPATCH
引入新的向后不兼容更改引入新的向后兼容更改在保持向后兼容性的同时修复一个错误
1.0.0 -> 2.0.01.0.0 -> 1.1.01.0.0 -> 1.0.1

如表所示,向后兼容性是版本冲突的一大区别。

有些人仍然没有搞懂,所以在这里我将使用 JSON REST API 来说明什么构成补丁、功能或重大更改。

让我们假设一个称职的开发人员已经实现了这个 REST API,并且它成功地遵循了 Postel 定律

Major

  • 删除一个操作,比如删除 HTTP 动词/路径组合
  • 向某个请求添加必填字段
  • 向某个请求添加/删除字段
    • 如果所有用户被认为足够宽容,那么添加一个字段可能不会是一个重大变化

Minor

  • 添加一个新操作,即新的 HTTP 动词/路径组合
  • 向请求添加可选字段

Patch

  • 没有接口变化

约定式提交

如今,尽管工具非常好,但它们还无法识别代码更改的本质。通过机器学习的那一天将会到来,这将是可能的,但就目前而言,我们必须依靠良好的老式人类智能。

约定式提交提供了在开发人员和 CI/CD 工具之间传达提交中更改的性质的机制。

简而言之,开发人员提供了一个提交消息,该消息明确地标识了更改的性质。然后 CI/CD 工具可以扫描自上一个版本以来的所有提交消息,并确定如何更新版本。

除了这种自动化之外,这种方法还为其他团队成员提供了清晰的变更沟通,甚至让我们自动生成发布 release note 和 changelog。

约定是提交的官方总结示例非常简洁,我没有必要在这里重复它们。来看看吧。我会在这里等你。

以下是我们的 JSON REST API 的一些示例提交:

Major

  • feat: disable deletion of records

    BREAKING CHANGE: removed and endpoint

  • feat: expiry date must be provided by the user

    BREAKING CHANGE: new mandatory field in create operation

Minor

  • feat(my-operation): allow users to provide an optional name to override the default
  • feat: add operation to retrieve sub-records

Patch

  • fix: remove the infinite loop
  • fix(my-operation): handle null input field

N/A

  • test: refactor user management test cases
  • ci: point to the new registry
  • docs: add missing method documentation in create operation

如果您的团队更“有趣”,您可以随时尝试这些替代信号:✨ (feat), 🐛 (fix), 📚 (docs), 💎 (style), ♻️ (refactor), 🚀 (perf), ✅ (test), 📦 (build), 👷 (ci), 🔧 (chore)

你在构建什么?

如果您曾在软件开发中花费过任何时间,您就会知道提倡“一刀切”的人需要滚蛋了。因此,在这篇文章中,我想研究两种截然不同的软件发布方法,以及如何将这种技术应用于这两种方法。

合并后发布

  • 描述
    • 主干分支上的每个提交都是具有新语义版本的发布
    • 新版本没什么大不了的,版本不被视为神圣的有限资源
    • 一个应用程序每天可能会经历多个版本
    • 修改需要通过 QA 关卡,比如单元测试、同事评审、MR 构建甚至 review app
  • 优势
    • 无预先发版计划。在需要时选择要发布的版本
    • 对测试人员和最终用户的反馈循环更短
  • 使用时机
    • 供应商/顾问交付的项目
    • 有多个非生产环境可用于快速部署和测试更改的项目

预发布后正式发布

  • 描述
    • 每个版本之前都有一个或多个 alpha 和 beta 版本形式的预发布
    • 版本更新被认为是珍贵的
    • 从预发布版本中删除版本时需要重新测试
  • 优势
    • 通过在引入稳定版本之前发布预发布来获得充分的信心
    • 您的用户不必像在其他方法中那样想知道预发布版本发生了什么
  • 使用时机
    • 库、软件产品和开源项目
    • 稳定发布节奏较慢的项目

我已经在很多地方看到了这两种方法的许多变体,因此我们将在这里讨论的内容也应该同样适用于这些方法。 虽然我应该提到我倾向于避免破坏版本不变性的样式,例如 Maven 中的 SNAPSHOT 版本或 NPM 中的 @next disributaion channel。

Pull Requests

无论您如何发布,我都希望您通过 PR(有时称为 MR)引入新功能。 如果你不是,我们有比版本控制更大的问题。

理想情况下,每个拉取请求都应包含一个功能或修复。 作为拉取请求审查的一部分,开发人员可能需要提交更多更改以解决审查意见。 但是这些额外的提交并不是主干上的新功能或代码修复。

我的解决方案是使用 squash merge 策略。 这样,开发人员可以在功能/修复分支上对他们的提交消息做任何他们喜欢的事情。 这些提交将全部消失,开发人员可以在合并点为整个拉取请求提供约定是提交消息。

大多数体面的 Git 存储库还允许您使用您的拉取请求名称作为您的 squash 提交消息。 如果您希望从开发人员那里看到一致的拉取请求名称并让审阅者甚至在批准之前审阅 squash 提交消息,这很好。

合并后发布的分支策略

不管发布风格如何,我倾向于基于主干(主线)的分支。我避免使用 Gitflow,因为我足够关心我的开发人员,以免他们每天都花在解决合并冲突上。更不用说,仅仅因为你从 develop 合并到 master 就重新构建相同版本的应用程序,面对“一次构建,多次部署”的CI/CD实践。

现在我已经完成了我对 GitFlow 的每日一喷,接下来让我们来谈谈我们在每次合并时发布时如何进行分支。

这很简单:创建一个功能/修复分支并遵循上面的拉取请求过程。

作为 CI/CD 流程设计师,您的主要目标之一应该是:开发人员最常做的任何事情都应该是最容易做到的。我觉得上面符合这个标准。

不太常见的情况也没有那么困难。假设开发人员正在构建应用程序的新主要版本,但之前的主要版本存在生产缺陷,需要热修复。这是执行此修补程序的剧本:

  1. 找出生产中的次要版本。比方说v1.3。
  2. 从该次要版本的最新补丁版本创建 v1.3.x 分支,例如v1.3.6。
  3. 创建一个修复分支并将修复请求拉回 v1.3.x
  4. 从 v1.3.x 分支构建、部署和升级到生产
  5. 通过将 v1.3.x 分支合并到 master 将修复移植到主线

预发布后正式发布的分支策略

在此方法中,您将在预发布分支上引入重大更改。 你可以使用任何你喜欢的滑稽名称,但我会坚持使用传统的 alpha/beta 术语。

本质上,您在一个或多个预发布分支上工作,直到您准备好向您的用户发布新版本;此时,您只需将预发布分支合并到主干中。

该图通过在下一个规范版本升级之前发布 2 个 alpha 和 1 个 beta 版本来证明这一点。

上图还展示了在所有这些预发布工作期间对生产的修补程序。

不用说,您可以拥有更少或更多的预发布分支,甚至可以根据需要在它们之间来回合并。 这完全取决于您的个人发布风格。

工具中的实现

让我们看看我们的版本控制和分支方法如何应用于各种源代码管理和 CI/CD 平台。

虽然您可以让开发人员遵守约定是提交,但这需要我在我们的专业中几乎看不到的僧侣般的纪律水平。因此,在相关分支上强制执行提交消息格式更为明智。大多数 SCM 提供此功能作为服务器端挂钩。

对于一些人来说,意识到他们的提交消息在服务器上不正确已经太晚了。幸运的是,有一些工具,例如 Commitizencommitlint 甚至是一个简单的 pre-commit,可以尽早警告用户。

虽然如果你遵循我们的 squash merge 策略,你就不必关心你的本地提交消息。

然而,我们并没有免除关心长期存在的分支上的提交。因此,始终建议确保构建过程验证自上次构建以来这些分支上的提交历史记录,以确保所有提交消息都符合要求。

通常,我们在 CI/CD 流程中至少具备以下条件:

  • 在每次 PR 构建时,验证自上次构建以来的所有提交消息,以确保它们遵循约定式提交
  • 在主干构建中,除了常规测试之外:
  1. 验证提交消息
  2. 通过分析提交消息和以前的标签确定下一个版本
  3. 用新版本标记当前提交
  4. 可选择生成 relase note
  5. 发布 artifacts

给提交打 tag 以将其标识为应用程序版本的来源是一种有价值的做法。它使 Git 存储库成为一个独立的真实来源,并消除了对 CI/CD 工具的一些过度依赖。我不止一次看到团队在使用 CI/CD 作为事实来源时不得不重新开始他们的版本控制,但随后他们不得不迁移到不同的工具,或者该工具出现了不可恢复的故障。

现在我们知道需要做什么,我们需要弄清楚如何去做。这似乎需要实现很多功能。幸运的是,已经有一些工具可以为我们完成所有这些工作。我目前首选的工具是 semantic-release,它提供了以上所有以及更多。

semantic-release

semantic-release 提供了一个命令行界面 (CLI),任何 CI/CD 工具都可以调用它。 先决条件是 NodeJS 和 Git。

语义发布的优势之一是它可以使用插件进行扩展。 官方插件让您可以创建发布说明和变更日志文件,将发布发布到 GitHub 和 Gitlab,将包发布到 NPM 和 APM 等。还有很多社区插件。

我将使用 GitLab 来说明如何配置和使用这个工具,但这种方法可以很容易地转换为其他 CI/CD 工具。

我将使用 GitLab 的 Docker 执行器在容器中运行我的构建,所以首先,我需要创建一个具有语义发布和我需要的所有插件的 Docker 映像。

FROM node:14.3.0
LABEL maintainer="sohrab"
RUN npm install --global \\
[email protected] \\
@semantic-release/[email protected] \\
@semantic-release/[email protected]

(是的,我对所有内容都进行了版本控制。就像专业人士一样。“可重复、可靠的构建”是另一个 CI/CD 原则。如果您要重复使用它,请查看 npmjs.com 以获取最新版本。)

接下来,我需要为我的存储库配置语义发布。 有几种方法可以做到这一点,但在这里我将 .releaserc.json 文件放在我的存储库的根目录中,其中包含以下内容:

{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/gitlab",
[
"@semantic-release/exec", {
"successCmd": "echo \\"VERSION=${nextRelease.version}\\" >> vars.env"
}
]
]
}

这将只启用我想要使用的插件,在这种情况下:

  • commit-analyzer 通过分析 repo 的提交历史来确定下一个版本
  • release-notes-generator 以传统的变更日志格式生成发行说明
  • gitlab 将发行说明发布为 GitLab Release
  • exec 将发布版本写入 dot-env 文件,以便在后续阶段中可用

最后,我们需要配置 CI/CD 本身。 这里它显示在 GitLab YAML 中。 即使您以前从未使用过 GitLab,这也应该是不言自明的,并且可以翻译成其他 CI/CD 工具:

stages:
- version
- build

version:
image: semantic-release
script:
- semantic-release
artifacts:
reports:
dotenv: vars.env
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
when: on_success

build:
image: ...
script:
# run all tests, build, package and publish the artifact
- ...
rules:
- if: '$VERSION'
when: on_success

由 version stage 生成的 vars.env 文件中的 VERSION 环境变量随后可以被构建阶段用于对 artifact 进行版本控制。值得注意的是,如果没有产生新版本,我们会跳过构建和发布。

最后一点配置是确保语义发布可以将标签推送到您的存储库中。为此,您需要向该工具提供 Git 身份验证详细信息。在我的用例中,这是设置 GITLAB_TOKEN 环境变量的问题。

提示:如果您使用的是自托管 Gitlab 实例,您还需要配置 GITLAB_URL 以指向您的实例。如果您使用的是 gitlab.com,则不需要这样做。

我应该注意,如果构建失败,版本阶段不应再次运行,因为它已经用版本标记了提交。因此,如果您的 CI/CD 工具允许您从构建阶段恢复失败的管道,则上述顺序是合适的。如果此支持不存在,那么您需要:

在重新运行管道之前手动清理标签,或者更改管道以使用 --dry-run 标志执行语义发布以获取新版本,运行构建并最终真正运行语义发布。

就是这样!

我们在相对成功的项目中使用了这种方法,尤其是每次合并发布风格。

我必须对你说实话,如果你没有好的工具,你将需要良好的开发人员纪律。 如果您两者都没有,那么这可能不适合您。 但是,如果您可以使用这些技术,那么您将永远不必考虑在提交和拉取请求中交付的内容之后的版本控制。