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
releaseGitHub environment, optionally with a required reviewer to gate publishing
For each release:
- On the
mainbranch, with a clean working tree — no uncommitted changes - Network access —
release:preparerunsverify, which builds a generated project for every integration test release.ymlmust already be onmain— 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_KEYStep 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:prepareMaven 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 \
-BIf preparation fails before it pushes, roll back cleanly:
./mvnw release:rollbackThis 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=trueThe 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.0Reproducible 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-buildplanStep 3 — After the Release
- Confirm the artifact appears on Maven Central (typically within minutes).
- Create a GitHub Release from the tag, summarising the changes.
mainnow carries the next-SNAPSHOT; update any docs or examples that reference a specific archetype version.

