Skip to content

Commit 0b8ebf0

Browse files
authored
feat(ci): enhance release notes with issue tracking (#77)
1 parent 3311745 commit 0b8ebf0

3 files changed

Lines changed: 186 additions & 76 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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 SHAs since last release
34+
if [ -z "$PREVIOUS_TAG" ]; then
35+
COMMIT_SHAS=$(git log --pretty=format:"%H" --no-merges)
36+
else
37+
COMMIT_SHAS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%H" --no-merges)
38+
fi
39+
40+
echo "Found $(echo "$COMMIT_SHAS" | wc -l) commits since last release"
41+
42+
# Collect all closed issues by tracing commits -> PRs -> issues
43+
declare -A CLOSED_ISSUES # issue_number -> issue_title
44+
declare -A HIGHLIGHTS # issue_number -> highlight message
45+
46+
for SHA in $COMMIT_SHAS; do
47+
echo "Processing commit: $SHA"
48+
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 "[]")
51+
52+
if [ "$PR_DATA" != "[]" ] && [ -n "$PR_DATA" ]; then
53+
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.[0].number // empty')
54+
PR_BODY=$(echo "$PR_DATA" | jq -r '.[0].body // ""')
55+
56+
if [ -n "$PR_NUMBER" ]; then
57+
echo " Found PR #$PR_NUMBER"
58+
59+
# Extract issue numbers from PR body (Fixes #XX, Closes #XX, Resolves #XX)
60+
ISSUE_NUMBERS=$(echo "$PR_BODY" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")
61+
62+
for ISSUE_NUM in $ISSUE_NUMBERS; do
63+
if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
64+
echo " Found linked issue #$ISSUE_NUM"
65+
66+
# Get issue title
67+
ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")
68+
69+
if [ -n "$ISSUE_TITLE" ]; then
70+
CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"
71+
72+
# Check for /release-note comment on the issue
73+
COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "")
74+
75+
RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "")
76+
77+
if [ -n "$RELEASE_NOTE" ]; then
78+
echo " Found /release-note: $RELEASE_NOTE"
79+
HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE"
80+
fi
81+
fi
82+
fi
83+
done
84+
fi
85+
fi
86+
done
87+
88+
# Also check for issues closed directly (not via PR) in the commit range
89+
# by looking at commit messages for "Fixes #XX" patterns
90+
for SHA in $COMMIT_SHAS; do
91+
COMMIT_MSG=$(git log -1 --pretty=format:"%B" "$SHA")
92+
ISSUE_NUMBERS=$(echo "$COMMIT_MSG" | grep -oiE "(fixes|closes|resolves)\s*#[0-9]+" | grep -oE "[0-9]+" || echo "")
93+
94+
for ISSUE_NUM in $ISSUE_NUMBERS; do
95+
if [ -z "${CLOSED_ISSUES[$ISSUE_NUM]}" ]; then
96+
echo "Found issue #$ISSUE_NUM in commit message"
97+
98+
ISSUE_TITLE=$(gh issue view "$ISSUE_NUM" --json title --jq '.title' 2>/dev/null || echo "")
99+
100+
if [ -n "$ISSUE_TITLE" ]; then
101+
CLOSED_ISSUES[$ISSUE_NUM]="$ISSUE_TITLE"
102+
103+
# Check for /release-note comment
104+
COMMENTS=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[].body' 2>/dev/null || echo "")
105+
RELEASE_NOTE=$(echo "$COMMENTS" | grep "^/release-note " | sed 's|^/release-note ||' | head -1 || echo "")
106+
107+
if [ -n "$RELEASE_NOTE" ]; then
108+
echo " Found /release-note: $RELEASE_NOTE"
109+
HIGHLIGHTS[$ISSUE_NUM]="$RELEASE_NOTE"
110+
fi
111+
fi
112+
fi
113+
done
114+
done
115+
116+
# Build changelog
117+
CHANGELOG=""
118+
119+
# Add Highlights section if any /release-note comments exist
120+
if [ ${#HIGHLIGHTS[@]} -gt 0 ]; then
121+
CHANGELOG="### ✨ Highlights"$'\n\n'
122+
for ISSUE_NUM in "${!HIGHLIGHTS[@]}"; do
123+
CHANGELOG="${CHANGELOG}- ${HIGHLIGHTS[$ISSUE_NUM]}"$'\n'
124+
done
125+
CHANGELOG="${CHANGELOG}"$'\n'
126+
fi
127+
128+
# Add Closed Issues section (always show if there are any)
129+
if [ ${#CLOSED_ISSUES[@]} -gt 0 ]; then
130+
CHANGELOG="${CHANGELOG}### 📋 Closed Issues"$'\n\n'
131+
# Sort issue numbers for consistent ordering
132+
for ISSUE_NUM in $(echo "${!CLOSED_ISSUES[@]}" | tr ' ' '\n' | sort -n); do
133+
CHANGELOG="${CHANGELOG}- #${ISSUE_NUM} - ${CLOSED_ISSUES[$ISSUE_NUM]}"$'\n'
134+
done
135+
fi
136+
137+
# If changelog is empty, use a fun message
138+
if [ -z "$CHANGELOG" ]; then
139+
CHANGELOG="So much goodness, we lost track! 🎉"
140+
fi
141+
142+
echo "Generated changelog:"
143+
echo "$CHANGELOG"
144+
145+
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
146+
echo "$CHANGELOG" >> $GITHUB_OUTPUT
147+
echo "EOF" >> $GITHUB_OUTPUT
148+
shell: bash
149+
env:
150+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Preview Changelog
2+
3+
run-name: Preview release notes for next release
4+
5+
on:
6+
workflow_dispatch:
7+
8+
jobs:
9+
generate:
10+
name: Generate
11+
uses: ./.github/workflows/generate-changelog.yml
12+
13+
preview:
14+
name: Display Preview
15+
runs-on: ubuntu-latest
16+
needs: generate
17+
18+
steps:
19+
- name: Display changelog preview
20+
run: |
21+
echo "=========================================="
22+
echo "CHANGELOG PREVIEW"
23+
echo "=========================================="
24+
echo ""
25+
echo "## What's New in v<VERSION>"
26+
echo ""
27+
echo "${{ needs.generate.outputs.changelog }}"
28+
shell: bash

.github/workflows/release.yml

Lines changed: 8 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,19 @@ jobs:
226226
install.ps1
227227
retention-days: 1
228228

229+
changelog:
230+
name: Generate Changelog
231+
needs: build
232+
uses: ./.github/workflows/generate-changelog.yml
233+
229234
release:
230235
name: Create GitHub Release
231236
runs-on: ubuntu-latest
232-
needs: build
237+
needs: [build, changelog]
233238

234239
steps:
235240
- name: Checkout code
236241
uses: actions/checkout@v4
237-
with:
238-
fetch-depth: 0 # Fetch all history for changelog generation
239242

240243
- name: Download all build artifacts
241244
uses: actions/download-artifact@v4
@@ -248,77 +251,6 @@ jobs:
248251
name: install-scripts
249252
path: .
250253

251-
- name: Generate changelog
252-
id: changelog
253-
run: |
254-
# Get commits since last release tag
255-
# NOTE: This must run BEFORE creating the new tag, otherwise git describe
256-
# will find the new tag and the changelog will be empty
257-
PREVIOUS_TAG=$(git describe --abbrev=0 --tags --match "v*" 2>/dev/null || echo "")
258-
if [ -z "$PREVIOUS_TAG" ]; then
259-
# First release - get all commits
260-
COMMITS=$(git log --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ')
261-
else
262-
# Get commits since previous tag
263-
COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"%s (%h)|%b" --no-merges | tr '\n' ' ')
264-
fi
265-
266-
# Categorize commits by conventional commit type
267-
BUGS=""
268-
FEATURES=""
269-
CHORES=""
270-
271-
# Process each commit
272-
while IFS= read -r line; do
273-
subject=$(echo "$line" | cut -d'|' -f1)
274-
body=$(echo "$line" | cut -d'|' -f2-)
275-
276-
# Extract issue number from body if present
277-
issue=$(echo "$body" | grep -oE "Fixes #[0-9]+" | head -1 || echo "")
278-
if [ -n "$issue" ]; then
279-
entry="- $subject - $issue"
280-
else
281-
entry="- $subject"
282-
fi
283-
284-
# Categorize by prefix
285-
if echo "$subject" | grep -qE "^fix(\(|:)"; then
286-
BUGS="${BUGS}${entry}"$'\n'
287-
elif echo "$subject" | grep -qE "^feat(\(|:)"; then
288-
FEATURES="${FEATURES}${entry}"$'\n'
289-
elif echo "$subject" | grep -qE "^chore(\(|:)"; then
290-
CHORES="${CHORES}${entry}"$'\n'
291-
else
292-
# Default to chores for uncategorized
293-
CHORES="${CHORES}${entry}"$'\n'
294-
fi
295-
done < <(git log ${PREVIOUS_TAG:+$PREVIOUS_TAG..}HEAD --pretty=format:"%s (%h)|%b---END---" --no-merges | sed 's/---END---/\n/g')
296-
297-
# Build changelog with categories
298-
CHANGELOG=""
299-
300-
if [ -n "$BUGS" ]; then
301-
CHANGELOG="${CHANGELOG}### 🐛 Bugs Squashed"$'\n\n'"${BUGS}"$'\n'
302-
fi
303-
304-
if [ -n "$FEATURES" ]; then
305-
CHANGELOG="${CHANGELOG}### 🎉 Features Added"$'\n\n'"${FEATURES}"$'\n'
306-
fi
307-
308-
if [ -n "$CHORES" ]; then
309-
CHANGELOG="${CHANGELOG}### 🧹 Chores Addressed"$'\n\n'"${CHORES}"$'\n'
310-
fi
311-
312-
# If changelog is empty, use a fun message
313-
if [ -z "$CHANGELOG" ]; then
314-
CHANGELOG="So much goodness, we lost track! 🎉"
315-
fi
316-
317-
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
318-
echo "$CHANGELOG" >> $GITHUB_OUTPUT
319-
echo "EOF" >> $GITHUB_OUTPUT
320-
shell: bash
321-
322254
- name: Create and push release tag
323255
run: |
324256
VERSION="${{ github.event.inputs.version }}"
@@ -348,9 +280,9 @@ jobs:
348280
install.sh
349281
install.ps1
350282
body: |
351-
## Changes in v${{ github.event.inputs.version }}
283+
## What's New in v${{ github.event.inputs.version }}
352284
353-
${{ steps.changelog.outputs.CHANGELOG }}
285+
${{ needs.changelog.outputs.changelog }}
354286
355287
## Installation
356288

0 commit comments

Comments
 (0)