all posts
Open Source

Publishing My First Java Library to Maven Central

A step-by-step walkthrough of getting my first Java library onto Maven Central — namespace verification, GPG signing, POM metadata, and a one-button automated release.

I just published my first Java library to Maven Central, and I want to walk through exactly how I did it — partly so other people can copy the steps, and partly so future me remembers. The package is campus-auth-java, a small, demo-first toolkit that models campus-style member verification workflows.

Note: This is an educational/demo package. It ships a safe in-memory provider and is not affiliated with any university and does not log into or automate any real authentication system.

If you just want the result:

<dependency>
  <groupId>io.github.usmanovmahmudkhan</groupId>
  <artifactId>campus-auth-java</artifactId>
  <version>0.1.0</version>
</dependency>
implementation("io.github.usmanovmahmudkhan:campus-auth-java:0.1.0")

What the library does

The API is intentionally tiny. You pick a provider, build a request, and read a result:

import io.github.usmanovmahmudkhan.campusauth.*;

CampusAuthClient client = CampusAuthClient.withProvider(new DemoAuthProvider());

AuthResult result = client.verify(
    AuthRequest.of("demo-student", "demo-password")
);

if (result.isAuthenticated()) {
    System.out.println(result.member().id());
}

The bundled DemoAuthProvider is fully in-memory and talks to no external service, so the examples and tests run anywhere with no credentials and no network. There's also a small CLI that reads the password without echo and never prints it.

The part nobody tells you: Maven Central is not just mvn deploy

Coming from "just push the jar somewhere," the real work is the publishing requirements. Maven Central (via the new Sonatype Central Portal) requires:

  1. A verified namespace that you own.
  2. GPG-signed artifacts.
  3. A sources jar and a javadoc jar alongside the main jar.
  4. Complete POM metadata: name, description, URL, license, developer, SCM.

Here's how I satisfied each one.

1. Pick coordinates and verify the namespace

I used the io.github.<username> convention, which is the easiest namespace to claim if you don't own a custom domain:

  • groupId: io.github.usmanovmahmudkhan
  • artifactId: campus-auth-java
  • version: 0.1.0

To prove ownership in the Central Portal, you add the namespace and it gives you a verification key — a random string like 3ajrd1i1c2. You prove you own the matching GitHub account by creating a public repository with that exact name:

gh repo create 3ajrd1i1c2 --public

Click Confirm in the portal, it checks that the repo exists, and the namespace flips to Verified. After that you can delete the throwaway repo — it's only needed during verification.

2. POM metadata

The POM needs the full block of metadata Central validates. The important parts:

<groupId>io.github.usmanovmahmudkhan</groupId>
<artifactId>campus-auth-java</artifactId>
<version>0.1.0</version>

<name>campus-auth-java</name>
<description>Educational, demo-first JVM toolkit for campus-style member verification workflows.</description>
<url>https://github.com/UsmanovMahmudkhan/campus-auth-java</url>

<licenses>...</licenses>
<developers>...</developers>
<scm>...</scm>

3. Sources jar, javadoc jar, and GPG signing

I keep the signing and publishing plugins in a release profile so that normal mvn clean verify stays green in CI without needing a GPG key. The release profile adds maven-gpg-plugin (signing) and the central-publishing-maven-plugin (upload). The source and javadoc jars are attached in the main build:

<plugin>
  <groupId>org.sonatype.central</groupId>
  <artifactId>central-publishing-maven-plugin</artifactId>
  <version>0.7.0</version>
  <extensions>true</extensions>
  <configuration>
    <publishingServerId>central</publishingServerId>
    <autoPublish>true</autoPublish>
    <waitUntil>published</waitUntil>
  </configuration>
</plugin>

autoPublish means I don't have to click "Publish" in the UI, and waitUntil=published makes the build block until Central finishes validating — so a green workflow really means "it's live."

4. The GPG key

Maven Central won't accept unsigned artifacts. I generated a 4096-bit RSA key, published the public half to a keyserver (so Central can verify signatures), and exported the private half for CI:

gpg --full-generate-key                     # choose RSA 4096, set a passphrase
gpg --list-secret-keys --keyid-format=long  # find the key id
gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID>
gpg --armor --export-secret-keys <KEY_ID>   # this block goes into a secret

Automating the release with GitHub Actions

I didn't want to run releases from my laptop, so the whole thing is a manual (workflow_dispatch) GitHub Actions job. It imports the GPG key, reads the portal token, and runs the release profile:

name: Release
on:
  workflow_dispatch:
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'
          cache: maven
          server-id: central
          server-username: CENTRAL_USERNAME
          server-password: CENTRAL_PASSWORD
          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
          gpg-passphrase: MAVEN_GPG_PASSPHRASE
      - run: mvn -B -Prelease clean deploy
        env:
          CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
          CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}

Four repository secrets make it work, and nothing is ever hardcoded:

SecretWhere it comes from
CENTRAL_USERNAMECentral Portal user token (username)
CENTRAL_PASSWORDCentral Portal user token (password)
GPG_PRIVATE_KEYgpg --armor --export-secret-keys
GPG_PASSPHRASEthe passphrase I set when generating the key

The Central Portal user token is not your login — you generate it under your account settings and it gives you a username/password pair. One gotcha I hit: each token is a single matched pair, and generating a new one revokes the old one, so don't mix the username from one token with the password from another.

Pressing the button

With main green and the secrets in place, I triggered the release:

gh workflow run release.yml --ref main

The build compiled, ran the tests, signed every artifact, uploaded the bundle, and waited for Central to validate it. A few minutes later the workflow went green — and the files were already serving from the mirror:

https://repo1.maven.org/maven2/io/github/usmanovmahmudkhan/campus-auth-java/0.1.0/

pom, jar, -sources.jar, -javadoc.jar, and the .asc signatures all returning 200. That's the moment it became real: anyone in the world can now add three lines to their pom.xml and pull in my code.

Lessons for my next package

  • The code is the easy part. Packaging, signing, and metadata took longer than the library itself.
  • Keep signing in a profile so mvn verify doesn't need a GPG key locally or in CI.
  • io.github.<username> is the fastest namespace to verify when you don't own a domain.
  • waitUntil=published turns a green build into a trustworthy signal.
  • Search lag is normal. The artifact was downloadable immediately, but search.maven.org, mvnrepository.com, and Google take hours to days to index it. Don't panic when you can't Google it yet.

If you want to try it, it's one dependency away:

<dependency>
  <groupId>io.github.usmanovmahmudkhan</groupId>
  <artifactId>campus-auth-java</artifactId>
  <version>0.1.0</version>
</dependency>

Thanks for reading — this was my first Maven Central package, and definitely not my last.