Continuous deployment for Android libraries to Maven Central with Gradle

Developer Advocate

This article will guide you through setting up CI/CD integration for building, testing, and publishing libraries to Maven Central using Gradle and JReleaser. Maven Central is the primary destination for all Android and Java libraries.
Note: Maven Central has modernized its publishing process with the new Central Publisher Portal. If you’ve been using the traditional Gradle publishing approach with the maven-publish
plugin or the Gradle Nexus plugin (as covered in our previous tutorial), this article will show you how to migrate to the new process using JReleaser. The steps covered here work equally well for migrating existing projects and setting up new library publishing workflows.
This article focuses on CI/CD integration using the modern JReleaser approach. Setting up the library itself for manual publishing is out of scope, but you will find some links to useful articles and guides to help you get started.
Prerequisites
You will need a few things to get the most from this article:
- Some experience with Android or Java
- Experience with Gradle
- Familiarity with the Java or Android library publishing process
- A CircleCI account
About the project
This tutorial uses a sample Android library present in the android-example GitHub repository. The sample library includes the following:
- A simple
Calculator.kt
file that provides basic mathematical operations. - Some sample unit tests in the
test
directory.
Publishing libraries to Maven Central with Gradle
Whether you’re publishing your first library to Maven Central or migrating an existing project from the traditional publishing approach, this section will guide you through the modern process using JReleaser.
For new projects: If you have not published libraries with Gradle to Maven Central before, follow all the steps in this section.
For existing projects: If you’re currently using the Gradle Nexus plugin or traditional maven-publish
approach as described in our previous tutorial, you can migrate to JReleaser by replacing your existing publishing configuration with the JReleaser setup shown below.
Maven Central, now known as the Central Publisher Portal, provides an official guide for the publishing process.
The process requires several steps, but before starting, create a new account on Maven Central portal.
Register a namespace to publish under
You can register a new namespace on the Publishing Settings > Namespace page. This can be your package name; for my project I used com.maskaravivek
. You will publish all your projects under this group. Note that if you use com.[domain name]
nomenclature you might be asked to prove that you own the domain.
Prepare your library for publishing and signing using JReleaser
Since the new Maven Central Portal doesn’t officially support Gradle projects yet, we need to use JReleaser as a community plugin. JReleaser provides excellent support for publishing to the Central Publisher Portal. You’ll need to configure the JReleaser Gradle plugin in your build.gradle.kts
file with the proper Maven publication setup, signing configuration, and deployment settings pointing to the Central Portal API.
Create and generate a GPG key
Create a GPG key. This is what you will use to sign the packages.
To generate a new GPG key, run the following command in your terminal:
gpg --full-generate-key
This will prompt you for various details:
- Select the type of key as RSA
- Choose the key size as 4096 bits
- Create a secure passphrase
Export and encrypt your GPG keys
Once you have generated your GPG key, you’ll need to export both the public and private keys. First, list your keys to get the key ID:
gpg --list-secret-keys --keyid-format LONG
Then export your keys using the key ID:
Export public key:
gpg --armor --export <YOUR_KEY_ID> > public.key.asc
Export private key:
gpg --armor --export-secret-keys <YOUR_KEY_ID> > private.key.asc
You need to encrypt the public and private key so that they can be checked into version control. This will enable you to decrypt and access the key in a CI environment.
Encrypt public key:
openssl aes-256-cbc -a -salt -pbkdf2 -in public.key.asc -out public.key.asc.enc
Encrypt private key:
openssl aes-256-cbc -a -salt -pbkdf2 -in private.key.asc -out private.key.asc.enc
You’ll be prompted to enter a password for encryption. Store this password securely as you’ll need it to decrypt the keys later in your CI/CD pipeline.
You can find a complete example in my sample project on GitHub.
Complete JReleaser configuration
Here’s how to configure the complete JReleaser setup in your build.gradle.kts
:
Configure project:
jreleaser {
project {
inceptionYear = "2024"
author("@maskaravivek")
}
gitRootSearch = true
}
Configure GitHub release settings:
release {
github {
skipTag = true
sign = true
branch = "main"
branchPush = "main"
overwrite = true
}
}
Maven Central Deployment
The deployment configuration handles the actual publishing to Maven Central:
deploy {
maven {
mavenCentral.create("sonatype") {
active = Active.ALWAYS
url = "https://central.sonatype.com/api/v1/publisher"
stagingRepository(layout.buildDirectory.dir("staging-deploy").get().toString())
setAuthorization("Basic")
applyMavenCentralRules = false
sign = true
checksums = true
sourceJar = true
javadocJar = true
retryDelay = 60
}
}
}
This configuration:
- Uses the new Central Publisher Portal API
- Points to a local staging directory for artifacts
- Enables signing, checksums, and includes source/javadoc JARs
- Sets a retry delay for failed uploads
Setting up signing keys
You need to configure signing keys that can be passed to JReleaser at build time. JReleaser handles GPG signing through its configuration block in your build.gradle.kts
file.
Here’s how to configure the signing section in JReleaser:
jreleaser {
signing {
active = Active.ALWAYS
armored = true
verify = true
mode = Signing.Mode.MEMORY
passphrase = System.getenv("JRELEASER_GPG_PASSPHRASE")
publicKey = System.getenv("JRELEASER_GPG_PUBLIC_KEY")
secretKey = System.getenv("JRELEASER_GPG_SECRET_KEY")
}
}
This configuration tells JReleaser to:
- Always sign artifacts (
active = Active.ALWAYS
) - Use ASCII-armored output (
armored = true
) - Verify signatures after creation (
verify = true
) - Load keys from memory rather than files (
mode = Signing.Mode.MEMORY
)
For CI/CD environments, you’ll need to decrypt your encrypted keys and set these environment variables before running the JReleaser tasks.
Testing the setup locally
Before you can publish the library to Maven Central from your local machine, you need to set a few environment variables:
GPG_ENCRYPTION_PASSWORD
: The password used to encrypt your GPG keys with OpenSSL.JRELEASER_GITHUB_TOKEN
: A GitHub personal access token withrepo
permissionsJRELEASER_GPG_PASSPHRASE
: The passphrase for your GPG key.JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN
: Your Maven Central portal authentication token. You can generate a new token from the accounts page.JRELEASER_MAVENCENTRAL_USERNAME
: Your Maven Central portal username.JRELEASER_GPG_PUBLIC_KEY
: Your public GPG key content.JRELEASER_GPG_SECRET_KEY
: Your private GPG key content.LIBRARY_VERSION
: Semver library version that you want to publish.
You can set the public and private key environment variables directly from the files:
export JRELEASER_GPG_PUBLIC_KEY="$(cat public.key.asc)"
export JRELEASER_GPG_SECRET_KEY="$(cat private.key.asc)"
To test the setup locally, run the following commands in sequence:
- Clean and build the project:
./gradlew clean assemble test
- Configure JReleaser and publish artifacts:
./gradlew jreleaserConfig build publish
- Execute the full release process:
./gradlew jreleaserFullRelease
If all commands complete successfully, your library should be published to Maven Central. The process will handle signing, uploading, and releasing the artifacts automatically through JReleaser.
Setting up CI/CD with CircleCI
Now that you have JReleaser configured for local publishing, let’s set up continuous deployment using CircleCI. The CI/CD pipeline will automate the publishing process whenever you create a new release tag.
Understanding the deployment flow
The CI/CD pipeline for a library does a few things:
- Tests the library on every commit. The library tests are run on a corresponding sample application. This catches any commits that would break the build as soon as possible.
- Releases a new version on every tag. This is the versions most end users will interact with when they use the library.
Setting up a CI/CD pipeline for releasing your libraries means your publishing process will still always work the same, no matter what happens to your developer machine, or who is doing the development.
Versioning the library with Git tags
All projects in Maven Central need to be properly versioned. I like using Git tags to drive versioning. You can create them from the command line and they are always tied to a commit.
For any tagged commit, CircleCI will surface the tag as the environment variable $CIRCLE_TAG
. I used that tag to denote a release build’s version. For all other commits that are missing tags, I treat them as snapshot builds.
You will store it as another environment variable $LIBRARY_VERSION
that can be picked up in Gradle. The way to make it available is to push any variables to $BASH_ENV
in CircleCI’s run
step.
This is the script I used. It either sets the current Git tag as an environment variable (for example 1.0.21
), or if that is missing, uses the most recently used tag with -SNAPSHOT
appended to it.
- run:
name: Define LIBRARY_VERSION Environment Variable at Runtime
command: |
if [ $CIRCLE_TAG ]
then
echo 'export LIBRARY_VERSION=$CIRCLE_TAG' >> $BASH_ENV
else
echo "export LIBRARY_VERSION=`git tag | tail -1`-SNAPSHOT" >> $BASH_ENV
fi
source $BASH_ENV
Publishing the artifacts to Maven Central
The final step of publishing is to run the JReleaser commands to build and release:
./gradlew jreleaserConfig build publish
- Configures JReleaser and publishes artifacts../gradlew jreleaserFullRelease
- Executes the full release process.
This builds, signs, and deploys the library through JReleaser to Maven Central:
- run:
name: Build and publish artifacts
command: ./gradlew jreleaserConfig build publish
- run:
name: Execute full release
command: ./gradlew jreleaserFullRelease
The full deployment job in my .circleci/config.yml
:
jobs:
...
deploy-to-maven-central:
executor:
name: android/android_machine
resource_class: large
tag: 2024.11.1
steps:
- checkout
- run:
name: Define LIBRARY_VERSION Environment Variable at Runtime
command: |
if [ $CIRCLE_TAG ]
then
echo 'export LIBRARY_VERSION=$CIRCLE_TAG' >> $BASH_ENV
else
echo "export LIBRARY_VERSION=`git tag | tail -1`-SNAPSHOT" >> $BASH_ENV
fi
source $BASH_ENV
- run:
name: Decrypt GPG keys
command: |
# Decrypt the private key
openssl aes-256-cbc -d -a -pbkdf2 -in private.key.asc.enc -out private.key.asc -k $GPG_ENCRYPTION_PASSWORD
# Decrypt the public key
openssl aes-256-cbc -d -a -pbkdf2 -in public.key.asc.enc -out public.key.asc -k $GPG_ENCRYPTION_PASSWORD
# Set the keys as environment variables for JReleaser
echo 'export JRELEASER_GPG_SECRET_KEY="$(cat private.key.asc)"' >> $BASH_ENV
echo 'export JRELEASER_GPG_PUBLIC_KEY="$(cat public.key.asc)"' >> $BASH_ENV
source $BASH_ENV
- run:
name: Build and publish artifacts
command: ./gradlew jreleaserConfig build publish
- run:
name: Execute full release
command: ./gradlew jreleaserFullRelease
For CircleCI to run this job you need to do include it in a workflow.
Publishing a release version
My release workflow:
workflows:
build-test-deploy:
when:
not: << pipeline.parameters.run-schedule >>
jobs:
- build-library:
name: build-and-test
filters:
tags:
only: /^[0-9]+.*/
- hold-for-approval:
type: approval
requires:
- build-and-test
filters:
tags:
only: /^[0-9]+.*/
branches:
ignore: /.*/
- deploy-to-maven-central:
name: Deploy to Maven Central
requires:
- hold-for-approval
filters:
tags:
only: /^[0-9]+.*/
It is best practice to set up the tests to run on every single commit, on any branch, but to proceed with deployment only when the commit is tagged like a version. That is what the filters
stanza allows. In this case the deployment job only runs on versioned tags that begin with a number and a period, like 1.something
. That way you can still use tags for other purposes, but they will not trigger a release build.
filters:
tags:
only: /^[0-9]+.*/
branches:
ignore: /.*/
I also added a hold-for-approval
, which is an approval
job that requires a manual confirmation before deploying the library. This is optional, but I find the manual step reassuring.
Publishing snapshots
Snapshots are special types of releases that are faster to publish, and can be easily overridden, unlike regular, versioned releases, which cannot be overridden. This makes snapshots useful for testing new versions of the library.
To release a snapshot all you need to do is append -SNAPSHOT
to the end of the version string. That procedure is covered in the versioning section of this article. I have set my snapshot deployment pipeline so that the library is built each night at midnight UTC, using CircleCI’s scheduled triggers feature:
nightly-snapshot:
triggers: #use the triggers key to indicate a scheduled build
- schedule:
cron: "0 0 * * *" # use cron syntax to set the schedule
filters:
branches:
only:
- main
jobs:
- build-library:
name: build-and-test
- deploy-to-maven-central:
name: Deploy Snapshot to Maven Central
requires:
- build-and-test
Running the workflow on CircleCI
In this section, you will learn how to automate the workflow using CircleCI.
Setting up the project on CircleCI
On the CircleCI dashboard, click the Projects tab, search for the GitHub repo name and click Set Up Project.
You will be prompted to add a new configuration file manually or use an existing one. You have already pushed the required configuration file to the codebase, so select the Fastest option and enter the name of the branch hosting your configuration file. Click Set Up Project to continue.
Completing the setup will trigger the pipeline. The first build might fail as the environment variables are not set.
Set environment variables
On the project page, click Project settings.
Go to the Environment variables tab and click Add environment variable. Add the following environment variables:
GPG_ENCRYPTION_PASSWORD
: The password you used to encrypt your GPG keys with OpenSSL.JRELEASER_GITHUB_TOKEN
: Your GitHub personal access token withrepo
permissions.JRELEASER_GPG_PASSPHRASE
: The passphrase for your GPG key.JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN
: Your Maven Central portal authentication. tokenJRELEASER_MAVENCENTRAL_USERNAME
: Your Maven Central portal username.
Note: You don’t need to set the JRELEASER_GPG_PUBLIC_KEY
and JRELEASER_GPG_SECRET_KEY
environment variables manually. The CI pipeline will decrypt the encrypted key files and set these variables automatically during the build process.
Once you add the environment variables, the key values are available on the dashboard.
![Set environment variables on CircleCI]2025-07-06-android-maven-environment-variables)
Now that the environment variables are configured, trigger the pipeline again. This time the build should succeed.
![Successful build on CircleCI]2025-07-06-android-maven-successful-build)
Once the build is successful, go to the Maven central publishing page to review the library status.
Conclusion
In this article, I covered how to set up a continuous deployment pipeline for an Android or Java library to Maven Central using Gradle and JReleaser. Whether you’re starting a new library project or migrating from the traditional publishing approach, you have learned how to:
- Set up JReleaser for publishing to the new Maven Central Portal
- Configure GPG signing for secure artifact publishing
- Set up environment variables for both local testing and CI/CD
- Create a CircleCI pipeline that automatically publishes libraries on Git tags
- Migrate from legacy publishing methods (like the Gradle Nexus plugin) to the modern JReleaser approach
Using JReleaser simplifies the Maven Central publishing process significantly compared to traditional approaches, eliminates the need for manual staging repository management, and provides excellent automation for CI/CD environments. For teams migrating from our previous tutorial, JReleaser offers a more streamlined and maintainable solution for modern Maven Central publishing.