3. GitHub Action 自动发布 Maven 中央仓库

一灰灰blogQuick系列QuickChineseTransfer约 1723 字大约 6 分钟

开源项目发布到 Maven 中央仓库,最麻烦的不是写代码,而是发包流程:早期的 OSSRH(issues.sonatype.orgopen in new window)要在本地 mvn deploy + GPG 签名 + 手动 close/release staging 仓库,一次发包至少十来分钟。Sonatype 迁移到新版中央仓库 central.sonatype.comopen in new window 之后,配合 central-publishing-maven-plugin + GitHub Action,完全可以做到 git push 即发包,全程无人工。

这篇教程以 quick-chinese-transferopen in new window 的真实配置为例,完整走一遍这套流程。

对应配置文件:

1. 前置准备

1.1 Sonatype Central Portal 账号与命名空间

新版中央仓库的管理入口是 central.sonatype.comopen in new window(老的 issues.sonatype.orgopen in new window 工单系统已不再受理新申请):

  1. 注册/登录 Sonatype 账号;
  2. 注册一个 namespace(groupId),比如本项目用的是 com.github.liuyueyi。github 用户可以用 io.github.<用户名> 免验证;
  3. 在账号设置里 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_USERNAMECentral Portal 的 Token Username
MAVEN_PASSWORDCentral Portal 的 Token Password
GPG_PRIVATE_KEYGPG 私钥(armor 格式全文)
GPG_PASSPHRASEGPG 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.comopen in new window 搜到新版本(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. 小结

整套自动发包的配置量不大,收益很高:

  1. pom 侧:release profile 收纳 gpg 签名 + central-publishing-maven-plugin(autoPublish=true);
  2. CI 侧:4 个 secrets + 一个 40 行的 workflow,按分支决定发布模块;
  3. 发版动作收敛为"改版本号 → push 发布分支"两步,彻底告别本地 deploy 和手动 staging。

如果你的开源项目还在手动发包,非常推荐按这个思路改造一遍,一次配置,终身受益。

Loading...