Jenkins pipelines can automatically generate semantic version tags, but it’s not as simple as just bumping a number; it’s about creating a predictable, automated versioning strategy that reflects your code’s changes.

Let’s see this in action. Imagine a pipeline that builds a Java application.

pipeline {
    agent any
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Determine Version') {
            steps {
                script {
                    def currentVersion = sh(returnStdout: true, script: 'git describe --tags --abbrev=0').trim()
                    def commitMessage = sh(returnStdout: true, script: 'git log -1 --pretty=%B').trim()

                    def major = currentVersion.split('\\.')[0].toInteger()
                    def minor = currentVersion.split('\\.')[1].toInteger()
                    def patch = currentVersion.split('\\.')[2].toInteger()

                    if (commitMessage.contains('[major]')) {
                        major++
                        minor = 0
                        patch = 0
                    } else if (commitMessage.contains('[minor]')) {
                        minor++
                        patch = 0
                    } else {
                        patch++
                    }
                    env.NEW_VERSION = "${major}.${minor}.${patch}"
                    echo "Determined new version: ${env.NEW_VERSION}"
                }
            }
        }
        stage('Tag Version') {
            steps {
                script {
                    // Only tag if we're on the main branch and it's a new version
                    if (env.BRANCH_NAME == 'main' && env.NEW_VERSION != null) {
                        sh "git tag -a ${env.NEW_VERSION} -m 'Version ${env.NEW_VERSION}'"
                        sh "git push origin ${env.NEW_VERSION}"
                        echo "Tagged and pushed version ${env.NEW_VERSION}"
                    } else {
                        echo "Not tagging. Branch: ${env.BRANCH_NAME}, New Version: ${env.NEW_VERSION}"
                    }
                }
            }
        }
        stage('Deploy') {
            when {
                expression { env.BRANCH_NAME == 'main' && env.NEW_VERSION != null }
            }
            steps {
                echo "Deploying version ${env.NEW_VERSION} to production..."
                // Add your deployment steps here
            }
        }
    }
}

This pipeline illustrates a common approach: using Git commit messages to signal version bumps. A commit like feat: Add new user authentication [minor] would trigger a minor version increment. The git describe --tags --abbrev=0 command fetches the latest tag, and git log -1 --pretty=%B retrieves the most recent commit message. The script then parses the current version, increments the appropriate segment (major, minor, or patch) based on keywords in the commit message, and constructs the NEW_VERSION. Finally, it tags the commit with this new version and pushes the tag to the remote repository.

The core problem this solves is maintaining a consistent and auditable release history. Without automated versioning, teams often resort to manual tagging, which is error-prone and leads to inconsistencies. Semantic Versioning (SemVer) provides a structured way to communicate the intent of a change: MAJOR.MINOR.PATCH. A MAJOR version indicates incompatible API changes, a MINOR version indicates backward-compatible new features, and a PATCH version indicates backward-compatible bug fixes.

Internally, this relies heavily on Git’s tagging capabilities. When you git tag -a <tagname> -m <message>, you’re creating an annotated tag that points to a specific commit. This tag is essentially a bookmark in your Git history. The pipeline then pushes these tags to the remote repository (git push origin <tagname>), making them accessible to others and for CI/CD integrations. The when condition in the Deploy stage ensures that deployments only happen on the main branch when a new version has been successfully determined and tagged, further solidifying the release process.

Many teams overlook the power of leveraging Git’s branching strategy in conjunction with versioning. For instance, you might choose to only determine and tag new versions from your main or master branch. Feature branches could have their own development versions (e.g., 1.2.0-dev.123) generated using a different strategy, perhaps tied to the number of commits since the last release tag, and these wouldn’t trigger the production deployment or the final release tag. This separation allows for continuous integration and testing on feature branches without polluting the release history.

The next logical step is to integrate this with your artifact repository, ensuring that the artifact deployed matches the version tag.

Want structured learning?

Take the full Jenkins course →