Automatic Version Tagging using Github Actions
As someone who writes code, I have always been on the lookout for ways to streamline my workflow and make my code more organized. One of the practices that has helped me the most in this regard is using semantic commit messages and automatic version tagging in Github for my projects.
For those who may not be familiar, semantic commit messages are a way of structuring commit messages in a way that makes them more meaningful and informative. This can include things like using specific keywords to indicate what changes were made (e.g. “fix”, “add”, “remove”), as well as providing clear and concise descriptions of what was done.
Similarly, automatic version tagging is a way of automatically assigning version numbers to commits based on certain criteria (e.g. specific keywords in the commit messages). This helps to keep track of the different versions of your code and makes it easier to revert to a previous version if necessary.
Versioning Scheme:
When it comes to versioning in this context, I will be utilizing the “v{Major}.{Minor}.{Fix}” convention. This means that:
- The Major version number will increase whenever a developer determines that a breaking change is necessary.
- The Minor version number will increase each time a new set of features is released.
- The Fix version number will increase whenever a bug is fixed, regardless of whether it was reported by users or found internally.
Pre-Requisites
The method outlined in this post can be applied using any programming language, package manager, or even a repository that holds miscellaneous files. However, for the sake of practicality, we will primarily be utilizing Composer and NPM.
- Node 18+
- npm
- composer (optional)
- working github repository for your codebase
- ability to run github actions
Configuring Semantic Commits and Releases
Lets start in a new branch to keep things from getting out of hand:
git checkout -b semver
Now that we have a clean workspace, let’s install some dependencies:
yarn add --dev \
ambimax/semantic-release-composer \
@commitlint/cli \
semantic-release/changelog \
@semantic-release/git \
semantic-release/github \
semantic-release/npm \
semantic-release/release-notes-generator \
commitizen \
cz-conventional-changelog \
semantic-release
Once that commands succeeds, let’s update our package.json file with this default configuration, you can customize this once things are up and running to fit your organization’s needs.
For now, just update the branch and repository you want to tag releases under. In this sample file, I will be using the main
branch, “https://github.com/my-org/some-repo.git” as the repository, and some-repo
as the name.
{
...
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"release": {
"branches": [
"main"
],
"tagFormat": "${version}",
"plugins": [
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@ambimax/semantic-release-composer",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"package.json",
"yarn.lock",
"composer.json",
"composer.lock"
],
"message": "chore(release): v${nextRelease.version} [release]"
}
],
"@semantic-release/github"
]
},
"repository": {
"type": "git",
"url": "https://github.com/my-org/some-repo.git"
},
"name": "some-repo",
"version": "0.0.0",
}
Add the following command definition to your package.json
“script” section:
{
...
"scripts": {
...
"cm": "git cz"
},
{
From now on, you should use yarn cm
or npm run cm
to commit change to the repository. This will launch a very user friendly flow to guide you through creating semantic commit messages. Try it now!
yarn cm
yarn run v1.22.19
$ git cz
cz-cli@4.3.0, cz-conventional-changelog@3.3.0
? Select the type of change that you are committing: feat: A new feature
? What is the scope of this change (e.g. component or file name): (press enter to skip) semver
? Write a short, imperative tense description of the change (max 86 chars):
(22) added semantic commits
? Provide a longer description of the change: (press enter to skip)
commits are now semantic and can be used to automatically generate version tags
? Are there any breaking changes? No
? Does this change affect any open issues? No
[semver 5ed826a] feat(semver): added semantic commits
1 file changed, 1 insertion(+)
✨ Done in 43.69s.
Automating releases with GitHub actions
Now let’s automate the creation of new tags in Github. Create the “.github/workflows/tag-version.yml” file with the following contents:
name: Tag Version
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
name: Tag New Version
if: "!contains(github.event.head_commit.message, '[release]')"
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_TOKEN }}
- name: Set Node.js 19.x
uses: actions/setup-node@v3
with:
node-version: 19.x
# node dependency cache
# this is not required but speeds up the process
- uses: actions/cache@v3
with:
path: ./node_modules
key: node-${{ hashFiles('**/composer.lock') }}
restore-keys: |
node
- name: Install JS deps
uses: borales/actions-yarn@v4
with:
cmd: install
- name: Tag Version
run: ./node_modules/semantic-release/bin/semantic-release.js --no-ci
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
Alrighty, let’s commit this workflow to your branch and push the branch to Github. Once the branch is pushed, get a personal developer token from your settings tab and configure it as one of the secrets for your repository in the “Settings” page under “Actions”. Use the name GH_TOKEN
.
Once that is done, you can merge your semver
branch to the main
branch and follow along with your “Tag Version / New” workflow.
If everything is configured right, you will see the workflow succeed and you should:
- see a CHANGELOG.md file in the root of your project
- see the version number appear in the package.json and composer.json
- see the version number appear in the “Tags” section of the repo
Cleanup action running on main
branch only
If your current workflow involves running an action on the main branch every time a change is pushed, you may want to consider modifying it to skip the action before the version bump commit is made.
Before:
name: My Job
on:
push:
branches:
- main
jobs:
create-release:
name: Do Something
runs-on: ubuntu-latest
steps:
...
After:
name: My Job
on:
push:
branches:
- main
jobs:
create-release:
name: Do Something
# job now runs if the commit message is the version bump commit
if: "contains(github.event.head_commit.message, '[release]')"
runs-on: ubuntu-latest
steps:
...
Cleanup action running on all branches
When executing an action on all branches, I recommend to skip it on the main branch for commits made other than the version bump commits.
Before
name: Test
on:
push:
branches:
- "*"
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
...
After
name: Test
on:
push:
branches:
- "*"
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
if: "!contains('refs/heads/main', github.ref) || contains(github.event.head_commit.message, '[release]'))"
We are done here!
This guide is intended to be a reference for anyone who may need it in the future. I will be using it for future projects and updating it as necessary. If you encounter any issues or find outdated information while following this guide, please leave a comment and I will do my best to address them. Additionally, if you have alternative methods for automating these steps, I would love to hear about them in the comments.
Additional Resources
- Semantic Release
https://github.com/semantic-release/semantic-release - Semantic Release Composer Plugin
https://github.com/ambimax/semantic-release-composer