Why Maven Central?
If you maintain an open-source Java library, Maven Central is the standard distribution channel. It is the default repository for Gradle and Maven, meaning any developer can add your library as a dependency without configuring custom repositories. Publishing there gives your project discoverability, trust, and seamless integration into existing build pipelines.
However, the publishing process has changed significantly in recent years. If you have published to Maven Central before using the legacy OSSRH (OSS Repository Hosting) workflow, much of what you know no longer applies. This article covers the current process using the new Central Portal, including what changed and why your old setup may have stopped working.
Migrating from Legacy OSSRH to Central Portal
If you have never published to Maven Central before, you can skip this section and go straight to Step 1.
What Changed
In March 2025, Sonatype completed the migration from the legacy OSSRH system (oss.sonatype.org) to the new Central Portal. This was not a minor upgrade — it was a complete replacement of the publishing infrastructure. The legacy Nexus-based staging workflow is gone.
The key differences:
| Aspect | Legacy OSSRH | New Central Portal |
|---|---|---|
| Portal URL | oss.sonatype.org |
central.sonatype.com |
| Namespace registration | JIRA ticket, manually reviewed | Self-service via DNS/repo verification |
| Staging API | Nexus REST API (/service/local/staging/...) |
Central Publisher API |
| Credentials | Sonatype JIRA username/password | Portal-generated tokens |
| Upload endpoint | https://oss.sonatype.org/service/local/staging/deploy/maven2/ |
https://central.sonatype.com/api/v1/publisher/upload |
What Broke
If your publishing pipeline suddenly stopped working, the cause is almost certainly one of these:
1. The gradle-nexus-publish-plugin no longer works.
The widely used io.github.gradle-nexus.publish-plugin was designed for the legacy Nexus staging API. It targets oss.sonatype.org endpoints that no longer exist. If your build.gradle.kts contains something like this, it will fail:
// This no longer works
nexusPublishing {
repositories {
sonatype {
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(
uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
)
}
}
}
2. Gradle’s built-in maven-publish plugin cannot target Central Portal directly.
The standard maven-publish plugin supports publishing to arbitrary Maven repositories, but the Central Portal does not expose a standard Maven repository upload endpoint. You cannot simply point repositories { maven { url = ... } } at the new portal.
3. Old Sonatype JIRA credentials are no longer valid.
The legacy system used your Sonatype JIRA account for authentication. The new portal requires you to generate API tokens through the Central Portal UI.
The Fix
Replace your legacy publishing plugin with one that supports the Central Portal API. The vanniktech maven-publish plugin (version 0.29.0+) has built-in support for the new Central Portal. It handles the new upload API, token-based authentication, and automatic release — all through the same mavenPublishing DSL.
The migration steps:
- Remove the
gradle-nexus-publish-pluginor any OSSRH-specific configuration. - Add the vanniktech plugin (see Step 3 below).
- Generate new credentials at central.sonatype.com under your account settings.
- Verify your namespace if you have not already — legacy namespaces registered through JIRA were migrated automatically, but it is worth confirming at the portal.
- Update your
~/.gradle/gradle.propertieswith the newmavenCentralUsernameandmavenCentralPasswordvalues (portal tokens, not JIRA credentials).
Your GPG keys and POM metadata configuration remain unchanged — only the publishing infrastructure and credentials need updating.
Step 1 — Namespace Ownership Validation
Before you can publish any artifact, Maven Central requires you to prove ownership of your group ID (e.g., dev.alimov). This prevents anyone from publishing artifacts under a namespace they do not control.
The process depends on how your group ID maps to a domain or hosting provider:
- Custom domain (e.g.,
dev.alimov). You verify ownership by adding a DNS TXT record to the domain. - GitHub-based (e.g.,
io.github.username). Verification is done by creating a temporary public repository with a specific name. - Other hosting providers. GitLab, Bitbucket, and others follow similar verification flows.
Namespace verification is handled through the Maven Central Portal. The full process, including step-by-step instructions for each verification method, is documented in the official Central Portal: Namespace Registration Guide.
Once your namespace is verified, you can publish any artifact under that group ID and its sub-groups.
Step 2 — GPG Signing
Maven Central requires all published artifacts to be cryptographically signed with a GPG key. This ensures that consumers can verify the authenticity and integrity of your library.
Generating a GPG Key Pair
If you do not already have a GPG key, generate one:
gpg --full-generate-key
When prompted:
- Key type. Select
RSA and RSA(default). - Key size. Use
4096bits for strong security. - Expiration. Choose an appropriate expiration period or set to no expiration.
- Name and email. Use the same identity you plan to associate with your Maven Central account.
- Passphrase. Set a strong passphrase — you will need it during the publishing process.
Finding Your Key ID
After generation, list your keys to find the key ID:
gpg --list-keys --keyid-format SHORT
Output will look similar to:
pub rsa4096/ABCD1234 2026-03-15 [SC]
1234567890ABCDEF1234567890ABCDEFABCD1234
uid [ultimate] Your Name <your@email.com>
sub rsa4096/EFGH5678 2026-03-15 [E]
The key ID is the 8-character value after rsa4096/ — in this example, ABCD1234.
Publishing Your Key to a Keyserver
Maven Central verifies signatures against public keyservers. Upload your public key:
gpg --keyserver keyserver.ubuntu.com --send-keys ABCD1234
You can verify the upload:
gpg --keyserver keyserver.ubuntu.com --search-keys your@email.com
Exporting the Secret Keyring
Gradle’s signing plugin needs access to your secret keyring file. Export it:
gpg --export-secret-keys -o ~/.gnupg/secring.gpg
This file will be referenced in your Gradle configuration.
Step 3 — Configuring Gradle
The most streamlined way to publish to Maven Central from Gradle is the vanniktech maven-publish plugin. It handles POM generation, source/javadoc JAR creation, signing, and uploading — all with minimal configuration.
Plugin Setup
In your root build.gradle.kts:
plugins {
id("java")
id("com.vanniktech.maven.publish") version "0.36.0"
}
For multi-module projects, apply the plugin to each submodule:
subprojects {
apply(plugin = "java-library")
apply(plugin = "com.vanniktech.maven.publish")
java {
withSourcesJar()
}
tasks.matching { it.name == "generateMetadataFileForMavenPublication" }
.configureEach {
dependsOn(tasks.matching { it.name == "plainJavadocJar" })
}
}
The withSourcesJar() call ensures a -sources.jar is generated alongside your main artifact — a Maven Central requirement.
The generateMetadataFileForMavenPublication task dependency ensures Gradle module metadata is generated after the javadoc JAR, avoiding task ordering issues during publishing.
Publishing and Signing Configuration
Configure the plugin to target Maven Central with automatic release and sign all publications:
subprojects {
// ... plugin applications from above
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
signAllPublications()
}
}
publishToMavenCentral(automaticRelease = true)uploads artifacts to the Maven Central staging repository and automatically promotes them to release. WithoutautomaticRelease = true, you would need to manually log into the Central Portal and click “Release” after each upload.signAllPublications()applies GPG signatures to every artifact (JAR, POM, sources JAR, javadoc JAR).
POM Metadata
Maven Central enforces a set of required POM fields. The vanniktech plugin provides a DSL to configure them:
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
signAllPublications()
pom {
name.set("Your Library Name")
description.set("A concise description of your library")
url.set("https://github.com/your-org/your-project")
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("your-id")
name.set("Your Name")
url.set("https://your-website.dev")
}
}
scm {
connection.set("scm:git:git://github.com/your-org/your-project.git")
developerConnection.set("scm:git:ssh://github.com/your-org/your-project.git")
url.set("https://github.com/your-org/your-project")
}
}
}
All of these fields are mandatory. Missing any of them will cause the Maven Central validation to reject your upload.
Project Coordinates
Define your group ID and version in gradle.properties:
group=dev.example.mylib
version=1.0.0
The artifact ID is derived from the Gradle module name by default. For a module named my-library-core, the published coordinates would be dev.example.mylib:my-library-core:1.0.0.
Step 4 — Configuring Credentials
The publishing process requires two sets of credentials: Maven Central account credentials and GPG signing details. These should never be committed to version control.
Using ~/.gradle/gradle.properties
Add the following to your user-level Gradle properties file (~/.gradle/gradle.properties):
mavenCentralUsername=your-central-username
mavenCentralPassword=your-central-password
signing.keyId=ABCD1234
signing.password=your-gpg-passphrase
signing.secretKeyRingFile=/Users/you/.gnupg/secring.gpg
| Property | Description |
|---|---|
mavenCentralUsername |
Your Maven Central Portal username (or token name) |
mavenCentralPassword |
Your Maven Central Portal password (or token) |
signing.keyId |
Last 8 characters of your GPG key ID |
signing.password |
Passphrase for your GPG key |
signing.secretKeyRingFile |
Absolute path to your exported secret keyring |
Using Environment Variables
For CI/CD pipelines, use environment variables instead. The vanniktech plugin and Gradle’s signing plugin recognize these:
| Environment Variable | Maps To |
|---|---|
ORG_GRADLE_PROJECT_mavenCentralUsername |
mavenCentralUsername |
ORG_GRADLE_PROJECT_mavenCentralPassword |
mavenCentralPassword |
ORG_GRADLE_PROJECT_signing.keyId |
signing.keyId |
ORG_GRADLE_PROJECT_signing.password |
signing.password |
ORG_GRADLE_PROJECT_signing.secretKeyRingFile |
signing.secretKeyRingFile |
Step 5 — Publishing
With everything configured, publish your artifacts:
./gradlew publishAllPublicationsToMavenCentralRepository
This command will:
- Compile your source code.
- Generate the main JAR, sources JAR, and javadoc JAR.
- Generate the POM file with your configured metadata.
- Sign all artifacts with your GPG key.
- Upload everything to the Maven Central staging repository.
- Automatically promote the staged artifacts to release (if
automaticRelease = true).
After a successful publish, your artifacts will be available on Maven Central within a few minutes. They will appear in search results on search.maven.org within a few hours.
Complete Configuration Example
Here is a minimal but complete build.gradle.kts for a multi-module project publishing to Maven Central:
plugins {
id("java")
id("com.vanniktech.maven.publish") version "0.36.0"
}
subprojects {
apply(plugin = "java-library")
apply(plugin = "com.vanniktech.maven.publish")
repositories {
mavenCentral()
}
java {
withSourcesJar()
}
tasks.matching { it.name == "generateMetadataFileForMavenPublication" }
.configureEach {
dependsOn(tasks.matching { it.name == "plainJavadocJar" })
}
mavenPublishing {
publishToMavenCentral(automaticRelease = true)
signAllPublications()
pom {
name.set("Your Library Name")
description.set("A concise description of your library")
url.set("https://github.com/your-org/your-project")
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("your-id")
name.set("Your Name")
url.set("https://your-website.dev")
}
}
scm {
connection.set("scm:git:git://github.com/your-org/your-project.git")
developerConnection.set("scm:git:ssh://github.com/your-org/your-project.git")
url.set("https://github.com/your-org/your-project")
}
}
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
}
Common Issues
Signature Verification Failure
If Maven Central rejects your upload with a signature error, ensure your public key is available on a keyserver:
gpg --keyserver keyserver.ubuntu.com --send-keys ABCD1234
It can take a few minutes for the key to propagate.
Missing POM Fields
Maven Central validates that the POM contains all required fields: name, description, url, licenses, developers, and scm. If any are missing, the upload will fail with a descriptive error message.
Task Dependency Errors
If you see errors about plainJavadocJar not being found during metadata generation, add the explicit task dependency shown in the configuration above. This is a known ordering issue in the vanniktech plugin with certain Gradle versions.
SNAPSHOT Versions
Artifacts with a version ending in -SNAPSHOT cannot be published to Maven Central’s release repository. Either remove the -SNAPSHOT suffix or publish to a snapshot repository instead.
Conclusion
Publishing to Maven Central involves three main prerequisites — namespace ownership, GPG signing, and correct POM metadata — and a well-configured build plugin to tie them together. The vanniktech maven-publish plugin reduces this to a single mavenPublishing block in your Gradle build, handling artifact generation, signing, and upload in one command.
Once configured, the publishing process is a single command that takes under a minute. The initial setup is the hard part — after that, every subsequent release is straightforward.