diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml index f2540533..c625b51e 100644 --- a/.github/workflows/maven-central-release.yml +++ b/.github/workflows/maven-central-release.yml @@ -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: @@ -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: | @@ -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 @@ -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 }} @@ -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: | @@ -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 }} @@ -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 }} @@ -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 @@ -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" @@ -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 \ @@ -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