DEV Community

Cover image for Solved: Automate Release Notes Generation from Git Commit Messages
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Automate Release Notes Generation from Git Commit Messages

🚀 Executive Summary

TL;DR: Manual release note generation is a significant bottleneck, prone to errors, and often doesn’t justify expensive SaaS solutions. This guide provides a robust automation pipeline using a consistent Git commit message convention and a Python script to generate accurate release notes directly from your Git history, streamlining the release process.

🎯 Key Takeaways

  • Adopting a consistent commit message convention, such as type(scope): description (e.g., feat(user-profile): Add avatar upload functionality), is fundamental for automated parsing and categorization of changes.
  • The git log –pretty=format:”%s” start\_ref..end\_ref command is crucial for efficiently extracting raw commit subject lines between specified Git references, providing the necessary input for release note generation.
  • Integrating the Python script into CI/CD pipelines (e.g., GitLab CI) allows for automatic execution upon events like new tag pushes, ensuring consistent and timely release note generation as part of the deployment workflow.

Automate Release Notes Generation from Git Commit Messages

As development teams accelerate their release cycles, the manual process of crafting release notes often becomes a significant bottleneck. This tedious task is not only time-consuming but also prone to human error, leading to inconsistent information or delays in communicating new features and bug fixes to users and stakeholders. For many organizations, investing in expensive SaaS solutions solely for release note generation might not be justifiable, especially when a powerful tool like Git already holds most of the raw material.

This tutorial from TechResolve will guide you through building a simple yet robust automation pipeline to generate comprehensive release notes directly from your Git commit messages. By leveraging a consistent commit message convention and a Python script, you can eliminate manual effort, ensure accuracy, and free up your team to focus on what they do best: building great software.

Prerequisites

Before we begin, ensure you have the following installed and configured:

  • Git: Version Control System, installed and accessible from your command line.
  • Python 3.x: The scripting language we’ll use for parsing and generation.
  • Basic understanding of Git: Familiarity with commands like git log, tags, and branching.
  • A text editor: Visual Studio Code, Sublime Text, or similar.
  • A Git repository: With some commit history to experiment with.

Step-by-Step Guide

Step 1: Establish a Consistent Commit Message Convention

The foundation of automated release notes lies in consistent, structured commit messages. We’ll adopt a simplified version of Conventional Commits, which categorizes changes and provides clear descriptions.

A typical commit message format will look like this: type(scope): description.

  • type: Mandatory. Describes the kind of change (e.g., feat for a new feature, fix for a bug fix, chore for routine tasks, docs for documentation, refactor for code changes that don’t add features or fix bugs).
  • scope: Optional. Specifies the part of the codebase affected (e.g., (auth), (frontend)).
  • description: Mandatory. A concise description of the change.

Examples:

  • feat(user-profile): Add avatar upload functionality
  • fix: Resolve critical login redirection bug
  • chore(dependencies): Update Python libraries to latest versions
  • docs: Add usage instructions for new API endpoint

Ensure your team adheres to this convention for accurate release note generation.

Step 2: Retrieve Git Log Data

The first step in our automation is to extract the relevant commit messages from your Git history. We’ll use the git log command, formatted to provide just the commit subject lines, which contain our structured messages.

To get commits between two specific points (e.g., from your last release tag to the current HEAD), you can use the following command. Replace v1.0.0 with your actual last release tag.

git log --pretty=format:"%s" v1.0.0..HEAD
Enter fullscreen mode Exit fullscreen mode

Let’s break down the command:

  • git log: The command to display commit logs.
  • --pretty=format:"%s": Formats the output to show only the commit subject line (the first line of the commit message).
  • v1.0.0..HEAD: Specifies the range of commits. It will show all commits reachable from HEAD but not from v1.0.0. This effectively gives us all commits since the v1.0.0 tag was created.

You can test this command in your repository to see the raw commit messages it returns.

Step 3: Parse Commit Messages with Python

Now, let’s write a Python script to take the output from our git log command, parse each commit message according to our convention, and categorize them.

Create a file named generate_release_notes.py and add the following code:

import subprocess
import re
import os

def get_git_log(start_ref, end_ref="HEAD"):
    """
    Retrieves commit messages from Git history between two references.
    """
    # Using --no-merges to exclude merge commits if desired, focus on feature commits
    command = f"git log --no-merges --pretty=format:\"%s\" {start_ref}..{end_ref}"
    try:
        process = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
        return process.stdout.strip().split('\n')
    except subprocess.CalledProcessError as e:
        print(f"Error fetching git log: {e.stderr}")
        return []

def parse_commit_message(message):
    """
    Parses a single commit message based on the conventional commit structure.
    """
    # Regex to match 'type(scope): description' or 'type: description'
    match = re.match(r"^(feat|fix|build|chore|ci|docs|refactor|style|test)(\([^)]+\))?: (.+)", message)
    if match:
        commit_type = match.group(1)
        # Remove parentheses from scope if present, else empty string
        scope = match.group(2).strip("()") if match.group(2) else ""
        description = match.group(3)
        return {"type": commit_type, "scope": scope, "description": description, "raw": message}
    return {"type": "other", "scope": "", "description": message, "raw": message} # Fallback for non-compliant messages

def generate_release_notes_markdown(commits_data):
    """
    Generates release notes in Markdown format from parsed commit data.
    """
    categories = {
        "feat": {"title": "✨ New Features", "items": []},
        "fix": {"title": "🐛 Bug Fixes", "items": []},
        "refactor": {"title": "♻️ Refactoring", "items": []},
        "docs": {"title": "📚 Documentation", "items": []},
        "chore": {"title": "🛠️ Chores & Improvements", "items": []},
        "other": {"title": "📝 Other Changes", "items": []}
    }

    for commit in commits_data:
        # Use commit type from parser, default to 'other'
        commit_type = commit.get("type", "other")
        description = commit.get("description", "No description provided")
        scope = commit.get("scope")

        # Add scope to description if it exists
        formatted_description = f"**{scope}:** {description}" if scope else description

        if commit_type in categories:
            categories[commit_type]["items"].append(formatted_description)
        else:
            categories["other"]["items"].append(f"[{commit_type}] {formatted_description}")

    output_lines = ["## Release Notes\n"]

    for category_key, category_data in categories.items():
        if category_data["items"]:
            output_lines.append(f"### {category_data['title']}\n")
            for item in category_data["items"]:
                output_lines.append(f"- {item}")
            output_lines.append("") # Add a blank line for readability

    return "\n".join(output_lines)

if __name__ == "__main__":
    # In a CI/CD environment, these would be passed as arguments or environment variables.
    # For local testing, you can hardcode or dynamically determine them.
    # Example: get the latest tag for 'start_ref'
    try:
        # Get the latest annotated tag as the starting reference
        # We use '> /dev/null 2>&1' to suppress errors if no tags are found
        # and then check if the command was successful.
        last_release_tag_cmd = "git describe --tags --abbrev=0 > /dev/null 2>&1 || echo ''"
        last_release_tag_process = subprocess.run(last_release_tag_cmd, shell=True, capture_output=True, text=True, check=False)
        last_release_tag = last_release_tag_process.stdout.strip()

        if not last_release_tag:
            print("Warning: No previous tags found. Generating notes from initial commit. "
                  "Consider tagging your releases (e.g., git tag -a v1.0.0 -m \"First release\").")
            # If no tags, get commits from the very beginning (root commit)
            start_reference = "HEAD^0" # Refers to the first commit in reverse chronological order
        else:
            start_reference = last_release_tag

    except subprocess.CalledProcessError as e:
        print(f"Error determining last release tag: {e.stderr}")
        start_reference = "HEAD^0" # Fallback to initial commit

    current_reference = os.environ.get("CI_COMMIT_SHA", "HEAD") # Use CI_COMMIT_SHA if in CI, else HEAD

    print(f"Generating release notes from '{start_reference}' to '{current_reference}'...")

    raw_commits = get_git_log(start_reference, current_reference)

    parsed_commits = []
    for commit_msg in raw_commits:
        parsed_commits.append(parse_commit_message(commit_msg))

    release_notes_content = generate_release_notes_markdown(parsed_commits)

    # Output to stdout or a file
    output_filename = "release_notes.md"
    with open(output_filename, "w") as f:
        f.write(release_notes_content)

    print(f"\nRelease notes generated successfully and saved to {output_filename}:\n")
    print(release_notes_content)
Enter fullscreen mode Exit fullscreen mode

Explanation of the Python script:

  • get_git_log: Executes the git log command and returns a list of raw commit messages. Note the use of subprocess.run for executing external commands and check=True to raise an error if the command fails.
  • parse_commit_message: Uses a regular expression (re.match) to extract the type, optional scope, and description from each commit message. It includes a fallback for messages that don’t match the convention.
  • generate_release_notes_markdown: Takes the parsed commit data and structures it into a human-readable Markdown format, grouped by commit type.
  • if __name__ == "__main__":: This block demonstrates how to run the script. It dynamically tries to find the last Git tag as the starting point for release notes. In a real CI/CD scenario, you’d likely pass start_ref and end_ref as environment variables or command-line arguments. The script then writes the generated notes to a file named release_notes.md.

Step 4: Generate Structured Release Notes

The Python script already includes the logic to generate release notes in Markdown format. When you run the script, it will parse the commits and print the formatted notes to your console, and also save them to release_notes.md.

To run the script:

python3 generate_release_notes.py
Enter fullscreen mode Exit fullscreen mode

You should see output similar to this (assuming you have commits that match the convention):

Generating release notes from 'v1.0.0' to 'HEAD'...

Release notes generated successfully and saved to release_notes.md:

## Release Notes

### ✨ New Features

- **user-profile:** Add avatar upload functionality
- Introduce a new dashboard widget

### 🐛 Bug Fixes

- Resolve critical login redirection bug
- Fix pagination issue on user list

### 🛠️ Chores & Improvements

- **dependencies:** Update Python libraries to latest versions
- Refactor CI/CD pipeline configuration
Enter fullscreen mode Exit fullscreen mode

Step 5: Integrate into CI/CD Pipeline

The true power of this automation comes when integrated into your CI/CD pipeline. You can configure your pipeline (e.g., GitHub Actions, GitLab CI, Jenkins, Azure DevOps) to run this script automatically on specific events, such as when a new tag is pushed or a merge occurs into your main branch.

Here’s a conceptual example for a GitLab CI/CD pipeline (the exact syntax will vary for other platforms):

# .gitlab-ci.yml or similar CI configuration file

stages:
  - build
  - test
  - deploy

generate_release_notes:
  stage: deploy
  image: python:3.9-slim-buster # Use a Python image
  script:
    - python3 generate_release_notes.py # The script will automatically pick up CI_COMMIT_SHA
    - echo "--- Generated Release Notes ---"
    - cat release_notes.md
    - # Further steps:
    - # 1. Upload 'release_notes.md' to a GitHub Release
    - # 2. Post to a Slack channel
    - # 3. Update a Jira ticket
    - # 4. Store as a build artifact
  only:
    - tags # Trigger this job only when a new Git tag is pushed
Enter fullscreen mode Exit fullscreen mode

In this example, the generate_release_notes job runs when a new tag is pushed. The Python script then creates a release_notes.md file, which can subsequently be used by other steps in your pipeline to publish the release notes wherever needed.

Common Pitfalls

While automating release notes is highly beneficial, a few common issues can arise:

  1. Inconsistent Commit Messages: The most significant hurdle. If developers deviate from the defined commit message convention, the parsing script will either categorize messages incorrectly or omit them entirely.
    • Solution: Implement pre-commit Git hooks (e.g., using Husky or a custom script) to validate commit messages before they are allowed into the repository. Regular code reviews and team training on the convention are also crucial.
  2. Incorrect Git References: Specifying the wrong start_ref or end_ref can lead to missing commits or including irrelevant ones. For instance, generating notes from HEAD to HEAD will result in empty notes.
    • Solution: Ensure your CI/CD pipeline passes the correct Git references (e.g., previous tag, current branch HEAD, or a specific commit SHA) to the script. Double-check your git log command in the terminal before automating.

Conclusion

Automating release note generation from Git commit messages is a powerful way to streamline your development workflow, reduce manual overhead, and ensure consistent, accurate communication for every release. By investing a small amount of effort into defining a commit message convention and setting up a simple Python script, you can reclaim valuable engineering time and enhance your team’s agility.

This tutorial provides a solid foundation. From here, you can extend the script to support more sophisticated features, such as integrating with issue trackers (e.g., Jira, GitHub Issues) to link pull requests, generating different output formats (HTML, JSON), or publishing notes directly to external platforms like Confluence or Slack. Embrace the automation, and make manual release note creation a thing of the past!


Darian Vance

👉 Read the original article on TechResolve.blog


Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)