You’ve consumed many third-party libraries from Maven Central. What if you want to publish a library to Maven Central?
NOTE: This guide uses Gradle 8 with the Kotlin DSL.
To publish to Maven Central, your project must fulfill certain requirements.
We will create (most of) a buildSrc plugin that fulfills these requirements.
If buildSrc does not exist, set it up.
buildSrc/settings.gradle.kts:
rootProject.name = "buildSrc"
buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}
Create a plugin with this code initially.
buildSrc/src/main/kotlin/buildlogic.java-publish.gradle.kts:
plugins {
`java-library`
`maven-publish`
signing
}
To consume this plugin in a project, replace the java-library plugin with this plugin:
plugins {
id("buildlogic.java-publish") // replaces `java-library`
}
Verification
./gradlew buildReferences
buildSrcgradle init for the latest release of GradleAdd this code to your plugin:
java {
withSourcesJar()
withJavadocJar()
}
tasks.javadoc {
(options as StandardJavadocDocletOptions).addBooleanOption("Werror", true)
if (JavaVersion.current().isJava9Compatible) {
(options as StandardJavadocDocletOptions).addBooleanOption("html5", true)
}
}
Werror is optional; this will enforce strict compliance with Javadoc standards.
if statement if your project uses Java 9 or later.Verification
./gradlew buildbuild/libs contains a sources JAR (-sources.jar) and a Javadoc JAR (-javadoc.jar)NOTE: You may need to fix Javadoc warnings if you have
Werrorset.
References
Required Metadata
The POM must have the following metadata:
Create a Publication
Add this code to your plugin, filling in the template:
NOTE: This template is for a GitHub repository. The Maven Central documentation can help you adapt it to other options.
group = "[groupId]"
version = "[major].[minor].[patch]-SNAPSHOT"
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
versionMapping {
usage("java-api") {
fromResolutionOf("runtimeClasspath")
}
usage("java-runtime") {
fromResolutionResult()
}
}
pom {
name = "[projectName]"
description = "[projectDescription]"
url = "https://github.com/[username]/[repository]"
licenses {
license {
name = "[licenseName]"
url = "[licenseUrl]"
}
}
developers {
developer {
id = "[username]"
name = "[fullName]"
email = "[id]+[username]@users.noreply.github.com"
url = "https://github.com/[username]"
}
}
scm {
connection = "scm:git:git://github.com/[username]/[repository].git"
developerConnection = "scm:git:ssh://github.com:[username]/[repository].git"
url = "https://github.com/[username]/[repository]/tree/main"
}
}
}
}
}
io.github.mikewacker namespace, I can use io.github.mikewacker.darc as a group ID.rootProject.name in settings.gradle.kts.include() call in settings.gradle.kts.versionMapping block uses the versions that were resolved during the build.
maven-publish plugin uses the versions that were declared in the dependencies block.)Verification
./gradlew clean generatePomFileForMavenJavaPublicationbuild/publications/mavenJava/pom-default.xml.When you build the code, the libraries will also have version numbers in their filenames (e.g., artifact-1.0.0.jar).
References
Generate and Distribute a GPG Key
gpg --gen-key. You will be prompted for a real name, email address, and passphrase.gpg -k to find the key ID of your key (e.g., CA925CD6C9E8D064FF05B4728190C4130ABA0F98).gpg --keyserver keyserver.ubuntu.com --send-keys [key-id] to distribute your public key to a key server.To verify that the key has been distributed, you can search for your key by its ID at https://keyserver.ubuntu.com/.
NOTE: This key expires in 2 years, but you can extend the expiration and redistribute the updated key to the key server.
Sign a Publication
Add this code to your plugin:
signing {
sign(publishing.publications["mavenJava"])
}
gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg~/.gradle/gradle.properties, filling in the template:
signing.keyId, use the last 8 symbols of the key ID. signing.keyId=[key-id-last-8]
signing.password=[passphrase]
signing.secretKeyRingFile=[home-dir]/.gnupg/secring.gpg
WARNING: It may not be feasible to securely provide a keyring file to a CI server. We’ll switch to an in-memory key later.
Verification
./gradlew clean signMavenJavaPublicationbuild/libs has a corresponding signature file (.asc).References
(The maven-publish plugin will automatically add checksum files when it publishes a project.)
References
IMPORTANT: OSSRH reached end of life on June 30, 2025. The
maven-publishplugin does not yet support the new Portal API.
io.github.[username]) may already be registered.Verification
NOTE: Don’t worry about making mistakes. You can drop a deployment in the Central Portal if you make a mistake.
Add this code to the publications block of your plugin:
repositories {
maven {
name = "ossrhStagingApi"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
val portalUsername: String? by project
val portalPassword: String? by project
username = portalUsername
password = portalPassword
}
}
}
Add this script (publish-maven-central.sh) to the root directory of your repository, filling in the template:
#!/usr/bin/env bash
# Publish to OSSRH Staging API.
./gradlew publish
# Transfer from OSSRH Staging API to Central Publisher Portal.
BEARER=$(printf "$ORG_GRADLE_PROJECT_portalUsername:$ORG_GRADLE_PROJECT_portalPassword" | base64)
curl --request POST \
--include \
--header "Authorization: Bearer $BEARER" \
https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/[namespace]
IMPORTANT: For security reasons,
--includeis used instead of--verbose; this option will not display theAuthorizationheader in the output.
Add these environment variable to ~/.gradle/gradle.env, filling in the template:
export ORG_GRADLE_PROJECT_portalUsername=[username]
export ORG_GRADLE_PROJECT_portalPassword=[password]
Verification
source ~/.gradle/gradle.env./publish-maven-central.sh./gradlew publish succeeded.curl request succeeded with a 200 response code.From there, you can either drop or publish the deployment.
NOTE: This workflow does not support
-SNAPSHOTreleases.
References
Replace the signing block of your plugin with this code:
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing.publications["mavenJava"])
}
gpg --armor --export-secret-keys [keyId] > ~/.gradle/seckey.ascAdd these environment variables to ~/.gradle/gradle.env, filling in the template:
export ORG_GRADLE_PROJECT_signingKey=$(cat ~/.gradle/seckey.asc)
export ORG_GRADLE_PROJECT_signingPassword=[passphrase]
Verification
source ~/.gradle/gradle.env./gradlew clean signMavenJavaPublicationbuild/libs has a corresponding signature file (.asc).References
We will create a GitHub workflow that will be triggered when a GitHub release is created.
Add this workflow (.github/workflows/publish.yml) to your repository (updating action versions as you see fit):
name: Publish
on:
release:
types: [created]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup Java
uses: actions/setup-java@v4.7.1
with:
java-version: 21
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.4.1
- name: Publish
run: ./publish-maven-central.sh
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
ORG_GRADLE_PROJECT_portalUsername: ${{ secrets.PORTAL_USERNAME }}
ORG_GRADLE_PROJECT_portalPassword: ${{ secrets.PORTAL_PASSWORD }}
SIGNING_KEY, SIGNING_PASSWORD, PORTAL_USERNAME, and PORTAL_PASSWORD.
SIGNING_KEY, copy and paste the contents of ~/.gradle/seckey.asc.Verification
IMPORTANT: The workflow may “succeed” if
./publish-maven-central.shfails; you will have to examine the output for this action.
NOTE: Since the workflow includes
workflow_dispatch, you can rerun it manually if something goes wrong.
References
Source: https://github.com/mikewacker/darc
buildSrc/src/main/kotlin/buildlogic.java-publish.gradle.kts:
plugins {
`java-library`
`maven-publish`
signing
}
tasks.compileJava {
options.release = 11
}
java {
withSourcesJar()
withJavadocJar()
}
tasks.javadoc {
(options as StandardJavadocDocletOptions).addBooleanOption("Werror", true)
(options as StandardJavadocDocletOptions).addBooleanOption("html5", true)
}
group = "io.github.mikewacker.darc"
version = "0.1.1"
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
versionMapping {
usage("java-api") {
fromResolutionOf("runtimeClasspath")
}
usage("java-runtime") {
fromResolutionResult()
}
}
pom {
name = "DARC"
description = "Dagger And Request Context (for Dropwizard)"
url = "https://github.com/mikewacker/darc"
licenses {
license {
name = "MIT License"
url = "https://opensource.org/license/mit"
}
}
developers {
developer {
id = "mikewacker"
name = "Mike Wacker"
email = "11431865+mikewacker@users.noreply.github.com"
url = "https://github.com/mikewacker"
}
}
scm {
connection = "scm:git:git://github.com/mikewacker/darc.git"
developerConnection = "scm:git:ssh://github.com:mikewacker/darc.git"
url = "https://github.com/mikewacker/darc/tree/main"
}
}
}
}
repositories {
maven {
name = "ossrhStagingApi"
url = uri("https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/")
credentials {
val portalUsername: String? by project
val portalPassword: String? by project
username = portalUsername
password = portalPassword
}
}
}
}
signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing.publications["mavenJava"])
}
publish-maven-central.sh:
#!/usr/bin/env bash
# Publish to OSSRH Staging API.
./gradlew publish
# Transfer from OSSRH Staging API to Central Publisher Portal.
BEARER=$(printf "$ORG_GRADLE_PROJECT_portalUsername:$ORG_GRADLE_PROJECT_portalPassword" | base64)
curl --request POST \
--include \
--header "Authorization: Bearer $BEARER" \
https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.mikewacker
.github/workflows/publish.yml:
name: Publish
on:
release:
types: [created]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup Java
uses: actions/setup-java@v4.7.1
with:
java-version: 21
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4.4.1
- name: Publish
run: ./publish-maven-central.sh
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
ORG_GRADLE_PROJECT_portalUsername: ${{ secrets.PORTAL_USERNAME }}
ORG_GRADLE_PROJECT_portalPassword: ${{ secrets.PORTAL_PASSWORD }}