Blog

How to publish Java libraries to Maven Central using your GitHub account

By  
Matti Tahvonen
Matti Tahvonen
·
On Nov 12, 2024 4:06:29 PM
·

The Central Repository, often referred also as Maven Central or simply Central, is the primary repository for publicly shared JVM libraries, independent of the actual build tool. For example, since JCenter was closed, you are most likely using Maven Central, even if you wouldn’t be using Maven. Publishing to the Central Repository has a reputation for requiring a complex setup, especially for the first artifact, and a lot of good libraries are still missing from there. For example, Vaadin developers still tend to publish their add-on’s only through a custom repository provided by the vaadin.com/directory even though the Central based listing of add-ons has been available already for a while.

Against the common conception, the publishing process to Central is not that difficult. There are several “right ways” to do it, but my suggestion for new users is the central.sonatype.com portal and your GitHub account verified namespace. This tutorial gives you opinionated step-by-step instructions on how to become a credible author in the most trusted artifact repository among Java developers. First two steps are build tool independent, last steps only apply for Maven built projects.

Why Central over Custom Repositories?

Build tools like Maven or Gradle support multiple repositories, but having a single source for the most commonly used artifacts has several advantages. Even if you would use a mirror (and you probably should) in front of public repositories.

  • Unlike many other repositories, Maven Central has been fast and stable, especially the repository part. Having only trusted artifact repositories makes your build more stable.
  • A single place for all artifacts, makes Maven builds faster, especially for those who don’t have a mirror in use.
  • Central is often the only external repository allowed in corporations.
  • There is no need to pollute your pom.xml (or settings.xml) with extra repositories.
  • A solid set of enforcements (verified namespaces, Javadoc jars, signing…) helps to keep out low-quality artifacts.

For the reasons above, in my own examples and applications, I’m generally avoiding usage of libraries that are NOT available in the Central Repository. Naturally, your own private artifacts should be stored in your own repositories, but the Central Repository is the place where all publicly shared JVM libraries should end up.

1. Secure a namespace by creating an account using GitHub

Namespace is essentially the beginning of your artifact’s groupId. To publish to Central, you need to own (or have rights for) a namespace. The easiest option is io.github.your-gh-username style namespace, for which you’ll gain rights automatically if you create a new account to central.sonatype.com with a GitHub login. From the top-right corner, click “Sign In” and then “Continue with GitHub.”

Once you have granted permissions, the service will automatically register that namespace for you. Nothing else is needed. 

Once you are logged in via GitHub, “View namespaces” from your account menu should show your GitHub namespace as verified: 

If you’d like a custom groupId for your libraries in the future, you can register additional namespaces to your account. Typically, this requires owning the domain and verifying it through DNS records, which is one reason why publishing to Central can seem tricky. However, with GitHub integration, gaining a verified namespace is straightforward. Other services like GitLab, Bitbucket, and Gitee can also be used, though GitHub currently offers the most seamless verification experience.

While you are in the web portal, click View Account from the user menu and then Generate User Token. You’ll need it to automate the release process. The central.sonatype.com also supports uploading as a zip archive via web UI, but I don’t really see why anybody would want to do that. With tokens, you can configure your local workstation or CI server to deploy artifacts during the release. If you are cutting releases locally (easiest option), save the credentials like this to your Maven settings (~/.m2/settings.xml):

    <servers>
        <server>
            <id>central</id>
            <username>*****</username>
            <password>**************</password>
        </server>
    </servers>

Later on, automated publishing with a Maven plugin will find the credentials from there.

2. Make sure you have PGP and keys to sign files

One of Central Repository’s requirements is that artifacts must be digitally signed. This is done using PGP (~ GnuPG) tooling and automated by your build system. There is a good chance you already have PGP set up for other purposes, like signing your emails. If so, you can skip over this step. 

If you’re new to PGP, this part may seem challenging, but it’s simpler than it appears. There’s no need to become a PGP expert, as we’ll configure Maven in the next step to handle the signing for you. The easiest method on Linux/Mac is probably to install GnuPG with a package manager, whatever comes with your distro (or Homebrew on Mac), or use some of the binary installers. If you will eventually cut the releases on a CI server, you’ll need to configure PGP there.

Once you have PGP installed, you’ll need to generate a key. Execute the following command and answer the questions:
gpg --gen-key

For most questions, you can give defaults, e.g., name and email, and give good trustworthy details. A passphrase is optional but highly suggested. Once you have a key you can see its identifier with a command:

gpg --list-secret-key

Now, the last step is to make your public key available for others so they can verify your signatures. Sending it to one of the known servers is enough. Copy your key ID from the previous command and replace it in the following example:
gpg --keyserver hkp://pgp.mit.edu --send-keys 55A475FD149483ED99C68C0F157CD51E7D89FECF

To test if your PGP is correctly set up, you can, for example try the following command in your project directory:

gpg -ab pom.xml

That should create a signature file pom.xml.asc, which you can verify with:

gpg --verify pom.xml.asc

3. Modify your library to utilize the namespace and contain the required metadata

In Step 1, you secured a namespace for your projects, which in Maven corresponds to the groupId. For example, with my GitHub namespace, the groupId must be (or begin with) io.github.mstahv. Before your artifact can be shared in Maven Central, several fields are required: artifactId, name, URL, description, license, and more. Additionally, some fields in pom.xml are essential for tooling compatibility, such as for IDEs or release automation.

I suggest copying the minimum set of metadata to your pom.xml from a library I created as an example for this article. The relevant lines are highlighted in this link. If you happen to start with a Viritin add-on archetype for Vaadin add-ons, you’ll automatically get placeholders for these fields in your pom.xml.

4. Configure your build to sign artifacts, generate Javadoc & source jars, and publish via central.sonatype.com

When cutting new releases, signing build artifacts manually before the deployment phase is the last thing you want, and you also don’t want to slow down development builds with signing. The same goes for two other Maven Central requirements: javadocs and sources. To streamline the process, you’ll need two things:

  1. A release profile in your pom.xml
  2. Configuration for the maven-gpg-plugin, maven-source-plugin, and maven-javadoc-plugin within that profile

Pro tip: Central only checks that a Javadoc JAR exists, so it won’t actually validate the contents—just don’t tell any lazy developers!

These settings don’t need to be in your project’s pom.xml directly; they can be placed in a parent pom.xml to share across similar artifacts. For instance, if you’re building Vaadin add-on libraries, consider inheriting from my parent project, which handles this (and much more) for you automatically.

If you prefer to set everything up in your own pom.xml, start with the following configuration and adjust it as needed for your project:

<profiles>
   <profile>
       <id>release</id>
       <build>
           <plugins>
               <plugin>
                   <groupId>org.sonatype.central</groupId>
                   <artifactId>central-publishing-maven-plugin</artifactId>
                   <version>0.6.0</version>
                   <extensions>true</extensions>
                   <configuration>
                       <publishingServerId>central</publishingServerId>
                       <autoPublish>true</autoPublish>
                   </configuration>
               </plugin>

               <plugin>
                   <groupId>org.apache.maven.plugins</groupId>
                   <artifactId>maven-source-plugin</artifactId>
                   <version>3.3.1</version>
                   <executions>
                       <execution>
                           <id>attach-sources</id>
                           <goals>
                               <goal>jar-no-fork</goal>
                           </goals>
                       </execution>
                   </executions>
               </plugin>

               <plugin>
                   <groupId>org.apache.maven.plugins</groupId>
                   <artifactId>maven-javadoc-plugin</artifactId>
                   <version>3.10.1</version>
                   <executions>
                       <execution>
                           <id>attach-javadocs</id>
                           <goals>
                               <goal>jar</goal>
                           </goals>
                       </execution>
                   </executions>
               </plugin>

               <plugin>
                   <groupId>org.apache.maven.plugins</groupId>
                   <artifactId>maven-gpg-plugin</artifactId>
                   <version>3.2.4</version>
                   <executions>
                       <execution>
                           <id>sign-artifacts</id>
                           <phase>verify</phase>
                           <goals>
                               <goal>sign</goal>
                           </goals>
                       </execution>
                   </executions>
               </plugin>
           </plugins>
       </build>
   </profile>
</profiles>

Test a “release” build with the following command. This won’t push artifacts to Central yet; it simply creates them in the target directory:

mvn verify -Prelease

If your PGP key has a passphrase, you may be prompted to enter it during the build. To automate this, set the passphrase in the MAVEN_GPG_PASSPHRASE environment variable.

After the build, check your target folder to ensure it contains three different JAR archives, the pom.xml, and signature files (.asc) for each. In my example project, the result looks like this:

ls -la target/sliders-0.0.2*
-rw-r--r--  1 mstahv  staff  4149729 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT-javadoc.jar
-rw-r--r--  1 mstahv  staff      488 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT-javadoc.jar.asc
-rw-r--r--  1 mstahv  staff     4309 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT-sources.jar
-rw-r--r--  1 mstahv  staff      488 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT-sources.jar.asc
-rw-r--r--  1 mstahv  staff     5130 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT.jar
-rw-r--r--  1 mstahv  staff      488 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT.jar.asc
-rw-r--r--  1 mstahv  staff     1549 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT.pom
-rw-r--r--  1 mstahv  staff      488 Nov  5 22:26 target/sliders-0.0.2-SNAPSHOT.pom.asc

If all looks good, you could, in theory, be able to proceed to the deployment phase, and your artifacts would be on their way to the Central using the central-publishing-maven-plugin. But naturally, you should follow a set of release procedures, like fixing the version number of the pom.xml, before doing that.

5. Cutting the release 

There’s always a debate about the “right” way to create releases. Some prefer a fully manual process, others build custom scripts, and some go all-in with automated systems that handle everything—including social media posts and nearly baking the release cake! Personally, as a straightforward Maven user with simple needs, I’ve found that the traditional maven-release-plugin works perfectly for me.

One gotcha with the maven-release-plugin is to lock it to a reliable version. The version provided by the super POM is (or was) outdated, and, as with all Maven plugins, specifying a fixed version helps stabilize your build. Remember, you can also define this in a parent POM. Here’s how my Vaadin add-on’s parent POM configures it:

<plugin>
    <!-- Fixing version & activating "release" profile for those who use release plugin -->
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-release-plugin</artifactId>
    <version>3.1.1</version>
    <configuration>
        <autoVersionSubmodules>true</autoVersionSubmodules>
        
        <!-- Forward compatibility: "release-profile" will be removed from super POM.
             It's better to define javadocs, sources, signing, etc. in an independent "release" profile. -->
        <useReleaseProfile>false</useReleaseProfile>
        <releaseProfiles>release</releaseProfiles>
    </configuration>
</plugin>

The configuration above automatically enables the release profile we created earlier, so you no longer need to remember to activate it manually. For my libraries, once I’ve completed a development iteration and all changes are committed, I simply execute:

mvn release:prepare release:perform

As a quick summary of what this command does:

  • Makes sure all code is committed and tests pass
  • Removes -SNAPSHOT from the version number, commits this to Git, and tags a release.
  • Updates the version number to be ready for the next development iteration (increments + adds -SNAPSHOT) and commits this to the repository.
  • Checks out a clean source from the repository (with the tagged release) and makes a clean build (so that no accidental files get to your artifacts) with the release profile.
  • Deploys all artifacts to a staging repository in central.sonatype.com. With the configuration we set above, they are automatically published to Maven Central after the staging repository performs validity checks. If you disable autoPublish, you can make final checks on your artifacts before manually releasing them to Central, where they cannot be removed once published.

Tip: If you are absolutely sure your tests pass (typical these days with CI servers), you can often safely skip the tests at this point. As the release plugin forks the process, this is a bit cumbersome, and thus I have the following alias in my shell init:

alias fastmavenrelease="mvn release:prepare release:perform -Darguments=-DskipTests=true"

What's next?

Congratulations if you read these instructions thus far! I guess you have now contributed your Java library to Maven Central. If it happens to be a Vaadin add-on, share it with the rest of the community via Directory. Just submit your add-on’s Maven coordinates and fill in some Vaadin-specific metadata (like compatibility information). The Directory will automatically detect new versions you push to the Central Repository.

Questions, comments, or additional tips? Share them in the comments section below! I have considered a possible next article to showcase how we have automated releases for the whole development team using GitHub Actions with Team Parttio. Would you find that helpful?

Matti Tahvonen
Matti Tahvonen
Matti Tahvonen has a long history in Vaadin R&D: developing the core framework from the dark ages of pure JS client side to the GWT era and creating number of official and unofficial Vaadin add-ons. His current responsibility is to keep you up to date with latest and greatest Vaadin related technologies. You can follow him on Twitter – @MattiTahvonen
Other posts by Matti Tahvonen