diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfde6c3..d2e9c2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -156,11 +156,31 @@ jobs: test -f gradle.properties test -f gradle/verification-metadata.xml - - name: Build release distribution, signed Maven bundle, and SBOM + - name: Generate release changelog for tagged builds + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/release@') + shell: bash + run: | + set -euo pipefail + chmod +x ./tools/generate-release-notes.sh + mkdir -p build/generated/release-notes + ./tools/generate-release-notes.sh "${GITHUB_REF_NAME}" > build/generated/release-notes/CHANGELOG.md + + - name: Build release inputs, signed Maven bundle, and SBOM env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew --no-daemon clean build pmdMain javadoc jacocoTestReport distZip cyclonedxBom centralBundle + run: ./gradlew --no-daemon clean build pmdMain javadoc jacocoTestReport cyclonedxBom centralBundle + + - name: Generate release changelog + shell: bash + run: | + set -euo pipefail + chmod +x ./tools/generate-release-notes.sh + mkdir -p build/generated/release-notes + ./tools/generate-release-notes.sh "${GITHUB_REF_NAME}" > build/generated/release-notes/CHANGELOG.md + + - name: Package release distribution + run: ./gradlew --no-daemon distZip - name: Publish bundle to Maven Central shell: bash @@ -188,7 +208,7 @@ jobs: - name: Publish GitHub release assets uses: softprops/action-gh-release@v2 with: - generate_release_notes: true + body_path: build/generated/release-notes/CHANGELOG.md files: | build/distributions/*.zip build/reports/sbom/radixor-sbom.json diff --git a/build.gradle b/build.gradle index c0fe029..f0d4c19 100644 --- a/build.gradle +++ b/build.gradle @@ -187,6 +187,54 @@ pitest { application { mainClass = 'org.egothor.stemmer.Compile' + applicationName = 'radixor' + executableDir = 'bin' +} + +distributions { + main { + distributionBaseName = 'radixor' + + contents { + from('README.md') { + into '' + } + + from('LICENSE') { + into '' + } + + from('docs') { + into 'docs' + include 'quick-start.md' + include 'cli-compilation.md' + include 'dictionary-format.md' + include 'built-in-languages.md' + include 'programmatic-usage.md' + include 'architecture-and-reduction.md' + include 'quality-and-operations.md' + include 'benchmarking.md' + } + + from(layout.buildDirectory.dir('generated/release-notes')) { + into '' + include 'CHANGELOG.md' + } + } + } +} + +tasks.named('startScripts') { + applicationName = 'radixor' +} + +tasks.named('distZip', Zip) { + archiveBaseName = 'radixor' + archiveClassifier = 'bin' +} + +tasks.named('distTar') { + enabled = false } jmh { diff --git a/tools/generate-release-notes.sh b/tools/generate-release-notes.sh new file mode 100755 index 0000000..4528f4c --- /dev/null +++ b/tools/generate-release-notes.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +current_tag="${GITHUB_REF_NAME:-${1:-}}" +if [[ -z "${current_tag}" ]]; then + echo "Current tag is not set. Provide it as GITHUB_REF_NAME or as the first argument." >&2 + exit 1 +fi + +release_prefix="release@" + +if [[ "${current_tag}" != "${release_prefix}"* ]]; then + echo "Current tag '${current_tag}' does not start with expected prefix '${release_prefix}'." >&2 + exit 1 +fi + +git fetch --tags --force >/dev/null 2>&1 || true + +all_versions="$(git tag --list "${release_prefix}*" | sed "s/^${release_prefix}//" | sort -V)" + +previous_tag="" +for version in ${all_versions}; do + if [[ "${release_prefix}${version}" == "${current_tag}" ]]; then + break + fi + previous_tag="${release_prefix}${version}" +done + +if [[ -n "${previous_tag}" ]]; then + range="${previous_tag}..${current_tag}" +else + range="${current_tag}" +fi + +echo "Generating release notes for range: ${range}" >&2 + +declare -a CATEGORY_ORDER=( + "feat|Features" + "fix|Bug Fixes" + "perf|Performance" + "refactor|Refactoring" + "docs|Documentation" + "test|Tests" + "build|Build System" + "ci|CI/CD" + "style|Style" + "chore|Maintenance" + "revert|Reverts" +) + +declare -A CATEGORY_TITLES +declare -A CATEGORY_ITEMS + +for entry in "${CATEGORY_ORDER[@]}"; do + key="${entry%%|*}" + title="${entry##*|}" + CATEGORY_TITLES["${key}"]="${title}" + CATEGORY_ITEMS["${key}"]="" +done + +supported_prefix_pattern='^(feat|fix|perf|refactor|docs|test|build|ci|style|chore|revert)(\([^)]+\))?!?:[[:space:]]*(.+)$' +separator=$'\x1f' + +append_line() { + local line="$1" + local normalized_line + local category + local message + + normalized_line="$(printf '%s' "${line}" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + [[ -z "${normalized_line}" ]] && return 0 + + if [[ "${normalized_line}" =~ ${supported_prefix_pattern} ]]; then + category="${BASH_REMATCH[1]}" + message="${BASH_REMATCH[3]}" + + [[ -z "${message}" ]] && return 0 + + CATEGORY_ITEMS["${category}"]+="- ${message}"$'\n' + fi +} + +while IFS="${separator}" read -r commit_hash subject body; do + [[ -z "${commit_hash}" ]] && continue + + if [[ "${subject}" =~ ^Merge[[:space:]] ]] || [[ "${subject}" == "Initial commit" ]]; then + continue + fi + + append_line "${subject}" + + while IFS= read -r body_line; do + append_line "${body_line}" + done <<< "${body}" +done < <(git log "${range}" --no-merges --pretty=format:"%H${separator}%s${separator}%b") + +body_text="## What's New" + +for entry in "${CATEGORY_ORDER[@]}"; do + key="${entry%%|*}" + title="${CATEGORY_TITLES[${key}]}" + items="${CATEGORY_ITEMS[${key}]}" + + if [[ -n "${items}" ]]; then + body_text+=$'\n\n'"### ${title}"$'\n' + body_text+="$(printf '%s' "${items}" | sed '/^[[:space:]]*$/d')" + fi +done + +if [[ "${body_text}" == "## What's New" ]]; then + body_text+=$'\n\n'"No categorized changes were found in commit subjects or bodies for this release range." +fi + +printf '%s\n' "${body_text}"