Generate Release Notes Markdown from PR Titles in Python
Generate structured Markdown release notes from a list of pull request titles using conventional commit types.
Python code
52 linesimport json
from datetime import datetime, timezone
PRS = [
{"title": "feat: add user login", "number": 12, "merged_at": "2025-01-10"},
{"title": "fix: resolve payment timeout", "number": 13, "merged_at": "2025-01-11"},
{"title": "chore: bump dependencies", "number": 14, "merged_at": "2025-01-12"},
{"title": "feat: export CSV reports", "number": 15, "merged_at": "2025-01-13"},
{"title": "fix: handle empty cart state", "number": 16, "merged_at": "2025-01-14"},
{"title": "refactor: streamline auth middleware", "number": 17, "merged_at": "2025-01-15"},
]
def generate_release_notes(pull_requests):
"""Build Markdown release notes from a list of PR dicts."""
sections = {}
for pr in pull_requests:
title = pr["title"]
# Extract the conventional-commit type (before the first colon)
if ": " in title:
commit_type, description = title.split(": ", 1)
else:
commit_type, description = "misc", title
# Map verbose types to release-note categories
mapping = {
"feat": "🚀 Features",
"fix": "🐛 Bug Fixes",
"refactor": "♻️ Refactors",
"chore": "🧹 Maintenance",
}
section = mapping.get(commit_type, "📝 Other")
item = f"- #{pr['number']} {description}"
sections.setdefault(section, []).append(item)
sorted_sections = sorted(sections.items(), key=lambda x: list(mapping.values()).index(x[0]) if x[0] in mapping.values() else len(mapping.values()))
lines = ["# Release Notes", ""]
for section, items in sorted_sections:
lines.append(f"## {section}")
lines.extend(items)
lines.append("")
lines.append("---")
lines.append(f"*Generated on {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*")
return "\n".join(lines).strip()
if __name__ == "__main__":
notes = generate_release_notes(PRS)
print(notes)
Output
# Release Notes
## 🚀 Features
- #12 add user login
- #15 export CSV reports
## 🐛 Bug Fixes
- #13 resolve payment timeout
- #16 handle empty cart state
## ♻️ Refactors
- #17 streamline auth middleware
## 🧹 Maintenance
- #14 bump dependencies
---
*Generated on 2025-01-15 08:00 UTC*
How it works
This script parses conventional commit prefixes (feat, fix, chore, refactor) from PR title strings, extracts the description after the colon, and groups PRs into category sections. The mapping dictionary translates standard commit types into user-friendly Markdown headings with emojis. Sorted sections ensure stable ordering: Features first, then Bug Fixes, Refactors, Maintenance, and Other. The final timestamp line provides traceability for when the notes were generated.
Common mistakes
- Not handling PR titles that lack a conventional commit prefix, causing KeyError
- Assuming all PR titles follow the 'type: description' format exactly
- Forgetting to handle duplicate section names when multiple commit types map to the same category
- Sorting sections alphabetically instead of by the custom category order
Variations
- Use the `conventional-commits` or `commitizen` package to enforce parsing standards
- Add a `compare_url` or version tag to the header for linking to GitHub diffs
- Filter out `chore` entries or include them in a separate 'Dependencies' section
Real-world use cases
- Automatically building changelog drafts from merged PRs in a CI pipeline before a release
- Generating weekly team summary emails from merged feature and fix PR titles
- Creating API documentation update notes from PR titles that reference endpoint changes or fixes
Sponsored
More from Git + Python
- Amend Last Commit Message in Python easy
- Bisect Good Bad Automation Script in Python easy
- Build a Simple Log Graph in Python easy
- Bump Semantic Version Git Tag in Python easy
- Count Unique Contributors from Git Shortlog in Python easy
- Create a Mock GitHub Release API in Python for Testing gh CLI easy
Keep learning
Related tutorials and quizzes for this topic.