Publishing to Maven Central in 2025

The process of publishing artifacts to Maven Central Repository is well documented, but important information are scattered throughout the web and there are some caveats. This guide focuses on publishing a library using Gradle. The steps will be the same for Java, Kotlin (also Multiplatform) and Android projects as well. This is not a complete guide, rather a collection of gotchas I couldn’t find anywhere else; for the rest of the steps just follow the official guides.

An overview of the necessary steps is the following:

First it is important to understand that the OSSRH hosting project and the Maven Central Repository are not the same thing. This is why if you’re following a guide which is mentioning OSSRH or oss.sonatype.org or s01.oss.sonatype.org (e.g. the article about publishing on GitHub), sadly you’re on the wrong path. In my understanding the OSSRH project is a legacy thing Sonatype is in the process of moving away from.

By the way, for an example of a GitHub workflow for automatically publishing to Maven Central, please look at the end of this article.

Register to Central Portal

There are a couple of options for signing up, like using email, Google or GitHub account. It doesn’t matter which option you choose. After signing in, click your name in the top right corner and go to your account page to generate a user token. The generated credentials will be used to authenticate against the Maven Central Repository during publishing. Please note that your credentials for the Central Portal and the Maven Central Repository are two separate things.

Verify a namespace

It is also necessary to register a namespace for publishing which is quite straightforward. The namespace will be your group id when publishing. In case you signed up with a GitHub account you can automatically use io.github.<username> as a namespace.

If you would like to use a custom domain, you will have to add a verification code as a TXT record in the DNS configuration of that domain. In my case this verification via DNS has concluded quite quickly.

Generate and distribute a PGP key

This step can be somewhat tricky. Depending on the GPG version you have, the default options for the key generation might differ. For this reason I recommend generating your key using the gpg --full-gen-key command which offers different options. This will help you avoid “Could not read PGP secret key” error during running the publishing Gradle task. When running the command select (4) RSA (sign only). The command gpg --list-keys will give you the list of available keys along with their ids.

Once you have your key you need to distribute it using the following command:

gpg --keyserver <server> --send-keys <key-id>

The list of supported key servers are the following:

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

Add Gradle Maven Publishing Plugin

The Gradle Maven Publishing Plugin by vanniktech is a tried and robust tool used by some of the most well-known projects in the open source world, like OkHttp and Timber among others. It makes it possible to have a unified publishing configuration regardless of the type of your project. It can be used to publish Java, Kotlin and Android artifacts alike.

The official plugin documentation is really detailed, however there can be some caveats. These include the link to a guide on PGP keys which, unlike the present article, does not mention the gotcha with the different GPG versions.

Nowadays we’re using plugins to configure our Gradle builds, especially when it comes to multi-module projects. Unfortunately, using plugins makes things a bit more complicated.

Using script plugin for configuration

A script plugin is something like module.publication.gradle.kts inside your build-logic or buildSrc directory. Such a setup could be generated by a wizard in IntelliJ or from a template by JetBrains for multiplatform. In this case simply adding the following to your plugin is problematic:

plugins {
    id("com.vanniktech.maven.publish") version "0.30.0"
}

If you try to run Gradle with this in your script plugin, you’ll get an error similar tho the following:

Invalid plugin request [id: 'com.vanniktech.maven.publish', version: '0.30.0']. Plugin requests from precompiled scripts must not include a version number. Please remove the version from the offending request and make sure the module containing the requested plugin 'com.vanniktech.maven.publish' is an implementation dependency of project ':build-logic'.

This means that the requested plugin version number cannot be specified within the plugins block in a script plugin, hence the version part must be removed. But obviously without the version number the plugin won’t be found by Gradle either. So to specify the version number in a script plugin you can put the plugin as an implementation dependency into your build logic module’s build.gradle.kts. file:

dependencies {
    implementation("com.vanniktech:gradle-maven-publish-plugin:0.30.0")
}

Using binary plugin for configuration

A binary plugin is something like PublishingPlugin.kt inside your build logic module and registered in the gradlePlugin block of the module’s build file. Personally, I went with this solution because binary plugins are just normal Kotlin classes without much of the fancy DSL.

To write a binary plugin you have to use the Maven Publishing Plugin as a compileOnly dependency and register your custom plugin in the build.gradle.kts file of your build logic module, something like this:

dependencies {
    compileOnly("com.vanniktech:gradle-maven-publish-plugin:0.30.0")
}

gradlePlugin {
    plugins {
        register("publishingPlugin") {
            id = "maven.publishing"
            implementationClass = "com.example.project.buildLogic.PublishingPlugin"
        }
    }
}

This will register your custom plugin under the id maven.publishing in your project and the class itself must reside in build-logic/src/main/kotlin/com/example/project/buildLogic/PublishingPlugin.kt. Then you can define the plugin along these lines:

package com.example.project.buildLogic

import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class PublishingPlugin : Plugin<Project> {
    override fun apply(target: Project) = with(target) {
        // replace "mavenPublish" with the id from your version catalog
        // alternatively try to hard-code the id here:
        //
        //   pluginManager.apply("com.vanniktech.maven.publish")
        //
        pluginManager.apply(libs.findPlugin("mavenPublish").get().get().pluginId)

        extensions.configure<MavenPublishBaseExtension> {
            configurePublishing()
        }
    }
}

private fun MavenPublishBaseExtension.configurePublishing() {
    publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)

    signAllPublications()

    pom {
        name.set("foobar")
        description.set("This is the description of my project")
        url.set("https://github.com/johndev/foobar")

        licenses {
            license {
                name.set("The Apache License, Version 2.0")
                url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
                distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
            }
        }

        developers {
            developer {
                id.set("johndev")
                name.set("John Doe")
                email.set("john.doe@example.com")
            }
        }

        scm {
            url.set("https://github.com/johndev/foobar")
        }
    }
}

Complete example

For a complete setup please allow me to point you to my UDF application framework project hurok. It contains all the relevant pieces of configuration, along with a GitHub workflow for automating the release.

Links

Copyright © 2025 Károly Kiripolszky