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
86 changes: 60 additions & 26 deletions .github/workflows/maven-central-release.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
# GitHub Action workflow for releasing artifacts to Maven Central.
# Triggered manually via workflow_dispatch with an explicit release_version
# input (there is no tag trigger). It handles:
# - Version management from the release_version input (not from git tags)
# Triggered manually via workflow_dispatch (no inputs; there is no tag trigger).
# The release version is derived from the current POM snapshot version by
# stripping the -SNAPSHOT suffix, so main must always be on a -SNAPSHOT version.
# It handles:
# - Version derivation from the POM (not from a manual input or git tags)
# - GPG signing of artifacts
# - Deployment to Maven Central via Sonatype
# - Next-snapshot version bump committed on the release branch

name: Release to Maven Central

on:
workflow_dispatch:
inputs:
release_version:
description: 'Release version (e.g., 0.2.0)'
required: true
type: string

# Serialize releases: never run two release jobs at once, and never cancel an
# in-progress release (cancelling mid-deploy could leave a partial publish).
Expand Down Expand Up @@ -53,34 +51,43 @@ jobs:

- name: Determine version
id: version
env:
RELEASE_VERSION: ${{ github.event.inputs.release_version }}
run: |
# Do NOT interpolate the workflow input directly into the shell;
# read the untrusted input from an environment variable instead to
# avoid script injection.
VERSION="$RELEASE_VERSION"
# Read the current version from the root POM. The POM must be on a
# -SNAPSHOT version; the release version is derived by stripping the
# suffix. This eliminates the risk of releasing the wrong version due
# to a manual input typo.
CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)

if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then
echo "::error::Current POM version is '$CURRENT_VERSION', expected a -SNAPSHOT version. main must always be on a snapshot version."
exit 1
fi

# Strip the -SNAPSHOT suffix to derive the release version.
VERSION="${CURRENT_VERSION%-SNAPSHOT}"

# Validate against a strict semver pattern before using it anywhere.
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid release version '$VERSION'. Expected semver, e.g. 0.2.0 or 0.2.0-beta.1"
echo "::error::Derived release version '$VERSION' is not valid semver. Expected e.g. 0.2.0 or 0.2.0-beta.1"
exit 1
fi

echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing version: $VERSION"
echo "Releasing version: $VERSION (derived from POM snapshot $CURRENT_VERSION)"

- name: Update POM versions
run: |
VERSION=${{ steps.version.outputs.VERSION }}
echo "Setting version to $VERSION"

# Update parent POM
mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false

# Update module POMs
mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -pl core
mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -pl processor

# Update ALL module POMs. The modules have no <parent> element, so
# each must be set individually. Only core and processor are deployed
# to Maven Central, but all modules must share the same version for
# the reactor build to work correctly.
mvn versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -pl core,processor,example,example-custom-generator

- name: Update reproducible build timestamp
run: |
Expand All @@ -102,7 +109,7 @@ jobs:
# update to main goes through a reviewed pull request instead of a
# direct push that bypasses branch protection.
git checkout -B "$BRANCH"
git add pom.xml core/pom.xml processor/pom.xml
git add pom.xml core/pom.xml processor/pom.xml example/pom.xml example-custom-generator/pom.xml
echo "branch=$BRANCH" >> $GITHUB_OUTPUT

# Only commit if there are changes. Note: no "[skip ci]" so the
Expand Down Expand Up @@ -163,7 +170,33 @@ jobs:
subject-path: |
core/target/simple-builders-core-*.jar
processor/target/simple-builders-processor-*.jar


- name: Prepare next snapshot version
id: next_version
if: success() && steps.commit.outputs.has_changes == 'true'
run: |
VERSION=${{ steps.version.outputs.VERSION }}

# Compute the next snapshot version by incrementing the minor version
# and resetting the patch to 0. For example:
# 0.5.0 -> 0.6.0-SNAPSHOT
# 0.6.0 -> 0.7.0-SNAPSHOT
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
NEXT_MINOR=$((MINOR + 1))
NEXT_VERSION="${MAJOR}.${NEXT_MINOR}.0-SNAPSHOT"
echo "NEXT_VERSION=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
echo "Next snapshot version: $NEXT_VERSION"

# Set all POMs to the next snapshot version.
mvn versions:set -DnewVersion=$NEXT_VERSION -DgenerateBackupPoms=false
mvn versions:set -DnewVersion=$NEXT_VERSION -DgenerateBackupPoms=false -pl core,processor,example,example-custom-generator

# Commit as a second commit on the release branch. The tag (already
# created on the first commit) is unaffected by this second commit.
git add pom.xml core/pom.xml processor/pom.xml example/pom.xml example-custom-generator/pom.xml
git commit -m "chore: prepare next version $NEXT_VERSION"

- name: Push release branch and tag
if: success()
run: |
Expand All @@ -172,7 +205,7 @@ jobs:

# Push the release branch (never push directly to main).
if [ "${{ steps.commit.outputs.has_changes }}" = "true" ]; then
git push origin "$BRANCH"
git push origin "$BRANCH" --force-with-lease
fi

# Push tag if it was newly created (check if tag exists remotely)
Expand All @@ -188,6 +221,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
VERSION=${{ steps.version.outputs.VERSION }}
NEXT_VERSION="${{ steps.next_version.outputs.NEXT_VERSION }}"
BRANCH="${{ steps.commit.outputs.branch }}"

# Open a reviewed PR to update main instead of pushing to it. This
Expand All @@ -198,8 +232,8 @@ jobs:
gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: release version $VERSION" \
--body "Automated version bump for release v$VERSION. Review and merge to update \`main\`; created via PR so branch protection is not bypassed."
--title "chore: release $VERSION and prepare $NEXT_VERSION" \
--body "Automated release of v$VERSION plus next-snapshot bump to \`$NEXT_VERSION\`. Review and merge to update \`main\`; created via PR so branch protection is not bypassed."
fi

- name: Create GitHub Release
Expand Down
34 changes: 21 additions & 13 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ Configure these GitHub secrets in **Settings** → **Secrets and variables** →

## How to Release

**Actions** → **Release to Maven Central** → **Run workflow** → Enter version (e.g., `0.2.0`)
**Actions** → **Release to Maven Central** → **Run workflow**

No version input is required. The workflow derives the release version from the current POM by stripping the `-SNAPSHOT` suffix (e.g., `0.5.0-SNAPSHOT` → release `0.5.0`). The `main` branch must always be on a `-SNAPSHOT` version.

### What the Workflow Does

1. Updates POM versions to release version
2. Commits changes and creates tag `v0.2.0`
3. Builds and verifies project with `-Prelease` (reproducible builds via `project.build.outputTimestamp`)
4. Signs artifacts with GPG
5. Generates a CycloneDX **SBOM** (JSON + XML) for each published module
6. Runs tests and **stages** the deployment to the Sonatype Central portal (does **not** auto-publish)
7. Creates a **build-provenance attestation** for the published jars
8. Pushes commit and tag to `main`
9. Creates **draft** GitHub release (jars, sources, javadoc **and SBOMs** attached; requires manual publish)
1. Derives the release version from the current POM snapshot version (strips `-SNAPSHOT`)
2. Updates POM versions to the release version in **all** modules (core, processor, example, example-custom-generator)
3. Commits the version bump on a dedicated `release/vX.Y.Z` branch and creates tag `vX.Y.Z`
4. Builds and verifies the project with `-Prelease` (reproducible builds via `project.build.outputTimestamp`)
5. Signs artifacts with GPG
6. Generates a CycloneDX **SBOM** (JSON + XML) for each published module
7. Runs tests and **stages** the deployment to the Sonatype Central portal (does **not** auto-publish)
8. Creates a **build-provenance attestation** for the published jars
9. Bumps all module versions to the next snapshot (e.g., `0.6.0-SNAPSHOT` after releasing `0.5.0`) and commits it as a second commit on the release branch
10. Pushes the release branch and tag (never pushes directly to `main`)
11. Opens a **pull request** against `main` containing both the release version commit and the next-snapshot bump
12. Creates **draft** GitHub release (jars, sources, javadoc **and SBOMs** attached; requires manual publish)

## After Release

Expand All @@ -38,9 +43,10 @@ Configure these GitHub secrets in **Settings** → **Secrets and variables** →
<dependency>
<groupId>io.github.java-helpers</groupId>
<artifactId>simple-builders-core</artifactId>
<version>0.2.0</version>
<version>0.5.0</version>
</dependency>
```
5. **Merge the version-bump pull request**: Review and merge the PR opened by the workflow. This updates `main` with both the release version commit (tagged `vX.Y.Z`) and the next-snapshot bump (e.g., `0.6.0-SNAPSHOT`), so development can continue.

## Local Release (Advanced)

Expand Down Expand Up @@ -74,7 +80,9 @@ Each release produces, in addition to the GPG-signed jars:
## Notes

- Project uses [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PATCH)
- The `main` branch must always be on a `-SNAPSHOT` version; the release version is derived automatically
- After each release, all modules are bumped to the next minor snapshot (e.g., `0.5.0` → `0.6.0-SNAPSHOT`)
- Versions with `-` (e.g., `0.2.0-beta`) are marked as pre-releases
- Releases are **staged** to the Sonatype Central portal and require a **manual publish** step; nothing is auto-released
- Both `core` and `processor` modules are published to Maven Central
- The `example` module is excluded from releases
- Only `core` and `processor` modules are published to Maven Central
- The `example` and `example-custom-generator` modules are version-updated alongside the released modules but are **not** deployed to Maven Central
2 changes: 1 addition & 1 deletion example-custom-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ To use the custom generator in your project:
<version>${project.version}</version>
</path>
<path>
<groupId>org.javahelpers.simple.builders.example</groupId>
<groupId>org.javahelpers.simple.builders</groupId>
<artifactId>example-custom-generator</artifactId>
<version>${project.version}</version>
</path>
Expand Down
2 changes: 1 addition & 1 deletion example-custom-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.javahelpers.simple.builders.example</groupId>
<groupId>org.javahelpers.simple.builders</groupId>
<artifactId>example-custom-generator</artifactId>
<version>0.5.0-SNAPSHOT</version>
<name>Simple Builders - Example Custom Generator</name>
Expand Down
4 changes: 2 additions & 2 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.github.java-helpers</groupId>
<groupId>org.javahelpers.simple.builders</groupId>
<artifactId>simple-builders-example</artifactId>
<version>0.5.0-SNAPSHOT</version>
<name>Simple Builders - Example</name>
Expand Down Expand Up @@ -96,7 +96,7 @@
<version>${project.version}</version>
</path>
<path>
<groupId>org.javahelpers.simple.builders.example</groupId>
<groupId>org.javahelpers.simple.builders</groupId>
<artifactId>example-custom-generator</artifactId>
<version>${project.version}</version>
</path>
Expand Down
Loading