Skip to content

Tag and Release

Tag and Release #1

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