Automate Git Tag Versioning Usingย Bash
Versioning your source code with git tags is not difficult to do manually, but it's also easy to automate which means:
- You can use it in your CI/CD pipeline
- Reduce the chance of errors when working in a team
- Save time googling or looking up steps in case your forget
I'll show you how we can automate this process with a simple bash script.
This post assumes basic knowledge of git tags and semantic versioning
Manual versioning process
Before we can automate the process, let's first look at how to manually version our source code with git tags.
Generally speaking, the basic approach is along the lines of:
- Modify your source code and are ready to release a new version
- Increment the version number in your project file for example,
package.json
for a Node project - Commit your changes to git
- Create a git tag for the commit with the new version number, e.g.
v1.0.4
- Push your commit and the tag to your remote repository
From these steps we can automate everything except the first step, understandably.
Automated versioning steps
Based on the steps above, let's break down the different parts needed for our script before putting it all together.
Verify your git local repo is clean
Before we start making automated changes to our local repo, it's best to verify that it's in a clean state.
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
Get most recent version from git tags
For us to increment the version number, we need to know what the last version was. There are 2 ways we could get this:
- From our manifest file such as
package.json
orCargo.toml
- From our previous git tags
I prefer the latter for a few reasons:
- Independent of specific programming languages and environments
- Maintains git tags as the source of truth for our versions
# List git tags sorted based on semantic versioning
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.0.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.0.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
Increment our version
There are 3 types of version increments we will support:
- Patch:
v1.0.0 -> v1.0.1
- Minor:
v1.0.0 -> v1.1.0
- Major:
v1.0.0 -> v2.0.0
The script will take a single argument determining how to bump the version number. Then use awk
to increment the correct segment of the version while zeroing latter segments as needed.
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
Update version in manifest file
This step is optional but a good idea for keeping things in sync.
It's a good idea to update the version number in your respective manifest file like package.json
or Cargo.toml
. Here are 2 examples which you can modify and adapt to other files.
package.json
# Update version in package.json
sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION_NEXT\"/" package.json
# Commit the changes
git add .
git commit -m "build: bump package.json version - v$VERSION_NEXT"
Cargo.toml
# Update version in Cargo.toml
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo generate-lockfile
# Commit the changes
git add .
git commit -m "build: bump Cargo.toml version - v$VERSION_NEXT"
Create the git tag
Now that everything else is ready, we can create the git tag for our new version.
# Create an annotated tag
git tag -a "v$VERSION_NEXT" -m "Release: v$VERSION_NEXT"
# Optional: push commits and tag to remote 'main' branch
git push origin main --follow-tags
Full versioning script
Now that we understand all the individual steps, we can put it all together.
#!/usr/bin/env bash
# Exit script if command fails or uninitialized variables used
set -euo pipefail
# ==================================
# Verify repo is clean
# ==================================
# List uncommitted changes and
# check if the output is not empty
if [ -n "$(git status --porcelain)" ]; then
# Print error message
printf "\nError: repo has uncommitted changes\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Get latest version from git tags
# ==================================
# List git tags sorted lexicographically
# so version numbers sorted correctly
GIT_TAGS=$(git tag --sort=version:refname)
# Get last line of output which returns the
# last tag (most recent version)
GIT_TAG_LATEST=$(echo "$GIT_TAGS" | tail -n 1)
# If no tag found, default to v0.0.0
if [ -z "$GIT_TAG_LATEST" ]; then
GIT_TAG_LATEST="v0.0.0"
fi
# Strip prefix 'v' from the tag to easily increment
GIT_TAG_LATEST=$(echo "$GIT_TAG_LATEST" | sed 's/^v//')
# ==================================
# Increment version number
# ==================================
# Get version type from first argument passed to script
VERSION_TYPE="${1-}"
VERSION_NEXT=""
if [ "$VERSION_TYPE" = "patch" ]; then
# Increment patch version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$NF++; print $1"."$2"."$NF}')"
elif [ "$VERSION_TYPE" = "minor" ]; then
# Increment minor version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$2++; $3=0; print $1"."$2"."$3}')"
elif [ "$VERSION_TYPE" = "major" ]; then
# Increment major version
VERSION_NEXT="$(echo "$GIT_TAG_LATEST" | awk -F. '{$1++; $2=0; $3=0; print $1"."$2"."$3}')"
else
# Print error for unknown versioning type
printf "\nError: invalid VERSION_TYPE arg passed, must be 'patch', 'minor' or 'major'\n\n"
# Exit with error code
exit 1
fi
# ==================================
# Update manifest file (optional)
# assuming Rust project with Cargo.toml
# modify this as needed for your project
# ==================================
# Update version in Cargo.toml
sed -i "s/^version = .*/version = \"$VERSION_NEXT\"/" Cargo.toml
# Update Cargo.lock as this changes when
# updating the version in your manifest
cargo generate-lockfile
# Commit the changes
git add .
git commit -m "build: bump Cargo.toml version - v$VERSION_NEXT"
# ==================================
# Create git tag for new version
# ==================================
# Create an annotated tag
git tag -a "v$VERSION_NEXT" -m "Release: v$VERSION_NEXT"
# Optional: push commits and tag to remote 'main' branch
git push origin main --follow-tags
Now simply execute the script:
# Bump version
bash ./version.sh patch
bash ./version.sh minor
bash ./version.sh major
# Make the script executable to execute without bash
chmod +x ./version.sh
./version.sh patch
And there you have it, managing versions with git tags is as easy as that!
Comments