53  Auto version bumping (Python)

When we want to finally publish our package, we still need to manage the metadata in the pyproject.toml file to make sure the version value is up to date.

53.1 Package versioning

Semantic versioning uses the major.minor.patch numbering scheme for package versions

Calenar versioning (calver) is another way to version package, it uses part of the date of publishing in the package version number

53.2 Auto version bumping

PyOpenSci discusses the pros and cons of different version bumping tools: https://www.pyopensci.org/python-package-guide/package-structure-code/python-package-versions.html#tools-for-bumping-python-package-versions

53.3 python semantic-release

The Python Semantic Release (PSR) package can be used to help automatically bump your package versions: https://python-semantic-release.readthedocs.io/en/latest/

It will auto bump the respective major/minor/patch value based on specific keywords in your commit messages. These commit messages follow the conventional commits pattern https://www.conventionalcommits.org/en/v1.0.0/

Resources:

53.4 hatch-vcs + git tags

We will make our auto version bumping a bit more flexiable by allowing us to create the version bump on a git tag trigger. We will be using hatch-vcs for this:https://github.com/ofek/hatch-vcs

53.4.1 Prepare the pyproject.toml file

We will need to add hatch-vcs to the [build-system] table:

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

Next we do not need to manually provide the version variable in the [project] table anymore, instead we will use dynamic = ["version"] to set the package version.

[project]
name = "pyos-pytest"  # note your package name
# version = "0.1.0"   # you do not need this anymore if you
dynamic = ["version"]

We will need to install hatch-vcs to test the behavior locally, but also when we use GitHub actions it needs to be installed in the workflow. Best now to add hatch-vcs to the optional dependencies table where we have hatch installed for building the package.

[project.optional-dependencies]
dev = [
    'hatch',
    'hatch-vcs',
]

Then we need to configure the hatch tool so it knows to use hatch-vcs, If you need to configure more hatch-vcs options you cand find it in the documentation: https://github.com/ofek/hatch-vcs?tab=readme-ov-file#version-source-options

For example “to prevent incrementing version numbers on non-release commits, you can adjust the version_scheme parameter”.

[tool.hatch.version]
source = "vcs"
raw-options = { version_scheme = "no-guess-dev", local_scheme = "no-local-version" }

Finally we want to make sure the version information is saved into a file, this is usally named _version.py or __version__.py

Here it will save it to our src/pytest1/__version__.py file (it will create it for us). Note the src/pytest name will differ based on your package import name and directory.

[tool.hatch.build.hooks.vcs]
version-file = "src/pytest1/__version__.py"

Finally let’s run hatch build, you may need to install your build dependencies:

pip install -e .[dev]

Then you can run hatch build, which will run hatch-vcs

$ hatch build

Since we haven’t set up a git tag yet, it may give us a version number that is a 0.0 with some dev+ information. We’ll deal with that in a bit.

You will notice that the src/pytest1/__version__.py has been created with a lot of text. Including a message at the top:

# file generated by setuptools-scm
# don't change, don't track in version control
Important

Follow those comments and ignore the __version__.py file in the .gitignore file! This will prevent all the .post1.dev0 text that may show up after the version number. This is because when hatch build runs it will see a “dirty” git environment (something has been changed from the last git commmit/tag), and this will cause hatch-vcs to create a dev version number of the package.

53.4.2 Create the GitHub Action

We will use git tags to trigger the version value and publishing, so we can create a workflow that will only run when a new git tag is pushed.

Caution

If you are publishing from another workflow, remember to remove those steps.

name: Publish to Test PyPI

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Check-out repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Important: need full history for version detection

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.13'

      - name: Install build dependencies
        run: |
          python -m pip install --upgrade pip
          pip install .[dev] # this will install hatch and hatch-vcs

      - name: Build package
        run: hatch build # remember to ignore the __version__.py file

      - name: Publish to TestPyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          verbose: true
          repository-url: https://test.pypi.org/legacy/
          password: ${{ secrets.TEST_PYPI_API_TOKEN }}

53.4.3 Prepare repository

It’ll be a good time to run hatch build on your local computer and see if the dist/ directory is properly creating the package and version in the bundle. If so, you can push and merge your changes into main/master.

53.5 Github Actions and PyPI/Test

  1. Make sure you have a PyPI token in your github actions secrets
  2. Make sure your package on PyPI / PyPI test is set to accept github as a publisher
  • Go to your package page
  • Open up the Manage Project setting
  • Go to the Publisher setting
  • Scroll down to Add a new publisher setting
  • Fill in your corresponding Github information (owner, repo name, workflow file name)

If your PyPi package is building and fails on the last step with a Bad Request, this may be the reason why

ERROR    HTTPError: 400 Bad Request from https://test.pypi.org/legacy/
         Bad Request

53.5.1 Git Tags

Once you are on the commit that will represent your new release (usually this is the lastest commit on main), you’ll be ready to trigger your release with a git tag.

To create a tag we will use the git tag command:

git tag v0.0.1

Here you can use whatever number you’d like for your package, if your package is already published, make sure the tag version you are using is “newer” than the version already pusbliehd in PyPI / PyPI Test

Also, we are giving our tag name a v prefix. This is also there so our workflow trigger has something to search for, just in case you use tags for other things in your repository.

on:
  push:
    tags:
      - 'v*.*.*'

Finally you can push your branch and tag (make sure there are no more newer commits from your tag)

git push origin main # push to the main branch
git push origin v0.0.1 # push your tag

You can navigate to your repository, and you will see a new tag being listed by the release section. And your automation will start.