Due to JCenter shutting down, I recently migrated the publishing process of several active open-source projects to MavenCentral. The two reference articles below already explain the details of each step quite well:
However, I still encountered some pitfalls during this process, as well as some baffling operations, which prompted me to write this article to share.
When applying for an OSSRH Ticket, what we're actually applying for is a Group ID. The key parameter is the Group Id; the title doesn't even need to mention the specific Artifact.

Generally, the Group ID is the reversed domain name. You'll be asked to verify domain ownership, Github repository ownership, JCenter Group ownership, etc. Just follow the corresponding reply instructions. Once approved, all future new package publishing won't require another application. For example: if I apply for the me.2bab group, then all future me.2bab.* publishing will be supported.
To verify the legitimacy of uploads, we sign the packages to be uploaded using GPG, using Gradle's official The Signing Plugin. When I first integrated it, I followed the steps in the two tutorials above and always felt something was off: I didn't see myself passing the key information needed by the signing plugin into the plugin.
// The most basic DSL configuration for the plugin is just this one line
signing {
sign(publishing.publications)
}
After briefly browsing the documentation, you'll discover that it actually defines some Keys by convention, and the plugin configuration reads directly from the Project's Properties.
// So you can see the reference tutorial writes it like this
ext["signing.keyId"] = ...
ext["signing.password"] = ...
ext["signing.secretKeyRingFile"] = ...
And this convention that I never knew was possible, referring to Build Environment:
// Using the following setup, you can pass the secret key (in ascii-
// armored format) and the password using the
// ORG_GRADLE_PROJECT_signingKey and ORG_GRADLE_PROJECT_signingPassword
// environment variables, respectively:
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(tasks["stuffZip"])
}
I really dislike this overly "implicit" convention - you can't know what you've actually written without carefully reading the documentation. Fortunately, there's also an explicit configuration method:
signing {
val signingKeyId: String? by project // Where to put it is optional, not necessarily using Project Properties
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) // This line is the key
sign(tasks["stuffZip"])
}
Similar approaches are used in some experimental configs of Android Gradle Plugin, but due to their widespread existence and usage, perhaps people are too tired to complain (though most switches can still be configured directly from the DSL). If you don't see the problem here, consider this scenario:
Next time I update the plugin, I plan to change to useInMemoryPgpKeys(...), otherwise after a year I'll forget this pitfall, or anyone taking over your project who doesn't understand the Signing plugin will be confused again.
If you use the signing.secretKeyRingFile configuration, you need to consider different configurations for local and CI environments:
../local/secret.gpg, recommended to place in the project root directory or create a local folder and add the entire folder to gitignore. The reason is that one machine might use more than one secret.gpg; keeping the key with the project makes it easier to find, and setup is also convenient for other collaborators;/secret.gpg, placed directly in the virtual environment root directory, convenient for coordinating with the RingFile generation script;I recently saw someone's MavenCentral publishing tutorial on Juejin mentioning not to upload multiple packages and then Close together. In fact, this is supported and recommended - packages with the same Group ID will be placed in one staging repo, and can then be closed & released together. If you've referenced plugins that automatically handle the close & release process, aggregate upload (batch upload) can actually improve the success rate of subsequent operations (SonaType's API and webpage are not very stable). For example, this project of mine has six modules and actually uses the Batch Upload strategy.