Publishing to the Maven Central Repository

So you've written a cool or nifty library for Java and want to share it with the rest of the world. You could share it around on a USB drive like the olden days of the Sneakernet but a much better option is to publish it on the Maven central repository.

The Maven central repository is the de-facto repository used by Maven and Gradle for downloading dependencies for a Java project. Every time you add a new library to a project Maven or Gradle will look for it in the Maven central repository (unless otherwise configured).

You can actually view and browse the central repository from its root directory here.

This post will detail the newest way to publish your Java library using the new Central Publishing Maven Plugin.

Before starting, it is recommended to go over the checklist below:

  • Is the code publicly available and opensource through a popular host like Github, Gitlab or Bitbucket 🌐.
  • No commercial or sensitive information. 🕵️
  • Code is using Maven or Gradle as its build tool (Ant build is out of scope for this post).
  • The project is using a sensible Java package structure e.g. com.myplugin and not something silly or using an example package com.example, com.foobar. 📦
  • Domain name; Java packages generally use a reverse domain naming style to them. In my case its "com.tomaytotomato". Therefore to publish your package under you must own a domain name for your company or individual self. 🪧
    • Register a domain name, I use hover.com, its a friendly and simple domain registrar.
    • (Alternative) Use Github/Gitlab domain name, if you don't wish to purchase a domain name you can use the domain of your Github or Gitlab account. The reverse domain would be something like com.github.tomaytotomato

Sign up on Maven Central (Sonatype OSSRH)

Sonatype are the company behind the Maven central repository. To push your library you will need to setup an account with them.

  • Account setup - https://central.sonatype.org/register/central-portal/
    • Essentially you can register with an email and username.
  • Once registered you will need to verify your email
  • Verify domain ownership, to prevent anyone signing up and claiming they are Google or Facebook, you will need to verify you own the domain (groupId) you are publishing under.
    • Domain name; this will require adding a verification code to your domain's TXT records. You can do this from your registrar's control panel usually.
Adding TXT record with Maven verification code

Once completed you can then submit for verification, this may take some time to complete.

Screenshot of Maven Central namespace verification pending

Eventually the status will then change to Verified

Namespace verification complete

(Alternative) Add a Github Namespace

In my setup my main domain is based of this website com.tomaytotomatotg however if you do not have a personal site, you can use a Github domain e.g. io.github.tomaytotomato)

Simply create a new namespace on the Maven central admin panel and enter your username in the format shown below.

You will then be given a generated verification key. All you have to do is go onto your Github account and create a new repository with its name equal to the verification key e.g. io.github.foobar/hav4vdj5sd

Once this is done go back to the Maven central console and click the "Verify Namespace" button. It should verify the domain within 5 minutes.

More info and instructions can be found on the Maven central docs.

Maven Project Configuration

Below is an example Maven pom.xml file with all the necessary information and metadata to publish to Maven central. If you wish to see more information on how it is used, then please refer to my project location4j.

<?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">

  <modelVersion>4.0.0</modelVersion>
  
  <groupId>com.tomaytotomato</groupId>
  <artifactId>location4j</artifactId>
  <version>1.0.0</version>
  <description>A Java project to publish to Maven Central</description>

  <licenses>
    <license>
      <name>MIT License</name>
      <url>https://opensource.org/licenses/MIT</url>
      <distribution>repo</distribution>
    </license>
  </licenses>
  
  <scm>
    <connection>scm:git:https://github.com/tomaytotomato/location4j.git</connection>
    <developerConnection>scm:git:ssh://git@github.com:tomaytotomato/location4j.git</developerConnection>
    <url>https://github.com/tomaytotomato/location4j</url>
  </scm>

  <developers>
    <developer>
      <id>tomaytotomato</id>
      <name>Bruce Taylor</name>
      <email>dev@tomaytotomato.com</email>
    </developer>
  </developers>

  <distributionManagement>
    <repository>
      <id>central</id>
      <url>https://maven.pkg.github.com/tomaytotomato/location4j</url>
    </repository>
  </distributionManagement>

  <properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven-deploy-plugin.version>3.1.2</maven-deploy-plugin.version>
    <maven-gpg-plugin.version>3.2.4</maven-gpg-plugin.version>
    <central-publishing-maven-plugin.version>0.5.0</central-publishing-maven-plugin.version>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <goals>
              <goal>jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>3.4.1</version>
        <executions>
          <execution>
            <goals>
              <goal>jar</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-gpg-plugin</artifactId>
        <version>${maven-gpg-plugin.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>sign</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-deploy-plugin</artifactId>
        <version>${maven-deploy-plugin.version}</version>
      </plugin>

      <plugin>
        <groupId>org.sonatype.central</groupId>
        <artifactId>central-publishing-maven-plugin</artifactId>
        <version>${central-publishing-maven-plugin.version}</version>
        <extensions>true</extensions>
        <configuration>
          <publishingServerId>central</publishingServerId>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

Breaking down the key components in this pom.xml:

  • groupId, artifactId, version: Defines the project's coordinates in Maven Central.
  • licenses: Specifies the license for your project.
  • scm: Source Control Management details for the project's repository.
  • developers: Information about the project maintainers. If you are concerned about privacy you can use a dummy or non-personal email address
  • distributionManagement: Specifies the repository where the artifacts are published (Maven Central or GitHub Packages).
  • build section with plugins: Plugins for source, javadoc, GPG signing, and deploying the artifact.

Missing any of the above mentioned values will cause a failure when publishing to Maven and your Java artifact will be rejected.

Maven Central Publishing Plugin

<plugin>
        <groupId>org.sonatype.central</groupId>
        <artifactId>central-publishing-maven-plugin</artifactId>
        <version>${central-publishing-maven-plugin.version}</version>
        <extensions>true</extensions>
        <configuration>
          <publishingServerId>central</publishingServerId>
        </configuration>
</plugin>

This is a new plugin that streamlines the way we publish artifacts to Maven central, in the past you had to do some more steps during the Maven release process, but now its much easier.

For more info on this plugin you can read the documentation here. As of writing this article, the plugin is still in early access.

Gradle Configuration

I am not a Gradle user but here is a transposed build file equivalent to the Maven example.

plugins {
    id 'java'
    id 'maven-publish'
    id 'signing'
}

group = 'com.tomaytotomato'
version = '1.0.0'
description = 'A Java project to publish to Maven Central'
sourceCompatibility = '21'
targetCompatibility = '21'

repositories {
    mavenCentral()
}

dependencies {
    // Add your dependencies here
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java

            pom {
                name = 'location4j'
                description = 'A Java project to publish to Maven Central'
                url = 'https://github.com/tomaytotomato/location4j'

                licenses {
                    license {
                        name = 'MIT License'
                        url = 'https://opensource.org/licenses/MIT'
                        distribution = 'repo'
                    }
                }

                scm {
                    connection = 'scm:git:https://github.com/tomaytotomato/location4j.git'
                    developerConnection = 'scm:git:ssh://git@github.com:tomaytotomato/location4j.git'
                    url = 'https://github.com/tomaytotomato/location4j'
                }

                developers {
                    developer {
                        id = 'tomaytotomato'
                        name = 'Bruce Taylor'
                        email = 'dev@tomaytotomato.com'
                    }
                }
            }
        }
    }

    repositories {
        maven {
            name = 'central'
            url = uri('https://maven.pkg.github.com/tomaytotomato/location4j')
            credentials {
                username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
                password = project.findProperty("gpr.token") ?: System.getenv("TOKEN")
            }
        }
    }
}

signing {
    sign publishing.publications.mavenJava
}

tasks.withType(Javadoc) {
    options.addBooleanOption('Xdoclint:none', true)
    options.encoding = 'UTF-8'
}

task sourcesJar(type: Jar) {
    from sourceSets.main.allSource
    archiveClassifier.set('sources')
}

task javadocJar(type: Jar) {
    from javadoc
    archiveClassifier.set('javadoc')
}

artifacts {
    archives sourcesJar
    archives javadocJar
}

// Plugin configuration for central-publishing-maven-plugin
plugins {
    id 'org.sonatype.gradle.plugins.central-publishing' version '0.5.0'
}

centralPublishing {
    publishingServerId = 'central'
}

Configuring CI

GPG Key and Passphrase

To publish securely to Maven Central, all artifacts need to be signed with a GPG key. This ensures that they are traceable and that users can verify their source. Let’s go through the GPG setup process and how to add the keys securely to GitHub.

1. Generate a GPG Key

  • Follow the prompts to create a key, selecting RSA with a minimum length of 4096 bits.

Export the key ID:

gpg --armor --export-secret-keys <YOUR_KEY_ID> > gpg-key.asc

Once created, list your GPG keys to confirm:

gpg --list-secret-keys --keyid-format LONG

Run the following command on your local machine to generate a GPG key:

gpg --full-generate-key

2. Add GPG Key to GitHub Secrets

  • Go to your GitHub repository’s settings and navigate to Secrets and variables > Actions.
  • Add a new repository secret for your GPG private key (GPG_PRIVATE_KEY) by copying the contents of gpg-key.asc.
  • Add another secret for the GPG passphrase as GPG_PASSPHRASE.

With this setup, the GPG_PRIVATE_KEY and GPG_PASSPHRASE will be available securely within GitHub Actions, allowing the pipeline to sign your artifacts automatically.

Versioning Schema

Versioning is crucial for keeping track of changes and ensuring that users can reliably access different releases. While a detailed discussion on versioning is beyond the scope of this article, the Semantic Versioning (SemVer) standard is widely used for Java projects. This system organizes versions into a MAJOR.MINOR.PATCH structure, where:

  • MAJOR changes introduce breaking changes,
  • MINOR updates add new, backward-compatible functionality, and
  • PATCH versions resolve backward-compatible bugs.

For more information on SemVer, you can read the official Semantic Versioning documentation.

CI Pipeline Phases

To release our library, we’ll need to configure the following key phases in GitHub Actions:

1. Package Phase

  • The package phase ensures that the project compiles, tests pass, and it’s ready for deployment. In this phase, we’ll use Maven to validate and package the library.

Add a Package step in the CI pipeline, where it will run Maven commands to compile and verify the code:

- name: Build with Maven
  run: mvn clean verify
  working-directory: location4j
  env:
    MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

2. Deploy Phase

  • The deploy phase pushes the artifacts to both GitHub Packages (or another repository of your choice) and Maven Central.

Here, we will use the mvn deploy command with GPG signing and credentials for Maven Central configured via secrets:

- name: Package with Maven
  run: mvn versions:set -DnewVersion=${{ env.RELEASE_VERSION }} && mvn package -DskipTests
  working-directory: location4j

- name: Deploy Github and Maven Central
  run: mvn deploy
  working-directory: location4j
  env:
    GITHUB_USERNAME: ${{ secrets.GITHUB_ACTOR }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Testing the Pipeline

Testing is key to verifying your CI/CD setup before pushing a release. In your pipeline, the Release job should trigger when a tag is pushed, such as v1.0.0. You can test this by creating a release tag in your repository, which should automatically initiate the release workflow:

Tagging example:

git tag v1.0.0
git push origin v1.0.0

Full GitHub Actions Example

Below is a complete example of the GitHub Actions workflow, including each of the above steps and the release trigger.

name: Release

on:
  push:
    tags:
      - '*.*.*'

jobs:
  release:
    name: "Release"
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '21'
          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
          gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}

      - name: Setup Maven Settings
        uses: s4u/maven-settings-action@v3.0.0
        with:
          servers: '[{"id": "central", "username": "${{secrets.MAVEN_USERNAME}}", "password": "${{secrets.MAVEN_PASSWORD}}"}]'

      - name: Extract version from tag
        id: extract_version
        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV

      - name: Build with Maven
        run: mvn clean verify
        working-directory: location4j
        env:
          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

      - name: Package with Maven
        run: mvn versions:set -DnewVersion=${{ env.RELEASE_VERSION }} && mvn package -DskipTests
        working-directory: location4j

      - name: Deploy Github and Maven Central
        run: mvn deploy
        working-directory: location4j
        env:
          GITHUB_USERNAME: ${{ secrets.GITHUB_ACTOR }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

By following these steps, you’ll have a secure and automated setup that handles your Java library’s build, sign, and release to Maven Central. With GitHub Actions, releasing new versions is as simple as creating a tag—giving you more time to focus on development and less on manual deployment steps.

In my pipeline I have a release phase which is triggered when I create a tag on Git, e.g. v1.0.0 or v1.0.1. There are other ways to do this but having a manual step to manage releases provides some control.

That's it, happy shipping 🚢!