Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/actions/jdk-setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
name: "JDK Setup"
description: "Set up Temurin JDK 21 with Maven dependency caching"
inputs:
server-id:
description: "Maven settings.xml server id for Maven Central authentication."
required: false
default: ''
server-username:
description: "Environment variable name for the Maven Central username."
required: false
default: ''
server-password:
description: "Environment variable name for the Maven Central password."
required: false
default: ''
gpg-private-key:
description: "GPG private key to import for artifact signing."
required: false
default: ''
gpg-passphrase:
description: "Environment variable name for the GPG passphrase."
required: false
default: ''
runs:
using: "composite"
steps:
- name: Set up JDK 21
if: ${{ inputs.server-id == '' }}
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: maven

- name: Set up JDK 21 with Maven Central credentials
if: ${{ inputs.server-id != '' }}
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: maven
server-id: ${{ inputs.server-id }}
server-username: ${{ inputs.server-username }}
server-password: ${{ inputs.server-password }}
gpg-private-key: ${{ inputs.gpg-private-key }}
gpg-passphrase: ${{ inputs.gpg-passphrase }}
14 changes: 11 additions & 3 deletions .github/workflows/deploy-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,30 @@ on:
types: [completed]
branches: [main]

permissions:
contents: read

env:
MAVEN_COMMAND: ./mvnw
MAVEN_CLI_COMMON: "-e -B"

concurrency:
group: snapshot-deploy
cancel-in-progress: false

jobs:
deploy-snapshot:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- uses: ./.github/actions/jdk-setup
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
server-id: central-publish
server-username: CENTRAL_USERNAME
server-password: CENTRAL_TOKEN
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release to Maven Central

on:
push:
tags: ['v*'] # created by maven-release-plugin (tagNameFormat v@{project.version})

permissions:
contents: read

concurrency:
group: release-deploy
cancel-in-progress: false

env:
MAVEN_COMMAND: ./mvnw
MAVEN_CLI_COMMON: "-e -B"

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 120
environment: release
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: ./.github/actions/jdk-setup
with:
server-id: central-publish
server-username: CENTRAL_USERNAME
server-password: CENTRAL_TOKEN
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE

# Tests already ran during release:prepare; skip surefire and the archetype
# integration tests (archetype.test.skip — -DskipTests does not cover them).
- name: Deploy release to Maven Central
env:
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} deploy -Prelease -DskipTests -Darchetype.test.skip=true
18 changes: 17 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ See the https://jeffjensen.github.io/java-service-archetype/[project site] for t

== CI / CD

Three GitHub Actions workflows are included.
Four GitHub Actions workflows are included.

=== Build any branch (`build-any-branch.yml`)

Expand All @@ -45,6 +45,22 @@ Required repository secrets:
|`CENTRAL_TOKEN` |Maven Central portal token
|===

=== Release (`release.yml`)

Triggered when `maven-release-plugin` pushes a `v*` tag.
Runs in a `release` environment and deploys a GPG-signed release to Maven Central via `./mvnw deploy -Prelease`.
See the https://jeffjensen.github.io/java-service-archetype/releasing.html[Releasing] guide for the full procedure and one-time setup.

Required repository secrets, in addition to `CENTRAL_USERNAME` and `CENTRAL_TOKEN` above:

[cols="1,2"]
|===
|Secret |Description

|`GPG_PRIVATE_KEY` |ASCII-armored GPG private key for artifact signing
|`GPG_PASSPHRASE` |Passphrase for the signing key
|===

=== Publish site (`publish-docs.yml`)

Triggered in three ways: when the build workflow succeeds on `main`; on any direct push to `main` that touches doc files (`.adoc`, `.md`, `src/site/**`); or manually via `workflow_dispatch`.
Expand Down
123 changes: 74 additions & 49 deletions src/site/asciidoc/releasing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,70 @@
:toc-title: Contents
:toclevels: 2

Releases are published to Maven Central using the Maven Release Plugin.
`release:perform` invokes `mvn deploy`, which the `central-publishing-maven-plugin` intercepts
to upload, validate, and publish the artifact automatically.
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-config>>):

* 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
* All tests green: `./mvnw verify`
* GPG key available in your local keyring for artifact signing
* Maven settings with Central credentials (see <<settings-xml>>)
* 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-config]]
== 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:

[cols="1,2"]
|===
|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 https://central.sonatype.com[central.sonatype.com] under Account → Generate User Token.

[[settings-xml]]
== `~/.m2/settings.xml`
=== `release` environment

The deploy needs a `<server>` entry whose `<id>` matches `publishingServerId` in the POM (`central-publish`):
`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.

[source,xml]
=== 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:

[source,bash]
----
<settings>
<servers>
<server>
<id>central-publish</id>
<username>YOUR_CENTRAL_USERNAME</username>
<password>YOUR_CENTRAL_TOKEN</password>
</server>
</servers>
</settings>
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
----

Obtain credentials from https://central.sonatype.com[central.sonatype.com] under Account → Generate User Token.

== Step 1 — Prepare
== 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:
next development version, and pushes all commits and the tag (`pushChanges` defaults to true):
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[source,bash]
----
Expand Down Expand Up @@ -67,50 +95,47 @@ To run non-interactively:
-B
----

If preparation fails for any reason, roll back cleanly:
If preparation fails before it pushes, roll back cleanly:

[source,bash]
----
./mvnw release:rollback
----

This reverses the release-version POM commit, resets the version back to the development snapshot, and removes the local tag.
If `release:prepare` already pushed commits and the tag to the remote, delete the remote tag manually:

[source,bash]
----
git push --delete origin v1.0.0
----

== Step 2 — Perform
== Step 2 — CI Signs and Deploys

`release:perform` checks out the tagged commit into `target/checkout`, builds it with the
`release` profile active (which enables GPG signing), and runs `mvn deploy`:
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:

[source,bash]
----
./mvnw release:perform
./mvnw deploy -Prelease -DskipTests -Darchetype.test.skip=true
----

The `central-publishing-maven-plugin` uploads the signed bundle to Maven Central,
waits for validation, and publishes automatically (`autoPublish=true`, `waitUntil=published`).
Expect this step to take a few minutes.
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.

== GPG Signing
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).

Maven Central requires all release artifacts to be signed.
The `release` profile (activated automatically by `release:perform` via `releaseProfiles=release`)
runs `maven-gpg-plugin` during the `verify` phase.
You do not run `release:perform` — CI performs the deploy.

If your GPG key requires a passphrase and you are not using an agent, pass it on the command line:
=== 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`:

[source,bash]
----
./mvnw release:perform -Dgpg.passphrase=YOUR_PASSPHRASE
git push --delete origin v1.0.0
----

Using `gpg-agent` is the preferred approach — it avoids exposing the passphrase in shell history.

== Reproducible Builds

`project.build.outputTimestamp` in the POM pins the timestamp embedded in JAR manifests,
Expand All @@ -122,9 +147,9 @@ byte-for-byte verification impossible.
automatically before committing the release POM, so released artifacts carry the exact
timestamp of the release commit.

During development the property holds the initial project creation date
(`2026-06-07T00:00:00Z`), ensuring that `./mvnw verify` is reproducible regardless of
when the build runs.
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:

Expand All @@ -136,5 +161,5 @@ To verify build-plan reproducibility locally:
== Step 3 — After the Release

. Confirm the artifact appears on https://central.sonatype.com[Maven Central] (typically within minutes).
. Update the `archetypeVersion` in the link:index.html[Quick Start], the link:usage.html[Usage] examples, and the `README.adoc` `archetype:generate` command to the released version.
. Create a GitHub Release from the tag, summarising the changes.
. `main` now carries the next `-SNAPSHOT`; update any docs or examples that reference a specific archetype version.
Loading