3. GitHub Action 自动发布 Maven 中央仓库
开源项目发布到 Maven 中央仓库,最麻烦的不是写代码,而是发包流程:早期的 OSSRH(issues.sonatype.org)要在本地 mvn deploy + GPG 签名 + 手动 close/release staging 仓库,一次发包至少十来分钟。Sonatype 迁移到新版中央仓库 central.sonatype.com 之后,配合 central-publishing-maven-plugin + GitHub Action,完全可以做到 git push 即发包,全程无人工。
这篇教程以 quick-chinese-transfer 的真实配置为例,完整走一遍这套流程。
对应配置文件:
- 构建发布配置:pom.xml 的
releaseprofile- CI 工作流:.github/workflows/maven-central-publish.yml
1. 前置准备
1.1 Sonatype Central Portal 账号与命名空间
新版中央仓库的管理入口是 central.sonatype.com(老的 issues.sonatype.org 工单系统已不再受理新申请):
- 注册/登录 Sonatype 账号;
- 注册一个 namespace(groupId),比如本项目用的是
com.github.liuyueyi。github 用户可以用io.github.<用户名>免验证; - 在账号设置里 Generate User Token,得到一对
Token Username/Token Password。
注意:这里用的是 Token,不是登录账号密码!后面 Maven 认证用的就是这对 token。
1.2 GPG 密钥
中央仓库要求所有构件(jar、sources、javadoc、pom)必须有 GPG 签名。本地生成并上传公钥:
# 生成密钥对(按提示输入姓名、邮箱、passphrase)
gpg --gen-key
# 查看密钥 ID
gpg --list-keys
# 上传公钥到 keyserver(central portal 校验签名用)
gpg --keyserver keyserver.ubuntu.com --send-keys <密钥ID>
后面 GitHub Action 里需要两样东西:
GPG_PRIVATE_KEY:私钥的 ASCII armored 导出内容(gpg --export-secret-keys --armor <密钥ID>的输出全文)GPG_PASSPHRASE:生成密钥时设置的 passphrase
1.3 配置 GitHub Secrets
在 GitHub 仓库的 Settings → Secrets and variables → Actions → New repository secret 中添加 4 个 secret:
| Secret 名 | 内容 |
|---|---|
MAVEN_USERNAME | Central Portal 的 Token Username |
MAVEN_PASSWORD | Central Portal 的 Token Password |
GPG_PRIVATE_KEY | GPG 私钥(armor 格式全文) |
GPG_PASSPHRASE | GPG passphrase |
2. pom.xml 的发布配置
2.1 release profile
所有发布相关的插件都收在 release profile 里,日常 mvn package 不会触发签名,只在发包时 -P release 激活:
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<!-- GPG 签名:verify 阶段对全部构件签名 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
<!-- 新版中央仓库发布插件 -->
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.9.0</version>
<extensions>true</extensions>
<configuration>
<!-- 对应 settings.xml 里 server 的 id -->
<publishingServerId>central</publishingServerId>
<!-- 上传校验通过后自动发布,无需手动去控制台点 release -->
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
除了这两个插件,中央仓库还要求每个构件附带 sources、javadoc 和必要的 pom 元数据(license、developers、scm),这些在父 pom 的公共构建里已经通过 maven-source-plugin、maven-javadoc-plugin 配好了。
2.2 多模块的版本管理
本项目是 Maven 多模块(transfer-core + hanzi-writer),两个模块版本号独立演进:父 pom 固定 0.2,子模块各自带版本(当前 quick-transfer-core:0.2.18、hanzi-writer:0.1.1)。发包时进入对应模块目录执行 mvn deploy 即可只发该模块——这也是后面工作流按分支区分模块的基础。
发包命令等价于(项目里也留了个 deploy.sh):
# 只发 transfer-core
cd transfer-core && mvn clean deploy -P release -B -DskipTests
# 全部模块
mvn clean deploy -P release -B -DskipTests
3. GitHub Action 工作流
3.1 触发策略:按分支决定发什么
因为各模块版本独立,本项目没有用 tag 触发,而是用三个发布分支:
| 分支 | 推送后发布的内容 |
|---|---|
transfer | 只发布 transfer-core 模块 |
hanzi | 只发布 hanzi-writer 模块 |
release | 发布全部模块 |
日常开发在 master,当某个模块准备发版时:改版本号 → commit → 把对应分支指向该提交并推送,Action 就会自动发包。工作流定义:
name: Publish to Maven Central
on:
push:
branches:
- transfer
- hanzi
- release
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'temurin'
- name: Import GPG Key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Create settings.xml
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'SETTINGS_EOF'
...省略 xml 头...
<servers>
<server>
<id>central</id>
<username>${{ secrets.MAVEN_USERNAME }}</username>
<password>${{ secrets.MAVEN_PASSWORD }}</password>
</server>
</servers>
SETTINGS_EOF
- name: Build, Sign and Deploy to Maven Central
run: |
if [ "${{ github.ref }}" == "refs/heads/transfer" ]; then
cd transfer-core && mvn clean deploy -P release -B -DskipTests
elif [ "${{ github.ref }}" == "refs/heads/hanzi" ]; then
cd hanzi-writer && mvn clean deploy -P release -B -DskipTests
elif [ "${{ github.ref }}" == "refs/heads/release" ]; then
mvn clean deploy -P release -B -DskipTests
fi
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
3.2 几个关键点拆解
① GPG 私钥导入用 crazy-max/ghaction-import-gpg
不要自己在 CI 里 echo $GPG_PRIVATE_KEY | gpg --import——这个 action 会同时把 passphrase 写进 gpg agent 缓存,让 maven-gpg-plugin 签名时不用人工输密码(本地发包时 gpg 弹窗要密码,CI 里没人输,这就是很多人 CI 签名卡死的原因)。
② settings.xml 里的 server id 必须是 central
central-publishing-maven-plugin 配置的 <publishingServerId>central</publishingServerId> 会在 deploy 时去 settings.xml 找 id 为 central 的 server 拿认证信息,两边必须对上。
③ deploy 从子目录执行
cd transfer-core && mvn deploy 而不是根目录全量 deploy,配合按分支触发的策略,实现"推哪个分支发哪个模块"。
④ -DskipTests
发包流水线只负责发布,测试放平常的 CI(如 travis/codecov)里跑,避免发包被测试失败阻塞。
4. 实际发版操作流程
假设 transfer-core 要从 0.2.18 升到 0.2.19:
# 1. 改版本号
vim transfer-core/pom.xml # 0.2.18 -> 0.2.19
vim readme.md # 顺手更新版本说明
# 2. 提交
git add . && git commit -m "release 0.2.19"
# 3. 把 transfer 分支指向该提交并推送(触发发布)
git push origin master:transfer
# 或者本地切到 transfer 分支合并 master 后 git push origin transfer
# 4. 观察 GitHub Actions 日志:build → sign → upload → validation → published
发布成功后,几分钟内即可在 central.sonatype.com 搜到新版本(autoPublish=true 时无需手动去控制台点 release)。新依赖一般 2~10 分钟同步到中央仓代理(阿里云镜像会慢一些)。
常见翻车点速查:
401 Unauthorized:secrets 里填了账号密码而不是 token;missing signature/ gpg 卡住:GPG 私钥没导入或 passphrase 不对;Invalid publisher:namespace 与 groupId 不匹配(pom 的 groupId 必须是你注册的 namespace);- 构件校验失败(missing javadoc/sources 等):确认
maven-source-plugin、maven-javadoc-plugin已在父 pom 中绑定package阶段(本项目已配好,自己改造时别漏)。
5. 小结
整套自动发包的配置量不大,收益很高:
- pom 侧:
releaseprofile 收纳 gpg 签名 +central-publishing-maven-plugin(autoPublish=true); - CI 侧:4 个 secrets + 一个 40 行的 workflow,按分支决定发布模块;
- 发版动作收敛为"改版本号 → push 发布分支"两步,彻底告别本地 deploy 和手动 staging。
如果你的开源项目还在手动发包,非常推荐按这个思路改造一遍,一次配置,终身受益。