@@ -30,29 +30,43 @@ jobs:
3030
3131 echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}"
3232
33- # Get commit SHAs since last release
33+ # Get commit data since last release
3434 if [ -z "$PREVIOUS_TAG" ]; then
35- COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges)
35+ COMMIT_RANGE="HEAD"
3636 else
37- COMMIT_SHAS=$(git log $ {PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges)
37+ COMMIT_RANGE="$ {PREVIOUS_TAG}..HEAD"
3838 fi
3939
40- echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release"
40+ # Collect highlights from PR comments and categorize commits
41+ # Each category stores entries with issue number prefix for sorting: "NNNN|entry"
42+ declare -a HIGHLIGHTS
43+ declare -a BUGS # fix
44+ declare -a IMPROVEMENTS # perf, refactor
45+ declare -a FEATURES # feat
46+ declare -a MAINTENANCE # docs, style, test, build, ci, chore
47+ declare -a OTHER # anything else (except revert which is omitted)
4148
42- # Collect all closed issues by tracing commits -> PRs -> issues
43- declare -A CLOSED_ISSUES # issue_number -> issue_title
44- declare -a HIGHLIGHTS # array of highlight messages
49+ # Process each commit
50+ while IFS= read -r line; do
51+ [ -z "$line" ] && continue
52+
53+ SHA=$(echo "$line" | cut -d'|' -f1)
54+ SUBJECT=$(echo "$line" | cut -d'|' -f2)
55+ SHORT_SHA=$(echo "$line" | cut -d'|' -f3)
56+ BODY=$(echo "$line" | cut -d'|' -f4-)
4557
46- for SHA in $COMMIT_SHAS; do
4758 echo "Processing commit: $SHA"
4859
49- # Find PR associated with this commit (squash merge)
50- PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,body --limit 1 2>/dev/null || echo "[]")
60+ # Skip revert commits
61+ if echo "$SUBJECT" | grep -qE "^revert(\(|:)"; then
62+ echo " Skipping revert commit"
63+ continue
64+ fi
5165
66+ # Check for PR and /release-note comments
67+ PR_DATA=$(gh pr list --search "$SHA" --state merged --json number --limit 1 2>/dev/null || echo "[]")
5268 if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then
5369 PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty')
54- PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""')
55-
5670 if [ -n "$PR_NUMBER" ]; then
5771 echo " Found PR #$PR_NUMBER"
5872
@@ -63,44 +77,44 @@ jobs:
6377 echo " Found /release-note: $RELEASE_NOTE"
6478 HIGHLIGHTS+=("$RELEASE_NOTE")
6579 fi
66-
67- # Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX)
68- ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")
69-
70- for ISSUE_NUM in $ISSUE_NUMBERS; do
71- if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
72- echo " Found linked issue #$ISSUE_NUM"
73-
74- # Get issue title
75- ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")
76-
77- if [ -n "$ISSUE_TITLE" ]; then
78- CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"
79- fi
80- fi
81- done
8280 fi
8381 fi
84- done
8582
86- # Also check for issues closed directly (not via PR) in the commit range
87- # by looking at commit messages for "Fixes #XX" patterns
88- for SHA in $COMMIT_SHAS; do
89- COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA")
90- ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")
83+ # Extract issue number from commit body for sorting
84+ ISSUE_NUM=$(echo "$BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" | head -1 || echo "99999")
85+ ISSUE_REF=$(echo "$BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | head -1 || echo "")
9186
92- for ISSUE_NUM in $ISSUE_NUMBERS; do
93- if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
94- echo "Found issue #$ISSUE_NUM in commit message"
87+ # Build commit entry
88+ if [ -n "$ISSUE_NUM" ] && [ "$ISSUE_NUM" != "99999" ]; then
89+ ENTRY="- #${ISSUE_NUM} - $SUBJECT ($SHORT_SHA)"
90+ else
91+ ENTRY="- $SUBJECT ($SHORT_SHA)"
92+ fi
9593
96- ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")
94+ # Store with issue number prefix for sorting
95+ SORTABLE_ENTRY="${ISSUE_NUM}|${ENTRY}"
96+
97+ # Categorize by conventional commit prefix
98+ if echo "$SUBJECT" | grep -qE "^fix(\(|:)"; then
99+ BUGS+=("$SORTABLE_ENTRY")
100+ elif echo "$SUBJECT" | grep -qE "^(perf|refactor)(\(|:)"; then
101+ IMPROVEMENTS+=("$SORTABLE_ENTRY")
102+ elif echo "$SUBJECT" | grep -qE "^feat(\(|:)"; then
103+ FEATURES+=("$SORTABLE_ENTRY")
104+ elif echo "$SUBJECT" | grep -qE "^(docs|style|test|build|ci|chore)(\(|:)"; then
105+ MAINTENANCE+=("$SORTABLE_ENTRY")
106+ else
107+ OTHER+=("$SORTABLE_ENTRY")
108+ fi
109+ done < <(git log ${COMMIT_RANGE} --pretty=format:"%H|%s|%h|%b---END---" --no-merges | sed 's/---END---/\n/g')
97110
98- if [ -n "$ISSUE_TITLE" ]; then
99- CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"
100- fi
101- fi
102- done
103- done
111+ # Helper function to sort entries by issue number and format output
112+ sort_entries() {
113+ local -n arr=$1
114+ if [ ${#arr[@]} -gt 0 ]; then
115+ printf '%s\n' "${arr[@]}" | sort -t'|' -k1 -n | cut -d'|' -f2-
116+ fi
117+ }
104118
105119 # Build changelog
106120 CHANGELOG=""
@@ -114,13 +128,45 @@ jobs:
114128 CHANGELOG="${CHANGELOG}"$'\n'
115129 fi
116130
117- # Add Closed Issues section (always show if there are any)
118- if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then
119- CHANGELOG="${CHANGELOG}### 📋 Closed Issues"$'\n\n'
120- # Sort issue numbers for consistent ordering
121- for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do
122- CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n'
123- done
131+ # Add categorized sections in order: Bug Fixes, Performance & Improvements, New Features, Maintenance, Other
132+ if [ ${#BUGS[@]} -gt 0 ]; then
133+ CHANGELOG="${CHANGELOG}### 🐛 Bug Fixes"$'\n\n'
134+ while IFS= read -r entry; do
135+ CHANGELOG="${CHANGELOG}${entry}"$'\n'
136+ done < <(sort_entries BUGS)
137+ CHANGELOG="${CHANGELOG}"$'\n'
138+ fi
139+
140+ if [ ${#IMPROVEMENTS[@]} -gt 0 ]; then
141+ CHANGELOG="${CHANGELOG}### ⚡ Performance & Improvements"$'\n\n'
142+ while IFS= read -r entry; do
143+ CHANGELOG="${CHANGELOG}${entry}"$'\n'
144+ done < <(sort_entries IMPROVEMENTS)
145+ CHANGELOG="${CHANGELOG}"$'\n'
146+ fi
147+
148+ if [ ${#FEATURES[@]} -gt 0 ]; then
149+ CHANGELOG="${CHANGELOG}### 🎉 New Features"$'\n\n'
150+ while IFS= read -r entry; do
151+ CHANGELOG="${CHANGELOG}${entry}"$'\n'
152+ done < <(sort_entries FEATURES)
153+ CHANGELOG="${CHANGELOG}"$'\n'
154+ fi
155+
156+ if [ ${#MAINTENANCE[@]} -gt 0 ]; then
157+ CHANGELOG="${CHANGELOG}### 🔧 Maintenance"$'\n\n'
158+ while IFS= read -r entry; do
159+ CHANGELOG="${CHANGELOG}${entry}"$'\n'
160+ done < <(sort_entries MAINTENANCE)
161+ CHANGELOG="${CHANGELOG}"$'\n'
162+ fi
163+
164+ if [ ${#OTHER[@]} -gt 0 ]; then
165+ CHANGELOG="${CHANGELOG}### 📦 Other"$'\n\n'
166+ while IFS= read -r entry; do
167+ CHANGELOG="${CHANGELOG}${entry}"$'\n'
168+ done < <(sort_entries OTHER)
169+ CHANGELOG="${CHANGELOG}"$'\n'
124170 fi
125171
126172 # If changelog is empty, use a fun message
0 commit comments