From 96148d33db8e47d4974b58747f84c75cc8bf747d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 8 Aug 2026 15:00:14 +0200 Subject: [PATCH 1/3] Fix release process: derive version from POM, update all modules, bump next snapshot The release workflow had three issues that could cause incorrect releases or a broken reactor build: 1. The release version came from a manual workflow_dispatch input with no cross-check against the current POM version. A typo (e.g. entering 0.4.0 when the POM is at 0.5.0-SNAPSHOT) would release the wrong version. The input is now removed entirely; the workflow reads the POM version, verifies it is a -SNAPSHOT, and strips the suffix to derive the release version. 2. Only core and processor POMs were version-updated, but the reactor build (mvn verify with no -pl) includes example and example-custom-generator. The version mismatch could cause build failures or incorrect dependency resolution. All four modules are now updated together; only core and processor are still deployed to Maven Central. 3. After a release, main was left on the release version (e.g. 0.5.0) with no next-snapshot bump. A new "Prepare next snapshot version" step now computes the next minor snapshot (0.5.0 -> 0.6.0-SNAPSHOT), sets all POMs, and commits it as a second commit on the release branch. The git tag points to the first commit (release version), so it is unaffected. Both commits reach main via the same pull request. Additionally, the groupId of the example modules is unified to org.javahelpers.simple.builders (was io.github.java-helpers for example and org.javahelpers.simple.builders.example for example-custom-generator). Java package names are unchanged. RELEASE.md is updated to match the actual workflow behavior. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/maven-central-release.yml | 88 +++++++++++++++------ RELEASE.md | 34 +++++--- example-custom-generator/README.md | 2 +- example-custom-generator/pom.xml | 2 +- example/pom.xml | 4 +- 5 files changed, 87 insertions(+), 43 deletions(-) diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml index f342bc7f..3101b56e 100644 --- a/.github/workflows/maven-central-release.yml +++ b/.github/workflows/maven-central-release.yml @@ -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). @@ -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 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: | @@ -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 @@ -163,7 +170,35 @@ 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) + # Strip any pre-release suffix from PATCH for the computation. + PATCH=$(echo "$VERSION" | cut -d. -f3 | cut -d- -f1) + 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: | @@ -172,7 +207,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) @@ -188,6 +223,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 @@ -198,8 +234,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 diff --git a/RELEASE.md b/RELEASE.md index 8c5ecd27..9ed11a80 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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 @@ -38,9 +43,10 @@ Configure these GitHub secrets in **Settings** → **Secrets and variables** → io.github.java-helpers simple-builders-core - 0.2.0 + 0.5.0 ``` +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) @@ -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 diff --git a/example-custom-generator/README.md b/example-custom-generator/README.md index 986d3e7e..e03ed248 100644 --- a/example-custom-generator/README.md +++ b/example-custom-generator/README.md @@ -100,7 +100,7 @@ To use the custom generator in your project: ${project.version} - org.javahelpers.simple.builders.example + org.javahelpers.simple.builders example-custom-generator ${project.version} diff --git a/example-custom-generator/pom.xml b/example-custom-generator/pom.xml index a28f77b3..8aeb7421 100644 --- a/example-custom-generator/pom.xml +++ b/example-custom-generator/pom.xml @@ -2,7 +2,7 @@ 4.0.0 - org.javahelpers.simple.builders.example + org.javahelpers.simple.builders example-custom-generator 0.5.0-SNAPSHOT Simple Builders - Example Custom Generator diff --git a/example/pom.xml b/example/pom.xml index a1ad407a..0f13efa7 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ 4.0.0 - io.github.java-helpers + org.javahelpers.simple.builders simple-builders-example 0.5.0-SNAPSHOT Simple Builders - Example @@ -96,7 +96,7 @@ ${project.version} - org.javahelpers.simple.builders.example + org.javahelpers.simple.builders example-custom-generator ${project.version} From ad9cbfd3e183c51d0daa8ca9aff2967c5a482a53 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 8 Aug 2026 18:13:47 +0200 Subject: [PATCH 2/3] Removing dead code --- .github/workflows/maven-central-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml index 3101b56e..f2540533 100644 --- a/.github/workflows/maven-central-release.yml +++ b/.github/workflows/maven-central-release.yml @@ -183,8 +183,6 @@ jobs: # 0.6.0 -> 0.7.0-SNAPSHOT MAJOR=$(echo "$VERSION" | cut -d. -f1) MINOR=$(echo "$VERSION" | cut -d. -f2) - # Strip any pre-release suffix from PATCH for the computation. - PATCH=$(echo "$VERSION" | cut -d. -f3 | cut -d- -f1) NEXT_MINOR=$((MINOR + 1)) NEXT_VERSION="${MAJOR}.${NEXT_MINOR}.0-SNAPSHOT" echo "NEXT_VERSION=$NEXT_VERSION" >> "$GITHUB_OUTPUT" From fa6781bc72547b4d17c87b13d92f6ca6d1d0a373 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 9 Aug 2026 10:14:42 +0200 Subject: [PATCH 3/3] Fixing issues in release-pipeline --- .github/workflows/maven-central-release.yml | 134 +++++++++++--------- 1 file changed, 75 insertions(+), 59 deletions(-) diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml index f2540533..4512b260 100644 --- a/.github/workflows/maven-central-release.yml +++ b/.github/workflows/maven-central-release.yml @@ -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,7 +72,7 @@ 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 @@ -110,16 +115,16 @@ 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 @@ -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