From 55d07416b533258623269e181ed3734d41856b08 Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Wed, 10 Dec 2025 15:26:17 -0500 Subject: [PATCH 1/3] feat(ci): enhance release notes with issue tracking - Trace commits to PRs to issues for accurate changelog - Add /release-note comment parsing for custom highlights - New release notes structure with Highlights and Closed Issues sections - Change header to "What's New in vX.Y.Z" Fixes #76 --- .github/workflows/release.yml | 144 ++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 43 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb7ea19..61491e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -251,62 +251,115 @@ jobs: - name: Generate changelog id: changelog run: | - # Get commits since last release tag + # Get previous release tag # NOTE: This must run BEFORE creating the new tag, otherwise git describe # will find the new tag and the changelog will be empty PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "") + + echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}" + + # Get commit SHAs since last release if [ -z "$PREVIOUS_TAG" ]; then - # First release - get all commits - COMMITS=$(git log --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ') + COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges) else - # Get commits since previous tag - COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ') + COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges) fi - # Categorize commits by conventional commit type - BUGS="" - FEATURES="" - CHORES="" - - # Process each commit - while IFS= read -r line; do - subject=$(echo "$line" | cut -d'|' -f1) - body=$(echo "$line" | cut -d'|' -f2-) - - # Extract issue number from body if present - issue=$(echo "$body" | grep -oE "Fixes #[0-9]+" | head -1 || echo "") - if [ -n "$issue" ]; then - entry="- $subject - $issue" - else - entry="- $subject" - fi + echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release" + + # Collect all closed issues by tracing commits -> PRs -> issues + declare -A CLOSED_ISSUES # issue_number -> issue_title + declare -A HIGHLIGHTS # issue_number -> highlight message + + for SHA in $COMMIT_SHAS; do + echo "Processing commit: $SHA" + + # Find PR associated with this commit (squash merge) + PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]") + + if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') + + if [ -n "$PR_NUMBER" ]; then + echo " Found PR #$PR_NUMBER" - # Categorize by prefix - if echo "$subject" | grep -qE "^fix(\(|:)"; then - BUGS="${BUGS}${entry}"$'\n' - elif echo "$subject" | grep -qE "^feat(\(|:)"; then - FEATURES="${FEATURES}${entry}"$'\n' - elif echo "$subject" | grep -qE "^chore(\(|:)"; then - CHORES="${CHORES}${entry}"$'\n' - else - # Default to chores for uncategorized - CHORES="${CHORES}${entry}"$'\n' + # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX) + ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") + + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo " Found linked issue #$ISSUE_NUM" + + # Get issue title + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment on the issue + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + fi fi - done < <(git log ${PREVIOUS_TAG:+$PREVIOUS_TAG..}HEAD --pretty=format:"%s (%h)|%b---END---" --no-merges | sed 's/---END---/\n/g') + done - # Build changelog with categories - CHANGELOG="" + # Also check for issues closed directly (not via PR) in the commit range + # by looking at commit messages for "Fixes #XX" patterns + for SHA in $COMMIT_SHAS; do + COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA") + ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") - if [ -n "$BUGS" ]; then - CHANGELOG="${CHANGELOG}### ๐Ÿ› Bugs Squashed"$'\n\n'"${BUGS}"$'\n' - fi + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo "Found issue #$ISSUE_NUM in commit message" + + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + done + + # Build changelog + CHANGELOG="" - if [ -n "$FEATURES" ]; then - CHANGELOG="${CHANGELOG}### ๐ŸŽ‰ Features Added"$'\n\n'"${FEATURES}"$'\n' + # Add Highlights section if any /release-note comments exist + if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then + CHANGELOG="### โœจ Highlights"$'\n\n' + for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do + CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n' + done + CHANGELOG="${CHANGELOG}"$'\n' fi - if [ -n "$CHORES" ]; then - CHANGELOG="${CHANGELOG}### ๐Ÿงน Chores Addressed"$'\n\n'"${CHORES}"$'\n' + # Add Closed Issues section (always show if there are any) + if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ“‹ Closed Issues"$'\n\n' + # Sort issue numbers for consistent ordering + for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do + CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n' + done fi # If changelog is empty, use a fun message @@ -314,10 +367,15 @@ jobs: CHANGELOG="So much goodness, we lost track! ๐ŸŽ‰" fi + echo "Generated changelog:" + echo "$CHANGELOG" + echo "CHANGELOG<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create and push release tag run: | @@ -348,7 +406,7 @@ jobs: install.sh install.ps1 body: | - ## Changes in v${{ github.event.inputs.version }} + ## What's New in v${{ github.event.inputs.version }} ${{ steps.changelog.outputs.CHANGELOG }} From cb5614660df1e5baf427f7aefaa39d726ba6800b Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Wed, 10 Dec 2025 15:28:31 -0500 Subject: [PATCH 2/3] feat(ci): add preview changelog workflow Allows running the changelog generation on-demand to preview release notes before creating an actual release. --- .github/workflows/preview-changelog.yml | 156 ++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 .github/workflows/preview-changelog.yml diff --git a/.github/workflows/preview-changelog.yml b/.github/workflows/preview-changelog.yml new file mode 100644 index 0000000..d93aca2 --- /dev/null +++ b/.github/workflows/preview-changelog.yml @@ -0,0 +1,156 @@ +name: Preview Changelog + +run-name: Preview release notes for next release + +on: + workflow_dispatch: + +jobs: + preview: + name: Generate Changelog Preview + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for changelog generation + + - name: Generate changelog preview + run: | + # Get previous release tag + PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "") + + echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}" + + # Get commit SHAs since last release + if [ -z "$PREVIOUS_TAG" ]; then + COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges) + else + COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges) + fi + + COMMIT_COUNT=$(echo "$COMMIT_SHAS" | grep -c . || echo "0") + echo "Found $COMMIT_COUNT commits since last release" + echo "" + + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "::warning::No commits found since last release tag ($PREVIOUS_TAG)" + echo "" + echo "## What's New" + echo "" + echo "No changes since $PREVIOUS_TAG" + exit 0 + fi + + # Collect all closed issues by tracing commits -> PRs -> issues + declare -A CLOSED_ISSUES # issue_number -> issue_title + declare -A HIGHLIGHTS # issue_number -> highlight message + + for SHA in $COMMIT_SHAS; do + echo "Processing commit: $SHA" + + # Find PR associated with this commit (squash merge) + PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]") + + if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') + + if [ -n "$PR_NUMBER" ]; then + echo " Found PR #$PR_NUMBER" + + # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX) + ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") + + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo " Found linked issue #$ISSUE_NUM" + + # Get issue title + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment on the issue + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + fi + fi + done + + # Also check for issues closed directly (not via PR) in the commit range + # by looking at commit messages for "Fixes #XX" patterns + for SHA in $COMMIT_SHAS; do + COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA") + ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") + + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo "Found issue #$ISSUE_NUM in commit message" + + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + done + + # Build changelog + CHANGELOG="" + + # Add Highlights section if any /release-note comments exist + if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then + CHANGELOG="### โœจ Highlights"$'\n\n' + for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do + CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n' + done + CHANGELOG="${CHANGELOG}"$'\n' + fi + + # Add Closed Issues section (always show if there are any) + if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ“‹ Closed Issues"$'\n\n' + # Sort issue numbers for consistent ordering + for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do + CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n' + done + fi + + # If changelog is empty, use a fun message + if [ -z "$CHANGELOG" ]; then + CHANGELOG="So much goodness, we lost track! ๐ŸŽ‰" + fi + + echo "" + echo "==========================================" + echo "CHANGELOG PREVIEW" + echo "==========================================" + echo "" + echo "## What's New in v" + echo "" + echo "$CHANGELOG" + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c552e74999c3c03fa3a9aa5bcf13e119ed9b5cdd Mon Sep 17 00:00:00 2001 From: "Calvin A. Allen" Date: Wed, 10 Dec 2025 15:31:42 -0500 Subject: [PATCH 3/3] refactor(ci): extract changelog generation to reusable workflow - Create generate-changelog.yml as reusable workflow - Update release.yml to use the reusable workflow - Simplify preview-changelog.yml to use the reusable workflow --- .github/workflows/generate-changelog.yml | 150 +++++++++++++++++++++++ .github/workflows/preview-changelog.yml | 144 ++-------------------- .github/workflows/release.yml | 140 ++------------------- 3 files changed, 165 insertions(+), 269 deletions(-) create mode 100644 .github/workflows/generate-changelog.yml diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 0000000..b77dbc9 --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,150 @@ +name: Generate Changelog + +on: + workflow_call: + outputs: + changelog: + description: "The generated changelog content" + value: ${{ jobs.generate.outputs.changelog }} + +jobs: + generate: + name: Generate Changelog + runs-on: ubuntu-latest + outputs: + changelog: ${{ steps.changelog.outputs.CHANGELOG }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for changelog generation + + - name: Generate changelog + id: changelog + run: | + # Get previous release tag + # NOTE: This must run BEFORE creating the new tag, otherwise git describe + # will find the new tag and the changelog will be empty + PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "") + + echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}" + + # Get commit SHAs since last release + if [ -z "$PREVIOUS_TAG" ]; then + COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges) + else + COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges) + fi + + echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release" + + # Collect all closed issues by tracing commits -> PRs -> issues + declare -A CLOSED_ISSUES # issue_number -> issue_title + declare -A HIGHLIGHTS # issue_number -> highlight message + + for SHA in $COMMIT_SHAS; do + echo "Processing commit: $SHA" + + # Find PR associated with this commit (squash merge) + PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]") + + if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') + PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') + + if [ -n "$PR_NUMBER" ]; then + echo " Found PR #$PR_NUMBER" + + # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX) + ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") + + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo " Found linked issue #$ISSUE_NUM" + + # Get issue title + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment on the issue + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + fi + fi + done + + # Also check for issues closed directly (not via PR) in the commit range + # by looking at commit messages for "Fixes #XX" patterns + for SHA in $COMMIT_SHAS; do + COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA") + ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") + + for ISSUE_NUM in $ISSUE_NUMBERS; do + if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then + echo "Found issue #$ISSUE_NUM in commit message" + + ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") + + if [ -n "$ISSUE_TITLE" ]; then + CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" + + # Check for /release-note comment + COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") + RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") + + if [ -n "$RELEASE_NOTE" ]; then + echo " Found /release-note: $RELEASE_NOTE" + HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" + fi + fi + fi + done + done + + # Build changelog + CHANGELOG="" + + # Add Highlights section if any /release-note comments exist + if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then + CHANGELOG="### โœจ Highlights"$'\n\n' + for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do + CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n' + done + CHANGELOG="${CHANGELOG}"$'\n' + fi + + # Add Closed Issues section (always show if there are any) + if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ“‹ Closed Issues"$'\n\n' + # Sort issue numbers for consistent ordering + for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do + CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n' + done + fi + + # If changelog is empty, use a fun message + if [ -z "$CHANGELOG" ]; then + CHANGELOG="So much goodness, we lost track! ๐ŸŽ‰" + fi + + echo "Generated changelog:" + echo "$CHANGELOG" + + echo "CHANGELOG<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/preview-changelog.yml b/.github/workflows/preview-changelog.yml index d93aca2..6e44905 100644 --- a/.github/workflows/preview-changelog.yml +++ b/.github/workflows/preview-changelog.yml @@ -6,151 +6,23 @@ on: workflow_dispatch: jobs: + generate: + name: Generate + uses: ./.github/workflows/generate-changelog.yml + preview: - name: Generate Changelog Preview + name: Display Preview runs-on: ubuntu-latest + needs: generate steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for changelog generation - - - name: Generate changelog preview + - name: Display changelog preview run: | - # Get previous release tag - PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "") - - echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}" - - # Get commit SHAs since last release - if [ -z "$PREVIOUS_TAG" ]; then - COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges) - else - COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges) - fi - - COMMIT_COUNT=$(echo "$COMMIT_SHAS" | grep -c . || echo "0") - echo "Found $COMMIT_COUNT commits since last release" - echo "" - - if [ "$COMMIT_COUNT" -eq 0 ]; then - echo "::warning::No commits found since last release tag ($PREVIOUS_TAG)" - echo "" - echo "## What's New" - echo "" - echo "No changes since $PREVIOUS_TAG" - exit 0 - fi - - # Collect all closed issues by tracing commits -> PRs -> issues - declare -A CLOSED_ISSUES # issue_number -> issue_title - declare -A HIGHLIGHTS # issue_number -> highlight message - - for SHA in $COMMIT_SHAS; do - echo "Processing commit: $SHA" - - # Find PR associated with this commit (squash merge) - PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]") - - if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then - PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') - PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') - - if [ -n "$PR_NUMBER" ]; then - echo " Found PR #$PR_NUMBER" - - # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX) - ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") - - for ISSUE_NUM in $ISSUE_NUMBERS; do - if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then - echo " Found linked issue #$ISSUE_NUM" - - # Get issue title - ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") - - if [ -n "$ISSUE_TITLE" ]; then - CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" - - # Check for /release-note comment on the issue - COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") - - RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") - - if [ -n "$RELEASE_NOTE" ]; then - echo " Found /release-note: $RELEASE_NOTE" - HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" - fi - fi - fi - done - fi - fi - done - - # Also check for issues closed directly (not via PR) in the commit range - # by looking at commit messages for "Fixes #XX" patterns - for SHA in $COMMIT_SHAS; do - COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA") - ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") - - for ISSUE_NUM in $ISSUE_NUMBERS; do - if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then - echo "Found issue #$ISSUE_NUM in commit message" - - ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") - - if [ -n "$ISSUE_TITLE" ]; then - CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" - - # Check for /release-note comment - COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") - RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") - - if [ -n "$RELEASE_NOTE" ]; then - echo " Found /release-note: $RELEASE_NOTE" - HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" - fi - fi - fi - done - done - - # Build changelog - CHANGELOG="" - - # Add Highlights section if any /release-note comments exist - if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then - CHANGELOG="### โœจ Highlights"$'\n\n' - for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do - CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n' - done - CHANGELOG="${CHANGELOG}"$'\n' - fi - - # Add Closed Issues section (always show if there are any) - if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then - CHANGELOG="${CHANGELOG}### ๐Ÿ“‹ Closed Issues"$'\n\n' - # Sort issue numbers for consistent ordering - for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do - CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n' - done - fi - - # If changelog is empty, use a fun message - if [ -z "$CHANGELOG" ]; then - CHANGELOG="So much goodness, we lost track! ๐ŸŽ‰" - fi - - echo "" echo "==========================================" echo "CHANGELOG PREVIEW" echo "==========================================" echo "" echo "## What's New in v" echo "" - echo "$CHANGELOG" + echo "${{ needs.generate.outputs.changelog }}" shell: bash - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61491e9..a7d7d90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -226,16 +226,19 @@ jobs: install.ps1 retention-days: 1 + changelog: + name: Generate Changelog + needs: build + uses: ./.github/workflows/generate-changelog.yml + release: name: Create GitHub Release runs-on: ubuntu-latest - needs: build + needs: [build, changelog] steps: - name: Checkout code uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for changelog generation - name: Download all build artifacts uses: actions/download-artifact@v4 @@ -248,135 +251,6 @@ jobs: name: install-scripts path: . - - name: Generate changelog - id: changelog - run: | - # Get previous release tag - # NOTE: This must run BEFORE creating the new tag, otherwise git describe - # will find the new tag and the changelog will be empty - PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "") - - echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}" - - # Get commit SHAs since last release - if [ -z "$PREVIOUS_TAG" ]; then - COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges) - else - COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges) - fi - - echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release" - - # Collect all closed issues by tracing commits -> PRs -> issues - declare -A CLOSED_ISSUES # issue_number -> issue_title - declare -A HIGHLIGHTS # issue_number -> highlight message - - for SHA in $COMMIT_SHAS; do - echo "Processing commit: $SHA" - - # Find PR associated with this commit (squash merge) - PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]") - - if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then - PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty') - PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""') - - if [ -n "$PR_NUMBER" ]; then - echo " Found PR #$PR_NUMBER" - - # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX) - ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") - - for ISSUE_NUM in $ISSUE_NUMBERS; do - if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then - echo " Found linked issue #$ISSUE_NUM" - - # Get issue title - ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") - - if [ -n "$ISSUE_TITLE" ]; then - CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" - - # Check for /release-note comment on the issue - COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") - - RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") - - if [ -n "$RELEASE_NOTE" ]; then - echo " Found /release-note: $RELEASE_NOTE" - HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" - fi - fi - fi - done - fi - fi - done - - # Also check for issues closed directly (not via PR) in the commit range - # by looking at commit messages for "Fixes #XX" patterns - for SHA in $COMMIT_SHAS; do - COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA") - ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "") - - for ISSUE_NUM in $ISSUE_NUMBERS; do - if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then - echo "Found issue #$ISSUE_NUM in commit message" - - ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "") - - if [ -n "$ISSUE_TITLE" ]; then - CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE" - - # Check for /release-note comment - COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "") - RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "") - - if [ -n "$RELEASE_NOTE" ]; then - echo " Found /release-note: $RELEASE_NOTE" - HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE" - fi - fi - fi - done - done - - # Build changelog - CHANGELOG="" - - # Add Highlights section if any /release-note comments exist - if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then - CHANGELOG="### โœจ Highlights"$'\n\n' - for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do - CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n' - done - CHANGELOG="${CHANGELOG}"$'\n' - fi - - # Add Closed Issues section (always show if there are any) - if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then - CHANGELOG="${CHANGELOG}### ๐Ÿ“‹ Closed Issues"$'\n\n' - # Sort issue numbers for consistent ordering - for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do - CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n' - done - fi - - # If changelog is empty, use a fun message - if [ -z "$CHANGELOG" ]; then - CHANGELOG="So much goodness, we lost track! ๐ŸŽ‰" - fi - - echo "Generated changelog:" - echo "$CHANGELOG" - - echo "CHANGELOG<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - shell: bash - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create and push release tag run: | VERSION="${{ github.event.inputs.version }}" @@ -408,7 +282,7 @@ jobs: body: | ## What's New in v${{ github.event.inputs.version }} - ${{ steps.changelog.outputs.CHANGELOG }} + ${{ needs.changelog.outputs.changelog }} ## Installation