TutorialsLast Updated Aug 4, 202510 min read

Continuous deployment for Android libraries to Maven Central with Gradle

Zan Markan

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 with repo permissions
  • JRELEASER_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:

  1. Clean and build the project:
    ./gradlew clean assemble test
    
  2. Configure JReleaser and publish artifacts:
    ./gradlew jreleaserConfig build publish
    
  3. 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.

Setup project on CircleCI

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.

Configure project on CircleCI

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.

Projects page on CircleCI

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 with repo permissions.
  • JRELEASER_GPG_PASSPHRASE: The passphrase for your GPG key.
  • JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN: Your Maven Central portal authentication. token
  • JRELEASER_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.

Successful build details on CircleCI

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.