Publishing to Maven Central

Maven Central is the defacto artifact repository for JVM based projects. Anyone can publish artifacts to it as long as they follow the rules. Follow this guide to register an account. You also must comply with all requirements otherwise deployment will fail. Fortunately JReleaser can verify many of those requirements before any artifacts are deployed.

Before continuing make sure that all artifacts to be deployed have been staged. Missing this step will make the deployment phase fail! We recommend performing deployments from a clean state.

Maven Central requires artifacts to be signed with PGP. The MavenCentral deployer automatically enables applyMavenCentralRules. The Nexus2 deployer automatically enables applyMavenCentralRules when the publication url contains oss.sonatype.org. This setting performs the following tasks:

  • verify POMs comply with publication rules (using PomChecker).

  • assert that matching -sources.jar and -javadoc.jar artifacts have been staged (when applicable).

  • calculate md5, sha1, sha256, and sha512 checksums for all staged artifacts.

  • sign all staged artifacts.

You have the option to close and release the staged repository automatically right after deployment, or keep the staged repository open and perform close and release operations using the UI. You must login into Sonatype OSSRH using your Sonatype account to do so.

Portal Publisher API

Publishing using the Portal Publisher API requires using the MavenCentral deployer.

  • YAML

  • TOML

  • JSON

  • Maven

  • Gradle

signing:
  active: ALWAYS
  armored: true

deploy:
  maven:
    mavenCentral:
      sonatype:
        active: ALWAYS
        url: https://central.sonatype.com/api/v1/publisher
        stagingRepositories:
          - target/staging-deploy
[signing]
  active = "ALWAYS"
  armored = true

[deploy.maven.mavenCentral.sonatype]
  active = "ALWAYS"
  url = "https://central.sonatype.com/api/v1/publisher"
  stagingRepositories = ["target/staging-deploy"]
{
  "jreleaser": {
    "signing": {
      "active": "ALWAYS",
      "armored": true
    },
    "deploy": {
      "maven": {
        "mavenCentral": {
          "sonatype": {
            "active": "ALWAYS",
            "url": "https://central.sonatype.com/api/v1/publisher",
            "stagingRepositories": [
              "target/staging-deploy"
            ]
          }
        }
      }
    }
  }
}
<jreleaser>
  <signing>
    <active>ALWAYS</active>
    <armored>true</armored>
  </signing>
  <deploy>
    <maven>
      <mavenCentral>
        <sonatype>
          <active>ALWAYS</active>
          <url>https://central.sonatype.com/api/v1/publisher</url>
          <stagingRepositories>target/staging-deploy</stagingRepositories>
        </sonatype>
      </mavenCentral>
    </maven>
  </deploy>
</jreleaser>
jreleaser {
  signing {
    active = 'ALWAYS'
    armored = true
  }
  deploy {
    maven {
      mavenCentral {
        sonatype {
          active = 'ALWAYS'
          url = 'https://central.sonatype.com/api/v1/publisher'
          stagingRepository('target/staging-deploy')
        }
      }
    }
  }
}

OSSRH

Publishing to OSSRH requires using the Nexus2 deployer.

We recommend setting releaseRepository to false for the first time. This lets you review staged artifacts in the Sonatype UI. You’ll have to perform a manual release on the UI. Once you’re happy with the settings and the release is successful you may switch this property to true.
As described here, Sonatype projects created before February 2021 may need to use "https://oss.sonatype.org/service/local" instead of "https://s01.oss.sonatype.org/service/local". Using incorrect url may cause a 'Could not find a staging profile matching …​' error.
  • YAML

  • TOML

  • JSON

  • Maven

  • Gradle

signing:
  active: ALWAYS
  armored: true

deploy:
  maven:
    nexus2:
      maven-central:
        active: ALWAYS
        url: https://s01.oss.sonatype.org/service/local
        snapshotUrl: https://s01.oss.sonatype.org/content/repositories/snapshots/
        closeRepository: true
        releaseRepository: false
        stagingRepositories:
          - target/staging-deploy
[signing]
  active = "ALWAYS"
  armored = true

[deploy.maven.nexus2.maven-central]
  active = "ALWAYS"
  url = "https://s01.oss.sonatype.org/service/local"
  snapshotUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
  closeRepository = true
  releaseRepository = false
  stagingRepositories = ["target/staging-deploy"]
{
  "jreleaser": {
    "signing": {
      "active": "ALWAYS",
      "armored": true
    },
    "deploy": {
      "maven": {
        "nexus2": {
          "maven-central": {
            "active": "ALWAYS",
            "url": "https://s01.oss.sonatype.org/service/local",
            "snapshotUrl": "https://s01.oss.sonatype.org/content/repositories/snapshots/",
            "closeRepository": true,
            "releaseRepository": false,
            "stagingRepositories": [
              "target/staging-deploy"
            ]
          }
        }
      }
    }
  }
}
<jreleaser>
  <signing>
    <active>ALWAYS</active>
    <armored>true</armored>
  </signing>
  <deploy>
    <maven>
      <nexus2>
        <maven-central>
          <active>ALWAYS</active>
          <url>https://s01.oss.sonatype.org/service/local</url>
          <snapshotUrl>https://s01.oss.sonatype.org/content/repositories/snapshots/</snapshotUrl>
          <closeRepository>true</closeRepository>
          <releaseRepository>false</releaseRepository>
          <stagingRepositories>target/staging-deploy</stagingRepositories>
        </maven-central>
      </nexus2>
    </maven>
  </deploy>
</jreleaser>
jreleaser {
  signing {
    active = 'ALWAYS'
    armored = true
  }
  deploy {
    maven {
      nexus2 {
        'maven-central' {
          active = 'ALWAYS'
          url = 'https://s01.oss.sonatype.org/service/local'
          snapshotUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
          closeRepository = true
          releaseRepository = false
          stagingRepository('target/staging-deploy')
        }
      }
    }
  }
}

The following secrets must be configured either using environment variables or the secrets configuration file:

  • JRELEASER_GPG_PUBLIC_KEY, unless signing.verify is set to false.

  • JRELEASER_GPG_SECRET_KEY

  • JRELEASER_GPG_PASSPHRASE

If using the MavenCentral deployer:

  • JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME or JRELEASER_MAVENCENTRAL_USERNAME

  • JRELEASER_MAVENCENTRAL_SONATYPE_PASSWORD or JRELEASER_MAVENCENTRAL_PASSWORD

Alternatively, you may set these for token based authentication:

  • JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME or JRELEASER_MAVENCENTRAL_USERNAME

  • JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN or JRELEASER_MAVENCENTRAL_TOKEN

If using the Nexus2 deployer:

  • JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME or JRELEASER_NEXUS2_USERNAME

  • JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD or JRELEASER_NEXUS2_PASSWORD

Alternatively, you may set these for token based authentication:

  • JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME or JRELEASER_NEXUS2_USERNAME

  • JRELEASER_NEXUS2_MAVEN_CENTRAL_TOKEN or JRELEASER_NEXUS2_TOKEN

Maven

The following pom.xml file shows the minimum required configuration

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.acme</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <name>app</name>
    <description>Sample application</description>
    <url>https://github.com/aalmiray/app</url>
    <inceptionYear>2021</inceptionYear>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>11</maven.compiler.release>
    </properties>

    <licenses>
        <license>
            <name>Apache-2.0</name>
            <url>https://spdx.org/licenses/Apache-2.0.html</url>
            <distribution>repo</distribution>
        </license>
    </licenses>

    <developers>
        <developer>
            <id>aalmiray</id>
            <name>Andres Almiray</name>
        </developer>
    </developers>

    <scm>
        <connection>scm:git:https://github.com/aalmiray/app.git</connection>
        <developerConnection>scm:git:https://github.com/aalmiray/app.git</developerConnection>
        <url>https://github.com/aalmiray/app.git</url>
        <tag>HEAD</tag>
    </scm>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>3.1.1</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-javadoc-plugin</artifactId>
                    <version>3.6.3</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>3.3.1</version>
                </plugin>
                <plugin>
                    <groupId>org.jreleaser</groupId>
                    <artifactId>jreleaser-maven-plugin</artifactId>
                    <version>1.12.0-SNAPSHOT</version>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.jreleaser</groupId>
                <artifactId>jreleaser-maven-plugin</artifactId>
                <configuration>
                    <jreleaser>
                        <signing>
                            <active>ALWAYS</active>
                            <armored>true</armored>
                        </signing>
                        <deploy>
                            <maven>
                                <nexus2>
                                    <maven-central>
                                        <active>ALWAYS</active>
                                        <url>https://s01.oss.sonatype.org/service/local</url>
                                        <snapshotUrl>https://s01.oss.sonatype.org/content/repositories/snapshots/</snapshotUrl>
                                        <closeRepository>true</closeRepository>
                                        <releaseRepository>true</releaseRepository>
                                        <stagingRepositories>target/staging-deploy</stagingRepositories>
                                    </maven-central>
                                </nexus2>
                                <!-- Portal Publisher API
                                <mavenCentral>
                                    <sonatype>
                                        <active>ALWAYS</active>
                                        <url>https://central.sonatype.com/api/v1/publisher</url>
                                        <stagingRepositories>target/staging-deploy</stagingRepositories>
                                    </sonatype>
                                </mavenCentral>
                                -->
                            </maven>
                        </deploy>
                    </jreleaser>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>publication</id>
            <properties>
                <altDeploymentRepository>local::file:./target/staging-deploy</altDeploymentRepository>
            </properties>
            <build>
                <defaultGoal>deploy</defaultGoal>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>attach-javadocs</id>
                                <goals>
                                    <goal>jar</goal>
                                </goals>
                                <configuration>
                                    <attach>true</attach>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-source-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>attach-sources</id>
                                <goals>
                                    <goal>jar</goal>
                                </goals>
                                <configuration>
                                    <attach>true</attach>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Store secrets in ~/.jreleaser/config.toml. The TOML format is chosen as it allows multiline strings which are required for defining both public and secret keys. You may opt instead to use the YAML format in which case be aware of indentation or use environment variables.

~/.jreleaser/config.toml
JRELEASER_MAVENCENTRAL_USERNAME = "<your-publisher-portal-username>"
JRELEASER_MAVENCENTRAL_PASSWORD = "<your-publisher-portal-password>"
JRELEASER_NEXUS2_USERNAME = "<your-sonatype-account-username>"
JRELEASER_NEXUS2_PASSWORD = "<your-sonatype-account-password>"
JRELEASER_GPG_PASSPHRASE = "<your-pgp-passphrase>"

JRELEASER_GPG_PUBLIC_KEY="""-----BEGIN PGP PUBLIC KEY BLOCK-----

<contents-of-your-public-key>

-----END PGP PUBLIC KEY BLOCK-----"""

JRELEASER_GPG_SECRET_KEY="""-----BEGIN PGP PRIVATE KEY BLOCK-----

<contents-of-your-private-key>

-----END PGP PRIVATE KEY BLOCK-----"""

You may export public and private keys with the following commands:

$ gpg --output public.pgp --armor --export username@email
$ gpg --output private.pgp --armor --export-secret-key username@email

If you do not wish to configure public and private keys in the secrets configuration file you may directly refer to the exported key files, in which case signing configuration should be updated to the following:

<jreleaser>
    <signing>
      <active>ALWAYS</active>
      <armored>true</armored>
      <mode>FILE</mode>
      <publicKey>path/to/public.pgp</publicKey>
      <secretKey>path/to/private.pgp</secretKey>
    </signing>
</jreleaser>
DO NOT commit public and private key files to source control!

Once all configuration is in place you may execute the following commands:

1) Verify release & deploy configuration

$ mvn jreleaser:config

2) Ensure a clean deployment

$ mvn clean

3) Stage all artifacts to a local directory

$ mvn -Ppublication

4) Deploy and release

$ mvn jreleaser:full-release

Gradle

The following build.gradle file shows the minimum required configuration

build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
    id 'org.jreleaser' version '1.12.0-SNAPSHOT'
}

java {
    withJavadocJar()
    withSourcesJar()
}

publishing {
    publications {
        maven(MavenPublication) {
            groupId = 'com.acme'
            artifactId = 'app'

            from components.java

            pom {
                name = 'app'
                description = 'Sample application'
                url = 'https://github.com/aalmiray/app'
                inceptionYear = '2021'
                licenses {
                    license {
                        name = 'Apache-2.0'
                        url = 'https://spdx.org/licenses/Apache-2.0.html'
                    }
                }
                developers {
                    developer {
                        id = 'aalmiray'
                        name = 'Andres Almiray'
                    }
                }
                scm {
                    connection = 'scm:git:https://github.com/aalmiray/app.git'
                    developerConnection = 'scm:git:ssh://github.com/aalmiray/app.git'
                    url = 'http://github.com/aalmiray/app'
                }
            }
        }
    }

    repositories {
        maven {
            url = layout.buildDirectory.dir('staging-deploy')
        }
    }
}

jreleaser {
    signing {
        active = 'ALWAYS'
        armored = true
    }
    deploy {
        maven {
            nexus2 {
                'maven-central' {
                    active = 'ALWAYS'
                    url = 'https://s01.oss.sonatype.org/service/local'
                    snapshotUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
                    closeRepository = true
                    releaseRepository = true
                    stagingRepository('build/staging-deploy')
                }
            }
            /* Portal Publisher API
            mavenCentral {
                sonatype {
                    active = 'ALWAYS'
                    url = 'https://central.sonatype.com/api/v1/publisher'
                    stagingRepository('target/staging-deploy')
                }
            }
            */
        }
    }
}

Store secrets in ~/.jreleaser/config.toml. The TOML format is chosen as it allows multiline strings which are required for defining both public and secret keys. You may opt instead to use the YAML format in which case be aware of indentation or use environment variables.

~/.jreleaser/config.toml
JRELEASER_MAVENCENTRAL_USERNAME = "<your-publisher-portal-username>"
JRELEASER_MAVENCENTRAL_PASSWORD = "<your-publisher-portal-password>"
JRELEASER_NEXUS2_USERNAME = "<your-sonatype-account-username>"
JRELEASER_NEXUS2_PASSWORD = "<your-sonatype-account-password>"
JRELEASER_GPG_PASSPHRASE = "<your-pgp-passphrase>"

JRELEASER_GPG_PUBLIC_KEY="""-----BEGIN PGP PUBLIC KEY BLOCK-----

<contents-of-your-public-key>

-----END PGP PUBLIC KEY BLOCK-----"""

JRELEASER_GPG_SECRET_KEY="""-----BEGIN PGP PRIVATE KEY BLOCK-----

<contents-of-your-private-key>

-----END PGP PRIVATE KEY BLOCK-----"""

You may export public and private keys with the following commands:

$ gpg --output public.pgp --armor --export username@email
$ gpg --output private.pgp --armor --export-secret-key username@email

If you do not wish to configure public and private keys in the secrets configuration file you may directly refer to the exported key files, in which case signing configuration should be updated to the following:

build.gradle
jreleaser {
    signing {
        active = 'ALWAYS'
        armored = true
        mode = 'FILE'
        publicKey = 'path/to/public.pgp'
        secreteKey = 'path/to/private.pgp'
    }
}
DO NOT commit public and private key files to source control!

Once all configuration is in place you may execute the following commands:

1) Verify release & deploy configuration

$ ./gradlew jreleaserConfig

2) Ensure a clean deployment

$ ./gradlew clean

3) Stage all artifacts to a local directory

$ ./gradlew publish

4) Deploy and release

$ ./gradlew jreleaserFullRelease