🚀 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\_refcommand 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.,
featfor a new feature,fixfor a bug fix,chorefor routine tasks,docsfor documentation,refactorfor 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 functionalityfix: Resolve critical login redirection bugchore(dependencies): Update Python libraries to latest versionsdocs: 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
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 fromHEADbut not fromv1.0.0. This effectively gives us all commits since thev1.0.0tag 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)
Explanation of the Python script:
-
get_git_log: Executes thegit logcommand and returns a list of raw commit messages. Note the use ofsubprocess.runfor executing external commands andcheck=Trueto raise an error if the command fails. -
parse_commit_message: Uses a regular expression (re.match) to extract thetype, optionalscope, anddescriptionfrom 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 passstart_refandend_refas environment variables or command-line arguments. The script then writes the generated notes to a file namedrelease_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
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
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
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:
-
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.
-
Incorrect Git References: Specifying the wrong
start_reforend_refcan lead to missing commits or including irrelevant ones. For instance, generating notes fromHEADtoHEADwill 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 logcommand in the terminal before automating.
-
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
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!
👉 Read the original article on TechResolve.blog
☕ Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)