Automatic Version Tagging using Github Actions

Jonas Weigert
Scratch Pad
Published in
5 min readJan 28, 2023

--

Photo by Kaung Myat Min on Unsplash

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

--

--