diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 4398fa92..69d3ce9e 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -231,6 +231,11 @@ jobs: permissions: contents: write steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Download build artifacts uses: actions/download-artifact@v8 with: @@ -258,6 +263,61 @@ jobs: tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" . tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" . + - name: Build release notes + shell: bash + env: + MAX_COMMITS: 200 + RELEASE_TAG: ${{ needs.init.outputs.release-tag }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + SHORT_SHA: ${{ needs.init.outputs.short-sha }} + VERSION: ${{ needs.init.outputs.version }} + run: | + set -euo pipefail + + previous_tag="$(git describe --tags --abbrev=0 --match 'v*' "${RELEASE_TAG}^" 2>/dev/null || true)" + + if [ -n "${previous_tag}" ]; then + range="${previous_tag}..${RELEASE_TAG}" + else + range="${RELEASE_TAG}" + fi + + git log --no-merges --reverse --pretty=tformat:"%H%x1f%h%x1f%s" "${range}" > commits.txt + total="$(wc -l < commits.txt)" + + { + printf 'Automated SharpEmu v%s build for commit [`%s`](%s/commit/%s).\n\n' \ + "${VERSION}" "${SHORT_SHA}" "${REPO_URL}" "${GITHUB_SHA}" + + if [ -n "${previous_tag}" ]; then + printf '## Changes since %s\n\n' "${previous_tag}" + else + printf '## Changes\n\n' + fi + + if [ "${total}" -eq 0 ]; then + printf '_No commits since %s._\n' "${previous_tag}" + else + head -n "${MAX_COMMITS}" commits.txt | while IFS=$'\x1f' read -r sha short subject; do + printf -- '- %s ([`%s`](%s/commit/%s))\n' "${subject}" "${short}" "${REPO_URL}" "${sha}" + done + + if [ "${total}" -gt "${MAX_COMMITS}" ]; then + printf '\n_…and %s more commits._\n' "$((total - MAX_COMMITS))" + fi + fi + + printf '\n' + + if [ -n "${previous_tag}" ]; then + printf '**Full changelog**: %s/compare/%s...%s\n' "${REPO_URL}" "${previous_tag}" "${RELEASE_TAG}" + else + printf '**Full changelog**: %s/commits/%s\n' "${REPO_URL}" "${RELEASE_TAG}" + fi + } > release-notes.md + + cat release-notes.md + - name: Create release shell: bash env: @@ -265,7 +325,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_NAME: ${{ needs.init.outputs.release-name }} RELEASE_TAG: ${{ needs.init.outputs.release-tag }} - VERSION: ${{ needs.init.outputs.version }} run: | mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort) if [ "${#assets[@]}" -ne 3 ]; then @@ -273,8 +332,6 @@ jobs: exit 1 fi - notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}." - if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2 exit 1 @@ -283,4 +340,4 @@ jobs: gh release create "${RELEASE_TAG}" "${assets[@]}" \ --verify-tag \ --title "${RELEASE_NAME}" \ - --notes "${notes}" + --notes-file release-notes.md diff --git a/scripts/release.py b/scripts/release.py index 0d7ee932..fdf8807a 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -157,6 +157,62 @@ def ensure_tag_does_not_exist( raise ReleaseError(f"Tag {tag} already exists on {remote}.") +def get_previous_tag(repository_root: Path) -> str: + try: + return run_git( + "describe", + "--tags", + "--abbrev=0", + "--match", + "v*", + "HEAD", + cwd=repository_root, + capture_output=True, + ) + except ReleaseError: + return "" + + +def get_release_commits( + repository_root: Path, + previous_tag: str, +) -> list[str]: + revision_range = f"{previous_tag}..HEAD" if previous_tag else "HEAD" + + output = run_git( + "log", + "--no-merges", + "--reverse", + "--pretty=tformat:%h %s", + revision_range, + cwd=repository_root, + capture_output=True, + ) + + return output.splitlines() + + +def print_release_notes_preview(repository_root: Path) -> None: + previous_tag = get_previous_tag(repository_root) + commits = get_release_commits(repository_root, previous_tag) + + print() + + if previous_tag: + print(f"Commits since {previous_tag} ({len(commits)}):") + else: + print(f"Commits in this release ({len(commits)}):") + + if not commits: + print(" (none)") + + for commit in commits: + print(f" {commit}") + + print() + print("These commits go into the generated release notes.") + + def read_version(props_path: Path) -> str: if not props_path.exists(): raise ReleaseError(f"Version file not found: {props_path}") @@ -323,6 +379,8 @@ def create_release_tag( remote, ) + print_release_notes_preview(repository_root) + run_git( "tag", "-a",