Tag and Release #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tag and Release | |
| # Cuts the git tag and GitHub Release for a version, then (by default) chains | |
| # straight into the Maven Central publish. Each stage gates the next: the JDK | |
| # matrix must pass before the tag is cut, and the publish step independently | |
| # refuses to run unless main.yml is green for the commit. Set | |
| # publish_to_central=false to stop after the GitHub Release and publish later. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version without v prefix (example: 1.0.0)" | |
| required: true | |
| type: string | |
| ref: | |
| description: "Git ref to release (branch, tag, or commit SHA)" | |
| required: false | |
| default: "main" | |
| type: string | |
| prerelease: | |
| description: "Mark GitHub Release as prerelease" | |
| required: false | |
| default: false | |
| type: boolean | |
| publish_to_central: | |
| description: "Publish to Maven Central automatically after the release is cut" | |
| required: false | |
| default: true | |
| type: boolean | |
| confirm: | |
| description: "Type RELEASE to confirm publish" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| actions: read | |
| concurrency: | |
| group: release-${{ github.event.inputs.version }} | |
| cancel-in-progress: false | |
| jobs: | |
| gate: | |
| name: Gate / JDK ${{ matrix.java }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # ADR-002: tests run on JDK 17, 21, 25 to catch forward-compat | |
| # regressions. Compilation is always pinned to --release 17. | |
| java: ['17', '21', '25'] | |
| steps: | |
| - name: Validate release request | |
| if: matrix.java == '17' | |
| shell: bash | |
| env: | |
| CONFIRM: ${{ inputs.confirm }} | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| test "$CONFIRM" = "RELEASE" || { | |
| echo "::error::confirm input must be exactly RELEASE" | |
| exit 1 | |
| } | |
| [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { | |
| echo "::error::version must be semver-like (for example: 1.0.0)" | |
| exit 1 | |
| } | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.ref }} | |
| # Mirror main.yml: install the matrix JDK (test execution) plus JDK 17 | |
| # (compile + Gradle daemon runtime). Order matters — setup-java points | |
| # JAVA_HOME at the LAST entry, so 17 must be last for matrix.java = 25. | |
| - name: Set up JDKs (test=${{ matrix.java }}, compile/daemon=17) | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: | | |
| ${{ matrix.java }} | |
| 17 | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Build, test, lint, coverage | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: ./gradlew build -PtestJdk=${{ matrix.java }} -PsdkVersion="$VERSION" --stacktrace | |
| - name: Upload test reports on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-test-reports-jdk${{ matrix.java }} | |
| path: | | |
| build/reports/tests/ | |
| build/test-results/ | |
| retention-days: 14 | |
| release: | |
| name: Tag and GitHub Release | |
| runs-on: ubuntu-latest | |
| needs: gate | |
| outputs: | |
| sha: ${{ steps.resolve.outputs.sha }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ inputs.ref }} | |
| # Resolve inputs.ref to a concrete commit SHA so the tag, the GitHub | |
| # Release, and the chained Maven Central build all point at the SAME | |
| # commit — even if the workflow was launched from a different branch. | |
| - name: Resolve commit SHA | |
| id: resolve | |
| run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Verify tag does not already exist | |
| shell: bash | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| TAG="v${VERSION}" | |
| if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then | |
| echo "::error::Tag ${TAG} already exists on origin" | |
| exit 1 | |
| fi | |
| - name: Extract release notes from CHANGELOG | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| shell: bash | |
| run: | | |
| # Extract the section for this version from CHANGELOG.md. | |
| # Keep a Changelog format: matches from "## [X.Y.Z]" until the next | |
| # "## [" heading or the trailing "[x]: url" link-reference block. | |
| awk -v ver="$VERSION" ' | |
| $0 ~ "^## \\[" ver "\\]" { found=1; next } | |
| found && /^## \[/ { exit } | |
| found && /^\[[^]]+\]: / { exit } | |
| found { print } | |
| ' CHANGELOG.md > release-notes.md | |
| # Verify we extracted something | |
| if [ ! -s release-notes.md ]; then | |
| echo "::error::Could not extract release notes for v${VERSION} from CHANGELOG.md" | |
| exit 1 | |
| fi | |
| echo "=== Extracted release notes ===" | |
| cat release-notes.md | |
| - name: Create and publish release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ inputs.version }} | |
| TARGET_REF: ${{ steps.resolve.outputs.sha }} | |
| IS_PRERELEASE: ${{ inputs.prerelease }} | |
| shell: bash | |
| run: | | |
| TAG="v${VERSION}" | |
| FLAGS="" | |
| if [ "$IS_PRERELEASE" = "true" ]; then | |
| FLAGS="${FLAGS} --prerelease" | |
| fi | |
| gh release create "${TAG}" \ | |
| --target "${TARGET_REF}" \ | |
| --title "Version ${VERSION}" \ | |
| --notes-file release-notes.md \ | |
| ${FLAGS} | |
| - name: Note next step (chaining disabled) | |
| if: ${{ ! inputs.publish_to_central }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: echo "::notice::Release v${VERSION} created. Chaining is off — run the 'Publish to Maven Central' workflow (publish.yml) for this commit to push the artifact." | |
| # Chained Maven Central publish. Reuses publish.yml so there is one source of | |
| # truth for publishing; its guard job still independently requires main.yml to | |
| # be green for this commit before anything is pushed. | |
| publish-central: | |
| name: Publish to Maven Central | |
| needs: release | |
| if: ${{ inputs.publish_to_central }} | |
| uses: ./.github/workflows/publish.yml | |
| with: | |
| version: ${{ inputs.version }} | |
| # Build & publish the exact commit that was tagged/released, not the | |
| # workflow's launch commit (github.sha) — Maven Central is immutable. | |
| ref: ${{ needs.release.outputs.sha }} | |
| release: true | |
| secrets: inherit |