CI/CD Versioning
Introduction
Versioning is a crucial aspect of CI/CD (Continuous Integration/Continuous Deployment) practices that ensures your software is properly identified, tracked, and deployed throughout its lifecycle. Effective versioning strategies help teams manage dependencies, troubleshoot issues, and maintain a reliable release process. In this guide, we'll explore how versioning fits into CI/CD pipelines and learn practical approaches to implement versioning in your projects.
What is CI/CD Versioning?
CI/CD versioning refers to the systematic approach of assigning unique identifiers to software artifacts as they move through the continuous integration and deployment pipeline. These identifiers help distinguish between different builds, releases, and deployments, making it possible to:
- Track which version of software is running in various environments
- Roll back to previous versions when issues arise
- Ensure consistent deployment across different environments
- Facilitate communication between development and operations teams
Common Versioning Schemes
Semantic Versioning (SemVer)
Semantic Versioning is one of the most widely adopted versioning schemes, following the format MAJOR.MINOR.PATCH
(e.g., 2.1.4
).
MAJOR: Breaking changes
MINOR: New features (backward compatible)
PATCH: Bug fixes (backward compatible)
Example implementation in a CI/CD pipeline:
# Example GitHub Actions workflow using SemVer
name: Build and Release
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Calculate version
id: semver
run: |
# Extract version components from package.json
CURRENT_VERSION=$(node -p "require('./package.json').version")
# For this example, we're incrementing patch number on each build
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Update package.json
run: |
# Update version in package.json
npm version ${{ steps.semver.outputs.new_version }} --no-git-tag-version
Calendar Versioning (CalVer)
Calendar Versioning ties releases to specific dates, often following the format YEAR.MONTH.SEQUENCE
(e.g., 2023.03.1
).
YEAR: Full year (2023) or shortened (23)
MONTH: Month number (01-12)
SEQUENCE: Build or release number within that month
Example in a Jenkins pipeline:
// Example Jenkins pipeline using CalVer
pipeline {
agent any
stages {
stage('Calculate Version') {
steps {
script {
def date = new Date()
def year = date.format('yyyy')
def month = date.format('MM')
// Get the last build number from a tracking file or service
def buildNumberFile = readFile(file: 'build-number.txt', text: true).trim() ?: '0'
def buildNumber = Integer.parseInt(buildNumberFile) + 1
// Create CalVer string
env.VERSION = "${year}.${month}.${buildNumber}"
// Update the build number for next time
writeFile file: 'build-number.txt', text: "${buildNumber}"
}
echo "Building version ${env.VERSION}"
}
}
stage('Build and Tag') {
steps {
sh "docker build -t myapp:${env.VERSION} ."
sh "docker tag myapp:${env.VERSION} myapp:latest"
}
}
}
}
Build Number Versioning
Simple incrementing build numbers can be effective, especially for internal or development builds.
Example in a GitLab CI configuration:
# Example GitLab CI configuration with build numbers
variables:
VERSION_PREFIX: "1.0"
stages:
- build
- deploy
build_job:
stage: build
script:
# Use GitLab's CI_PIPELINE_IID as an auto-incrementing counter
- export VERSION="${VERSION_PREFIX}.${CI_PIPELINE_IID}"
- echo "Building version ${VERSION}"
- echo "${VERSION}" > version.txt
- docker build -t myapp:${VERSION} .
artifacts:
paths:
- version.txt
Implementing Versioning in CI/CD Pipelines
Let's walk through how to implement versioning across different stages of a CI/CD pipeline:
1. Version Generation
The first step is automatically generating a version number when code changes are pushed:
Here's a practical implementation using GitHub Actions:
# Version generation in GitHub Actions
name: Generate Version
on:
push:
branches: [ main ]
jobs:
version:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.versioning.outputs.new_version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history for tags
- name: Generate version
id: versioning
run: |
# Get latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
echo "Latest tag: $LATEST_TAG"
# Parse the tag
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
# Increment patch version
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "New version: $NEW_VERSION"
# Set output for other jobs
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Git Tag
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git tag -a v${{ steps.versioning.outputs.new_version }} -m "Release ${{ steps.versioning.outputs.new_version }}"
git push origin v${{ steps.versioning.outputs.new_version }}
2. Artifact Labeling
Once you've generated a version, it's important to label all build artifacts with this version:
# Example for a Node.js project
npm version $VERSION --no-git-tag-version
# Example for a Java/Maven project
mvn versions:set -DnewVersion=$VERSION
# Example for Docker images
docker build -t myapp:$VERSION .
3. Environment-Specific Versioning
Different environments may need specific versioning approaches:
Example implementation for environment suffixes:
# Environment-specific versioning in GitHub Actions
deploy_job:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, production]
steps:
- name: Set environment-specific version
run: |
VERSION="${{ needs.build.outputs.version }}"
if [[ "${{ matrix.environment }}" != "production" ]]; then
VERSION="${VERSION}-${{ matrix.environment }}"
fi
echo "Using version: $VERSION"
- name: Deploy to ${{ matrix.environment }}
run: |
# Deploy using the environment-specific version
./deploy.sh ${{ matrix.environment }} $VERSION
Best Practices for CI/CD Versioning
1. Automate Everything
Manual versioning leads to mistakes. Automate version generation, tagging, and labeling as part of your pipeline.
# Example automation script for version bumping
- name: Auto-bump version
run: |
# Detect change type (major, minor, patch) based on commit message
if grep -q "BREAKING CHANGE" <<< "${{ github.event.head_commit.message }}"; then
VERSION_BUMP="major"
elif grep -q "feat:" <<< "${{ github.event.head_commit.message }}"; then
VERSION_BUMP="minor"
else
VERSION_BUMP="patch"
fi
# Bump version accordingly
npm version $VERSION_BUMP --no-git-tag-version
2. Immutable Artifacts
Once built and versioned, artifacts should never change. This applies to container images, binaries, and deployment packages.
# Example policy enforcement in a pipeline
if docker manifest inspect myapp:$VERSION > /dev/null 2>&1; then
echo "Error: Version $VERSION already exists. Cannot overwrite."
exit 1
fi
3. Version Visibility
Make versions visible in your application for easy troubleshooting:
// JavaScript example: Adding version to API responses
app.use((req, res, next) => {
res.setHeader('X-App-Version', process.env.APP_VERSION);
next();
});
// Java example: Exposing version via info endpoint
@RestController
public class InfoController {
@Value("${app.version}")
private String appVersion;
@GetMapping("/info")
public Map<String, String> getInfo() {
return Map.of("version", appVersion);
}
}
4. Version in Logs
Include version information in your application logs:
# Setting version environment variable in deployment
kubectl set env deployment/myapp APP_VERSION=${VERSION}
// Using version in logs (Node.js)
console.log(`[${process.env.APP_VERSION}] Application started`);
Real-World CI/CD Versioning Example
Let's look at a complete end-to-end example for a web application:
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline with Versioning
on:
push:
branches: [ main ]
jobs:
version:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.versioning.outputs.new_version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Calculate new version
id: versioning
run: |
# Get the latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.1.0")
# For simplicity, always bump the patch version
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Git tag
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git tag -a v${{ steps.versioning.outputs.new_version }} -m "Release ${{ steps.versioning.outputs.new_version }}"
git push origin v${{ steps.versioning.outputs.new_version }}
build:
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Update version in package.json
run: |
npm version ${{ needs.version.outputs.new_version }} --no-git-tag-version
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Build Docker image
run: |
docker build -t myapp:${{ needs.version.outputs.new_version }} .
docker tag myapp:${{ needs.version.outputs.new_version }} myapp:latest
- name: Push to registry
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push myapp:${{ needs.version.outputs.new_version }}
docker push myapp:latest
deploy-dev:
needs: [version, build]
runs-on: ubuntu-latest
steps:
- name: Deploy to Dev
run: |
# Example deployment script
VERSION="${{ needs.version.outputs.new_version }}"
echo "Deploying version $VERSION to development environment"
# Replace with your actual deployment commands
echo "APP_VERSION=$VERSION" > .env
# Deploy commands would go here
deploy-production:
needs: [version, build, deploy-dev]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to Production
run: |
VERSION="${{ needs.version.outputs.new_version }}"
echo "Deploying version $VERSION to production environment"
# Replace with your actual production deployment commands
Handling Version Conflicts and Rollbacks
Sometimes you'll need to roll back to a previous version due to issues:
# Example rollback script
#!/bin/bash
CURRENT_VERSION=$(kubectl get deployment myapp -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="APP_VERSION")].value}')
PREVIOUS_VERSION=$(git describe --tags --abbrev=0 "v$CURRENT_VERSION^")
echo "Rolling back from $CURRENT_VERSION to $PREVIOUS_VERSION"
# Update deployment to use previous version
kubectl set image deployment/myapp myapp=myapp:$PREVIOUS_VERSION
kubectl set env deployment/myapp APP_VERSION=$PREVIOUS_VERSION
Summary
Effective CI/CD versioning is essential for maintaining a reliable and traceable software delivery process. In this guide, we've covered:
- Different versioning schemes including Semantic Versioning, Calendar Versioning, and Build Numbers
- How to implement versioning throughout your CI/CD pipeline
- Best practices for automation, immutability, and visibility
- A real-world example of versioning in a complete CI/CD workflow
- Handling rollbacks and version conflicts
By implementing proper versioning in your CI/CD pipeline, you'll improve traceability, simplify troubleshooting, and make your releases more reliable and predictable.
Additional Resources
- Semantic Versioning 2.0.0 Specification
- Calendar Versioning
- Git Tags Documentation
- npm version Documentation
Exercises
- Implement a Semantic Versioning strategy in a simple Node.js project with automatic version bumping based on commit message prefixes.
- Create a Docker-based CI/CD pipeline that builds and tags images with the appropriate version numbers.
- Develop a version rollback script for your preferred deployment platform (Kubernetes, AWS, etc.).
- Add version visibility to an existing application by exposing the version number through an API endpoint and in logs.
- Compare the pros and cons of different versioning strategies for a project you're working on and decide which one fits best.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)