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

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.

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 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.
  2. 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. Signed Artifacts
    All artifacts published to Maven Central need to be signed with GPG/PGP.
  2. 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).

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.

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 --export-secret-keys --armor <key-id> | pbcopy
export GPG_SIGNING_KEY="`pbpaste`"

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

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()
}
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"))
}

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).

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")
}
}
}
}
}
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())
}
}
}
}

Release!

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

./gradlew publish closeAndReleaseStagingRepository
./gradlew publish

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:

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

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).