在日常开发时, 有时需要对版本进行发布. 而版本号的修改总是老大难的问题. 使用 IDE 修改版本号, 再提交总是会出现很多问题. 而且在项目发布时, 这时存在多个资源包都需要发布时, 工作量就很大, 而且容易产生疏忽.
手动修改并发布
我们先了解下如何手动对版本进行发布. 假设我们有如下的一个项目
release-test/
├── module-1
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java
├── module-2
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java
└── pom.xml
可以直接从 github 仓库 中直接拉取
默认版本为 0.1-SNAPSHOT , SNAPSHOT 都是测试版本, 而且 SNAPSHOT 可以无限制发布 (除非私有仓库限制了 redeploy 策略不允许). 而非 SNAPSHOT 版本则是发布版本, 发布版本有且只能发布一次!
发布流程
如果我们想要发布一个 0.1 的正式版本 (或者 0.1-RELEASE), 那步骤如下:
CTRL+SHIFT+R搜索<version>0.1-SNAPSHOT</version>- 将版本号替换为
<version>0.1</version> - 提交修改.
- 发布
release包 - 将版本号继续修改为
<version>0.2-SNAPSHOT</version> - 提交修改
使用 reivison 属性
这时候肯定有朋友会说: "可以用 revision 属性填充 <version> 字段啊".
的确, 通过添加 <revision> 属性, 并将所有项目的 <version> 都采用 ${revision} 填充是可以做到一次修改, 所有模块版本同步变更. 但是还是需要手动对其进行修改, 还是十分不便.
使用 revision 属性有个好处, 我们可以直接将上面步骤 直接变为一个打包指令.
mvn clean install -Drevision=<release version>
通过上方指令就可以直接发布指定版本的依赖包.
如何实现一键发布
那是否有一种能快速有效, 降低人为操作的方式来发布版本呢?
当然有的, 目前 MAVEN 的官方插件中就包含了一个 release 插件, maven-release-plugin (默认集成). 这时我们就能通过一行指令实现上述的所有操作.
mvn release:prepare release:perform
先看效果
# 拉取示例代码.
git clone https://github.com/LJiangTao/maven-version-release-demonstrate.git
# 进入 `release-plugin-demonstrate`
cd maven-version-release-demonstrate/release-plugin-demonstrate
可以使用 IDE 直接打开项目, 如果打开的 root 文件夹 (maven-version-release-demonstrate), 则需要在
project-settings (Win: CTRL+SHIFT+ALT+S, MacOs: Command+;中手动导入pom.xml
这时我们可以看到当前项目版本是 1.0-SNAPSHOT. 这时我们运行指令
mvn release:prepare release:perform
# ignore outputs
# 这里需要让我们手动确认或修改要发布的版本号
[INFO] 5/17 prepare:map-release-versions
What is the release version for "release-plugin-demonstrate"? (release-plugin-demonstrate) 1.0:
# 确认需要增加的 release tag 名称
[INFO] 6/17 prepare:input-variables
What is the SCM release tag or label for "release-plugin-demonstrate"? (release-plugin-demonstrate) v1.0:
# 确认下一个开发版本号
[INFO] 7/17 prepare:map-development-versions
What is the new development version for "release-plugin-demonstrate"? (release-plugin-demonstrate) 1.1-SNAPSHOT:
# 成功完成
[INFO] [INFO] ------------------------------------------------------------------------
[INFO] [INFO] Reactor Summary for release-plugin-demonstrate 1.0:
[INFO] [INFO]
[INFO] [INFO] release-plugin-demonstrate ......................... SUCCESS [ 0.304 s]
[INFO] [INFO] module-1 ........................................... SUCCESS [ 0.414 s]
[INFO] [INFO] module-2 ........................................... SUCCESS [ 0.049 s]
[INFO] [INFO] ------------------------------------------------------------------------
[INFO] [INFO] BUILD SUCCESS
[INFO] [INFO] ------------------------------------------------------------------------
[INFO] [INFO] Total time: 0.841 s
[INFO] [INFO] Finished at: 2024-10-26T01:41:58+08:00
[INFO] [INFO] ------------------------------------------------------------------------
[INFO] Cleaning up after release...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for release-plugin-demonstrate 1.0-SNAPSHOT:
[INFO]
[INFO] release-plugin-demonstrate ......................... SUCCESS [ 18.478 s]
[INFO] module-1 ........................................... SKIPPED
[INFO] module-2 ........................................... SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18.600 s
[INFO] Finished at: 2024-10-26T01:41:58+08:00
[INFO] ------------------------------------------------------------------------
在之后日志输出完成后, 就可以在对应的远程仓库上看到一个新的 tag. 并且增加了 2 笔提交, 分别为 (按照提交顺序从低到高排序):
- [release] prepare release v1.0
- [release] prepare for next development iteration
第一个是准备发布 v1.0 版本分支的修改, 第二个则是为准备下一个待发布分支的修改.
细看首个 [release] 会发现自动增加了一个 v1.0 的 tag 在这次提交上, 而且第二笔提交已经将版本号从 1.0 更新为 1.1-SNAPSHOT.
一个指令, 满足了手动提交的所有操作. 如此便捷快速, 且不会出现人为问题的才是我们值得推广的. 尽可能的流程化才能保证发布的高效率和高质量.
接下来我们看下这个插件怎么配置, 和插件的运行流程是如何的.
maven-release-plugin
该插件由 MAVEN 官方维护.
操作
首先我们了解下这个插件都有哪些可以操作的指令.
| 指令 | 说明 |
|---|---|
release:branch | 在 SCM 的项目地址上创建一个分支. |
release:clean | 清理 release:* 指令所产生的所有文件. 该指令会在 release:perform 成功后自动执行. |
release:prepare | 准备发布. 这个指令会自动更新项目版本号, 例如去除 SNAPSHOT 后缀. 并在生成一个提交, 这个提交同步标记一个 git tag 标签. 并再生成一个下一个 SNAPSHOT 版本提交. 在 prepare 完成后, 就可以执行 release:perform 正式发布. |
release:perform | 发布正式包 |
release:prepare-with-pom | 这时 release:prepare 中的一个步骤. 用于解决项目中依赖关系, 为发布做准备. |
release:rollback | 回滚到上一个版本. 前提是本地的 release.properties 还存有上一个版本的描述符 |
release:update-versions | 用于更新项目版本. 执行之后会自动更新到下一个 SNAPSHOT 版本 |
那我们知道了如何操作, 是不是可以直接执行指令发布版本号了呢? 并不行, 插件依赖 scm 信息, 我们还需要先在项目中定义 scm 信息才行.
定义 SCM
SCM (Source Control Management) 用来管理项目源代码的. 具体内容各位可以参考 maven scm
在使用这些指令前, 我们需要在文件中定义 <scm> 信息
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- IGNORED -->
<scm>
<!-- 用于指定当前项目的 tag. 通常为 HEAD, 也就是当前最新提交 -->
<tag>HEAD</tag>
<!-- 公开访问连接 -->
<url>https://github.com/LJiangTao/maven-version-release-demonstrate.git</url>
<!-- 可以读取代码的地址 -->
<connection>scm:git:git@github.com:LJiangTao/maven-version-release-demonstrate.git</connection>
<!-- 拥有提交代码的地址-->
<!-- 具体可以参考 https://maven.apache.org/pom.html#scm--> <!-- SCM URL 格式连接: https://maven.apache.org/scm/scm-url-format.html-->
<developerConnection>scm:git:git@github.com:LJiangTao/maven-version-release-demonstrate.git</developerConnection>
</scm>
</project>
配置插件
在使用 maven-release-plugin 插件前, 我们还需要对其进行设置, 来实现我们所期望的效果.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>${maven-release-plugin.version}</version>
<configuration>
<!-- 是否自动修改子模块版本 -->
<autoVersionSubmodules>true</autoVersionSubmodules>
<!-- tag 格式-->
<tagNameFormat>v@{version}</tagNameFormat>
<!-- 不向远程提交 Tag -->
<remoteTagging>false</remoteTagging>
<scmCommentPrefix>[release]</scmCommentPrefix>
<!-- 自动识别并处理 SNAPSHOT 版本依赖 -->
<autoResolveSnapshots>1</autoResolveSnapshots>
<!-- 版本方案 -->
<!-- https://maven.apache.org/maven-release/maven-release-plugin/usage/prepare-release.html#overriding-proposed-release-and-next-development-versions -->
<projectVersionPolicyId>default</projectVersionPolicyId>
<pomFileName>pom.xml</pomFileName>
<preparationGoals>clean verify</preparationGoals>
</configuration>
<dependencies>
<!-- git 依赖-->
<dependency>
<groupId>org.apache.maven.scm</groupId>
<artifactId>maven-scm-provider-gitexe</artifactId>
<version>${maven-scm.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.scm</groupId>
<artifactId>maven-scm-api</artifactId>
<version>${maven-scm.version}</version>
</dependency>
</dependencies>
</plugin>
| 参数 | 使用阶段 | 数据类型 | 说明 |
|---|---|---|---|
autoVersionSubmodules | prepare | bool | 是否同步更新子模块版本. 只要模块和 root 模块关联, 就会递归对其版本号进行修改. |
autoResolveSnapshots | prepare | bool | 是否自动处理 SNAPSHOT 版本依赖. 当开启时, 会将 SNAPSHOT 依赖去除 -SNAPSHOT 尾缀进行打包 |
developmentVersion | prepare | String | 下一个开发版本 |
dryRun | prepare | bool | 只测试配置文件是否规范, 不会产生任何文件和提交. |
javaHome | prepare | String | 指定 java 版本. |
lineSeparator | prepare | String | 将 pom.xml 按照指定换行符进行格式化. lf cr crlf source (default) |
preparationGoals | prepare | String | 前期准备时要运行的目标. 例如 clean compile flatten:flatten |
preparationProfiles | prepare | String | 前期准备时使用的 profile. 使用的 profile 必须在 settings.xml 中预先定义. |
projectTagNamingPolicyId | prepare | String | 用来计算项目分支和 tag 名称. 默认即可, 如有特殊需求, 可以通过实现 org.apache.maven.shared.release.policy.naming.NamingPolicy 并指定类的 FQDN. 并在 plugin.dependencies 中增加实现类依赖.默认实现 |
projectVersionPolicyId | prepare | String | 用于计算下一个 SNAPSHOT 或 RELEASE 版本的策略类. 如果有特殊需求, 可以通过实现 org.apache.maven.shared.release.policy.version.VersionPolicy并在 plugin.dependencies 中增加实现类依赖默认实现 其他版本方案 |
resume | prepare | bool | 是否从上次停止的地方继续开始. 默认继续, false 则会从头开始, 放弃之前操作的所有记录 |
pushChange | prepare | bool | 是否主动 push 提交. 默认 true, false 则只会提交到本地, 不推送至远程仓库 (scm 指定的 developerConnection) |
releaseVersion | prepare | String | 准备分支和版本的默认版本号 |
scmCommentPrefix | prepare | String | 提交记录前缀. 默认前缀为 [maven-release-plugin] |
scmDevelopmentCommitComment | prepare | String | 下一个开发版本提交信息. 控制提交信息的前缀, 默认是 @{preifx}. 还有 @{groupId} @{artifactId} @{releaseLabel} (对应发布的版本号) 可选 这里注意, 不要使用 ${} 替换符, 而是要使用 @{}. |
scmReleaseCommitComment | prepare | Stromg | 正式版本提交信息. 控制提交信息的前缀, 默认是 @{preifx}. 还有 @{groupId} @{artifactId} @{releaseLabel} (对应发布的版本号) 可选 这里注意, 不要使用 ${} 替换符, 而是要使用 @{}. |
scmShallowClone | prepare | bool | true 浅拷贝(默认), false 深拷贝. |
supressCommitBeforeTag | prepare | bool | 在进行 git tag 前不做改动提交. 这个可以有效避免出现重复的版本提交. |
tagNameFormat | prepare | String | tag 的命名格式. 例如 - v@{project.version} : v0.1- @{project.version}-RELEASE : 0.1-RELEASE- RELEASE-${project.version}: RELEASE-0.1还有 groupId artifactId 可选 |
branchName | branch | String | 创建分支的名称. |
remoteTagging | prepare | bool | 只有 SVN 有效 |
还有更多参数信息, 可以参考 maven-release-plugin 官方手册
prepare 流程
在我们执行 release:prepare 前, 确保本地没有修改还没有提交!
prepare 具体流程如下:
- 检查当前代码中是否存在未提交信息.
- 检查当前是否未
SNAPSHOT版本 - 修改当前
POM文件中的版本内容. 没有开启-B (--batch-mode)会提示输入- 确认发布版本号 (直接去掉
-SNAPSHOT后缀) - 确认
tag - 确认下一个开发版本号 (这个版本号则会按照
<projectVersionPolicyId>对应策略自动生成.)
- 确认发布版本号 (直接去掉
- 将
tag应用至SCM中的<tag>上, 并添加git tag. - 运行测试用例, 确保代码在修改
pom后依旧正常工作. - 提交修改的
pom文件 (这个阶段就是第一次插件自动提交, 也就是去除-SNAPSHOT) - 根据
projectVersionPolicyId>生成下一个开发版本 (将x.y变为x.(y+1)-SNAPSHOT) - 运行
<completionGoals>设定的指令 - 提交修改的
pom文件. 在运行完prepare后, 会在项目中看到release.properties文件, 其中记录了用于perform的相关信息.
perform 流程
perform 相当于我们自己运行 mvn deploy 指令发布可用的 release 包. 内部流程相对简单:
- 通过
scm的url拉取一份代码到target/checkout(可以通过) - 运行
deploy site-deploy指令发布release包. (可以通过releaseProfiles指定要使用的profile, 确保发布位置和依赖都可以正确的拉取.) 每次perform结束, 都会执行release:clean删除生成的所有文件.
rollback
rollback 顾名思义是回滚到上一个版本中. 但 rollback 依赖 release.properties , pom.xml.releaseBackup 文件. 如果文件不存在则无法进行回滚操作. 通常是通过 release:clean 进行删除
其流程具体如下:
- 将所有项目的
pom文件回滚到发布之前的版本. - 将创建的分支和 tag 进行删除.
branch
branch 是通过创建一个分支来对内容进行发布, 而不是使用 tag . 其按照指定的 <branchName> 进行命名 (不支持 @{} 变量).
其流程和 prepare 基于一模一样, 所以这里就不过多阐述.
update-versions
将当前版本号更新到下一个开发版本号. 即使当前是 x.y 版本, 也会更新为 x.(y+1)-SNAPSHOT 版本号.
指令只会更新 pom 文件中的版本号内容, 不会做其他操作 (提交, 推送, etc).
能否绑定在某个 maven 钩子上自动调用
这个当然是可以的, 我们在 maven-release-plugin 中增加 <excution> 来绑定在对应的 maven 生命周期上即可.
例如 flatten 插件的配置
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>bom</flattenMode>
<!-- 这里定义要处理的内容 -->
<pomElements>
<dependencyManagement>resolve</dependencyManagement>
</pomElements>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
其绑定了2个操作
flatten:flatten绑定在process-resoucesflatten:clean绑定在clean阶段
FAQs
1. fatal: Remote branch v1.0 not found in upstream origin
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.1.1:perform (default-cli) on project release-plugin-demonstrate: Unable to checkout from SCM
[ERROR] Provider message:
[ERROR] The git-clone command failed.
[ERROR] Command output:
[ERROR] Cloning into 'checkout'...
[ERROR] warning: Could not find remote branch v1.0 to clone.
[ERROR] fatal: Remote branch v1.0 not found in upstream origin
这是由于 release:prepare 阶段没有将 v1.0 的 tag 推送到远程仓库. 在 release:perform 阶段会从 scm 中重新拉去最新的一份代码到 target/checkout 中, 并对该份代码做校验看是否符合要求.
修复方案:
- 手动推送未推送的提交记录 (如果没有跳过此条).
- 回退分支
- 通过 IDE 或者其他工具回退到发布之前
- 使用
git push -f覆盖远程提交. (这步骤是用来删除远程仓库的发布提交记录) - 删除远程对应的 tag. (如果存在的话. 提示上述内容一般都不在)
- 删除本地对应 tag. (
git tag -l && git tag -d <tag>) - 执行指令
mvn release:clean release:prepare release:perform
2. hint: Updates were rejected bacause the tag already exists in the remote
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.1.1:prepare (default-cli) on project release-plugin-demonstrate: Unable to tag SCM
[ERROR] Provider message:
[ERROR] The git-push command failed.
[ERROR] Command output:
[ERROR] To https://github.com/LJiangTao/maven-version-release-demonstrate.git
[ERROR] ! [rejected] v1.0 -> v1.0 (already exists)
[ERROR] error: failed to push some refs to 'https://github.com/LJiangTao/maven-version-release-demonstrate.git'
[ERROR] hint: Updates were rejected because the tag already exists in the remote.
这个太明显了, 字面意思就是说 "远程仓库已经有一个一模一样的 tag 了." . 要解决这个问题有2个方式:
- 修改版本号, 跳过这个版本
- 修改 tag 命名规则. (修改
tagNameFormat) - 远程删除
tag如果选择 1, 2 两步, 则需要先执行release:clean将之前的配置丢弃 , 然后重新执行mvn release:prepare. (或者在release:prepare后面增加-Dresume=false也会重新生成). 其他选择项只需要重新执行mvn release:perform即可.
3. 各种 Connect Reset 或者 TIMEOUT.
这种都是网络问题, 可以使用 gitee 代替 github 仓库, 甚至可以使用私有仓库都可以.
如果想用 github 作为仓库, 那需要设置代理才行. git config --local http.proxy http://<proxy-address>:<proxy-port>. 当然可以把 http schema 替换为 socks5 git config --local httt.proxy socks5://
删除
git代理配置则可以使用git config --local -z http.proxy将设置清空也可以使用
git config --local --unset http.proxy删除属性.
4. 为什么版本没有更新?
首先确保项目的 <version> 字段不是使用 ${} 占位符, maven-release-plugin 无法更新引用属性内容, 当 <version> 为占位符的情况则会沿用占位符对应的版本, 不会替换.
所以项目中的 <version> 都需要指定明确的版本, 而不能再使用占位符 (包括子模块中的 <parent><version>. 然后你可以通过 mvn release:update-versions` 测验是否生效.
5. 多模块也能使用吗?
当然可以. 多模块的项目, 只要在插件的 <configuration> 中增加 <autoVersionSubmodules>true</autoVersionSubmodules> , 插件就会自动更新所有相关联的模块.
注意, 如果项目存在
BOM (Bill of Material),BOM由于缺少<parent>关联父模块, 所以其是一个完全独立的模块, 只是文件存放在同一个项目中. 同样也需要配置maven-release-plugin插件. 如果想要发布, 则需要进入BOM文件夹重新执行指令. 但是需要在maven-release-plugin配置中设置<pushChanges>false</pushChnages>.
6. 如何不进行交互
MAVEN 提供 batch-mode 的模式不进行界面交互, 只需要在指令的末尾或者 mvn 后面增加 --batch-mode (-B) 即可.
自动化发布
我们已经知道 mavne-release-plugin 是如何自动帮我们更新版本信息, 那我们是否可以实现自动化的版本发布呢?
首先这里说下结论, 发布是绝对不可能自动化! 发布是十分严谨和慎重的操作, 自动化发布会带来很多的灾难, 且无法确保发布的内容是否存在问题. 所以发布都是手动的.
但是, 我们可以将发布集成在 Github Action 或者 GitLab CI/CD 上, 通过手动按钮的形式. 通过点击按钮即可实现自动化式的发布.
对于代码相关的问题, 则需要各位开发人员设立良好的流程; 例如设立版本里程碑, 确认每个里程碑所要做的内容. 补全单元测试等一系列流程.
资料
总结
mavne-release-plugin 是一个对开发者十分友好的插件, 通过简短的指令实现了版本号变更, 减少了人为因素造成的错误. 插件的出现可以减少大量的时间让我们可以去做更多的事情.
我们再简单回顾下如何使用指令发布
- 已
tag形式发布版本mvn release:clean release:prepare release:perform - 已
branch形式发布版本mvn release:clean release:branch release:perform