Conventional Commits and Generating Changelogs
By Johan Hage
Documentation, while in many cases important, might not be the first thing you as a developer want to spend time on doing.
Something you might do like as a developer is to spend a lot of time automating simple but tedious tasks in order to never have to do it manually again!
With documentation being a particularly important part in our project we were extra keen on finding as many ways of automating documentation as possible.
In this post I will go through how we utilized Conventional Commits
, an Azure Devops extension called Commitizen
and an npm package called Standard version
to automatically generate changelogs in our Azure Devops CI environment.
Before getting in to the details of how our solution ended up working there are some concepts that are important.
Conventional Commits
Conventional Commits is a specification used to format commit messages consistently. The full specification can be found here, but to summarize, it is a convention where each commit message is divided into the following structure:
type(optional scope): subject
type
describes the purpose of the commit, such as afix
(for bug fix),feat
(for feature),refactor
(for refactorization) etc.scope
is optional and might refer to the specific feature you’re working on or what ever seems relevantsubject
is a relevant description of what has been done in the commit and should of course be written well enough to be present in a changelog!
Depending on what type is being used for the commit message it can be mapped to a MAJOR, MINOR or PATCH version number as explained below.
Semantic versioning
Semantic versioning is a commonly used standard for version numbers.
From the official page of Semantic Versioning (or semver)
Given a version number MAJOR.MINOR.PATCH, increment the:
- MAJOR version when you make incompatible API changes
- MINOR version when you add functionality in a backwards compatible manner
- PATCH version when you make backwards compatible bug fixes
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
Changelogs
Now, while Conventional Commits might just sound like you have to spend time on writing better commit messages that no one is going to read anyway, sticking to the convention will bring along some nifty benefits. Of course it will give you and your team a much clearer overview of what has been done on each commit, but also the structure of each commit can be utilized to generate changelogs automatically! Not only that, since there is a clear indication of what type of commits has been made the version number can be automatically increased, allowing you to not have to think about that at all!
The way that works is by connecting different types of commits to different version numbers:
- If a
BREAKING CHANGE
has been pushed since the last release, the MAJOR version will be increased (1.5.2 => 2.5.2) - If a
feat
has been pushed since the last release, the MINOR version will be increased (1.5.2 => 1.6.2) - If none of the above, the PATCH version will be increased (1.5.2 => 1.5.3)
In action, one common way of doing this is by creating a CI workflow, such as via Github Actions or, in our case, Azure Devops Pipelines. The workflow would be run every time a release was being made. Then a tool is used to automatically generate changelogs and increase the version number. The changelog generator would take all the commits made since the last time that the changelog was generated by keeping track of the git tag with the latest version number, generate a changelog and push it to the specified branch. More on the specifics later.
However, one issue with having the commit messages being directly used when generating the changelogs is that it does not always tie in well with your way of working with git. In our case for example, we squash merged our branches every time we made a pull request, meaning that all the commit messages would be merged into one. A workaround for this would be to enforce that each PR would only consist of one commit which adheres to the Conventional Commits standard. However this would of course affect how people use git where you would either have to make sure to only have one commit that you amend changes to (and force push if the commit was already pushed before), or that you manually squash all the commits before finalizing the PR.
We wanted a way that was less intrusive to the way of working.
Commitizen
While there are quite a few tools that can assist in sticking to the conventional commits format, such as this tool which can be used as a git pre-commit hook to enforce commit messages comply with the format, our case was a bit different and required some things to be done differently.
What we wanted was a way to utilize the title of our pull requests as the squashed commit message, after having made sure that the title follows the convention. This would allow us to work with our branches and commits as usual, and then make sure that the PR is explaining the work clearly enough. Unfortunately this wasn’t a trivial task as all we could find in regards to using the PR title for squash merge commits was that it could be done, but not without Azure Devops adding the PR number before the message, meaning it would be rendered unusable in regards to the changelog generation.
Meanwhile, Github had released the exact feature we were looking for just the week before, along with having a ton of different Github Actions that could be used for ensuring that PR titles adhere to the convention.
But after extensive searching we found an Azure Devops extension called Commitizen Pull Request. An extension that adds an option to each PR in Azure Devops which when used will provide a form. In the form you’d fill in things related to Conventional Commits, such as type, scope, breaking changes and of course subject. This will then be used to provide a message to the squashed commit when the PR is merged.
The only issue remaining with this is that we’ve not found a way to enforce that this option is being used when completing PRs in Azure Devops, meaning that people could still accidentally use the regular complete button and have there changes not be part of the changelogs.
Pipeline and Standard version
With the commit messages in place we could finally move on to actually generating changelogs. This part could of course differ a lot depending on your CI environment. In our case we have a build pipeline that is being run every time a push is being made to the main branch which in turn “releases” a new image that is pushed to our different live environments. We extended this pipeline with the following stage:
- stage: standard_version
displayName: Generate changelogs and bump version
jobs:
- job: standard_version
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
persistCredentials: true
- script: |
git config --global user.email 'azure@bot.com'
git config --global user.name 'Azure Bot'
npx standard-version
git push --follow-tags origin HEAD:$(Build.SourceBranchName)
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
This stage uses an npm package called Standard Version in order to generate the changelogs and increase the version number.
We also added the following settings in a file called .versionrc
in order to make Standard Version be compatible with Azure Devops.
{
"releaseCommitMessageFormat": "chore(release): {{currentTag}} [skip ci]",
"compareUrlFormat": "{{host}}/*Organization*/*Project*/_git/*Project*/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
"commitUrlFormat": "{{host}}/*Organization*/*Project*/_git/*Project*/commit/{{hash}}",
"packageFiles": [],
"bumpFiles": []
}
Note that [skip ci]
is being added to the commit that is pushed in order to not create a loop of triggering the build pipeline every time the changelog commit is being pushed.
The bumpFiles
array can be used to specify files which contains the version number of the project that should be automatically increased.
This is then used when generating changelogs to identify what commits should be included.
If left empty, the latest git tag is used instead.
(For node projects the package.json version
property is bumped automatically, without being specified in bumpFiles
)
In order for this to work we had to make sure our Azure Devops Build Service had access to pushing directly to our main branch, as this was of course not allowed for other users.
While it has become more and more good practise to release often, this has the unfortunate side effect of making our changelogs usually only contain one commit per version.
This blog post by Jazz Tong was of great help for making this work in Azure Devops.
The following is a sample from our CHANGELOG.MD
:
Changelog
All notable changes to this project will be documented in this file. See standard-version for commit guidelines.
2.12.5 (2022-07-26)
2.12.4 (2022-07-26)
2.12.3 (2022-07-25)
2.12.2 (2022-07-25)
2.12.1 (2022-07-25)
2.12.0 (2022-07-22)
Features
2.11.0 (2022-07-22)
Features
- Added default value current timestamp for all ITimeStampedModel (73d1f51)