如何快速更新版本号

1,578 阅读5分钟

在日常开发时, 有时需要对版本进行发布. 而版本号的修改总是老大难的问题. 使用 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), 那步骤如下:

  1. CTRL + SHIFT + R 搜索 <version>0.1-SNAPSHOT</version>
  2. 将版本号替换为 <version>0.1</version>
  3. 提交修改.
  4. 发布 release
  5. 将版本号继续修改为 <version>0.2-SNAPSHOT</version>
  6. 提交修改

使用 reivison 属性

这时候肯定有朋友会说: "可以用 revision 属性填充 <version> 字段啊". 的确, 通过添加 <revision> 属性, 并将所有项目的 <version> 都采用 ${revision} 填充是可以做到一次修改, 所有模块版本同步变更. 但是还是需要手动对其进行修改, 还是十分不便.

使用 revision 属性有个好处, 我们可以直接将上面步骤 [2,4][2, 4] 直接变为一个打包指令.

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:branchSCM 的项目地址上创建一个分支.
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>
参数使用阶段数据类型说明
autoVersionSubmodulespreparebool是否同步更新子模块版本. 只要模块和 root 模块关联, 就会递归对其版本号进行修改.
autoResolveSnapshotspreparebool是否自动处理 SNAPSHOT 版本依赖. 当开启时, 会将 SNAPSHOT 依赖去除 -SNAPSHOT 尾缀进行打包
developmentVersionprepareString下一个开发版本
dryRunpreparebool只测试配置文件是否规范, 不会产生任何文件和提交.
javaHomeprepareString指定 java 版本.
lineSeparatorprepareStringpom.xml 按照指定换行符进行格式化. lf cr crlf source (default)
preparationGoalsprepareString前期准备时要运行的目标. 例如 clean compile flatten:flatten
preparationProfilesprepareString前期准备时使用的 profile. 使用的 profile 必须在 settings.xml 中预先定义.
projectTagNamingPolicyIdprepareString用来计算项目分支和 tag 名称. 默认即可, 如有特殊需求, 可以通过实现 org.apache.maven.shared.release.policy.naming.NamingPolicy 并指定类的 FQDN. 并在 plugin.dependencies 中增加实现类依赖.
默认实现
projectVersionPolicyIdprepareString用于计算下一个 SNAPSHOTRELEASE 版本的策略类.
如果有特殊需求, 可以通过实现 org.apache.maven.shared.release.policy.version.VersionPolicy
并在 plugin.dependencies 中增加实现类依赖
默认实现 其他版本方案
resumepreparebool是否从上次停止的地方继续开始. 默认继续, false 则会从头开始, 放弃之前操作的所有记录
pushChangepreparebool是否主动 push 提交. 默认 true, false 则只会提交到本地, 不推送至远程仓库 (scm 指定的 developerConnection)
releaseVersionprepareString准备分支和版本的默认版本号
scmCommentPrefixprepareString提交记录前缀. 默认前缀为 [maven-release-plugin]
scmDevelopmentCommitCommentprepareString下一个开发版本提交信息. 控制提交信息的前缀, 默认是 @{preifx}. 还有 @{groupId} @{artifactId} @{releaseLabel} (对应发布的版本号) 可选 这里注意, 不要使用 ${} 替换符, 而是要使用 @{}.
scmReleaseCommitCommentprepareStromg正式版本提交信息. 控制提交信息的前缀, 默认是 @{preifx}. 还有 @{groupId} @{artifactId} @{releaseLabel} (对应发布的版本号) 可选 这里注意, 不要使用 ${} 替换符, 而是要使用 @{}.
scmShallowClonepreparebooltrue 浅拷贝(默认), false 深拷贝.
supressCommitBeforeTagpreparebool在进行 git tag 前不做改动提交. 这个可以有效避免出现重复的版本提交.
tagNameFormatprepareStringtag 的命名格式. 例如
- v@{project.version} : v0.1
- @{project.version}-RELEASE : 0.1-RELEASE
- RELEASE-${project.version}: RELEASE-0.1
还有 groupId artifactId 可选
branchNamebranchString创建分支的名称.
remoteTaggingpreparebool只有 SVN 有效

还有更多参数信息, 可以参考 maven-release-plugin 官方手册

prepare 流程

在我们执行 release:prepare 前, 确保本地没有修改还没有提交! prepare 具体流程如下:

  1. 检查当前代码中是否存在未提交信息.
  2. 检查当前是否未 SNAPSHOT 版本
  3. 修改当前 POM 文件中的版本内容. 没有开启 -B (--batch-mode) 会提示输入
    1. 确认发布版本号 (直接去掉 -SNAPSHOT 后缀)
    2. 确认 tag
    3. 确认下一个开发版本号 (这个版本号则会按照 <projectVersionPolicyId> 对应策略自动生成.)
  4. tag 应用至 SCM 中的 <tag> 上, 并添加 git tag.
  5. 运行测试用例, 确保代码在修改 pom 后依旧正常工作.
  6. 提交修改的 pom 文件 (这个阶段就是第一次插件自动提交, 也就是去除 -SNAPSHOT)
  7. 根据 projectVersionPolicyId> 生成下一个开发版本 (将 x.y 变为 x.(y+1)-SNAPSHOT)
  8. 运行 <completionGoals> 设定的指令
  9. 提交修改的 pom 文件. 在运行完 prepare 后, 会在项目中看到 release.properties 文件, 其中记录了用于 perform 的相关信息.

perform 流程

perform 相当于我们自己运行 mvn deploy 指令发布可用的 release 包. 内部流程相对简单:

  1. 通过 scmurl 拉取一份代码到 target/checkout (可以通过)
  2. 运行 deploy site-deploy 指令发布 release 包. (可以通过 releaseProfiles 指定要使用的 profile, 确保发布位置和依赖都可以正确的拉取.) 每次 perform 结束, 都会执行 release:clean 删除生成的所有文件.

rollback

rollback 顾名思义是回滚到上一个版本中. 但 rollback 依赖 release.properties , pom.xml.releaseBackup 文件. 如果文件不存在则无法进行回滚操作. 通常是通过 release:clean 进行删除 其流程具体如下:

  1. 将所有项目的 pom 文件回滚到发布之前的版本.
  2. 将创建的分支和 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-resouces
  • flatten: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.0tag 推送到远程仓库. 在 release:perform 阶段会从 scm 中重新拉去最新的一份代码到 target/checkout 中, 并对该份代码做校验看是否符合要求. 修复方案:

  1. 手动推送未推送的提交记录 (如果没有跳过此条).
  2. 回退分支
    1. 通过 IDE 或者其他工具回退到发布之前
    2. 使用 git push -f 覆盖远程提交. (这步骤是用来删除远程仓库的发布提交记录)
    3. 删除远程对应的 tag. (如果存在的话. 提示上述内容一般都不在)
    4. 删除本地对应 tag. (git tag -l && git tag -d <tag>)
    5. 执行指令 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个方式:

  1. 修改版本号, 跳过这个版本
  2. 修改 tag 命名规则. (修改 tagNameFormat)
  3. 远程删除 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