Developing software, at least great ones requires you to seek constant feedback for your product and the changes that you deliver. In other words, you need to bring your work to your client as often as possible by making it available in your production environment.
This means that your team needs to monitor closely any modifications and be able to roll back any changes by coming back to a clean and working version of your software in production.
This is evenly true for your client-facing application or for the libraries that you develop and use internally on your main applications.
Yet, as Dave Farley wrote in “Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation”:
Releasing software is too often an art; it should be an engineering discipline.
So how can we shift from a mystical art to a systemic approach?
The first step towards this can be made by using version control. This can be achieved with the help of GitHub releases. Nowadays, most development project uses Git as the main local tool for managing changes and leveraging platform for collaboration like GitLab, BitBucket, or Github which will be our focus here.
A release commonly refers to a set of changes that are intended to be delivered to a production environment. It usually contains some features as well as some bug fixes, among other stuff like documentation, etc.
A GitHub release is based on a specific git tag which refers to a specific historical point in a repository. It usually comes with a note on the changes that were made and of course the source code.
📕 Learn more about git tags here
A good release uses a principle called semantic versioning.
It provides a simple guide for numbering it consistently. Not going into too many details, a version consists of 3 numbers separated by dots. Those numbers are incremented based on the changes that are embedded in a specific release.
Note that you can also add a suffix for designating, alpha (2.1.4-a.1) or bêta version, pre-releases also known as release candidates (1.1.2-rc.3), and so on.
A good release comes with its associated documentation or Changelog. It describes the changes that were made from the previous one. It can be as simple as a bullet point list describing each change and providing a link to a potential ticket or issue associated with it.
Here is an example of a changelog for the Java / Android library okhttp
Still, we developers are lazy and, let’s admit it, not very keen on writing documentation of any sort so let’s see how we can automate that!
A conventional commit is a commit with a name following a specific pattern.
<type>[optional scope]: <description> [optional body] [optional footer(s)]
I usually only use the first part <type>[optional scope]: <description>
‘feat(user-dashboard): allow users to change their personal background color’
‘fix(ordering): remove unwanted duplicated items from shopping cart’
Type | Description |
---|---|
feat! | Breaking change |
feat | New feature |
fix | A bug fix |
revert | Rollback of a change |
docs | Adding some documentation |
ci | Changes made to an infrastructure job |
chore | Upgrades, cleaning up... |
perf | A performance improvement |
refactor | A change in the code that does not affect the overall behavior |
style | Formatting change |
test | Adding some test cases |
📕 Click here to see the full documentation
Of course, they are plenty of libraries that can help you achieve that based on which language you use. One of their key advantages is that they can be added to your build and therefore automatically install hooks without the developer noticing it.
npm
Python
Ruby
Ruby
, npm
and more
However, let me show you an easy way to enforce conventional commits through the use of git hooks. When you create a git repository by using git init
a .git
folder is created which contains different sub-folders like hooks
which is the one we are interested in here. A hook is a script that will run every time a specific event occurs, for example, you guessed it, a new commit is made. To activate one, just remove the hook file you want by removing the .sample
suffix.
💡 Tips: If you are using a JetBrains tool, this .git
is hidden by default. See here how to display it.
In order to enforce a simple pattern for your commit messages, just create the commit-msg file and add the following lines to it. This will check the format of your commit and will block it if it is not compliant with the pattern you defined
#!/bin/sh
commit_msg=$(cat "${1:?Missing commit message file}")
if ! echo "$commit_msg" | grep -Eq "^(build|chore|ci|docs|feat|feat!|fix|perf|refactor|revert|style|test)(\(.+\))?: .*$" ; then
echo "Invalid commit message"
exit 1
fi
echo "Commit message is valid!"
💡 You can always bypass the check by adding --no-verify
after your commit message.
Well done, you have successfully enforced conventional commits on... your local version of the project! But how to share it with your teammates?
In fact, you’ll find out that you can’t push your .git
folder to a remote repository. One of the most simple ways of achieving that is to add the file to a dedicated folder at the root of the project like git-hooks
and to push it on the remote repository so that everyone can use it.
You can even create a small script that will take care of copying the file to the .git/hooks folder like so:
#!/bin/sh
cp git-hooks/commit-msg ./.git/hooks
echo "Git commit hook successfully installed, you are ready to go!"
💡Add a line on your project Readme file to invite contributors to install it by executing the above file.
Let’s now leverage those conventional commits to create a Github action that will take care of the boring part when creating a release!
📕 Here is some documentation if you are not already familiar with the great CI/CD automation tool that is Github action.
Here we will create a manual action that will...
... all of this is in one single click.
Start by creating a release.yml
file under the .github/workflows folder at the root of your project.
name: Release # Workflow name displayed on GitHub
on:
workflow_dispatch: # Trigger manually
branches: main # Branch on which the workflow will run
jobs:
new-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3 # Checkout our working repository
- name: Conventional Changelog Action
id: changelog
uses: TriPSs/conventional-changelog-action@v3
with:
github-token: ${{ secrets.github_token }
output-file: "false"
- name: Create Release # This action will create the actual release
uses: actions/create-release@v1
if: ${{ steps.changelog.outputs.skipped == 'false' }
env:
GITHUB_TOKEN: ${{ secrets.github_token }
with: tag_name: ${{ steps.changelog.outputs.tag }
release_name: ${{ steps.changelog.outputs.tag }
body: ${{ steps.changelog.outputs.clean_changelog }
🗒️ In this example we are using the conventional-changelog-action from TriPSs for generating the changelog. There are many solutions available on Github to achieve that. Note that the numbering might be different than the one you expect based on the commit-hook regex you’ve chosen.
This file will create a GitHub action that you will be able to trigger manually directly from the GitHub Actions tab by clicking the run workflow as shown below.
The action will then create a new release based on your commits!
Releasing is a key part of the software delivery process and should therefore be kept seamless to do it as often as possible!
If you want to take a step forward and measure how far you are from being an elite DevOps, have a look at The Four Key Metrics.