Publish to Maven Central using Gradle

What it takes to publish your Kotlin library with Gradle to Maven Central.

A collection of records in wooden boxes for sale.
Photo by Pascal Brokmeier on Unsplash

With the Bintray sunset announced in February 2021, many developers are looking for a new home for their libraries. Maven Central has always been and still is a great place to publish your artifacts. Although — as the name suggests — it is more common in the Maven world, there is nothing to stop you from using it with Gradle as well.

I’m a long term Maven user and have already published some of my libraries like Elemento to Maven Central. After using Kotlin and Gradle for some time now, I first chose Bintray for publishing and finally switched to Maven Central.

In this article I will show what it takes to use Gradle to publish to Maven Central. The article is based on PatternFly Fritz2, a Kotlin/JS library which is available on Maven Central.

Why should I use Maven Central?

Maven Central is one of the largest collection of Java and other open source components. It is free, highly available and very popular. Maven Central already serves over 1,100,000 artifacts and supports millions of users. Given all that it won’t go away anytime soon.

The main company behind Maven Central is Sonatype. Sonatype is a steward, maintainer and financial sponsor of Maven Central. If you’re going to publish as an individual, you’re going to use Sonatype’s Open Source Software Repository Hosting (OSSRH) service. It is a hosted deployment of the Nexus Repository Manager used for the deployment process, validation and sync to Maven Central.

Prerequisites

One of the reasons why many developers start using Bintray is that it is very easy to get up and running. Bintray doesn’t have many requirements when it comes to publishing artifacts. Maven Central, on the other hand, involves a few additional prerequisites. These prerequisites are independent of Gradle and Kotlin and apply to all publications to Maven Central.

One Time Setup

If you publish to Maven Central for the first time, you have to do some initial setup. This mainly drills down to three steps:

  1. Create an account:
    Sonatype uses JIRA to track requests for publishing artifacts to Maven Central. So you need an account for JIRA. This account is also reused later when you release you artifacts with OSSRH.
  2. Create a ticket:
    This is the kickoff to get something published. You need to provide details about the planned publication. This includes the Maven coordinates (GAV) and URLs such as project and SCM URLs.
  3. Prove ownership:
    You must prove ownership for the domain that matches your group ID. In case of a GitHub group ID (io.github.username), this will be verified if the project URL matches the requested group ID. In any other cases you have to use other methods such as adding a TXT record to the DNS or setting up a redirect. See the 3rd bullet point in the OSSRH guide.

Mandatory Setup

Every artifact in Maven Central must meet certain quality standards. This allows users to find all the relevant details about the artifacts from the metadata provided in the Central Repository.

  1. Source & Javadoc
    You have to supply JAR files that contain sources and Javadoc of your library.
  2. Signed Artifacts
    All artifacts published to Maven Central need to be signed with GPG/PGP.
  3. Metadata
    As part of your publication, you are required to submit a pom file. This file defines the Project Object Model used by Apache Maven. It must include specific metadata such as GAV coordinates, the license and information about the developer(s).

All requirements and necessary metadata are also described in great detail in the related article on the Sonatype website.

Gradle Configuration

Once you’ve successfully finished the initial setup, it’s time to add the necessary plugins and configuration to your Gradle build script. These settings implement the mandatory information from the last section.

In this article I will use the Kotlin DSL, but the same settings should be more or less applicable to the Groovy syntax. The following sections show only the relevant snippets from my build script. You can find the complete build script here: build.gradle.kts.

Finally, please note that in Gradle there are unfortunately many different ways to configure one and the same thing. So the following sections show just one possible setup, but it’s the one that works for me! Also please note that this setup is tailored for Kotlin/JS. A setup for Kotlin multi platform or Java libraries might look slightly different.

Environment Variables

Since I run my build script not only locally, but also on GitHub via GitHub actions, I use different environment variables. These variables are configured as encrypted secrets in my GitHub organization.

GPG_SIGNING_KEY
The secret key (in ascii-armored format). Use the following commands to generate and copy the secret key to your clipboard (on MacOS). The second command sets the shell variable so you can use it to sign locally:

gpg --export-secret-keys --armor <key-id> | pbcopy
export GPG_SIGNING_KEY="`pbpaste`"

GPG_SIGNING_PASSPHRASE
The passphrase for unlocking the secret key.

OSSRH_USERNAME
The username of your OSSRH account.

OSSRH_PASSWORD
The password of your OSSRH account.

Plugins

Basically, I use these plugins to publish my artifacts on Maven Central:

plugins {
`maven-publish`
signing
id("org.jetbrains.dokka") version "1.4.20"
id("io.github.gradle-nexus.publish-plugin") version "1.0.0"
}

The Gradle Nexus Publish Plugin covers the whole releasing process to Maven Central. This is especially important if you want to reliably publish from a CI environment (e.g. using GitHub actions).

Source & Javadoc

As mentioned above, you need source and Javadoc JARs to publish on Maven Central. When used in the context of the java-library plugin, this can be configured using the following configuration:

java {
withSourcesJar()
withJavadocJar()
}

Since I’m using Kotlin/JS plugin and hence cannot use this context, I use the following code to create the source and Javadoc artifacts:

val sourcesJar by tasks.registering(Jar::class) {
archiveClassifier.set("sources")
from(kotlin.sourceSets.main.get().kotlin)
}
val javadocJar by tasks.creating(Jar::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Assembles Javadoc JAR"
archiveClassifier.set("javadoc")
from(tasks.named("dokkaHtml"))
}

I won’t go into details about how to configure Dokka. See the Dokka page about the Gradle plugin configuration for more details.

Sign

In order to sign your artifacts, you will need a key pair (instructions on creating a key pair using the GnuPG tools can be found in the GnuPG HOWTOs).

With the key pair and the environment variables in place you can sign all your artifacts by adding the following configuration to your build file:

signing {
val signingKey = providers
.environmentVariable("GPG_SIGNING_KEY")
.forUseAtConfigurationTime()
val signingPassphrase = providers
.environmentVariable("GPG_SIGNING_PASSPHRASE")
.forUseAtConfigurationTime()
if (signingKey.isPresent && signingPassphrase.isPresent) {
useInMemoryPgpKeys(signingKey.get(), signingPassphrase.get())
val extension = extensions
.getByName("publishing") as PublishingExtension
sign(extension.publications)
}
}

Publish & POM

Finally you need to provide the required metadata to generate a pom file. To avoid repeating myself all the time, I use a Meta object with the basic details.

object Meta {
const val desc = "Kotlin implementation of PatternFly 4"
const val license = "Apache-2.0"
const val githubRepo = "patternfly-kotlin/patternfly-fritz2"
const val release = "https://s01.oss.sonatype.org/service/local/"
const val snapshot = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
}
publishing {
publications {
create<MavenPublication>("maven") {
groupId = project.group.toString()
artifactId = project.name
version = project.version.toString()
from(components["kotlin"])
artifact(tasks["sourcesJar"])
artifact(tasks["javadocJar"])
pom {
name.set(project.name)
description.set(Meta.desc)
url.set("https://github.com/${Meta.githubRepo}")
licenses {
license {
name.set(Meta.license)
url.set("https://opensource.org/licenses/Apache-2.0")
}
}
developers {
developer {
id.set("hpehl")
name.set("Harald Pehl")
organization.set("Red Hat")
organizationUrl.set("https://developers.redhat.com/")
}
}
scm {
url.set(
"https://github.com/${Meta.githubRepo}.git"
)
connection.set(
"scm:git:git://github.com/${Meta.githubRepo}.git"
)
developerConnection.set(
"scm:git:git://github.com/${Meta.githubRepo}.git"
)
}
issueManagement {
url.set("https://github.com/${Meta.githubRepo}/issues")
}
}
}
}
}

The last missing piece is the configuration for publishing to OSSRH:

nexusPublishing {
repositories {
sonatype {
nexusUrl.set(uri(Meta.release))
snapshotRepositoryUrl.set(uri(Meta.snapshot))
val ossrhUsername = providers
.environmentVariable("OSSRH_USERNAME")
.forUseAtConfigurationTime()
val ossrhPassword = providers
.environmentVariable("OSSRH_PASSWORD")
.forUseAtConfigurationTime()
if (ossrhUsername.isPresent && ossrhPassword.isPresent) {
username.set(ossrhUsername.get())
password.set(ossrhPassword.get())
}
}
}
}

As described in the documentation of the Gradle Nexus Publish Plugin, you only need to specify the URIs if you registered in Sonatype after 24 Feb 2021.

Release!

If everything has been setup properly, you can finally release your artifacts by calling:

./gradlew publish closeAndReleaseStagingRepository

Please bare in mind that this closes and releases the corresponding staging repository on OSSRH, effectively making the artifacts available in Maven Central (usually after a few minutes).

It might be wise — especially on the initial project publishing — to just call

./gradlew publish

and manually verify the artifacts using the Nexus frontend. The frontend is available at https://oss.sonatype.org/ resp. https://s01.oss.sonatype.org/.

Snapshots

The Gradle Nexus Publish Plugin also supports snapshot versions. The plugin automatically detects if the version is a snapshot version or not and selects the correct repository where artifacts will be published.

Recap

Here’s a quick summary of what we need to publish to Maven Central:

  • OSSRH account
  • Proven ownership of your group ID
  • Key pair
  • Source & Javadoc
  • POM metadata

If all these details are available, publishing with the Gradle Nexus Publish plugin is not difficult.

Resources

If you want to dive deeper into the topic, here are some additional resources:

Senior Software Engineer at Red Hat. Member of the WildFly team. Component lead of HAL (WildFly admin console).

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store