Skip to content

Publicando en Maven Central

1. Requirements

1.1 CREATE ACCOUNT IN SONATYPE

Enter the central Sonatype portal and click on SignIn (never login with Social Logins):

./images/maven-central-signin.png

This will send us an email to validate the email. We click on the link

./images/maven-central-validateemail.png

1.2 GENERATE CREDENTIALS

Under your user area, click on the “view account” option

./images/maven-central-publish.png

We click on Generate Token and save the value in a safe place

./images/maven-central-publish.png

Copy the value of username and token

Expiration

Whenever you want you can regenerate and revoke the token.

With those credentials we configure the section of our settings.xml, configuring it as credentials for the central server in the servers section:

./images/maven-central-settings.xml.png

Central vs osssrh

The OSSRH server has been the reference for a long time, it was the legacy service that replicated to maven Central. This tutorial talks about how to publish to Central directly.

1.3 CREATE A NAMESPACE

A namespace is what we commonly know as the groupId.

The groupId is unique and identifies the publisher.

It is very important, because we don’t want anyone to publish libraries that are not from our organization/company within our namespace (groupId), so we must register and have Maven Central verify its veracity.

Veracity of groupId/Namespace

Veracity is checked through the domain. If we want to have, for example, the groupId: <groupId>com.mycompany</groupId>, we must be owners of the domain mycompany.com. This validation will be done by requesting to add a TXT record in our DNS.

PUBLIC NAMESPACE GITHUB.com

Since the purpose of this tutorial is to publish a personal library that we have hosted on Github, we are not going to perform the namespace validation through DNS (since we are not owners of github.com) we are going to do it by adding the github namespace format:

io.github.{myuser}

So we go to our account on the Sonatype website and in our user menu we click on Namespaces

Once created it is pending for validation:

./images/maven-central-namespace-creation.png

Validating our repository in GITHUB

The website indicates the instructions to validate our user in Github, for this you must create an empty repository in your account with the name they tell you.

They show a screen like this indicating that name:

./images/maven-central-verify-namespace.png

WE CREATE THE VALIDATION REPOSITORY IN OUR GITHUB ACCOUNT

We use the web wizard to create the repository. It doesn’t need to have content, just to exist:

./images/maven-central-create-repo-validation.png

After a while, we see on the sonatype page that they have verified it

images/maven-central-namespace-success.png

2. Preparing our Lib

Once we have the authorization from Maven Sonatype and the credentials to publish our artifacts, we are going to create and configure our Maven project to publish it.

2.1 GENERATING AN EMPTY MAVEN PROJECT

We generate a Maven project, in this case using one of the public archetypes:

mvn archetype:generate -DarchetypeGroupId=io.github.oliviercailloux -DarchetypeArtifactId=java-archetype
...

[INFO] Using property: version = 0.0.1-SNAPSHOT
Define value for property 'groupId': org.dppware
Define value for property 'artifactId': lib-utils-json
Define value for property 'package' org.dppware.lib-utils-json: org.dppware.lib.utils.json
Confirm properties configuration:
version: 0.0.1-SNAPSHOT
groupId: org.dppware
artifactId: lib-utils-json
package: org.dppware.lib.utils.json

2.2 MANDATORY SECTIONS IN POM.XML

We need to provide the meta-information that will be shown by Sonatype about your artifact in their directory.

To publish artifacts it is necessary that you provide at least this information:

  • licenses
  • developers
  • SCM
  • url
  • description

Licenses:

I have used the generic APACHE one:

<licenses>
  <license>
    <name>The Apache License, Version 2.0</name>
    <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
  </license>
</licenses>
Developers:

At least one should exist:

 <developers>
    <developer>
      <name>Daniel Peña</name>
      <email>danipenaperez@gmail.com</email>
      <organization>DPPWare</organization>
      <organizationUrl>http://www.danipenaperez.com</organizationUrl>
    </developer>
  </developers>
SCM

Information specific to the SCM associated with your artifact:

<scm>
  <connection>scm:git:git://github.com/danipenaperez/lib-utils-json.git</connection>
  <developerConnection>scm:git:ssh://github.com:danipenaperez/lib-utils-json.git</developerConnection>
  <url>http://github.com/danipenaperez/lib-utils-json/tree/master</url>
</scm>

URL

Repository URL, in my case:

    <url>https://github.com/danipenaperez/lib-utils-json</url>

Description

Basic description of the artifact

    <description>Utilities to transform and manage JSON Objects</description>

2.3 SOURCES AND JAVADOC

As good practice, it is good to provide the sources and the javadoc so that Sonatype can store and serve them, so we add these plugins to the pom.xml:

<build>
  ...
  <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <version>2.2.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>2.9.1</version>
                <executions>
                    <execution>
                        <id>attach-javadocs</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
      ....
<build>

2.4 SIGNING OUR ARTIFACTS

One of the fundamental requirements is that our artifacts are signed when publishing them, for this we are going to install the GPG utility on our pc

We will use this tool to generate keys for signing.

With those keys we can deploy from our machine or also upload them to github actions to use them to sign the artifacts.

Installation:

sudo apt-get install gnupg
and we verify the installation
$ gpg --version
gpg (GnuPG) 2.2.19
libgcrypt 1.8.5
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Home: /home/dpena/.gnupg
Available algorithms:
Public key: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
         CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

We are going to generate some keys (The key will also be secured on our pc to be able to recover it, since it has a validity of 2 years):

(You have more information https://central.sonatype.org/publish/requirements/gpg/#listing-keys

gpg --gen-key
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: Daniel Peña Perez
Email address: danipenaperez@gmail.com
You are using the 'utf-8' character set.
You selected this USER-ID:
    "Daniel Peña Perez <danipenaperez@gmail.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
gpg: key B83D1904C967D92D marked as ultimately trusted
gpg: directory '/home/dpena/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/dpena/.gnupg/openpgp-revocs.d/68811F75E52DFC6591553C9........rev'
public and secret key created and signed.

pub   rsa3072 2025-02-13 [SC] [expires: 2027-02-13]
      68811F75E52DFC6591553C9.......
uid                      Daniel Peña Perez <danipenaperez@gmail.com>
sub   rsa3072 2025-02-13 [E] [expires: 2027-02-13]

Securing keys on our PC

The keys generated by GPG on our pc will be associated with the superuser of our PC. That’s why we will find moments where the execution of GPG will ask us to enter our root credentials.

We list the keys:

$ gpg --list-keys
/home/dpena/.gnupg/pubring.kbx
------------------------------
pub   rsa3072 2025-02-13 [SC] [expires: 2027-02-13]
      68811F75E52DFC6591553C9.......
uid        [  ultimate ] Daniel Peña Perez <danipenaperez@gmail.com>
sub   rsa3072 2025-02-13 [E] [expires: 2027-02-13]

2.5 DISTRIBUTING OUR PUBLIC KEY

When our artifact is published, users will need to validate the encryption, so we need to publish our public key on public servers.

On the maven central page they indicate that there are these servers, so we will try to publish it on all of them

  • keyserver.ubuntu.com
  • keys.openpgp.org
  • pgp.mit.edu
  • keys.gnupg.net

We must indicate the id of the key (since we can have several), in my example my key is 68811F75E52DFC6591553C9........ You can get it like this

$ gpg --list-signatures --keyid-format 0xshort
/home/dpena/.gnupg/pubring.kbx
------------------------------
pub   rsa3072/0xC967D92D 2025-02-13 [SC] [expires: 2027-02-13]
      68811F75E52DFC6591553C9.......
uid        [  ultimate ] Daniel Peña Perez <danipenaperez@gmail.com>
sig 3        0xC967D92D 2025-02-13  Daniel Peña Perez <danipenaperez@gmail.com>
sub   rsa3072/0x9427FD35 2025-02-13 [E] [expires: 2027-02-13]
sig          0xC967D92D 2025-02-13  Daniel Peña Perez <danipenaperez@gmail.com>

So we publish on the servers:

$ gpg --keyserver keyserver.ubuntu.com --send-keys 68811F75E52DFC6591553C9.......
gpg: sending key B83D1904C967D92D to hkp://keyserver.ubuntu.com

$ gpg --keyserver keys.openpgp.org --send-keys 68811F75E52DFC6591553C9.......
gpg: sending key B83D1904C967D92D to hkp://keys.openpgp.org

$ gpg --keyserver pgp.mit.edu --send-keys 68811F75E52DFC6591553C9.......
gpg: sending key B83D1904C967D92D to hkp://pgp.mit.edu

To verify that the process went well, we can request our keys using –recv-keys

gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 6DB481EE8E135B14676CC13CA86A8FFE0C705F01

gpg: key 6DB481EE8E135B14676CC13CA86A8FFE0C705F01: "Daniel Peña Perez <danipenaperez@gmail.com>" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1

2.5.1 Revoking keys

When we generate a key, a revocation certificate is also generated, as seen in the creation message:

gpg: key B83D1904C967D92D marked as ultimately trusted
gpg: directory '/home/dpena/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/dpena/.gnupg/openpgp-revocs.d/68811F75E52DFC6591553C9........rev'
public and secret keys created and signed.

To delete the local certificate we use the revocation certificate:

First we edit the certificate removing the “:” from the ----PUBLIC KEY—

nano ~/.gnupg/openpgp-revocs.d/68811F75E52DFC6591553C92B83D1904C9XXXXX.rev

and inside we remove the : so that the certificate is properly recognized

We import the revocation certificate

gpg --import ~/.gnupg/openpgp-revocs.d/68811F75E52DFC6591553C92B83D1904C9XXXX.rev
gpg: key B83D1904C967D92D: "Daniel Peña Perez <danipenaperez@gmail.com>" revocation certificate imported
gpg: Total number processed: 1
gpg:    new key revocations: 1
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   2  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2027-02-13
The key is now deleted from our machine:

Each key has a public and secret key, so we must delete both from our machine:

# Deleting public key
gpg --delete-key 68811F75E52DFC6591553C92B83D1904C967D92D
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.


pub  rsa3072/B83D1904C967D92D 2025-02-13 Daniel Peña Perez <danipenaperez@gmail.com>

Delete this key from the keyring? (y/N) y

# Deleting secret key
gpg --delete-secret-key 68811F75E52DFC6591553C92B83D1904C9XXXX
# same process

We verify that our key no longer appears in the list:

$ gpg --list-keys
/home/dpena/.gnupg/pubring.kbx
------------------------------
pub   rsa3072 2025-11-28 [SC] [expires: 2027-11-28]
      6DB4xxxThis is another key I had registered
uid        [  ultimate ] Daniel Peña Perez <danipenaperez@gmail.com>
sub   rsa3072 2025-11-28 [E] [expires: 2027-11-28]

We revoke our certificate on public key servers

gpg --keyserver keyserver.ubuntu.com --send-keys 68811F75E52DFC6591553C92B83D1904CXXXX
gpg: sending key B83D1904C967D92D to hkp://keyserver.ubuntu.com

gpg --keyserver keys.openpgp.org --send-keys 68811F75E52DFC6591553C92B83D1904C9XXXX
gpg: sending key B83D1904C967D92D to hkp://keys.openpgp.org

gpg --keyserver pgp.mit.edu --send-keys 68811F75E52DFC6591553C92B83D1904C9XXXX
gpg: sending key B83D1904C967D92D to hkp://pgp.mit.edu

2.6 Signing and publishing from our machine

SIGNING

We could sign our jars with pgp, but there are maven plugins that make this tedious task simple. So we add the maven-gpg-plugin to the plugins section:

        <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-gpg-plugin</artifactId>
                <version>3.2.8</version>
                <executions>
                    <execution>
                    <id>sign-artifacts</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>sign</goal>
                    </goals>
                    </execution>
                </executions>
        </plugin>
This plugin will use the gpg installed on our machine and will use the first key it finds. If you have several gpg keys consult the documentation of maven-gpg-plugin.

PUBLISHING

Since maven will handle everything, we must indicate the distribution manager section indicating the maven central repositories for snapshot and release:

So we add to the project pom:

<distributionManagement>
  <snapshotRepository>
    <id>ossrh</id>
    <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
  </snapshotRepository>
  <repository>
    <id>ossrh</id>
    <url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
  </repository>
</distributionManagement>

And we will use the central-publishing-maven-plugin which is associated with the deploy phase to publish to maven central.

We add the plugin:

        <plugin>
                <groupId>org.sonatype.central</groupId>
                <artifactId>central-publishing-maven-plugin</artifactId>
                <version>0.7.0</version>
                <extensions>true</extensions>
                <configuration>
                    <publishingServerId>central</publishingServerId>
                    <waitUntil>published</waitUntil>
                </configuration>
        </plugin>

2.7 EXECUTION

To publish just execute mvn clean deploy. In this process all maven phases will be executed.

Remember that when the gpg-plugin is executed it needs to access our saved keys and it is possible that the process asks us to enter root authorization (or the associated user) to use the key.

2.8 ACCEPT THE PUBLICATION

As a last step you must go to your artifactory profile and click on the publish button of your deployment:

Validate Publication

3. PROFILES

As you can imagine, whenever we want to do a mvn clean install we don’t want to be signing the artifact, only in deploy phases, etc. That’s why it is recommended to encapsulate the execution of the plugins that we have added in previous sections to a maven profile, so that they only execute when we are interested in performing those tasks.

Here I leave how the final pom.xml has turned out with the profile (ci-cd):

mvn clean deploy -P ci-cd

<?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>io.github.danipenaperez</groupId>
    <artifactId>lib-utils-json</artifactId>
    <version>0.0.1</version>
    <url>https://github.com/danipenaperez/lib-utils-json</url>
    <description>Utilities to transform and manage JSON Objects</description>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>17</maven.compiler.release>
    </properties>
    <licenses>
        <license>
            <name>The Apache License, Version 2.0</name>
            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
        </license>
    </licenses>
    <developers>
        <developer>
            <name>Daniel Peña</name>
            <email>danipenaperez@gmail.com</email>
            <organization>DPPWare</organization>
            <organizationUrl>https://github.com/danipenaperez</organizationUrl>
        </developer>
    </developers>
    <scm>
        <connection>scm:git:git://github.com/danipenaperez/lib-utils-json.git</connection>
        <developerConnection>
            scm:git:ssh://github.com:danipenaperez/lib-utils-json.git</developerConnection>
        <url>http://github.com/danipenaperez/lib-utils-json/tree/master</url>
    </scm>
    <!-- GENERAL BUILD-->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
            </plugin>
        </plugins>
    </build>


    <profiles>
        <profile>
            <id>ci-cd</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-source-plugin</artifactId>
                        <version>2.2.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>2.9.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.8</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.7.0</version>
                        <extensions>true</extensions>
                        <configuration>
                            <publishingServerId>central</publishingServerId>
                            <waitUntil>published</waitUntil>
                        </configuration>
                    </plugin>

                </plugins>

            </build>
        </profile>
    </profiles>


    <distributionManagement>
        <snapshotRepository>
            <id>ossrh</id>
            <url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
        </snapshotRepository>
        <repository>
            <id>ossrh</id>
            <url>
                https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
        </repository>
    </distributionManagement>
    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.7</version>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.9.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>