Reemus Icon

Automate Git Tag Versioning Using Bash

👍🔥❤️😂😢😕
views
comments

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:

  1. Modify your source code and are ready to release a new version
  2. Increment the version number in your project file for example,
    package.json for a Node project
  3. Commit your changes to git
  4. Create a git tag for the commit with the new version number, e.g. v1.0.4
  5. 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
bash-icon

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 or Cargo.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//')
bash-icon

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
bash-icon

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"
bash-icon

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"
bash-icon

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
bash-icon

Full versioning script

Now that we understand all the individual steps, we can put it all together.

version.sh
#!/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
bash-icon

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
shell-icon

And there you have it, managing versions with git tags is as easy as that!

👍🔥❤️😂😢😕

Comments

...

Your name will be displayed publicly

Loading comments...