Skip to content

Commit 515f8e5

Browse files
committed
add changelog generation script
1 parent 321486a commit 515f8e5

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
name: Generate Changelog
2+
3+
on:
4+
workflow_call:
5+
outputs:
6+
changelog:
7+
description: "The generated changelog content"
8+
value: ${{ jobs.generate.outputs.changelog }}
9+
10+
jobs:
11+
generate:
12+
name: Generate Changelog
13+
runs-on: ubuntu-latest
14+
outputs:
15+
changelog: ${{ steps.changelog.outputs.CHANGELOG }}
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Fetch all history for changelog generation
22+
23+
- name: Generate changelog
24+
id: changelog
25+
run: |
26+
# Get previous release tag
27+
# NOTE: This must run BEFORE creating the new tag, otherwise git describe
28+
# will find the new tag and the changelog will be empty
29+
PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "")
30+
31+
echo "Previous tag: ${PREVIOUS_TAG:-'(none - first release)'}"
32+
33+
# Get commit data since last release
34+
if [ -z "$PREVIOUS_TAG" ]; then
35+
COMMIT_RANGE="HEAD"
36+
else
37+
COMMIT_RANGE="${PREVIOUS_TAG}..HEAD"
38+
fi
39+
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)
48+
49+
# Get list of commit SHAs
50+
COMMIT_SHAS=$(git log ${COMMIT_RANGE} --pretty=format:"%H" --no-merges)
51+
52+
# Track processed PRs to avoid duplicates
53+
declare -A PROCESSED_PRS
54+
55+
# Process each commit
56+
for SHA in $COMMIT_SHAS; do
57+
echo "Processing commit: $SHA"
58+
59+
# Find the PR associated with this commit
60+
PR_DATA=$(gh pr list --search "$SHA" --state merged --json number,title,body --limit 1 2>/dev/null || echo "[]")
61+
62+
if [ "$PR_DATA" = "[]" ] || [ -z "$PR_DATA" ]; then
63+
echo " No PR found, skipping"
64+
continue
65+
fi
66+
67+
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty')
68+
PR_TITLE=$(echo "$PR_DATA" | jq -r '.[0].title // empty')
69+
PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // empty')
70+
71+
if [ -z "$PR_NUMBER" ] || [ -z "$PR_TITLE" ]; then
72+
echo " Could not get PR details, skipping"
73+
continue
74+
fi
75+
76+
# Extract linked issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX)
77+
ISSUE_NUMS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" | sort -n | uniq || echo "")
78+
79+
# Skip if we've already processed this PR
80+
if [ -n "${PROCESSED_PRS[$PR_NUMBER]}" ]; then
81+
echo " PR #$PR_NUMBER already processed, skipping"
82+
continue
83+
fi
84+
PROCESSED_PRS[$PR_NUMBER]=1
85+
86+
echo " Found PR #$PR_NUMBER: $PR_TITLE"
87+
88+
# Skip revert PRs
89+
if echo "$PR_TITLE" | grep -qE "^revert(\(|:)"; then
90+
echo " Skipping revert PR"
91+
continue
92+
fi
93+
94+
# Check PR comments for commands
95+
PR_COMMENTS=$(gh pr view "$PR_NUMBER" --json comments --jq '.comments[].body' 2>/dev/null || echo "")
96+
97+
# Check for /skip-changelog command
98+
if echo "$PR_COMMENTS" | grep -q "^/skip-changelog"; then
99+
echo " Found /skip-changelog, skipping PR"
100+
continue
101+
fi
102+
103+
# Check for /release-note command
104+
RELEASE_NOTE=$(echo "$PR_COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "")
105+
if [ -n "$RELEASE_NOTE" ]; then
106+
echo " Found /release-note: $RELEASE_NOTE"
107+
HIGHLIGHTS+=("$RELEASE_NOTE")
108+
fi
109+
110+
# Build entry - use issue number(s) if available
111+
if [ -n "$ISSUE_NUMS" ]; then
112+
# Format multiple issues as "(#1, #2)"
113+
ISSUE_LIST=$(echo "$ISSUE_NUMS" | sed 's/^/#/' | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g')
114+
ENTRY="- $PR_TITLE (${ISSUE_LIST})"
115+
# Use first issue number for sorting
116+
SORT_NUM=$(echo "$ISSUE_NUMS" | head -1)
117+
else
118+
ENTRY="- $PR_TITLE"
119+
# Use high number to sort PRs without issues to the end
120+
SORT_NUM="99999"
121+
fi
122+
123+
# Store with sort number prefix for sorting
124+
SORTABLE_ENTRY="${SORT_NUM}|${ENTRY}"
125+
126+
# Categorize by conventional commit prefix in PR title
127+
if echo "$PR_TITLE" | grep -qE "^fix(\(|:)"; then
128+
BUGS+=("$SORTABLE_ENTRY")
129+
elif echo "$PR_TITLE" | grep -qE "^(perf|refactor)(\(|:)"; then
130+
IMPROVEMENTS+=("$SORTABLE_ENTRY")
131+
elif echo "$PR_TITLE" | grep -qE "^feat(\(|:)"; then
132+
FEATURES+=("$SORTABLE_ENTRY")
133+
elif echo "$PR_TITLE" | grep -qE "^(docs|style|test|build|ci|chore)(\(|:)"; then
134+
MAINTENANCE+=("$SORTABLE_ENTRY")
135+
else
136+
OTHER+=("$SORTABLE_ENTRY")
137+
fi
138+
done
139+
140+
# Helper function to sort entries by issue number and format output
141+
sort_entries() {
142+
local -n arr=$1
143+
if [ ${#arr[@]} -gt 0 ]; then
144+
printf '%s\n' "${arr[@]}" | sort -t'|' -k1 -n | cut -d'|' -f2-
145+
fi
146+
}
147+
148+
# Build changelog
149+
CHANGELOG=""
150+
151+
# Add Highlights section if any /release-note entries exist
152+
if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then
153+
CHANGELOG="### ✨ Highlights"$'\n\n'
154+
for HIGHLIGHT in "${HIGHLIGHTS[@]}"; do
155+
CHANGELOG="${CHANGELOG}- ${HIGHLIGHT}"$'\n'
156+
done
157+
CHANGELOG="${CHANGELOG}"$'\n'
158+
fi
159+
160+
# Add categorized sections in order: Bug Fixes, Performance & Improvements, New Features, Maintenance, Other
161+
if [ ${#BUGS[@]} -gt 0 ]; then
162+
CHANGELOG="${CHANGELOG}### 🐛 Bug Fixes"$'\n\n'
163+
while IFS= read -r entry; do
164+
CHANGELOG="${CHANGELOG}${entry}"$'\n'
165+
done < <(sort_entries BUGS)
166+
CHANGELOG="${CHANGELOG}"$'\n'
167+
fi
168+
169+
if [ ${#IMPROVEMENTS[@]} -gt 0 ]; then
170+
CHANGELOG="${CHANGELOG}### ⚡ Performance & Improvements"$'\n\n'
171+
while IFS= read -r entry; do
172+
CHANGELOG="${CHANGELOG}${entry}"$'\n'
173+
done < <(sort_entries IMPROVEMENTS)
174+
CHANGELOG="${CHANGELOG}"$'\n'
175+
fi
176+
177+
if [ ${#FEATURES[@]} -gt 0 ]; then
178+
CHANGELOG="${CHANGELOG}### 🎉 New Features"$'\n\n'
179+
while IFS= read -r entry; do
180+
CHANGELOG="${CHANGELOG}${entry}"$'\n'
181+
done < <(sort_entries FEATURES)
182+
CHANGELOG="${CHANGELOG}"$'\n'
183+
fi
184+
185+
if [ ${#MAINTENANCE[@]} -gt 0 ]; then
186+
CHANGELOG="${CHANGELOG}### 🔧 Maintenance"$'\n\n'
187+
while IFS= read -r entry; do
188+
CHANGELOG="${CHANGELOG}${entry}"$'\n'
189+
done < <(sort_entries MAINTENANCE)
190+
CHANGELOG="${CHANGELOG}"$'\n'
191+
fi
192+
193+
if [ ${#OTHER[@]} -gt 0 ]; then
194+
CHANGELOG="${CHANGELOG}### 📦 Other"$'\n\n'
195+
while IFS= read -r entry; do
196+
CHANGELOG="${CHANGELOG}${entry}"$'\n'
197+
done < <(sort_entries OTHER)
198+
CHANGELOG="${CHANGELOG}"$'\n'
199+
fi
200+
201+
# If changelog is empty, use a fun message
202+
if [ -z "$CHANGELOG" ]; then
203+
CHANGELOG="So much goodness, we lost track! 🎉"
204+
fi
205+
206+
echo "Generated changelog:"
207+
echo "$CHANGELOG"
208+
209+
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
210+
echo "$CHANGELOG" >> $GITHUB_OUTPUT
211+
echo "EOF" >> $GITHUB_OUTPUT
212+
shell: bash
213+
env:
214+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)