Publishing to Maven Central with GitHub Actions
Publish a Java library to Maven Central via GitHub Actions.
Note
This guide assumes you have a Sonatype account and a GPG key for signing JARs.
Publishing to Maven Central makes your Java library easily accessible to developers via a trusted, widely-used repository. This guide walks you through automating the release process using GitHub Actions.
Setting Up a GitHub Environment¶
Create a GitHub Environment¶
- Navigate to your GitHub repository's
Settingspage. - Under
Code and automation, navigate to theEnvironmentstab. - Select
New environment, and name it "central".
Add Environment Secrets¶
Create the following environment secrets.
GPG_KEY-
Your full GPG Private Key in ASCII armor format.(1)(2)
- The key block should look something like this:
-----BEGIN PGP PRIVATE KEY BLOCK----- lQdGBGfwSTMBEAC3A2g0DOL38UTj6ph9m9cr9059UgIx9PsO1Xxwp0GhC5WqESU2 zgHLRD9zdRD80LMbZgK7j7abkIs+NJeb0BwPR6noHWNCsRqpzL6RYW4u4Z/a1iqn 6Tah5SdnKcyCSviHNSZAKyLWcliOReiaTln2DmDqD0Hz1blZ+BX1u/V0aTrBE7aY KFuGcNzL3EmN+ef4Q0t1dkw8sxIQSQ5ldKmqBl0TSk7ruvRoxsghiiKsm7Oguizi /nXzLezXtcSzsHLsCdpX5WIrWxKzJRhqzOXyRRGV88/fp96Yh2b4HAM71a13GeUf 3xvh08lWwRJQ8fUwx9JN5sLKmV4CCElWtB1SU8LJKl0S4HGmSfU0IYmZfI+pNB20 NO1pA9izQte4hamhl+udw2tQzeWrw52CCsLjShVWUoXDoQN/CLt1fM9U8WbcVqWx A81HlBeVrPIUB2gnfpzri7Y78lsplG9yZ33d9+QOyme9v/wsXbLw0uocoV07XLJF 6nyI3YO/Mx1fE/WbTg+eyx7lFuqTwN+PWM627zNndgcG =z5qh -----END PGP PRIVATE KEY BLOCK----- - Learn how to create and upload a GPG key.
- The key block should look something like this:
GPG_PASSPHRASE-
This is the passphrase you created with your GPG Key.(1)
- You were likely prompted for this passphrase when exporting your key for
GPG_KEY.
- You were likely prompted for this passphrase when exporting your key for
OSSRH_USERNAME&OSSRH_PASSWORD- These can both be obtained from
your Sonatype account details
by clicking
Generate User Token.
Workflow Creation¶
Create a workflow config in .github/workflows.
name: Auto Publish
on:
release:
types: [ created ]
jobs:
publish:
runs-on: ubuntu-latest
environment: central
steps:
- uses: actions/checkout@v4
- name: Set up Maven Central Repository
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
server-id: central
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Set version
# Strip the leading 'v' from tag (e.g., v1.2.3 → 1.2.3)
run: |
VERSION=${GITHUB_REF_NAME#v}
mvn versions:set -DnewVersion=$VERSION
- name: Publish package
run: mvn -P release --batch-mode deploy -DskipTests
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
Automatic Publish Workflow Breakdown
- Triggers the workflow automatically whenever a new GitHub Release is created.
- Ideal for projects with Git-based versioning.
- name: Set up Maven Central Repository
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
server-id: central
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- Sets up Java 21 with Temurin and configures Maven for signed deployments.
- The
server-idmust match the<id>used in yourpom.xml's<distributionManagement>section. - Also imports your GPG key so artifacts can be signed as required by Maven Central.
- Extracts the version number from the GitHub release tag (e.g.,
v1.2.0→1.2.0).
- name: Publish package
run: mvn -P release --batch-mode deploy -DskipTests
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- Builds, signs, and deploys the project to Maven Central using the
releaseprofile. - Tests are skipped to reduce CI time. Remove
-DskipTestsif untested code is frequently pushed to your repo.
name: Manual Publish
on:
workflow_dispatch:
inputs:
version:
type: string
description: Version number for this release. Must be SemVer compliant.
required: true
dry_run:
type: boolean
description: 'Run build without deploying'
required: false
default: false
jobs:
publish:
runs-on: ubuntu-latest
environment: central
steps:
- uses: actions/checkout@v4
- name: Set up Maven Central Repository
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
server-id: central
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Set version
run: mvn versions:set -DnewVersion=${{ github.event.inputs.version }}
- name: Publish package
run: |
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "Running dry run (no deploy)..."
mvn clean verify -P release -Dgpg.skip
else
echo "Deploying to Maven Central..."
mvn -P release --batch-mode deploy -DskipTests
fi
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
Manual Publish Workflow Breakdown
on:
workflow_dispatch:
inputs:
version:
type: string
description: Version number for this release. Must be SemVer compliant.
required: true
dry_run:
type: boolean
description: 'Run build without deploying'
required: false
default: false
- Defines a manual trigger with inputs for version and dry run mode.
- name: Set up Maven Central Repository
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
server-id: central
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- Sets up Java 21 with Temurin and configures Maven for signed deployments.
- The
server-idmust match the ID in yourpom.xml<distributionManagement>. - Also imports your GPG key so artifacts can be signed as required by Maven Central.
- Applies the manually provided version input to the Maven project.
- name: Publish package
run: |
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
echo "Running dry run (no deploy)..."
mvn clean verify -P release -Dgpg.skip
else
echo "Deploying to Maven Central..."
mvn -P release --batch-mode deploy -DskipTests
fi
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- If
dry_runistrue, it builds the project and generates signed artifacts, but does not deploy. - If
dry_runisfalse, it performs a full release to Maven Central.
Maven Configuration¶
Modify your pom.xml to include distribution management & a profile for
signing & publishing.
Ensure Required POM Metadata
Maven Central requires your pom.xml to include specific metadata for your
library to be accepted:
<name>: The name of your library.<description>: A short description of what it does.<url>: Project homepage or GitHub repo.<licenses>: Must include valid SPDX-compliant license info.<developers>: At least one developer entry.<scm>: Source control metadata including connection and developerConnection.
These values are automatically validated during deployment. Missing or invalid fields will cause Sonatype to reject the upload.
Here's an example pom.xml from Wraith, my Java event library.
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--https://maven.apache.org/guides/mini/guide-naming-conventions.html-->
<groupId>dev.7ori</groupId>
<artifactId>wraith</artifactId>
<version>4.1.0</version>
<packaging>jar</packaging>
<name>Wraith</name>
<description>Capable, versatile, and easy to use Java event library.
</description>
<url>https://github.com/7orivorian/Wraith</url>
<licenses>
<license>
<name>MIT</name>
<url>https://www.mit.edu/~amini/LICENSE.md</url>
</license>
</licenses>
<developers>
<developer>
<id>7orivorian</id>
<name>Tori</name>
<url>https://7ori.dev</url>
</developer>
</developers>
<scm>
<url>https://github.com/7orivorian/Wraith</url>
<connection>scm:git:git://github.com/7orivorian/Wraith.git</connection>
<developerConnection>scm:git:ssh://git@github.com/7orivorian/Wraith.git</developerConnection>
</scm>
<properties>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>26.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Java Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Source and Javadoc JARs -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<additionalOptions>
<additionalOption>-Xdoclint:none</additionalOption>
</additionalOptions>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>central</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
<snapshotRepository>
<id>central</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.7.0</version>
<extensions>true</extensions>
<configuration>
<deploymentName>Wraith</deploymentName>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<checksums>all</checksums>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Here are the most important snippets. Remember to replace any highlighted sections as they contain project or user specific data.
<distributionManagement>
<repository>
<id>central</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
<snapshotRepository>
<id>central</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.7.0</version>
<extensions>true</extensions>
<configuration>
<deploymentName>Wraith</deploymentName>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<checksums>all</checksums>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>