Python library version management with GitHub action + commitizen + pre-commit hook

List of tools used in this post

Here is a sample repository which show the implementation below.

Context & overview

Lets assume that we are going to build a python library. Lets call it minipackage. We need to manage the version of the library & need to generate the CHANGELOGS.md whenever we release a version of the library. There are multiple ways to solve this problem. Here in the post we are going see how we can use semantic versioning to solve this problem.

Project directory structure

  • minipackage: All the source code for the python library will live here.
    • __init.py__: Library version, other meta data will be here. Also, we need to register any external methods we want to expose as API in the library.
    • main.py: main module is kind of representation of any module and sub-module which will be part of the python library.
  • pyproject.toml: This file will have all the meta-data related to the tools which we will be using this project. In the example, tool.commitizen example is present but there can be many more settings registered in this file related to the other tools being used in the project.
  • README.md: self explanatory.
  • requirements.txt: All the python dependencies required for the library.
  • .pre-commit-config.yaml: git pre-commit hooks. Here in this example we have just shown the commitizen pre-commit hook example. But there can be many other as well.
  • setup.py: all the library building related information will be here.

If you want to refresh some basics of python library building process checkout this link. Regarding pre-commit checkout this link.

.
├── minipackage
│   ├── __init__.py
│   ├── main.py
├── pyproject.toml
├── README.md
├── requirements.txt
├── .pre-commit-config.yaml
└── setup.py

version and version_files these are the two import field which has a major impact in this workflow. make sure that version_files field points to the file which is the single source of version. We also need to ensure that we will be using the same file and variable all across the project. While start a project __version__ in __init__.py and version variable in pyproject.toml file should match.

[tool.commitizen]
name = "cz_conventional_commits"
version = "0.0.1"
version_files = [
    "minipackage/__init__.py",
    "pyproject.toml:version"
]
tag_format = "v$version"
bump_message = "bump: $current_version → $new_version [skip-ci]"

Note, here in our example here we using the variable __version__ in all place in the project to manage the version and the same file is mentioned in the pyproject file. Now, when we using commitizen to bump version of the library it will change value of this variable depending on the commit message tags. Also, this will generate a changelog.md file in the project root directory and create a release tag.

In this section, notice that we using __version__ variable from minipackage module here. We are passing this __version__ variable to in version argument in setup function. This will ensure whenever we are building a library, the library gets tagged with this version.

Note, that whatever is being defined or imported in minipackage/__init__py file will be present in in minipackage module.

from setuptools import find_packages, setup
from minipackage import __version__

setup(
    author="Aritra Biswas",
    author_email="pandalearnstocode@gmail.com",
    python_requires=">=3.8",
    install_requires=requirements,
    include_package_data=True,
    keywords="minipackage",
    name="minipackage",
    packages=find_packages(include=["minipackage", "minipackage.*"]),
    version=__version__,
)

source/__init__.py: main place where the version variable is being used

This will be the initial state of the init file. Later when we bump library version in the CI pipeline, this __version__ variable will change depending upon commit message tags.

__version__ = "0.0.1"
from minipackage.main import hello_world, hello_mcu, hello_dc, 

Example CZ commits

.pre-commit-config.yaml : ensuring commit message format is being followed

This is not a mandatory thing but kind of a fail safe mechanism to implement conventional commit messages in our day to day workflow.

repos:
- repo: https://github.com/commitizen-tools/commitizen
  rev: v2.19.0
  hooks:
    - id: commitizen
      stages: [commit-msg]

One this pre-commit hook is install to a repository, whenever we are going to make commit this will check the commit tags are present in the commit message or not. To know more about this in depth go through this site.

SemVer

.github/workflows/bumpversion.yaml : GitHub action to update the version value in the respective file

For a commit message like bump: update library version. to the configured branch, this will update the library version, generate changelog and push it to the feature branch. Post that when we trigger a build, a library with the same version tag will be generated and sent to the python library repository.

name: NEW Bump version
on:
  push:
    branches:
      - develop
jobs:
  bump_version:
    if: "!startsWith(github.event.head_commit.message, 'bump:')"
    runs-on: ubuntu-latest
    name: "Bump version and create changelog with commitizen"
    steps:
      - name: Check out
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          token: "${{ secrets.GITHUB_TOKEN }}"
      - id: cz
        name: Create bump and changelog
        uses: commitizen-tools/commitizen-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: develop
      - name: Print Version
        run: echo "Bumped to version ${{ steps.cz.outputs.version }}"

Note: This workflow may change depending upon how you want to update the library version. Here action is driven by push to develop branch but it is possible to setup this process with pull request trigger or any other trigger as well.

Reference:

Aritra Biswas
Aritra Biswas
Senior Manager Analytics

My research interests include computational statistics, causal inference, simulation and mathematical optimization.