portrait

End of Line blog

Thoughts on software development, by Adam Ruka

Migrate Maven Central publishing to Central Portal for a Gradle project

Publishing Java libraries to Maven Central has been the default way of distributing dependencies in the Java ecosystem for decades. Traditionally, this was done through Sonatype’s OSSRH (Open Source Software Repository Hosting) service, which required publishing to staging repositories at oss.sonatype.org or s01.oss.sonatype.org, followed by a manual release confirmation.

However, Sonatype has recently deprecated OSSRH, and turned it off completely on January 30, 2025, replacing it with a new service called the Central Portal, which aims to simplify and modernize the publishing experience.

If you’re currently using Gradle with the maven-publish plugin to publish your Java libraries to Maven Central through OSSRH, this article will guide you through the migration process to Central Portal.

Step 1: Create a Central Portal account

First, you’ll need to create an account on the Central Portal:

  1. Navigate to central.sonatype.com.
  2. Click “Sign In” in the top right corner.
  3. Choose to sign up with Google, GitHub, or create a custom username/password.
  4. Verify your email address when prompted.

Note: make sure to use the same email address that’s associated with your project’s namespace (the group ID in Maven-speak) in OSSRH – this is crucial for the migration process.

Step 2: Migrate your namespace

Once you’re logged in to your Central Portal account, navigate to central.sonatype.com/publishing/namespaces. You should see a list of all namespaces you own. Any namespaces still configured to be published through OSSRH will be present in a section called “OSSRH Namespaces” (if you can’t see that section, that means there are no OSSRH namespaces associated with your account. If you expect to see namespaces listed in that section, you’ll need to contact Central Support).

Click “Migrate Namespace” for the namespace you want to migrate, and then follow the prompts to complete the migration. Note that only namespaces that meet certain criteria (no more than 6 authorized publishing accounts, no parent or child namespaces) can be migrated through the self-service process – if your namespace doesn’t meet these criteria, you’ll need to request a manual migration by contacting Central Support.

Step 3: Generate a Portal token

After your namespace is migrated, you’ll need to update your authentication credentials. For a long time, OSSRH required you to use your email and website password for publishing, but then introduced the concept of API tokens that could be generated (and revoked) independently from your website credentials.

Central Portal also uses tokens for authentication, but they are separate from the onesin OSSRH, you’ll need to generate a new one by navigating to central.sonatype.com/account, and clicking on Generate User Token. This will result in a username-password pair that looks something like aBcDef1/ and aBCdEFG1H2IjKlmnoPQ3R+STUVw4XYzAB5C6dEF7GH8I. You’ll need to copy these credentials, store them securely, and provide them in your Gradle build configuration when publishing your project – most likely in your ~/.gradle/gradle.properties file, for example:

centralPortalUsername=aBcDef1/
centralPortalPassword=aBCdEFG1H2IjKlmnoPQ3R+STUVw4XYzAB5C6dEF7GH8I

Step 4: Update your Gradle build configuration

If you’ve been using the maven-publish plugin to publish your project, I assume your configuration looks something like this:

// old configuration (using OSSRH)
publishing {
    repositories {
        maven {
            name = "MavenCentral"
            url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
            credentials {
                username = project.findProperty("ossrhUsername") ?: ""
                password = project.findProperty("ossrhPassword") ?: ""
            }
        }
    }
    // ...
}

This needs to be updated to point at the Central Portal API endpoint instead:

// new configuration (using Central Portal)
publishing {
    repositories {
        maven {
            name = "MavenCentral"
            url = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/"
            credentials {
                username = project.findProperty("centralPortalUsername") ?: ""
                password = project.findProperty("centralPortalPassword") ?: ""
            }
        }
    }
    // ...
}

Now, running the same publishing task as before (something like ./gradlew publishJarPublicationToMavenCentralRepository), but with a new version of the project, should succeed!

Step 5: Upload the created repository

Interestingly, even though the Gradle task should now succeed, if you go to central.sonatype.com/publishing/deployments after running it, you’ll probably see an empty list of deployments:

Central Portal Deployments

What’s happening here is that the maven-publish plugin creates a staging repository, and not a deployment. In order to move from a staging repository to a deployment, we need to use the manual API endpoints.

To authenticate against this API, we need to pass the Authorization HTTP header with the Base64-encoded string of the form <username>:<password>, and starting with Bearer.

So, for the example credentials we used above, we could get the Base64-encoded string by running:

$ printf "aBcDef1/:aBCdEFG1H2IjKlmnoPQ3R+STUVw4XYzAB5C6dEF7GH8I" | base64

YUJjRGVmMS86YUJDZEVGRzFIMklqS2xtbm9QUTNSK1NUVVZ3NFhZekFCNUM2ZEVGN0dIOEk=

And with that, we can call the “search repositories” manual API endpoint:

$ curl -H "Authorization: Bearer YUJjRGVmMS86YUJDZEVGRzFIMklqS2xtbm9QUTNSK1NUVVZ3NFhZekFCNUM2ZEVGN0dIOEk=" \
       https://ossrh-staging-api.central.sonatype.com/manual/search/repositories

This should return a response similar to:

{
  "repositories": [
    {
      "key": "aBcDef1//192.168.0.1/com.example--default-repository",
      "state": "open",
      "description": null,
      "portal_deployment_id": "26f6a366-29f6-411d-bf95-e3108760c935"
    }
  ]
}

The crucial piece of information here is the value of the "key" field. Using it, we can now upload the staging repository, using the “upload repository” manual API endpoint, passing that key as the last path parameter:

$ curl -H "Authorization: Bearer YUJjRGVmMS86YUJDZEVGRzFIMklqS2xtbm9QUTNSK1NUVVZ3NFhZekFCNUM2ZEVGN0dIOEk=" \
       -i -X POST https://ossrh-staging-api.central.sonatype.com/manual/upload/repository/aBcDef1//192.168.0.1/com.example--default-repository
HTTP/2 200 
date: Sun, 29 Jun 2025 05:10:15 GMT
content-length: 0

Now, if you refresh the page at central.sonatype.com/publishing/deployments, you should see your deployment on the list. Here’s how that page looks like for me after successfully uploading a release of Jilt:

Central Portal Deployments

Step 6: Test the deployed library

One of the downsides of the legacy OSSRH service was that there was no easy way to test whether your uploaded artifacts were correct when used as a dependency. This meant that you could only verify your artifacts after they were fully published to Maven Central, which forced publishing a test version of your library first. For example, I did it often with Jilt by adding an .rcX suffix to the version number – like this with 1.8.rc1.

With Central Portal, this is no longer necessary, and it’s now possible to depend on the artifacts in a Gradle project before they are fully published. The documentation does a great job explaining how to do this, I’ll just show you the code if you’re using Gradle with the Kotlin DSL, since the documentation only provides an example in Groovy:

repositories {
    // https://central.sonatype.org/publish/publish-portal-api/#gradle
    maven {
        name = "centralManualTesting"
        url = uri("https://central.sonatype.com/api/v1/publisher/deployments/download/")
        credentials(HttpHeaderCredentials::class)
        authentication {
            create<HttpHeaderAuthentication>("header")
        }
    }

    // ...other repositories here...
}

And your ~/.gradle/gradle.properties file should include:

centralManualTestingAuthHeaderName=Authorization
centralManualTestingAuthHeaderValue=Bearer YUJjRGVmMS86YUJDZEVGRzFIMklqS2xtbm9QUTNSK1NUVVZ3NFhZekFCNUM2ZEVGN0dIOEk=

With that in place, you can now add a dependency on the unpublished version of your library to this project, and it will be downloaded from the Central Portal deployments repository, just like if it was already published to Maven Central!

Step 7: Publish (or drop) the deployed library

After testing your library, and assuming everything works as expected, you can click the ‘Publish’ button on the deployments page, which will trigger the final publication of your library to Maven Central. Note that the publishing process takes around 15 minutes to finish, so don’t be surprised if you see your library “stuck” in the ‘Publishing’ state for a while.

If the testing revealed any issues with the candidate release, you can instead click the ‘Drop’ button, which will remove the deployment from Central Portal, and you can then fix the issues in your project, and repeat the process starting from Step 4.

Summary

So, this is my experience with migrating a Gradle project from the legacy OSSRH service to the new Central Portal. If you run into any issues during the migration, check out the official documentation, or feel free to comment below.