Skip to content

Commit 81fcab1

Browse files
jeffjensenclaude
andcommitted
ci: Add Maven Central release workflow triggered by version tags
* Add release.yml: on a v* tag (created by maven-release-plugin), set up the JDK with Central credentials and the GPG signing key, then run deploy -Prelease to sign and publish to Maven Central. Skip surefire and the archetype integration tests since release:prepare already ran them (-DskipTests does not cover archetype.test.skip). * Extend the jdk-setup composite action with optional server-id, credential, and GPG inputs so the release job can authenticate and sign artifacts; existing callers that pass no inputs are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DGSbyBPTxsj73xhYpnxcse
1 parent 5e54554 commit 81fcab1

4 files changed

Lines changed: 170 additions & 50 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
11
name: "JDK Setup"
2+
description: "Set up Temurin JDK 21 with Maven dependency caching"
3+
inputs:
4+
server-id:
5+
description: "Maven settings.xml server id for Maven Central authentication."
6+
required: false
7+
default: ''
8+
server-username:
9+
description: "Environment variable name for the Maven Central username."
10+
required: false
11+
default: ''
12+
server-password:
13+
description: "Environment variable name for the Maven Central password."
14+
required: false
15+
default: ''
16+
gpg-private-key:
17+
description: "GPG private key to import for artifact signing."
18+
required: false
19+
default: ''
20+
gpg-passphrase:
21+
description: "Environment variable name for the GPG passphrase."
22+
required: false
23+
default: ''
224
runs:
325
using: "composite"
426
steps:
527
- name: Set up JDK 21
28+
if: ${{ inputs.server-id == '' }}
629
uses: actions/setup-java@v5
730
with:
831
java-version: '21'
932
distribution: 'temurin'
1033
cache: maven
34+
35+
- name: Set up JDK 21 with Maven Central credentials
36+
if: ${{ inputs.server-id != '' }}
37+
uses: actions/setup-java@v5
38+
with:
39+
java-version: '21'
40+
distribution: 'temurin'
41+
cache: maven
42+
server-id: ${{ inputs.server-id }}
43+
server-username: ${{ inputs.server-username }}
44+
server-password: ${{ inputs.server-password }}
45+
gpg-private-key: ${{ inputs.gpg-private-key }}
46+
gpg-passphrase: ${{ inputs.gpg-passphrase }}

.github/workflows/release.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release to Maven Central
2+
3+
on:
4+
push:
5+
tags: ['v*'] # created by maven-release-plugin (tagNameFormat v@{project.version})
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: release-deploy
12+
cancel-in-progress: false
13+
14+
env:
15+
MAVEN_COMMAND: ./mvnw
16+
MAVEN_CLI_COMMON: "-e -B"
17+
18+
jobs:
19+
release:
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 120
22+
environment: release
23+
steps:
24+
- uses: actions/checkout@v7
25+
with:
26+
persist-credentials: false
27+
28+
- uses: ./.github/actions/jdk-setup
29+
with:
30+
server-id: central-publish
31+
server-username: CENTRAL_USERNAME
32+
server-password: CENTRAL_TOKEN
33+
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
34+
gpg-passphrase: MAVEN_GPG_PASSPHRASE
35+
36+
# Tests already ran during release:prepare; skip surefire and the archetype
37+
# integration tests (archetype.test.skip — -DskipTests does not cover them).
38+
- name: Deploy release to Maven Central
39+
env:
40+
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
41+
CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }}
42+
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
43+
run: ${{ env.MAVEN_COMMAND }} ${{ env.MAVEN_CLI_COMMON }} deploy -Prelease -DskipTests -Darchetype.test.skip=true

README.adoc

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ See the https://jeffjensen.github.io/java-service-archetype/[project site] for t
2222

2323
== CI / CD
2424

25-
Three GitHub Actions workflows are included.
25+
Four GitHub Actions workflows are included.
2626

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

@@ -45,6 +45,22 @@ Required repository secrets:
4545
|`CENTRAL_TOKEN` |Maven Central portal token
4646
|===
4747

48+
=== Release (`release.yml`)
49+
50+
Triggered when `maven-release-plugin` pushes a `v*` tag.
51+
Runs in a `release` environment and deploys a GPG-signed release to Maven Central via `./mvnw deploy -Prelease`.
52+
See the https://jeffjensen.github.io/java-service-archetype/releasing.html[Releasing] guide for the full procedure and one-time setup.
53+
54+
Required repository secrets, in addition to `CENTRAL_USERNAME` and `CENTRAL_TOKEN` above:
55+
56+
[cols="1,2"]
57+
|===
58+
|Secret |Description
59+
60+
|`GPG_PRIVATE_KEY` |ASCII-armored GPG private key for artifact signing
61+
|`GPG_PASSPHRASE` |Passphrase for the signing key
62+
|===
63+
4864
=== Publish site (`publish-docs.yml`)
4965

5066
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`.

src/site/asciidoc/releasing.adoc

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,70 @@
33
:toc-title: Contents
44
:toclevels: 2
55

6-
Releases are published to Maven Central using the Maven Release Plugin.
7-
`release:perform` invokes `mvn deploy`, which the `central-publishing-maven-plugin` intercepts
8-
to upload, validate, and publish the artifact automatically.
6+
Releases are published to Maven Central from CI.
7+
You run one command locally to tag the release; pushing that tag triggers the
8+
`release.yml` workflow, which signs and deploys the artifact.
9+
Driven by the Maven Release Plugin and the `central-publishing-maven-plugin`
10+
(`autoPublish=true`, `waitUntil=published`).
911

1012
== Prerequisites
1113

14+
One-time GitHub configuration (see <<github-config>>):
15+
16+
* Repository secrets: `CENTRAL_USERNAME`, `CENTRAL_TOKEN`, `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE`
17+
* A `release` GitHub environment, optionally with a required reviewer to gate publishing
18+
19+
For each release:
20+
1221
* On the `main` branch, with a clean working tree — no uncommitted changes
13-
* All tests green: `./mvnw verify`
14-
* GPG key available in your local keyring for artifact signing
15-
* Maven settings with Central credentials (see <<settings-xml>>)
22+
* Network access — `release:prepare` runs `verify`, which builds a generated project for every integration test
23+
* `release.yml` must already be on `main` — a tag runs the workflow as it exists at the tagged commit
24+
25+
[[github-config]]
26+
== GitHub Configuration
27+
28+
The deploy runs in CI, so no local Maven `settings.xml` or GPG keyring is required —
29+
the credentials and signing key live in repository secrets.
30+
31+
=== Secrets
32+
33+
Add under repository Settings → Secrets and variables → Actions:
34+
35+
[cols="1,2"]
36+
|===
37+
|Secret |Description
38+
39+
|`CENTRAL_USERNAME` |Central Portal user-token name (also used by `deploy-snapshot.yml`)
40+
|`CENTRAL_TOKEN` |Central Portal user-token password
41+
|`GPG_PRIVATE_KEY` |ASCII-armored private signing key (the full `-----BEGIN PGP PRIVATE KEY BLOCK-----` block)
42+
|`GPG_PASSPHRASE` |Passphrase for the signing key
43+
|===
44+
45+
Obtain the Central credentials from https://central.sonatype.com[central.sonatype.com] under Account → Generate User Token.
1646

17-
[[settings-xml]]
18-
== `~/.m2/settings.xml`
47+
=== `release` environment
1948

20-
The deploy needs a `<server>` entry whose `<id>` matches `publishingServerId` in the POM (`central-publish`):
49+
`release.yml` runs in a `release` environment (Settings → Environments → New environment → `release`).
50+
Add yourself as a required reviewer to turn the tag-triggered run into a manual approval gate before anything publishes.
2151

22-
[source,xml]
52+
=== Signing key
53+
54+
Maven Central requires release artifacts to be GPG-signed, and the public key must be on a public keyserver.
55+
If you do not already have a published key:
56+
57+
[source,bash]
2358
----
24-
<settings>
25-
<servers>
26-
<server>
27-
<id>central-publish</id>
28-
<username>YOUR_CENTRAL_USERNAME</username>
29-
<password>YOUR_CENTRAL_TOKEN</password>
30-
</server>
31-
</servers>
32-
</settings>
59+
gpg --full-generate-key # RSA 4096, with a passphrase
60+
gpg --list-secret-keys --keyid-format=long # note the key id
61+
gpg --keyserver keyserver.ubuntu.com --send-keys KEYID # publish the public key
62+
gpg --armor --export-secret-keys KEYID # paste all output into GPG_PRIVATE_KEY
3363
----
3464

35-
Obtain credentials from https://central.sonatype.com[central.sonatype.com] under Account → Generate User Token.
36-
37-
== Step 1 — Prepare
65+
== Step 1 — Prepare and Tag
3866

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

4371
[source,bash]
4472
----
@@ -67,50 +95,47 @@ To run non-interactively:
6795
-B
6896
----
6997

70-
If preparation fails for any reason, roll back cleanly:
98+
If preparation fails before it pushes, roll back cleanly:
7199

72100
[source,bash]
73101
----
74102
./mvnw release:rollback
75103
----
76104

77105
This reverses the release-version POM commit, resets the version back to the development snapshot, and removes the local tag.
78-
If `release:prepare` already pushed commits and the tag to the remote, delete the remote tag manually:
79-
80-
[source,bash]
81-
----
82-
git push --delete origin v1.0.0
83-
----
84106

85-
== Step 2 — Perform
107+
== Step 2 — CI Signs and Deploys
86108

87-
`release:perform` checks out the tagged commit into `target/checkout`, builds it with the
88-
`release` profile active (which enables GPG signing), and runs `mvn deploy`:
109+
The pushed `v*` tag triggers the `release.yml` workflow.
110+
If you configured a required reviewer on the `release` environment, approve the run in the Actions tab.
111+
The workflow imports the signing key and Central credentials from the secrets, then runs:
89112

90113
[source,bash]
91114
----
92-
./mvnw release:perform
115+
./mvnw deploy -Prelease -DskipTests -Darchetype.test.skip=true
93116
----
94117

95-
The `central-publishing-maven-plugin` uploads the signed bundle to Maven Central,
96-
waits for validation, and publishes automatically (`autoPublish=true`, `waitUntil=published`).
97-
Expect this step to take a few minutes.
118+
The `release` profile activates `maven-gpg-plugin` to sign the artifact, and the
119+
`central-publishing-maven-plugin` uploads the signed bundle, waits for validation, and publishes
120+
automatically. Expect this step to take a few minutes.
98121

99-
== GPG Signing
122+
Tests are not re-run — `release:prepare` already verified them.
123+
`-DskipTests` skips the unit tests and `-Darchetype.test.skip=true` skips the archetype integration tests
124+
(which `-DskipTests` does not cover).
100125

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

105-
If your GPG key requires a passphrase and you are not using an agent, pass it on the command line:
128+
=== Aborting after the tag is pushed
129+
130+
`release:prepare` pushes immediately, so once the tag is on the remote the approval gate is the stop point.
131+
Reject the pending `release` deployment in the Actions tab so nothing publishes, then delete the remote tag
132+
and revert the two release commits on `main`:
106133

107134
[source,bash]
108135
----
109-
./mvnw release:perform -Dgpg.passphrase=YOUR_PASSPHRASE
136+
git push --delete origin v1.0.0
110137
----
111138

112-
Using `gpg-agent` is the preferred approach — it avoids exposing the passphrase in shell history.
113-
114139
== Reproducible Builds
115140

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

125-
During development the property holds the initial project creation date
126-
(`2026-06-07T00:00:00Z`), ensuring that `./mvnw verify` is reproducible regardless of
127-
when the build runs.
150+
Between releases the property holds a fixed timestamp — the last release's instant, or the
151+
initial project creation date before the first release — ensuring that `./mvnw verify` is
152+
reproducible regardless of when the build runs.
128153

129154
To verify build-plan reproducibility locally:
130155

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

138163
. Confirm the artifact appears on https://central.sonatype.com[Maven Central] (typically within minutes).
139-
. 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.
140164
. Create a GitHub Release from the tag, summarising the changes.
165+
. `main` now carries the next `-SNAPSHOT`; update any docs or examples that reference a specific archetype version.

0 commit comments

Comments
 (0)