From 0ef5f8ca82d00c055737ef2c52a189d34c78acea Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Sun, 28 Jun 2026 06:34:01 -0500 Subject: [PATCH 1/3] 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 Claude-Session: https://claude.ai/code/session_01DGSbyBPTxsj73xhYpnxcse --- .github/actions/jdk-setup/action.yml | 36 ++++++++ .github/workflows/release.yml | 43 ++++++++++ README.adoc | 18 +++- src/site/asciidoc/releasing.adoc | 123 ++++++++++++++++----------- 4 files changed, 170 insertions(+), 50 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/actions/jdk-setup/action.yml b/.github/actions/jdk-setup/action.yml index 65f7e7e..e81775f 100644 --- a/.github/actions/jdk-setup/action.yml +++ b/.github/actions/jdk-setup/action.yml @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..91f6509 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.adoc b/README.adoc index 0225864..b357d69 100644 --- a/README.adoc +++ b/README.adoc @@ -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`) @@ -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`. diff --git a/src/site/asciidoc/releasing.adoc b/src/site/asciidoc/releasing.adoc index ebf649f..17c5868 100644 --- a/src/site/asciidoc/releasing.adoc +++ b/src/site/asciidoc/releasing.adoc @@ -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 <>): + +* 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 <>) +* 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 `` entry whose `` 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] ---- - - - - central-publish - YOUR_CENTRAL_USERNAME - YOUR_CENTRAL_TOKEN - - - +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): [source,bash] ---- @@ -67,7 +95,7 @@ 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] ---- @@ -75,42 +103,39 @@ If preparation fails for any reason, roll back cleanly: ---- 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, @@ -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: @@ -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. From d8e6f6f4e8325d8d884d82d7996493060c0a4220 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Sun, 28 Jun 2026 06:48:47 -0500 Subject: [PATCH 2/3] ci: Collapse deploy-snapshot JDK setup into the jdk-setup action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The jdk-setup composite action now accepts Maven Central credentials, so deploy-snapshot no longer needs a separate setup-java step after it. Pass the server inputs to jdk-setup and drop the redundant second JDK setup. Behavior is unchanged — snapshots are unsigned, so no GPG inputs are needed, and the action still configures JDK 21 with Maven caching. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DGSbyBPTxsj73xhYpnxcse --- .github/workflows/deploy-snapshot.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/deploy-snapshot.yml b/.github/workflows/deploy-snapshot.yml index 0088ddb..f0a03b9 100644 --- a/.github/workflows/deploy-snapshot.yml +++ b/.github/workflows/deploy-snapshot.yml @@ -18,10 +18,7 @@ jobs: steps: - uses: actions/checkout@v7 - 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 From f7b9f2f499faa74b769a034e835f51498924fdcf Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Sun, 28 Jun 2026 06:57:05 -0500 Subject: [PATCH 3/3] ci: Harden deploy-snapshot with least-privilege token, concurrency, and pinned checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring deploy-snapshot in line with core's workflow hygiene: * permissions: contents: read — the job only reads the repo and deploys externally, so it needs no write scope. * concurrency group snapshot-deploy (cancel-in-progress: false) — serialize snapshot deploys so two runs cannot race to publish to Central. * checkout the exact commit that passed CI (workflow_run head_sha) and stop persisting the GITHUB_TOKEN in the local git config. source:jar-no-fork is intentionally not adopted: the archetype has no Java sources, so a sources jar would be empty and Central does not require one for maven-archetype artifacts. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DGSbyBPTxsj73xhYpnxcse --- .github/workflows/deploy-snapshot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/deploy-snapshot.yml b/.github/workflows/deploy-snapshot.yml index f0a03b9..7d51647 100644 --- a/.github/workflows/deploy-snapshot.yml +++ b/.github/workflows/deploy-snapshot.yml @@ -6,10 +6,17 @@ 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' @@ -17,6 +24,10 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + persist-credentials: false + - uses: ./.github/actions/jdk-setup with: server-id: central-publish