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.

Medium Python 3.9+ Aug 9, 2026 Git + Python 15 views 0 copies

Python code

52 lines
Python 3.9+
import 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

stdout
# 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

  1. Use the `conventional-commits` or `commitizen` package to enforce parsing standards
  2. Add a `compare_url` or version tag to the header for linking to GitHub diffs
  3. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Git + Python

Related tutorials and quizzes for this topic.