The bumpversion CLI

Tagging New Versions of a Project

git
version control
python
bash
Author

Gavin Masterson

Published

January 28, 2023

As the lifespan of a project increases, the incremental changes add up. While we all know that using version control is the best way to keep track of the changes that are accruing in a project, version control tools like Git have so many intricacies that it sometimes feels more like we are having our chaotic impulses and tendencies recorded for posterity, rather than gaining control of the project.

In this post, I will focus on the process of managing project / package / code versions within the lifespan of a project. The goal is to create a repo, mark it with a version, add some additional features and increment the version of the project each time we do so. This is an important consideration and workflow for collaborative and/or public-facing projects.

A Crash Course in Semantic Versioning

I am writing this blog post using quarto, but which version of quarto am I using?

quarto --version
# 1.2.335

The output of the tool tells me the semantic version of the code that the tool is running. There are many excellent posts and discussions explaining what these sematic version numbers mean and how to use them, but in short:

  • the first number is the MAJOR version;
  • the second number is the MINOR version; and
  • the third number is the PATCH version.

As quarto receives updates and new features, these numbers change to reflect the degree to which the tool has changed.

Changes to the PATCH number indicate that bugs have been fixed in a manner that is backwards compatible. Code written by users in an earlier version with the same MAJOR and MINOR version number should still run, but it will encounter fewer bugs when doing so. Changes to the MINOR version are made when new functionality is added (also backwards compatible). When users see a new MINOR version, they can expect to have the old feature set work the way it did previously, but also to have access to features/functions that extend the tool’s capabilities.

Changes to the PATCH or MINOR version can occur without users needing to be too concerned with the specifics of what is changing. For MAJOR version changes, this is not the case. When the MAJOR version number changes, users need to pay close attention to the release notes where they will be notified about ‘breaking changes’ to the tool which mean that it is no longer backwards compatible. As an example, when R changed from v3.6.1 to v4.0.0, one of the critical breaking changes introduced was that the default setting for stringsAsFactors changed from TRUE to FALSE.
This decision means that code written in any R version prior to version 4 could have new errors introduced simply because this previously-unspecified default argument to many R functions no longer converts a string to a factor. This is an important distinction for many modelling and data wrangling workflows that are type sensitive.

This blog post will not cover the details of semantic versioning in any further detail, but instead we will look at how to use semantic versioning in a simple and convenient way.

My Project

In this demo, I will be using bash commands to create and edit files, however the process of changing the version number, which is referred to as ‘bumping the version’, uses a Python CLI tool that is system agnostic.

To follow along you will need to have bumpversion installed.

pip install bumpversion

A Project Workflow

First we create a new directory and switch to it:

mkdir my-project && \
cd my-project

We initialise version control with Git, and we add a README.md and DESCRIPTION file:

git init && \
touch README.md && \
touch DESCRIPTION && \
ls -alF

Next we add a version number for our project to the DESCRIPTION file:

echo "Version Number: 0.0.0" > DESCRIPTION && \
cat DESCRIPTION

The setup is now complete so we can make our first commit of the project state:

git add . && \
git commit -m "feat: Initial commit"

At this point we are ready to add a feature/piece of functionality to our project, so let’s pretend that we add a function in a file called functions.R.

Note: I will use a bash command for simplicity’s sake, but in a real scenario we would be doing this in our preferred IDE.

echo \
"
new_function <- function(...) {
    ...
}
" > functions.R

We can now commmit the functions.R file:

git add functions.R && \
git commit -m "feat: Add new_function"

The project now has a new feature which means that it is in our best interest to bump the MINOR version of the project:

bumpversion --current-version 0.0.0 \
  minor \
  --tag \
  --commit \
  DESCRIPTION && \
cat DESCRIPTION

In the command above we specify that we are using bumpversion, pass in the current version of the project, i.e. 0.0.0 and specify that the MINOR part of the version number needs to be bumped in the DESCRIPTION file. The --tag and the --commit flags tell bumpversion to commit the change of the DECSRIPTION file using the default tag name, tag message and commit message. The output of the command cat DESCRIPTION shows us that the DESCRIPTION file now lists the version number as 0.1.0.

When we view our repo status and commit history, we see what bumpversion did for us:

git status && git log --oneline -3

The log output shows us that the most recent commit is tagged (tag:v0.1.0) with a commit message that shows the previous and the new version of the project. When we are browsing the git log output in the future, we can now search for specific version tags or any message with “Bump version:” in it. This is very handy when we need to make use of the time travel features of our version control system.

Unfortunately, while this command line approach is relatively simple and straightforward, it is not scalable because we have to pass in the arguments manually. If we are working on multiple projects all with different version numbers in different files, etc., it becomes very time-consuming to check that we are passing in the correct --current-version argument for the correct file, etc.

Using a Configuration File

The way to reduce all the complexity of bumping the version of our project is to use a .bumpversion.cfg file. This configuration file passes the arguments and flags to bumpversion without us having to remember them.

The format of a .bumpversion.cfg is straightforward:

[bumpversion]
current_version = 0.1.0
commit = True
tag = True

[bumpversion:file:DESCRIPTION]

Each line of the .bumpversion.cfg file matches one of the aruments that we passed in via the flags of the bumpversion CLI. The only slightly tricky line of the configuration file to remember is the format for specifying which files need to be updated when we bump the project version, i.e. [bumpversion:file:DESCRIPTION].

The nice thing is that if we need to update the version number in the README.md file, we can just add it as follows:

[bumpversion]
current_version = 0.1.0
commit = True
tag = True

[bumpversion:file:DESCRIPTION]

[bumpversion:file:README.md]

Note: If you don’t add the blank line between the two files, the next call of bumpversion will format the file to include it automatically.

Let’s create the configuration file and have some fun bumping versions:

echo \
"
[bumpversion]
current_version = 0.1.0
commit = True
tag = True

[bumpversion:file:DESCRIPTION]
" > .bumpversion.cfg

We commit the new configuration file:

git add . && \
git commit -m "feat: Add bumpversion config file"

Bumping Time

We can now work on our project with the confidence that when we make changes that warrant a change / update to the version number, we are able to manage the process with one simple command.

We have added the configuration file, which is not a big deal for any other user of our project, but is a new feature of our experience so let’s bump the MINOR version of our project again.

This time we use the very simple command:

bumpversion minor && \
git log --oneline -2

That’s a lot easier, right?

Let’s say that you discover a bug in new_function. First we update the functions.R file and commit the changes. Then we bump the PATCH version:

bumpversion patch && \
git log --oneline -1

When we reach the ever-important, stable release milestone in our project, we can bump the MAJOR version:

bumpversion major && \
git log --oneline -2

Happy bumping!

References

For more information, please refer to the official documentation of bumpversion.