Skip to content

Commit b017c2d

Browse files
jeremyederclaude
andauthored
feat(ci): enhance release changelog with author grouping and first-time contributors (#1039)
## Summary - **Group commits by author** with commit counts (e.g., "Gage Krumbach (18)"), sorted by contribution count - **Add "First-Time Contributors" section** with 🎉 emoji to celebrate new contributors - **Use Python** for reliable parsing of commit data with special characters - **Fix `--before` bug**: resolve tag to ISO date since `git log --before` requires a date, not a ref name — passing a tag name silently returns wrong results, causing incorrect first-timer detection ## Example Output (v0.0.34) ### Before (flat list): ``` - fix(frontend): resolve agent response buffering (#991) (de50276) - fix(frontend): export chat handles compacted MESSAGES_SNAPSHOT events (#1010) (b204abd) - fix(frontend): binary file download corruption (#996) (5b584f8) ... ``` ### After (grouped by author): ``` ## 🎉 First-Time Contributors - Derek Higgins - Pete Savage - Rahul Shetty ### Gage Krumbach (5) - fix(runner): improve ACP MCP tools (#1006) (26be0f9) - chore(manifests): scale up frontend replicas (#1008) (b331da1) ... ### Pete Savage (1) - fix(frontend): resolve agent response buffering (#991) (de50276) ``` ## Bug Fix: `--before` with tag names `git log --before=v0.0.33` does **not** filter by the tag's date — it resolves differently and returns commits from after the tag. This caused first-timer detection to produce wrong results. Fixed by resolving the tag to its ISO date first: ```python tag_date = subprocess.run( ['git', 'log', '-1', '--format=%ci', latest_tag], capture_output=True, text=True ).stdout.strip() ``` ## Test Plan - [x] Verified `--before=<tag>` vs `--before=<date>` returns different results - [x] Tested changelog generation locally against v0.0.33→v0.0.34 and v0.0.34→v0.0.35 - [x] Confirmed first-time contributor detection works correctly with date-based filtering - [x] YAML validates (`check-yaml` hook passes) - [ ] Will validate in next production release 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c69a397 commit b017c2d

File tree

1 file changed

+74
-16
lines changed

1 file changed

+74
-16
lines changed

.github/workflows/prod-release-deploy.yaml

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,81 @@ jobs:
9292
LATEST_TAG="${{ steps.get_latest_tag.outputs.latest_tag }}"
9393
NEW_TAG="${{ steps.next_version.outputs.new_tag }}"
9494
95-
echo "# Release $NEW_TAG" > RELEASE_CHANGELOG.md
96-
echo "" >> RELEASE_CHANGELOG.md
97-
echo "## Changes since $LATEST_TAG" >> RELEASE_CHANGELOG.md
98-
echo "" >> RELEASE_CHANGELOG.md
99-
100-
# Generate changelog from commits
101-
if [ "$LATEST_TAG" = "v0.0.0" ]; then
102-
# First release - include all commits
103-
git log --pretty=format:"- %s (%h)" >> RELEASE_CHANGELOG.md
104-
else
105-
# Get commits since last tag
106-
git log ${LATEST_TAG}..HEAD --pretty=format:"- %s (%h)" >> RELEASE_CHANGELOG.md
107-
fi
95+
# Use Python for reliable changelog generation with author grouping
96+
python3 -c "
97+
import subprocess, sys
98+
99+
latest_tag = sys.argv[1]
100+
new_tag = sys.argv[2]
101+
repo = sys.argv[3]
102+
103+
commit_range = 'HEAD' if latest_tag == 'v0.0.0' else f'{latest_tag}..HEAD'
104+
105+
result = subprocess.run(
106+
['git', 'log', commit_range, '--format=%an<<<<DELIM>>>>%s (%h)'],
107+
capture_output=True, text=True
108+
)
108109
109-
echo "" >> RELEASE_CHANGELOG.md
110-
echo "" >> RELEASE_CHANGELOG.md
111-
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LATEST_TAG}...${NEW_TAG}" >> RELEASE_CHANGELOG.md
110+
if result.returncode != 0:
111+
print(f'Error: git log failed: {result.stderr}', file=sys.stderr)
112+
sys.exit(1)
113+
114+
commits_by_author = {}
115+
count_by_author = {}
116+
117+
for line in result.stdout.strip().split('\n'):
118+
if line and '<<<<DELIM>>>>' in line:
119+
author, commit = line.split('<<<<DELIM>>>>', 1)
120+
if author not in commits_by_author:
121+
commits_by_author[author] = []
122+
count_by_author[author] = 0
123+
commits_by_author[author].append(commit)
124+
count_by_author[author] += 1
125+
126+
sorted_authors = sorted(count_by_author.items(), key=lambda x: x[1], reverse=True)
127+
128+
# Detect first-time contributors
129+
first_timers = []
130+
if latest_tag != 'v0.0.0':
131+
# Resolve tag to ISO date — --before requires a date, not a ref name
132+
tag_date_result = subprocess.run(
133+
['git', 'log', '-1', '--format=%ci', latest_tag],
134+
capture_output=True, text=True
135+
)
136+
tag_date = tag_date_result.stdout.strip()
137+
if tag_date_result.returncode == 0 and tag_date:
138+
# Get all unique author names before the tag date in one call
139+
prior = subprocess.run(
140+
['git', 'log', '--all', f'--before={tag_date}', '--format=%an'],
141+
capture_output=True, text=True
142+
)
143+
prior_authors = set()
144+
if prior.returncode == 0 and prior.stdout.strip():
145+
prior_authors = set(prior.stdout.strip().split('\n'))
146+
for author, _ in sorted_authors:
147+
if author not in prior_authors:
148+
first_timers.append(author)
149+
150+
print(f'# Release {new_tag}')
151+
print()
152+
print(f'## Changes since {latest_tag}')
153+
print()
154+
155+
if first_timers:
156+
print('## 🎉 First-Time Contributors')
157+
print()
158+
for author in sorted(first_timers):
159+
print(f'- {author}')
160+
print()
161+
162+
for author, count in sorted_authors:
163+
print(f'### {author} ({count})')
164+
for commit in commits_by_author[author]:
165+
print(f'- {commit}')
166+
print()
167+
168+
print(f'**Full Changelog**: https://github.com/{repo}/compare/{latest_tag}...{new_tag}')
169+
" "$LATEST_TAG" "$NEW_TAG" "${{ github.repository }}" > RELEASE_CHANGELOG.md
112170
113171
cat RELEASE_CHANGELOG.md
114172

0 commit comments

Comments
 (0)