Git Flow, Semantic Versioning, and CHANGELOG.md, oh my!

March 11, 2019

How we use Git Flow, SemVer, and CHANGELOG.md

Here at Ordinary Experts, we love discussing git branching strategies and how they relate to the software development process. While this is a large topic with many considerations, our go-to approach for new projects includes adopting three related standards:

In this post, I will briefly discuss these three concepts and then provide a complete walk-through of how we would apply them to a typical software development project.

Git Flow

Git Flow is a git branching model developed initially by Vincent Driessen in a blog post. He also did a git extension, which is now developed at the gitflow-avh repo.

You really should go read the post to learn about it, but the high-level idea is that there are two long-lived branches in your git repo - master, which is always what is deployed in production, and develop which is the latest and greatest development code. To make new features, you branch off of develop into feature branches i.e. feature/my-new-feature. To prep a new release for deployment, you make a release branch off of develop i.e. release/1.4.9, which you then merge to master. To do a hotfix on production, you make a hotfix branch off of master, i.e. hotfix/fix-prod-bug, which you merge back into master.

Semantic Versioning or SemVer

SemVer is a standard for versioning your software. It was originally authored by Tom Preston-Werner, inventor of Gravatars and cofounder of GitHub. It is designed to allow systems to programmatically specify the versions of their dependencies and prevent unexpected breaking changes. At its most basic, SemVer breaks down a version into three components, the major, minor, and patch numbers, i.e.:

1.4.9

Where 1 is the major version and should only change for major updates that are not backwards compatible, 4 in the minor version and should change when new backwards-compatible features are added, and 9 is the patch number which should change when bug fixes are made.

Keep a changelog

Keep a changelog is a practice of keeping a human-curated file (we use CHANGELOG.md) which lists all the notable changes to the project, grouped by their release. During development there is also a list of unreleased changes.

Walk-through

Ok, now that we have our basic concepts out there, let’s get to our walk-through. I will go through the following use cases:

  • Creating a new git project
  • Doing a feature branch
  • Doing a release branch
  • Doing a hotfix branch

BTW, the following is a walk-through of how we use Git Flow, SemVer, and a CHANGELOG.md together - while this is representative of how we do things for some projects, it is by no means the only way.

Setup

For this walk-through you will need the following software installed on your computer - I’m not going to go through how to install them - just follow the links below:

I am doing this on a Lenovo ThinkPad running Ubuntu 18.04. Here is some relevant version and config info:

~$ git --version
git version 2.17.1
~$ git flow version
1.11.0 (AVH Edition)
~$ git config -l
user.name=Dylan Vaughn
user.email=dylan@ordinaryexperts.com
alias.ci=commit
alias.co=checkout
alias.st=status
alias.br=branch
push.default=simple
~$

Note the push.default=simple config setting here - that is why I have to push branches individually later in the walk-through. If you have this set to matching then a git push will push all your matching branches.

You’ll also need an empty GitHub project called git-flow-example. You should replace the GitHub URLs in the walk-through below with the URL to your repo.

One more important note before we begin - in general when using git flow commands, you should be sure you have the latest code in your git repo. So before starting any of these workflows, be sure to do a git fetch and then merge in upstream changes as appropriate.

Creating a new git project

First we’ll create our project directory, initialize git, create a CHANGELOG.md, commit it, and push to GitHub.

~$ mkdir git-flow-example
~$ cd git-flow-example
~/git-flow-example$ git init
Initialized empty Git repository in /home/dylan/git-flow-example/.git/
~/git-flow-example$ printf "Unreleased\n==========\n* Initial commit\n" > CHANGELOG.md
~/git-flow-example$ cat CHANGELOG.md
Unreleased
==========
* Initial commit
~/git-flow-example$ git add .
~/git-flow-example$ git commit -m "initial CHANGELOG.md"
[master (root-commit) 1decb45] initial CHANGELOG.md
 1 file changed, 3 insertions(+)
 create mode 100644 CHANGELOG.md
~/git-flow-example$ git remote add origin git@github.com:ordinaryexperts/git-flow-example.git
~/git-flow-example$ git push -u origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 267 bytes | 267.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
~/git-flow-example$

Now we’ll initialize git flow. The -d flag to git flow init command just accepts all the default values. The git flow command also creates the develop branch, which we then push to GitHub.

~/git-flow-example$ git flow init -d
Using default branch names.

Which branch should be used for bringing forth production releases?
   - master
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Hooks and filters directory? [/home/dylan/git-flow-example/.git/hooks]
~/git-flow-example$ git push -u origin develop
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'develop' on GitHub by visiting:
remote:      https://github.com/ordinaryexperts/git-flow-example/pull/new/develop
remote:
To github.com:ordinaryexperts/git-flow-example.git
 * [new branch]      develop -> develop
Branch 'develop' set up to track remote branch 'develop' from 'origin'.
~/git-flow-example$ git branch -a
* develop
  master
  remotes/origin/develop
  remotes/origin/master
~/git-flow-example$

Ok, at this point we have a GitHub repo with a master and develop branch and a CHANGELOG.md. There aren’t any releases yet - we’ll get to that later.

At this point I will typically sign into GitHub and set the develop branch as the default branch for the repo under branch settings. This will default the web UI, pull requests, and checkouts to use the develop branch first, which is normally what we want.

Next, let’s add a README.md file - we’ll do this as our first feature.

Doing a feature branch

Whenever we are adding a new feature in the normal development process, we code that feature in a branch prefixed with feature/, called a ‘feature branch’. Let’s create a feature branch now for our feature of adding a README.md file.

~/git-flow-example$ git flow feature start add-readme
Switched to a new branch 'feature/add-readme'

Summary of actions:
- A new branch 'feature/add-readme' was created, based on 'develop'
- You are now on branch 'feature/add-readme'

Now, start committing on your feature. When done, use:

     git flow feature finish add-readme

~/git-flow-example$ printf "Git Flow, SemVer, and CHANGELOG.md example.\n" > README.md
~/git-flow-example$ vim CHANGELOG.md # Add a line in the 'Unreleased' section about the change
~/git-flow-example$ git add .
~/git-flow-example$ git commit -m "adding README.md"
[feature/add-readme 5a95e6f] adding README.md
 2 files changed, 2 insertions(+)
 create mode 100644 README.md
~/git-flow-example$ git diff develop..feature/add-readme
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0aa64ac..34bd9ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
 Unreleased
 ==========
+* Added README.md
 * Initial commit
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..87dff93
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Git Flow, SemVer, and CHANGELOG.md example.
~/git-flow-example$ git flow feature publish
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 396 bytes | 396.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'feature/add-readme' on GitHub by visiting:
remote:      https://github.com/ordinaryexperts/git-flow-example/pull/new/feature/add-readme
remote:
To github.com:ordinaryexperts/git-flow-example.git
 * [new branch]      feature/add-readme -> feature/add-readme
Branch 'feature/add-readme' set up to track remote branch 'feature/add-readme' from 'origin'.
Already on 'feature/add-readme'
Your branch is up to date with 'origin/feature/add-readme'.

Summary of actions:
- The remote branch 'feature/add-readme' was created or updated
- The local branch 'feature/add-readme' was configured to track the remote branch
- You are now on branch 'feature/add-readme'

~/git-flow-example$

Ok great. Now we have a feature branch with our new feature and also a new addition in the CHANGELOG.md pushed up to GitHub. At this point we would do a pull request to get feedback from others on the team.

After the review, we can finish the feature branch.

~/git-flow-example$ git flow feature finish add-readme
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.
Updating 1decb45..5a95e6f
Fast-forward
 CHANGELOG.md | 1 +
 README.md    | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 README.md
To github.com:ordinaryexperts/git-flow-example.git
 - [deleted]         feature/add-readme
Deleted branch feature/add-readme (was 5a95e6f).

Summary of actions:
- The feature branch 'feature/add-readme' was merged into 'develop'
- Feature branch 'feature/add-readme' has been locally deleted; it has been remotely deleted from 'origin'
- You are now on branch 'develop'

~/git-flow-example$ git push
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 396 bytes | 396.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
   1decb45..5a95e6f  develop -> develop
~/git-flow-example$

All done! Our feature branch has been merged and removed; we are back on develop and have pushed, and are ready to move on to our next feature. If we have a CICD pipeline going, the latest from the develop branch may be deployed to our development environment for review.

Doing a release branch

When it is time to do a release, the first step is to create a release branch, which will have a prefix of /release/[version]. Before we can create the release branch, we need to know what version number the release will be. When making release branches, we need to increment either the major or minor version number of the current version, depending on whether the changes are deemed backwards compatible. If they are backwards compatible, then keep the major number the same, increment the minor number, and set the patch number to 0. If they are not backwards compatible, increment the major number, set the minor and patch numbers to 0.

Let’s see if git can tell us what the current version is:

~/git-flow-example$ git describe
fatal: No names found, cannot describe anything.
~/git-flow-example$ git tag -l
~/git-flow-example$

In this case we haven’t had any releases yet, so there aren’t any tags to describe. So since we are starting from 0.0.0, let’s increment the minor version and make a 0.1.0 release. Typically we keep the major version at 0 until there is a production release.

~/git-flow-example$ git flow release start 0.1.0
Switched to a new branch 'release/0.1.0'

Summary of actions:
- A new branch 'release/0.1.0' was created, based on 'develop'
- You are now on branch 'release/0.1.0'

Follow-up actions:
- Bump the version number now!
- Start committing last-minute fixes in preparing your release
- When done, run:

     git flow release finish '0.1.0'

~/git-flow-example$ vim CHANGELOG.md # Add a '0.1.0' section with existing Unreleased items
~/git-flow-example$ git commit -a -m "bumping version to 0.1.0"
[release/0.1.0 866262b] bumping version to 0.1.0
 1 file changed, 3 insertions(+)
~/git-flow-example$ git diff develop..release/0.1.0
index 34bd9ef..341e92a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,7 @@
 Unreleased
 ==========
+
+0.1.0
+=====
 * Added README.md
 * Initial commit
~/git-flow-example$ git flow release publish
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 354 bytes | 354.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'release/0.1.0' on GitHub by visiting:
remote:      https://github.com/ordinaryexperts/git-flow-example/pull/new/release/0.1.0
remote:
To github.com:ordinaryexperts/git-flow-example.git
 * [new branch]      release/0.1.0 -> release/0.1.0
Branch 'release/0.1.0' set up to track remote branch 'release/0.1.0' from 'origin'.
Already on 'release/0.1.0'
Your branch is up to date with 'origin/release/0.1.0'.

Summary of actions:
- The remote branch 'release/0.1.0' was created or updated
- The local branch 'release/0.1.0' was configured to track the remote branch
- You are now on branch 'release/0.1.0'

~/git-flow-example$

Ok, now we have a new release branch created and pushed up to GitHub. At this point we can review the changes that will be deployed by comparing this branch to master. If we had a CICD pipeline, commits to this release branch would be automatically deployed to a QA environment.

After review and validation, let’s finish the release branch. The git flow release finish command will prompt us to create a commit message for the merge as well as a message for the tag of the release.

~/git-flow-example$ git flow release finish 0.1.0
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Merge made by the 'recursive' strategy.
 CHANGELOG.md | 4 ++++
 README.md    | 1 +
 2 files changed, 5 insertions(+)
 create mode 100644 README.md
Already on 'master'
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.
Merge made by the 'recursive' strategy.
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)
To github.com:ordinaryexperts/git-flow-example.git
 - [deleted]         release/0.1.0
Deleted branch release/0.1.0 (was 866262b).

Summary of actions:
- Release branch 'release/0.1.0' has been merged into 'master'
- The release was tagged '0.1.0'
- Release tag '0.1.0' has been back-merged into 'develop'
- Release branch 'release/0.1.0' has been locally deleted; it has been remotely deleted from 'origin'
- You are now on branch 'develop'

~/git-flow-example$

Ok great, now we have finished our release branch, but we just have our updates locally - we still need to push everything to origin, including the new tag.

~/git-flow-example$ git status
On branch develop
Your branch is ahead of 'origin/develop' by 3 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
~/git-flow-example$ git push
Counting objects: 5, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 657 bytes | 657.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:ordinaryexperts/git-flow-example.git
   5a95e6f..fdf05c1  develop -> develop
~/git-flow-example$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)
~/git-flow-example$ git push
Total 0 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
   1decb45..bee6250  master -> master
~/git-flow-example$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 163 bytes | 163.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
 * [new tag]         0.1.0 -> 0.1.0
~/git-flow-example$

Whew! All done! Now we have merged the release branch into master, tagged the release, and merged all those changes back into develop. If we had CICD, this would have triggered a production deployment.

Doing a hotfix branch

If an issue is found in production that needs to be fixed before the next scheduled release, a hotfix branch should be used. A hotfix branch is branched off of master so it will be based on the latest from production. In most cases, we increment the patch version number each time a hotfix branch is released for a particular minor version.

So let’s find our current version and make our hotfix branch.

~/git-flow-example$ git describe
0.1.0
~/git-flow-example$ head CHANGELOG.md
Unreleased
==========

0.1.0
=====
* Added README.md
* Initial commit
~/git-flow-example$

So our current version is 0.1.0. Since this is a bugfix, we will increment the patch number, so the hotfix verison number will be 0.1.1.

~/git-flow-example$ git flow hotfix start 0.1.1
Switched to a new branch 'hotfix/0.1.1'

Summary of actions:
- A new branch 'hotfix/0.1.1' was created, based on 'master'
- You are now on branch 'hotfix/0.1.1'

Follow-up actions:
- Start committing your hot fixes
- Bump the version number now!
- When done, run:

     git flow hotfix finish '0.1.1'
~/git-flow-example$

Now we have our hotfix branch - let’s make our fix and update the CHANGELOG.md.

~/git-flow-example$ printf "\nOur hotfix fix\n" >> README.md
~/git-flow-example$ vim CHANGELOG.md # Add a 0.1.1 section with a list of changes
~/git-flow-example$ git add .
~/git-flow-example$ git commit -m "hotfix for README.md"
[hotfix/0.1.1 68d05e8] hotfix for README.md
 2 files changed, 6 insertions(+)
~/git-flow-example$ git diff master..hotfix/0.1.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 341e92a..730f310 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 Unreleased
 ==========

+0.1.0
+=====
+* hotfix for README.md
+
 0.1.0
 =====
 * Added README.md
diff --git a/README.md b/README.md
index 87dff93..dfa4dfa 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
 Git Flow, SemVer, and CHANGELOG.md example.
+
+Our hotfix fix
~/git-flow-example$ git flow hotfix publish
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 437 bytes | 437.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'hotfix/0.1.1' on GitHub by visiting:
remote:      https://github.com/ordinaryexperts/git-flow-example/pull/new/hotfix/0.1.1
remote:
To github.com:ordinaryexperts/git-flow-example.git
 * [new branch]      hotfix/0.1.1 -> hotfix/0.1.1
Branch 'hotfix/0.1.1' set up to track remote branch 'hotfix/0.1.1' from 'origin'.
Already on 'hotfix/0.1.1'
Your branch is up to date with 'origin/hotfix/0.1.1'.

Summary of actions:
- The remote branch 'hotfix/0.1.1' was created or updated
- The local branch 'hotfix/0.1.1' was configured to track the remote branch
- You are now on branch 'hotfix/0.1.1'

~/git-flow-example$

Now we have a hotfix branch based off of master and pushed up to GitHub. If we had CICD, this might trigger a deployment to the QA environment for validation.

Once we are ready, we can finish our hotfix branch.

~/git-flow-example$ git flow hotfix finish 0.1.1
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Merge made by the 'recursive' strategy.
 CHANGELOG.md | 4 ++++
 README.md    | 2 ++
 2 files changed, 6 insertions(+)
Switched to branch 'develop'
Your branch is up to date with 'origin/develop'.
Merge made by the 'recursive' strategy.
 CHANGELOG.md | 4 ++++
 README.md    | 2 ++
 2 files changed, 6 insertions(+)
To github.com:ordinaryexperts/git-flow-example.git
 - [deleted]         hotfix/0.1.1
Deleted branch hotfix/0.1.1 (was 68d05e8).

Summary of actions:
- Hotfix branch 'hotfix/0.1.1' has been merged into 'master'
- The hotfix was tagged '0.1.1'
- Hotfix tag '0.1.1' has been back-merged into 'develop'
- Hotfix branch 'hotfix/0.1.1' has been locally deleted; it has been remotely deleted from 'origin'
- You are now on branch 'develop'

~/git-flow-example$

Again, we have finished the hotfix but need to push up our branches and tags.

~/git-flow-example$ git status
On branch develop
Your branch is ahead of 'origin/develop' by 3 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
~/git-flow-example$ git push
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 735 bytes | 735.00 KiB/s, done.
Total 6 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:ordinaryexperts/git-flow-example.git
   fdf05c1..6b91256  develop -> develop
~/git-flow-example$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
~/git-flow-example$ git push
Total 0 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
   bee6250..2552428  master -> master
~/git-flow-example$ git push --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 164 bytes | 164.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To github.com:ordinaryexperts/git-flow-example.git
 * [new tag]         0.1.1 -> 0.1.1
~/git-flow-example$

There we go!

We now have done a feature branch, release branch, and a hotfix branch. We updated our versions according to SemVer and listed our changes in our CHANGELOG.md.

I hope this walk-through helps bring some detail into one possible workflow that has worked well for us.

Happy Coding!

Dylan