掌握Python项目设置的艺术:逐步指南
无论您是经验丰富的开发人员还是刚刚开始学习🐍 Python ,都需要知道如何构建健壮且易于维护的项目。本教程将指导您使用行业中最受欢迎和有效的工具来设置Python项目的过程。您将学习如何使用GitHub和GitHub Actions进行版本控制和持续集成,以及其他用于测试、文档编写、打包和分发的工具。本教程的灵感来自于资源,如Hypermodern Python和一个新的Python项目的最佳实践。然而,这并不是唯一的方法,您可能有不同的偏好或意见。本教程旨在适合初学者,但也涵盖了一些高级主题。在每个部分中,您将自动化一些任务,并向您的项目添加徽章,以展示您的进展和成就。
此系列的存储库可以在github.com/johschmidt42/python-project-johannes找到
这一部分的灵感来自于这篇博文:
使用Python、Poetry和GitHub Actions进行语义化发布🚀 我计划为Dr. Sven添加几个功能,这都是因为我的同事们对此表现出了兴趣。在此之前,我需要…
要求
- 操作系统:Linux、Unix、macOS、Windows(使用例如Ubuntu 20.04 LTS的WSL2)
- 工具:python3.10、bash、git、tree
- 版本控制系统(VCS)主机:GitHub
- 持续集成(CI)工具:GitHub Actions
预计您熟悉版本控制系统(VCS)git。如果不熟悉,请参阅以下内容:Git简介
提交将基于git提交的最佳实践和常规提交。有针对PyCharm的常规提交插件或VSCode扩展,可以帮助您以此格式编写提交。
概览
- 第一部分(GitHub、IDE)
- 第二部分(格式化、Linting、CI)
- 第三部分(测试、CI)
- 第四部分(文档编写、CI/CD)
- 第五部分(版本控制和发布、CI/CD)
- 第六部分(容器化、Docker、CI/CD)
结构
- Git分支策略(GitHub流程)
- 什么是发布?(zip、tar.gz)
- 语义化版本控制(v0.1.0)
- 手动创建发布(git tag、GitHub)
- 自动创建发布(常规提交、语义化发布)
- CI/CD(release.yml)
- 创建个人访问令牌(PAT)
- GitHub Actions流程(编排工作流程)
- 徽章(发布)
- 奖励(强制执行常规提交)
发布软件是软件开发过程中的重要步骤,因为它使用户可以使用新功能和错误修复。发布软件的一个关键方面是版本控制,它有助于跟踪和传达每个发布中所做的更改。语义化版本控制是一种广泛使用的软件版本控制标准,它使用Major.Minor.Patch(例如1.2.3)的版本号来指示发布中所做的更改级别。
常规提交是为提交消息添加人类可读和机器可读的含义的规范。它是一种以一致方式格式化提交消息的方式,使得确定所做更改类型变得容易。常规提交通常与语义化版本控制一起使用,因为提交消息可用于自动确定发布的版本号。语义化版本控制和常规提交一起提供了一种清晰且一致的方式来跟踪和传达软件项目中每个发布所做的更改。
Git 分支策略
对于 git 来说,有许多不同的分支策略。许多人倾向于使用 GitFlow(或其变体)、Three Flow 或者 Trunk based Flows。有些人会采用介于这些策略之间的策略,比如这个。我使用了非常简单的 GitHub 流分支策略,其中所有的 bug 修复和功能都有自己独立的分支,完成后,每个分支都会合并到主分支并进行部署。简单、好用。
无论你使用何种策略,最终你都会合并一个拉取请求(pull request)并(可能)创建一个发布。
什么是发布?
简而言之,发布就是打包一个版本的代码(例如 zip 文件)并将其推送到生产环境(对于你来说可能是什么)。
发布管理可能会很混乱。因此,需要有一种简明扼要的方式来定义(以及他人遵循)什么是一个发布,以及一个发布与下一个发布之间的变化。如果不追踪发布之间的变化,那么你可能无法理解每个发布中发生了什么变化,也无法识别可能随新代码引入的任何问题。没有变更日志,很难理解软件如何随着时间的推移而发展。这也可能使得需要时回滚更改变得困难。
语义化版本控制
语义化版本控制只是软件开发行业的一种数字模式和标准做法。它指示了该版本与上一个版本之间的变化级别。语义化版本号由三个部分组成,例如 1.8.42,遵循以下模式:
- 主版本号.次版本号.修订号
它们中的每一个表示不同程度的变化。修订号表示 bug 修复或微小的更改(例如从 1.0.0 到 1.0.1)。次版本号表示添加/删除功能或向后兼容的功能更改(例如从 1.0.0 到 1.1.0)。主版本号表示添加/删除功能并且可能包含不向后兼容的更改,如破坏性变更(例如从 1.0.0 到 2.0.0)。
如果你想要对语义化版本控制的发布进行视觉介绍,我推荐看一下 Mike Miles 的演讲。它总结了发布是什么,以及如何使用 git 标签进行语义化版本控制来创建发布。
关于git 标签:git 中有轻量级标签和带注释的标签。轻量级标签只是指向特定提交的指针,而带注释的标签是 git 中的一个完整对象。
手动创建发布
让我们先手动创建一个发布,然后再自动化它。
如果你还记得,我们的 example_app 的 __init__.py
文件包含了版本信息
# src/example_app/__init__.py__version__ = "0.1.0"
以及 pyproject.toml
文件
# pyproject.toml[tool.poetry]name = "example_app"version = "0.1.0"...
因此,我们必须首先创建一个带注释的 git 标签 v0.1.0
,并将其添加到主分支的最新提交中:
> git tag -a v0.1.0 -m "version v0.1.0"
请注意,如果命令的末尾没有指定提交哈希值,则 git 将使用当前所在的提交。
我们可以使用以下命令获取标签列表:
> git tagv0.1.0
如果我们想要删除它:
> git tag -d v0.1.0Deleted tag 'v0.1.0'
如果想要获取有关标签的更多信息:
> git show v0.1.0tag v0.1.0Tagger: Johannes Schmidt <johannes.schmidt.vik@gmail.com>Date: Sat Jan 7 12:55:15 2023 +0100version v0.1.0commit efc9a445cd42ce2f7ddfbe75ffaed1a5bc8e0f11 (HEAD -> main, tag: v0.1.0, origin/main, origin/HEAD)Author: Johannes Schmidt <74831750+johschmidt42@users.noreply.github.com>Date: Mon Jan 2 11:20:25 2023 +0100...
我们可以使用以下命令将新创建的标签推送到源:
> git push origin v0.1.0Enumerating objects: 1, done.Counting objects: 100% (1/1), done.Writing objects: 100% (1/1), 171 bytes | 171.00 KiB/s, done.Total 1 (delta 0), reused 0 (delta 0), pack-reused 0To github.com:johschmidt42/python-project-johannes.git * [new tag] v0.1.0 -> v0.1.0
这样,此git标签现在在GitHub上可用:
让我们在GitHub中手动使用此git标签创建一个新的发布:
我们点击 创建新的发布
,选择我们已经绑定到提交的现有标签,然后点击 生成发布说明
按钮自动生成发布说明,最后使用 发布发布
按钮发布该发布。
GitHub将自动为源代码创建一个 tar
和一个 zip
(资源),但不会构建应用程序!结果将如下所示:
总结一下,发布的步骤如下:
- 从默认分支创建一个新分支(例如特性或修复分支)
- 进行更改并增加版本号(例如pyproject.toml和__init__.py)
- 将功能/错误修复提交到默认分支(可能通过Pull Request)
- 为提交添加带注释的git标签(语义版本)
- 在GitHub上发布该发布并提供一些附加信息
自动创建发布
作为程序员,我们不喜欢重复劳动。因此,有很多工具可以让我们的这些步骤变得非常容易。在这里,我将介绍Semantic Releases,这是一个专门用于Python项目的工具。
这是一个可以自动在您的代码库中设置版本号、使用版本号标记代码并创建发布的工具!而且,所有这些都是使用常规提交风格的消息完成的。
常规提交
语义版本控制和常规提交之间有什么关联?
某些提交类型可以用于自动确定语义版本的增量!
- 修复提交是一个修补版本(PATCH)。
- 功能提交是一个次要版本(MINOR)。
- 带有
BREAKING CHANGE
或!
的提交是一个主要版本(MAJOR)。
其他类型,例如 build
、 chore
、 ci
、 docs
、 style
、 refactor
、 perf
、 test
通常不会增加版本号。
在最后的附加部分中,我们将介绍如何在您的项目中强制执行常规提交!
本地自动语义发布
我们可以使用以下命令添加该库:
> poetry add --group semver python-semantic-release
让我们来看一下允许我们自动生成变更日志和发布的配置设置。在 pyproject.toml
中,我们可以将semantic_release作为一个工具添加:
# pyproject.toml...[tool.semantic_release]branch = "main"version_variable = "src/example_app/__init__.py:__version__"version_toml = "pyproject.toml:tool.poetry.version"version_source = "tag"commit_version_number = true # required for version_source = "tag"tag_commit = trueupload_to_pypi = falseupload_to_release = falsehvcs = "github" # gitlab is also supported
branch
:指定发布应基于的分支,在这种情况下为“main”分支。version_variable
:指定源代码中版本号的文件路径和变量名。在这种情况下,版本号存储在文件src/example_app/__init__.py
中的__version__
变量中。version_toml
:指定pyproject.toml
文件中版本号的文件路径和变量名。在这种情况下,版本号存储在pyproject.toml
文件的tool.poetry.version
变量中。version_source
:指定版本号的来源。在这种情况下,版本号从标签中获取(而不是从提交中获取)。commit_version_number
:当version_source = "tag"
时,此参数是必需的。它指定是否将版本号提交到代码库中。在这种情况下,它设置为true,表示版本号将被提交。tag_commit
:指定是否为发布提交创建新标签。在这种情况下,它设置为true,表示将创建一个新标签。upload_to_pypi
:指定是否应将软件包上传到PyPI软件包仓库。在这种情况下,它设置为false,表示软件包将不会上传到PyPI。upload_to_release
:指定是否应将软件包上传到GitHub发布页面。在这种情况下,它设置为false,表示软件包将不会上传到GitHub发布。hvcs
:指定项目的托管版本控制系统。在这种情况下,它设置为“github”,表示项目托管在GitHub上。也支持“gitlab”。
我们可以更新定义了项目/模块版本的文件。对于普通文件,我们使用变量version_variable
,对于.toml文件,我们使用version_toml
。 version_source
定义了版本的真实来源。因为这两个文件中的版本与git的带注释的标签紧密耦合,例如我们自动为每个发布创建一个git标签(标志tag_commit
设置为true),我们可以使用源tag
替代默认值commit
,后者在提交消息中查找最后一个版本。为了能够更新文件并提交更改,我们需要将commit_version_number
标志设置为true。因为我们不想将任何内容上传到Python索引PyPi,所以将upload_to_pypi
标志设置为false。目前我们也不想将任何内容上传到我们的发布。 hvcs
设置为github
(默认值),其他值可以是:gitlab
。
我们可以通过运行一些命令在本地进行测试,我将直接将这些命令添加到我们的Makefile中:
# Makefile...##@ 发布current-version: ## 返回当前版本 @semantic-release print-version --currentnext-version: ## 返回下一个版本 @semantic-release print-version --nextcurrent-changelog: ## 返回当前变更日志 @semantic-release changelog --releasednext-changelog: ## 返回下一个变更日志 @semantic-release changelog --unreleasedpublish-noop: ## 发布命令(无操作模式) @semantic-release publish --noop
通过current-version
命令,我们可以从git树中的最后一个git标签获取版本:
> make current-version0.1.0
如果我们以常规提交样式添加了一些提交,例如feat: 新的酷功能
或fix: 讨厌的错误
,那么next-version
命令将计算版本的增加:
> make next-version0.2.0
目前,我们的项目中没有CHANGELOG文件,所以当我们运行:
> make current-changelog
输出将为空。但是基于提交记录,我们可以使用以下命令创建即将发布的changelog:
> make next-changelog### 功能* 添加发布版本([#8](https://github.com/johschmidt42/python-project-johannes/issues/8))([`5343f46`](https://github.com/johschmidt42/python-project-johannes/commit/5343f46d9879cc8af273a315698dd307a4bafb4d))* Docstrings([#5](https://github.com/johschmidt42/python-project-johannes/issues/5))([`fb2fa04`](https://github.com/johschmidt42/python-project-johannes/commit/fb2fa0446d1614052c133824150354d1f05a52e9))* 在app.py中添加应用([`3f07683`](https://github.com/johschmidt42/python-project-johannes/commit/3f07683e787b708c31235c9c5357fb45b4b9f02d))### 文档* 添加搜索栏和github链接([#6](https://github.com/johschmidt42/python-project-johannes/issues/6))([`3df7c48`](https://github.com/johschmidt42/python-project-johannes/commit/3df7c483eca91f2954e80321a7034ae3edb2074b))* 在README.py中添加pages.yml徽章([`b76651c`](https://github.com/johschmidt42/python-project-johannes/commit/b76651c5ecb5ab2571bca1663ffc338febd55b25))* 在Makefile中添加文档([#3](https://github.com/johschmidt42/python-project-johannes/issues/3))([`2294ee1`](https://github.com/johschmidt42/python-project-johannes/commit/2294ee105b238410bcfd7b9530e065e5e0381d7a))
如果我们推送新的提交(直接到主分支或通过PR),我们现在可以使用以下命令发布新版本:
> semantic-release publish
发布命令将执行一系列操作:
- 更新或创建changelog文件。
- 运行semantic-release version。
- 推送更改到git。
- 运行构建命令并将分发文件上传到您的仓库。
- 运行semantic-release changelog并发布到您的VCS提供者。
- 将由构建命令创建的文件附加到GitHub发布中。
当然,每个步骤都可以配置或禁用!
CI/CD
让我们使用GitHub Actions构建一个CI流水线,它会在每次提交到主分支时运行semantic-release的发布命令。
虽然整体结构与lint.yml、test.yml或pages.yml保持一致,但有一些需要注意的变化。在步骤Checkout repository
中,我们添加了一个用于检出分支的新令牌。这是因为默认值GITHUB_TOKEN
没有操作受保护分支所需的权限。因此,我们必须使用一个包含有权限的个人访问令牌的机密(GH_TOKEN)。稍后我将展示如何生成个人访问令牌。我们还定义fetch-depth: 0
以获取所有分支和标签的所有历史记录。
with: ref: ${{ github.head_ref }} token: ${{ secrets.GH_TOKEN }} fetch-depth: 0
我们只安装semantic-release工具所需的依赖项:
- name: 安装依赖项 run: poetry install --only semver
在最后一步中,我们更改一些git配置并运行semantic-release的发布命令:
- name: Python语义化发布 env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: | set -o pipefail # 设置git详细信息 git config --global user.name "github-actions" git config --global user.email "github-actions@github.com" # 运行semantic-release poetry run semantic-release publish -v DEBUG -D commit_author="github-actions <action@github.com>"
通过更改git配置,提交的用户将变为“github-actions”。我们使用DEBUG日志(stdout)运行发布命令,并将commit_author
显式设置为“github-actions”。除了这个命令之外,我们也可以直接使用semantic-release的GitHub action,但运行发布命令的设置步骤很少,并且该action使用需要每次拉取的docker容器。因此,我更喜欢使用简单的运行步骤。
由于发布命令会创建提交,您可能担心我们会陷入无限循环的工作流触发中。但是不用担心,由于GitHub设置的限制,生成的提交不会触发另一个GitHub Actions工作流运行。
创建个人访问令牌(PAT)
个人访问令牌是在使用GitHub API或命令行时,用于对GitHub企业服务器进行身份验证的密码替代方法。个人访问令牌用于代表您自己访问GitHub资源。要代表组织访问资源,或者用于长期集成,您应该使用GitHub App。有关更多信息,请参阅“关于应用程序”。
换句话说,我们可以创建一个个人访问令牌(PAT),并让GitHub actions存储和使用该密钥代表我们执行某些操作。请记住,如果PAT被泄露,它可能被用于对您的GitHub存储库执行恶意操作。因此,建议在组织中使用GitHub OAuth Apps和GitHub Apps。为了本教程的目的,我们将使用PAT来允许GitHub actions管道代表我们进行操作。
我们可以通过转到GitHub用户的设置
部分,并按照“创建个人访问令牌”中总结的说明来创建新的访问令牌。这将给我们一个如下所示的窗口:
通过选择权限范围,我们定义了令牌将具有的权限。对于我们的用例,我们需要对存储库有推送访问权限,因此新的PAT GH_TOKEN
应具有repo
权限范围。该范围将授权对受保护分支进行推送,前提是您在受保护分支的设置中没有设置“包括管理员”。
回到存储库概述,在设置菜单中,我们可以在Secrets部分下添加环境设置或存储库设置:
存储库密钥特定于单个存储库(及其中使用的所有环境),而环境密钥特定于环境。GitHub runner可以配置为在特定环境中运行,从而允许它访问环境的密钥。这在考虑到不同阶段(例如DEV vs PROD)时是有意义的,但是对于本教程,我对存储库密钥满意。
GitHub Actions流程
现在我们有了几个流水线(linting,testing,releasing,documentation),我们应该考虑到主分支的提交触发的操作流程。以下是一些我们应该注意的事项,其中一些是GitHub特定的。
理想情况下,我们希望对主分支的提交创建一个推送事件,触发测试和linting工作流。如果这些成功,我们将运行负责根据常规提交确定是否应该进行版本提升的发布工作流。如果是这样,发布工作流将直接推送到主分支,提升版本,添加git标签并创建发布。然后,发布的版本应通过运行文档工作流来更新文档。
问题和考虑事项
- 如果你仔细阅读了上面的最后一段或者查看了上面的流程图,你可能会注意到主分支有两个提交。一个是初始提交(即来自PR),另一个是用于发布的提交。因为我们的lint.yml和test.yml会对主分支上的推送事件作出反应,它们会运行两次!我们应该避免运行两次以节省资源。为了实现这一点,我们可以在版本提交信息中添加
[skip ci]
字符串。可以在pyproject.toml文件中为semantic_release工具定义自定义提交消息。
# pyproject.toml...[tool.semantic_release]...commit_message = "{version} [skip ci]" # 跳过版本提交触发的CI pipeline...
2. workflow pages.yml目前在对主分支进行推送事件时运行。更新文档可能只是在有新版本发布时才需要执行(我们可能在文档中引用了版本)。我们可以相应地更改pages.yml文件中的触发器:
# pages.ymlname: Documentationon: release: types: [published]
构建文档现在需要一个已发布的版本。
3. 发布工作流应该依赖于Linting & Testing工作流的成功。目前我们的工作流文件中没有定义依赖关系。我们可以让这些工作流依赖于特定分支中已定义工作流运行的完成,使用workflow_run
事件。然而,如果我们在workflow_run
事件中指定了多个workflows
:
on: workflow_run: workflows: [Testing, Linting] types: - completed branches: - main
只需要完成其中一个工作流!这不是我们想要的。我们期望所有的工作流都必须完成(并成功)。只有在这样的情况下,发布工作流才会运行。这与在单个工作流中定义作业之间的依赖关系不同。更多关于这种不一致性和缺陷的信息,请阅读这篇文章。
作为替代方案,我们可以使用顺序执行的流水线:
这个想法的一个很大的缺点是:a)它不允许并行执行,b)我们将无法在GitHub中看到依赖关系图。
解决方案
目前,我认为解决上述问题的唯一方法是在一个编排工作流中协调工作流。
当我们推送到分支main
时,调度工作流被触发。
只有当测试和Linting两个工作流都成功时,才会调用发布工作流。这是使用needs
关键字定义的。如果我们想对作业执行(工作流)有更精细的控制,可以考虑同时使用if
关键字。但要注意这篇文章中所解释的令人困惑的行为。
为了使我们的工作流lint.yml
、test.yml
和release.yml
可以被另一个工作流调用,我们需要更新触发器:
# lint.yml---name: Lintingon: pull_request: branches: - main workflow_call:jobs:...
# test.yml---name: Testingon: pull_request: branches: - main workflow_call:jobs:...
# release.yml---name: Releaseon: workflow_call:jobs:...
现在,只有在质量检查工作流(在本例中为linting和testing)成功后,新的工作流(发布)才会运行。
徽章
为了创建一个徽章,这次我将使用平台shields.io。
这是一个生成项目徽章的网站,可以显示版本、构建状态和代码覆盖率等信息。它提供了多种模板,并允许自定义外观和创建自定义徽章。徽章会自动更新,提供项目的实时信息。
对于发布徽章,我选择了GitHub发布(最新的语义版本)
:
可以复制徽章的markdown代码并添加到README.md中:
我们的GitHub首页现在看起来是这样的❤(我稍微整理了一下并提供了描述):
恭喜!你已经完成了本教程的主要部分!你已经学会了管理软件发布的基本步骤。我们首先手动创建了一个发布,然后利用了常规提交的强大功能,通过CI流水线自动化了我们的发布过程,这个过程会自动处理版本控制。最后,我们在README.md文件中添加了一个徽章,为我们的用户提供了项目的最新版本的清晰简洁显示。掌握了这些技巧,你将能够高效有效地管理你的软件发布。
下一部分将是最后一部分,涵盖:容器化!
使用我的推荐链接加入VoAGI – Johannes Schmidt
阅读Johannes Schmidt的每个故事(以及在VoAGI上的成千上万的其他作者的故事)。你的会费直接……
johschmidt42.medium.com
额外奖励
确保常规提交
我们已经看到,按照规定的格式提交可以帮助我们进行版本控制。在协作项目中,我们可能希望对默认分支上的所有提交强制执行这个格式。有两个流行的工具可以帮助开发人员遵循常规提交的格式:
- commitizen
- commitlint
然而,一些开发人员认为这些工具有些限制性,不愿意使用它们。因此,仅仅希望总是有常规提交可能是一个不好的想法。因此,有必要在服务器端强制执行规则,比如常规提交的格式!
*对于pre-commit hooks也是如此,这就是为什么我在本系列中将它们排除在外的原因。
不幸的是,目前(2023年5月)还不能根据规则阻止GitHub上的提交,因为这个功能仍然是开放的。但是我们可以尝试通过分支保护规则和CI工作流程尽可能接近。所以在我们的仓库中,我们需要以下几点:
- 对于受保护的默认分支(例如主分支),提交应该限制为拉取请求(PR)提交。
- 仅允许合并后的提交*
- 当合并拉取请求时,显示默认的提交消息应该是拉取请求的标题
如果只允许通过拉取请求(仅允许合并后的提交)提交到受保护的默认分支(例如主分支),我们可以使用GitHub Action,比如amannn/action-semantic-pull-request,确保拉取请求的标题与常规提交规范匹配。因此,当我们合并并压缩PR分支(假设所有必需的流水线都成功),建议的提交消息是之前由GitHub Action检查过的PR标题。
* 合并压缩策略是将来自功能分支的代码更改合并到主分支的一种流行方法,它将功能分支中的多个提交压缩为一个提交。这样可以创建一个线性一致的git历史,每个提交都代表一个特定的更改。然而,这种方法也有其缺点,因为它丢弃了详细的提交历史,而这些历史对于理解开发过程是有价值的。虽然使用rebase合并可以保留这些信息,但它会给工作流程引入复杂性。从这个意义上讲,合并压缩策略因其简单性而受到青睐。
工作流程
让我们为这个策略创建 GitHub Actions 的工作流程:
触发事件 pull_request_target 的说明在这里。我使用建议的类型 opened
,edited
,synchronize
。将 GITHUB_TOKEN
作为 env
传递给操作。因此,每当在 PR 中更改标题时,流水线就会触发。只有当 PR 的标题符合常规提交格式时,它才会成功。
请注意
您需要在主分支中拥有此配置,才能使操作运行(例如,它不会在最初添加操作的 PR 中运行)。此外,如果在 PR 中更改配置,只有在配置更改进入主分支后的后续 PR 才会反映这些更改。
因此,我们必须首先在我们的默认分支 main
中拥有这个工作流程,然后才能看到它的运行情况。
分支保护规则
接下来,在 GitHub 仓库的设置部分,我们可以为主分支创建一个 分支保护规则:
现在,在合并之前,提交需要通过 PR 的状态检查(必需的工作流程)。
所需的工作流程是由拉取请求事件触发的,并显示为必需的状态检查,它会阻止合并拉取请求,直到所需的工作流程成功。
组织所有者有能力在组织内强制执行特定的工作流程,例如要求在拉取请求上进行状态检查。不幸的是,此功能仅适用于组织,不能为个人账户激活,因此无法阻止合并。
*请注意,直到将私有存储库移动到 GitHub 团队或企业组织账户时,规则才会强制执行!
压缩合并策略
最后,我们可以配置 PR 选项,当我们选择压缩并合并按钮时,使用 PR 的标题作为默认提交消息:
这样,我们会看到一个像这样的窗口:
请注意,开发人员可能会在合并过程中更改标题的名称,从而绕过该策略!
尽管我们还无法完全确保在 GitHub 上进行常规提交,但我们应该尽力接近。