Conventional Commits

16 May 2021

Conventional Commits is a specification for adding meaning to commit messages for both human and machines. The specification is based heavily on Angular commit guildelines.

The commit message provides consumers of your software with intent regarding the change. This is done by using some predefined types being used for the message structure.

The commit messages can be used for the following cases:

  • Producing a CHANGELOG.
  • Communicates the type of change to your team and consumers.
  • Generating the next semantic version bump from the commit messages.
  • A structured commit history to help with maintainability.

Quick dive into Semantic Versioning

A Sematic Version is represented by the format MAJOR.MINOR.PATCH.

e.g. 1.2.3 - MAJOR is 1, MINOR is 2 and PATCH is 3.

There are rules that determine when these parts are incremented. Conventional Commits structures messages to help implement these rules.

Find out more about semantic versioning here: Semantic Versioning.

An example fix resulting in PATCH version bump

Assume that we have a library being consumed and it is at version 1.0.0.

fix: Handle exception when parsing an invalid date time format

This indicates that a bug fix has been applied and only the PATCH part would be bumped resulting in the next version number being 1.0.1. Consumers can safely update their version to get these bug fixes.

An example compatible feature change resulting in MINOR bump

feat: Add new option config option to allow a custom logger to be added as a dependency

This is a compatible feature change meaning that only the MINOR part would be bumped resulting in the next version number being 1.1.0.

An example feature breaking change resulting in a MAJOR bump

feat: change FindMe method on public IFinder interface to return IEnumerable<Thing> instead of List<Thing>

BREAKING CHANGE: usages of the IFinder interface will need to be updated for code to compile.

This is a breaking change since consumers of this library will have the update their code to fix compilation errors.

A breaking change will bump the MAJOR part resulting in 2.0.0.

Reasons to use it?

Don't let humans pick the version number

Move the responsibility of determining the next version from a human to the build system. Historic code commits were good but history showed that they didn't always communicate the intent of the change. Were they breaking changes, bug fixes and feature additions?

The various meta files holding the version number e.g. package.json or assemblyinfo.cs were bumped manually by a human and sometimes the decision could be error prone, especially if it's rolling in a bunch of changes from other humans that aren't too clear.

Automate using your build systems

The Continous Integration system such as Jenkins-CI or Azure DevOps will now be responsible for:

  • Determining the next version by parsing the git commit messages.
  • Updating the various meta files containing the version number.
  • Optionally, generating a CHANGELOG that is easily readable by humans.
  • Generating a git tag to easily link to change history by version number.
  • Committing the above changes.

I used standard-version with my website to test the integration.

My learnings with Azure DevOps

This is a thorn in my side and I still ponder it now!

I found that a ci loop can occur with Azure DevOps. Azure DevOps would see the conventional commit made by itself as a change thus triggering a build, bumping the PATCH part resulting in a loop. To workaround this, the word ***NO_CI*** need to be added to the commit message.

See Skipping CI for individual commits

See System.AccessToken regarding accessing the oAuth token so that Azure DevOps can push to the repository.

.versionrc.js is used by standard-version to update AssemblyInfo.cs

This is a example .versionrc.js file for updating AssemblyInfo.cs. A custom updater is defined that standard-version uses to update .NET (dotNet) AssemblyInfo.cs file.

var assemblyInfo = function(assemblyInfoFile) {
    return {
        filename: assemblyInfoFile,
        updater: {
            readVersion: function (contents) {
                return contents.match(/AssemblyVersion\(\"(.*).0\"\)/)[1];
            },
            writeVersion: function (contents, nextVersion) {
                var lastVersion = contents.match(/AssemblyVersion\(\"(.*).0\"\)/)[1]; ''
                return contents.replace(lastVersion + ".0", nextVersion + ".0");
            }
        }
    };
}
var updaters = [
    assemblyInfo('./source/HelloWorld/Properties/AssemblyInfo.cs'),
    assemblyInfo('./source/HelloWorld.Tests/Properties/AssemblyInfo.cs')
]
module.exports = {
    bumpFiles: updaters,
    packageFiles: updaters,
    releaseCommitMessageFormat: "chore(release): {{currentTag}} ***NO_CI***"
}

standard-version.msbuild sample using standard-version.js

Below is the contents of the msbuild file that I used for my website.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <PropertyGroup>
        <Release>false</Release>  
    </PropertyGroup>

    <Target Name="Install">
        <Exec Command="npm i -g standard-version"/>
    </Target>

    <Target Name="NextVersion"  DependsOnTargets="Install" Condition="$(Release)">
        <Exec Command="git config --global user.email &quot;bob@domain.com&quot;"/>
        <Exec Command="git config --global user.name &quot;Bob&quot;"/>
        <Exec Command="standard-version"/>
    </Target>

    <Target Name="Push"  DependsOnTargets="Install" Condition="$(Release)">
        <Exec Command="git push --follow-tags origin HEAD:master"/>
    </Target>
</Project>
DevOps