Skip to content
Merged
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
152 changes: 84 additions & 68 deletions .github/workflows/maven-central-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ jobs:
# Required to create build-provenance attestations for the released jars.
id-token: write
attestations: write

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Set up JDK 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
Expand All @@ -48,7 +48,7 @@ jobs:
server-password: MAVEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE

- name: Determine version
id: version
run: |
Expand All @@ -58,6 +58,11 @@ jobs:
# to a manual input typo.
CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)

# Strip any trailing whitespace/carriage returns that Maven may emit
# on CI runners where stdout can contain extra control characters.
CURRENT_VERSION="${CURRENT_VERSION//$'\r'/}"
CURRENT_VERSION="${CURRENT_VERSION// /}"

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
Expand All @@ -67,14 +72,14 @@ jobs:
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
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z+.-]+)?$ ]]; then
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 (derived from POM snapshot $CURRENT_VERSION)"

- name: Update POM versions
run: |
VERSION=${{ steps.version.outputs.VERSION }}
Expand All @@ -96,7 +101,7 @@ jobs:
echo "Setting project.build.outputTimestamp to $TIMESTAMP for release $VERSION"
# Pin archive entry timestamps to the release moment for reproducible builds.
mvn versions:set-property -Dproperty=project.build.outputTimestamp -DnewVersion="$TIMESTAMP" -DgenerateBackupPoms=false -pl core,processor

- name: Commit version changes
id: commit
run: |
Expand All @@ -110,31 +115,31 @@ jobs:
# direct push that bypasses branch protection.
git checkout -B "$BRANCH"
git add pom.xml core/pom.xml processor/pom.xml example/pom.xml example-custom-generator/pom.xml
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"

# Only commit if there are changes. Note: no "[skip ci]" so the
# release branch / PR is validated by CI.
if git diff --staged --quiet; then
echo "No version changes detected - version already set to $VERSION"
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
git commit -m "chore: release version $VERSION"
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi

- name: Create Git tag
run: |
VERSION=${{ steps.version.outputs.VERSION }}
git config user.name "GitHub Actions"
git config user.email "actions@github.com"

# Only create tag if it doesn't exist
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists"
else
git tag -a "v$VERSION" -m "Release version $VERSION"
fi

- name: Build and verify
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSWORD }}
Expand All @@ -145,7 +150,7 @@ jobs:
# Emit a CycloneDX SBOM (JSON + XML) for each published module.
mvn -B -Prelease -pl core,processor \
org.cyclonedx:cyclonedx-maven-plugin:makeBom -DskipTests

- name: Deploy (stage) to Maven Central
env:
MAVEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
Expand All @@ -171,40 +176,18 @@ jobs:
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()
env:
HAS_CHANGES: ${{ steps.commit.outputs.has_changes }}
run: |
VERSION=${{ steps.version.outputs.VERSION }}
BRANCH="${{ steps.commit.outputs.branch }}"

# Push the release branch (never push directly to main).
if [ "${{ steps.commit.outputs.has_changes }}" = "true" ]; then
# Push the release branch with the release-version commit (commit 1).
# This happens BEFORE the next-snapshot step so the tag and branch are
# on the remote even if the next-snapshot step fails.
if [ "$HAS_CHANGES" = "true" ]; then
git push origin "$BRANCH" --force-with-lease
fi

Expand All @@ -215,34 +198,13 @@ jobs:
echo "Tag v$VERSION already exists remotely"
fi

- name: Open version bump pull request
if: success() && steps.commit.outputs.has_changes == 'true'
env:
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
# preserves branch protection and required reviews on main.
if gh pr view "$BRANCH" >/dev/null 2>&1; then
echo "Pull request for $BRANCH already exists"
else
gh pr create \
--base main \
--head "$BRANCH" \
--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
if: success()
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION=${{ steps.version.outputs.VERSION }}

# Check if release already exists
if gh release view "v$VERSION" >/dev/null 2>&1; then
echo "Release v$VERSION already exists"
Expand All @@ -251,17 +213,19 @@ jobs:
if [[ "$VERSION" == *"-"* ]]; then
PRERELEASE_FLAG="--prerelease"
fi

# Generate release notes and replace version placeholder
gh api repos/java-helpers/simple-builders/releases/generate-notes \
-f tag_name="v$VERSION" \
-f target_commitish="main" \
--jq '.body' > release_notes.md

# Replace version placeholder in generated notes
sed -i "s/\$RESOLVED_VERSION/$VERSION/g" release_notes.md

# Create draft release with custom notes and attach artifacts

# Create draft release with custom notes and attach artifacts.
# The jars in target/ were built at the release version (before the
# next-snapshot bump) and are not affected by POM-only changes.
gh release create "v$VERSION" \
--title "Release $VERSION" \
--draft \
Expand All @@ -277,7 +241,59 @@ jobs:
processor/target/simple-builders-processor-${VERSION}-javadoc.jar \
processor/target/simple-builders-processor-${VERSION}-sbom.json \
processor/target/simple-builders-processor-${VERSION}-sbom.xml

# Clean up
rm -f release_notes.md
fi

- 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 and pushed on the first commit) is unaffected by this 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"

# Force-push the updated branch so the remote has both commits.
# --force-with-lease is safe: it only succeeds if the remote branch is
# at the commit we just pushed in the earlier "Push release branch" step.
git push origin "release/v$VERSION" --force-with-lease

- name: Open version bump pull request
if: success() && steps.commit.outputs.has_changes == 'true'
env:
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
# preserves branch protection and required reviews on main.
if gh pr view "$BRANCH" >/dev/null 2>&1; then
echo "Pull request for $BRANCH already exists"
else
gh pr create \
--base main \
--head "$BRANCH" \
--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
Loading