Automate Semantic Versioning with Conventional Commits in Python

Automatically bump a semantic version based on conventional commit messages (feat, fix, BREAKING CHANGE) and write the new version to a file.

Medium Python 3.9+ Aug 9, 2026 Production deployment patterns 14 views 0 copies

Python code

42 lines
Python 3.9+
import re
from pathlib import Path


def get_next_version(current: str, commit_messages: list[str]) -> str:
    """Return the next semantic version based on conventional commit messages."""
    major, minor, patch = map(int, current.split("."))
    if any(msg.startswith("BREAKING CHANGE") for msg in commit_messages):
        return f"{major + 1}.0.0"
    if any(msg.startswith("feat") for msg in commit_messages):
        return f"{major}.{minor + 1}.0"
    if any(msg.startswith("fix") for msg in commit_messages):
        return f"{major}.{minor}.{patch + 1}"
    return current


def update_version_file(path: Path, new_version: str) -> None:
    """Update the version string in a simple VERSION file."""
    path.write_text(new_version + "\n")
    print(f"Updated {path} to version {new_version}")


if __name__ == "__main__":
    # Mock commit history as a list of conventional commit messages
    commits = [
        "feat: add user authentication",
        "fix: correct login redirect bug",
        "docs: update README",
        "feat: add profile page",
        "chore: bump dependencies",
    ]

    version_file = Path("VERSION")
    version_file.write_text("1.2.3\n")

    current = version_file.read_text().strip()
    new_version = get_next_version(current, commits)
    update_version_file(version_file, new_version)

    print(f"Current: {current}")
    print(f"Next: {new_version}")
    print(f"Final file content: {version_file.read_text().strip()!r}")

Output

stdout
Updated VERSION to version 1.3.0
Current: 1.2.3
Next: 1.3.0
Final file content: '1.3.0'

How it works

The get_next_version function parses the current version into major, minor, and patch components using map(int, ...), then checks commit messages for prefixes indicating a bump type. The logic prioritizes breaking changes (major bump) over features (minor bump) and fixes (patch bump), falling back to the current version if no relevant commits are found. This mimics how tools like semantic-release determine the next release number from conventional commit history. The script writes the updated version to a VERSION file, simulating what a CI pipeline would do before tagging a release.

Common mistakes

  • Checking commit prefixes without handling case sensitivity or extra whitespace (e.g., 'Feat:' vs 'feat:').
  • Writing the version file before computing the new version, leaving it in an inconsistent state.
  • Not stripping the version string, causing parsing errors when reading from a file with a trailing newline.

Variations

  1. Use a changelog generator like `python-semantic-release` instead of a custom script for full CI integration.
  2. Parse the current version from a `pyproject.toml` file using `tomllib` instead of a separate VERSION file.

Real-world use cases

  • Bump the version number in a project's version file during a CI/CD pipeline before building and publishing a package.
  • Automatically tag releases in a Git repository based on merged pull requests that follow conventional commit conventions.
  • Generate release notes from the bump messages and attach them to a new GitHub release for changelog automation.

Sponsored

Run this sample

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

Open editor

More from Production deployment patterns

Related tutorials and quizzes for this topic.