Releasing

Releases are published to Maven Central from CI. You run one command locally to tag the release; pushing that tag triggers the release.yml workflow, which signs and deploys the artifact. Driven by the Maven Release Plugin and the central-publishing-maven-plugin (autoPublish=true, waitUntil=published).

Prerequisites

One-time GitHub configuration (see GitHub Configuration):

  • Repository secrets: CENTRAL_USERNAME, CENTRAL_TOKEN, GPG_PRIVATE_KEY, GPG_PASSPHRASE
  • A release GitHub environment, optionally with a required reviewer to gate publishing

For each release:

  • On the main branch, with a clean working tree — no uncommitted changes
  • Network access — release:prepare runs verify, which builds a generated project for every integration test
  • release.yml must already be on main — a tag runs the workflow as it exists at the tagged commit

GitHub Configuration

The deploy runs in CI, so no local Maven settings.xml or GPG keyring is required — the credentials and signing key live in repository secrets.

Secrets

Add under repository Settings → Secrets and variables → Actions:

Secret Description
CENTRAL_USERNAME Central Portal user-token name (also used by deploy-snapshot.yml)
CENTRAL_TOKEN Central Portal user-token password
GPG_PRIVATE_KEY ASCII-armored private signing key (the full -----BEGIN PGP PRIVATE KEY BLOCK----- block)
GPG_PASSPHRASE Passphrase for the signing key

Obtain the Central credentials from central.sonatype.com under Account → Generate User Token.

release environment

release.yml runs in a release environment (Settings → Environments → New environment → release). Add yourself as a required reviewer to turn the tag-triggered run into a manual approval gate before anything publishes.

Signing key

Maven Central requires release artifacts to be GPG-signed, and the public key must be on a public keyserver. If you do not already have a published key:

gpg --full-generate-key                                  # RSA 4096, with a passphrase
gpg --list-secret-keys --keyid-format=long               # note the key id
gpg --keyserver keyserver.ubuntu.com --send-keys KEYID   # publish the public key
gpg --armor --export-secret-keys KEYID                   # paste all output into GPG_PRIVATE_KEY

Step 1 — Prepare and Tag

release:prepare validates the build, sets the release version, commits the POM change (including updating project.build.outputTimestamp), creates a git tag, increments to the next development version, and pushes all commits and the tag (pushChanges defaults to true):

./mvnw release:prepare

Maven prompts for three values:

Prompt Default Example
Release version current version without -SNAPSHOT 1.0.0
SCM tag v{releaseVersion} v1.0.0
Next development version patch increment + -SNAPSHOT 1.0.1-SNAPSHOT

To run non-interactively:

./mvnw release:prepare \
  -DreleaseVersion=1.0.0 \
  -Dtag=v1.0.0 \
  -DdevelopmentVersion=1.0.1-SNAPSHOT \
  -B

If preparation fails before it pushes, roll back cleanly:

./mvnw release:rollback

This reverses the release-version POM commit, resets the version back to the development snapshot, and removes the local tag.

Step 2 — CI Signs and Deploys

The pushed v* tag triggers the release.yml workflow. If you configured a required reviewer on the release environment, approve the run in the Actions tab. The workflow imports the signing key and Central credentials from the secrets, then runs:

./mvnw deploy -Prelease -DskipTests -Darchetype.test.skip=true

The release profile activates maven-gpg-plugin to sign the artifact, and the central-publishing-maven-plugin uploads the signed bundle, waits for validation, and publishes automatically. Expect this step to take a few minutes.

Tests are not re-run — release:prepare already verified them. -DskipTests skips the unit tests and -Darchetype.test.skip=true skips the archetype integration tests (which -DskipTests does not cover).

You do not run release:perform — CI performs the deploy.

Aborting after the tag is pushed

release:prepare pushes immediately, so once the tag is on the remote the approval gate is the stop point. Reject the pending release deployment in the Actions tab so nothing publishes, then delete the remote tag and revert the two release commits on main:

git push --delete origin v1.0.0

Reproducible Builds

project.build.outputTimestamp in the POM pins the timestamp embedded in JAR manifests, ZIP entries, and other build outputs. Without it, every build produces a different binary even from the same source, making byte-for-byte verification impossible.

release:prepare updates project.build.outputTimestamp to the current UTC instant automatically before committing the release POM, so released artifacts carry the exact timestamp of the release commit.

Between releases the property holds a fixed timestamp — the last release’s instant, or the initial project creation date before the first release — ensuring that ./mvnw verify is reproducible regardless of when the build runs.

To verify build-plan reproducibility locally:

./mvnw artifact:check-buildplan

Step 3 — After the Release

  1. Confirm the artifact appears on Maven Central (typically within minutes).
  2. Create a GitHub Release from the tag, summarising the changes.
  3. main now carries the next -SNAPSHOT; update any docs or examples that reference a specific archetype version.