Git - CI/CD Integration
Advanced
Integrating Git with CI/CD pipelines automates testing, building, and deployment processes. Master these integrations to create robust, automated development workflows that ensure code quality and reliable deployments.
CI/CD Fundamentals with Git
Git serves as the trigger and source for CI/CD pipelines:
# CI/CD Pipeline Flow
Git Push → Trigger → Build → Test → Deploy
# Common triggers:
# - Push to main branch
# - Pull request creation/update
# - Tag creation
# - Scheduled (cron-like)
# - Manual triggers
GitHub Actions Integration
Basic Workflow Configuration
# .github/workflows/ci.yml
name: Continuous Integration
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 18]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
Advanced Workflow Patterns
# .github/workflows/deploy.yml
name: Deploy Application
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history needed
- name: Get version from tag
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
else
VERSION=$(git describe --tags --always)
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build application
run: |
npm ci
npm run build
- name: Create artifact
uses: actions/upload-artifact@v3
with:
name: build-${{ steps.version.outputs.version }}
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment || 'staging' }}
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: build-${{ needs.build.outputs.version }}
- name: Deploy to ${{ github.event.inputs.environment || 'staging' }}
run: |
echo "Deploying version ${{ needs.build.outputs.version }}"
# Deployment commands here
Git-specific Actions
# .github/workflows/git-ops.yml
name: Git Operations
on:
push:
branches: [ main ]
jobs:
git-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Need full history
- name: Get changed files
id: changes
run: |
if [[ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ]]; then
echo "files=$(git diff --name-only HEAD~1)" >> $GITHUB_OUTPUT
else
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }})" >> $GITHUB_OUTPUT
fi
- name: Check for breaking changes
run: |
if echo "${{ steps.changes.outputs.files }}" | grep -q "BREAKING"; then
echo "::warning::Breaking changes detected"
fi
- name: Generate changelog
run: |
git log --oneline --since="$(git log -2 --format=%cd --date=short | tail -1)" > CHANGES.md
- name: Validate commit messages
run: |
git log --pretty=format:"%s" ${{ github.event.before }}..${{ github.event.after }} | \
while read msg; do
if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+"; then
echo "Invalid commit message: $msg"
exit 1
fi
done
Jenkins Integration
Jenkinsfile Configuration
// Jenkinsfile
pipeline {
agent any
environment {
NODE_VERSION = '16'
DOCKER_IMAGE = 'myapp'
}
triggers {
pollSCM('H/5 * * * *') // Poll every 5 minutes
cron('H 2 * * *') // Daily build at 2 AM
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.GIT_BRANCH_NAME = sh(
script: 'git rev-parse --abbrev-ref HEAD',
returnStdout: true
).trim()
}
}
}
stage('Build') {
steps {
sh '''
npm ci
npm run build
'''
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
post {
always {
junit 'test-results/unit.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
post {
always {
junit 'test-results/integration.xml'
}
}
}
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
tag pattern: 'v\\d+\\.\\d+\\.\\d+', comparator: 'REGEXP'
}
}
steps {
script {
if (env.BRANCH_NAME == 'main') {
sh 'npm run deploy:staging'
} else if (env.TAG_NAME) {
sh 'npm run deploy:production'
}
}
}
}
}
post {
always {
cleanWs()
}
success {
slackSend(
color: 'good',
message: "✅ Build successful: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "❌ Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
Jenkins Shared Libraries
// vars/gitUtils.groovy
def getChangedFiles() {
return sh(
script: 'git diff --name-only HEAD~1',
returnStdout: true
).trim().split('\n')
}
def getCommitMessage() {
return sh(
script: 'git log -1 --pretty=%B',
returnStdout: true
).trim()
}
def isHotfix() {
def branchName = env.BRANCH_NAME ?: 'unknown'
return branchName.startsWith('hotfix/')
}
def generateVersion() {
def timestamp = new Date().format('yyyyMMddHHmmss')
def shortHash = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
return "${timestamp}-${shortHash}"
}
// Usage in Jenkinsfile:
// def version = gitUtils.generateVersion()
// def changedFiles = gitUtils.getChangedFiles()
GitLab CI/CD Integration
GitLab CI Configuration
# .gitlab-ci.yml
variables:
DOCKER_DRIVER: overlay2
NODE_VERSION: "16"
stages:
- test
- build
- deploy
before_script:
- echo "Starting CI/CD for commit $CI_COMMIT_SHA"
- echo "Branch: $CI_COMMIT_REF_NAME"
# Test stage
test:unit:
stage: test
image: node:$NODE_VERSION
script:
- npm ci
- npm run test:unit
artifacts:
reports:
junit: test-results/unit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
test:integration:
stage: test
image: node:$NODE_VERSION
services:
- postgres:13
- redis:6
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
script:
- npm ci
- npm run test:integration
only:
changes:
- "src/**/*"
- "tests/**/*"
- "package*.json"
# Build stage
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- develop
- /^release\/.*$/
# Deploy stages
deploy:staging:
stage: deploy
image: alpine/helm:latest
script:
- helm upgrade --install myapp-staging ./helm-chart
--set image.tag=$CI_COMMIT_SHA
--set environment=staging
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
deploy:production:
stage: deploy
image: alpine/helm:latest
script:
- helm upgrade --install myapp-prod ./helm-chart
--set image.tag=$CI_COMMIT_SHA
--set environment=production
environment:
name: production
url: https://myapp.com
when: manual
only:
- main
- tags
Azure DevOps Integration
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
paths:
exclude:
- docs/*
- README.md
pr:
branches:
include:
- main
paths:
exclude:
- docs/*
variables:
buildConfiguration: 'Release'
nodeVersion: '16.x'
stages:
- stage: Test
jobs:
- job: UnitTests
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
displayName: 'Install Node.js'
- script: |
npm ci
npm run test:unit
displayName: 'Run unit tests'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results/unit.xml'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage/cobertura-coverage.xml'
- stage: Build
dependsOn: Test
condition: succeeded()
jobs:
- job: BuildApp
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- script: |
npm ci
npm run build
displayName: 'Build application'
- task: ArchiveFiles@2
inputs:
rootFolderOrFile: 'dist'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/app-$(Build.BuildId).zip'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'app-build'
- stage: Deploy
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToStaging
environment: 'staging'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'app-build'
- task: AzureWebApp@1
inputs:
azureSubscription: 'Azure-Connection'
appType: 'webAppLinux'
appName: 'myapp-staging'
package: '$(System.ArtifactsDirectory)/app-build/app-$(Build.BuildId).zip'
Branch-based Deployment Strategies
Git Flow with CI/CD
# Branch → Environment mapping
main → Production
develop → Staging
feature/* → Development
hotfix/* → Hotfix testing
release/* → QA/UAT
# GitHub Actions branch strategy
name: Branch-based Deployment
on:
push:
branches: [ main, develop, 'feature/*', 'hotfix/*', 'release/*' ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Determine environment
id: env
run: |
if [[ $GITHUB_REF == refs/heads/main ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "url=https://myapp.com" >> $GITHUB_OUTPUT
elif [[ $GITHUB_REF == refs/heads/develop ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "url=https://staging.myapp.com" >> $GITHUB_OUTPUT
elif [[ $GITHUB_REF == refs/heads/feature/* ]]; then
FEATURE_NAME=$(echo $GITHUB_REF | sed 's/refs\/heads\/feature\///')
echo "environment=dev-$FEATURE_NAME" >> $GITHUB_OUTPUT
echo "url=https://$FEATURE_NAME.dev.myapp.com" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.env.outputs.environment }}
run: |
echo "Deploying to ${{ steps.env.outputs.environment }}"
echo "URL: ${{ steps.env.outputs.url }}"
Feature Branch Previews
# .github/workflows/preview.yml
name: Feature Preview
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
- name: Sanitize branch name for URL
run: |
SANITIZED=$(echo "${{ env.branch }}" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]')
echo "sanitized_branch=$SANITIZED" >> $GITHUB_ENV
- name: Build and deploy preview
run: |
npm ci
npm run build
# Deploy to preview environment
echo "Deploying to https://${{ env.sanitized_branch }}.preview.myapp.com"
- name: Comment on PR
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview deployed to https://${{ env.sanitized_branch }}.preview.myapp.com`
})
Testing Integration
Git-aware Testing
# Test only changed files
name: Selective Testing
on:
pull_request:
paths:
- 'src/**'
- 'tests/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get changed files
id: changes
run: |
# Get changed files between base and head
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
# Filter for test-relevant files
TEST_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(js|ts|jsx|tsx)$' | tr '\n' ' ')
echo "files=$TEST_FILES" >> $GITHUB_OUTPUT
- name: Run tests for changed files
if: steps.changes.outputs.files != ''
run: |
npm ci
# Run tests for specific files
for file in ${{ steps.changes.outputs.files }}; do
echo "Testing $file"
npm test -- --testPathPattern="$file"
done
- name: Run integration tests if core files changed
if: contains(steps.changes.outputs.files, 'src/core/')
run: npm run test:integration
Performance Testing Integration
# Performance regression testing
name: Performance Testing
on:
pull_request:
branches: [ main ]
jobs:
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/[email protected]
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
- name: Bundle size analysis
run: |
npm run analyze
# Compare with main branch
git checkout main
npm ci && npm run build
MAIN_SIZE=$(du -sb dist | cut -f1)
git checkout ${{ github.head_ref }}
npm ci && npm run build
PR_SIZE=$(du -sb dist | cut -f1)
DIFF=$((PR_SIZE - MAIN_SIZE))
PERCENT=$(echo "scale=2; $DIFF * 100 / $MAIN_SIZE" | bc)
echo "Bundle size change: $DIFF bytes ($PERCENT%)"
Security Integration
Security Scanning Pipeline
# .github/workflows/security.yml
name: Security Scanning
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Secret scanning
run: |
# Check for secrets in commit history
git log --all --full-history -- | grep -E "(password|secret|key|token)" && exit 1 || echo "No secrets found"
- name: Dependency vulnerability scan
run: |
npm audit --audit-level high
- name: SAST with Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
- name: Container security scan
if: github.ref == 'refs/heads/main'
run: |
docker build -t myapp:${{ github.sha }} .
# Scan with Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v $HOME/Library/Caches:/root/.cache/ \
aquasec/trivy:latest image myapp:${{ github.sha }}
- name: License compliance check
run: |
npx license-checker --onlyAllow 'MIT;Apache-2.0;BSD-3-Clause;ISC'
Monitoring and Observability
Deployment Tracking
# Track deployments with Git metadata
name: Deployment Tracking
on:
workflow_run:
workflows: ["Deploy to Production"]
types: [completed]
jobs:
track-deployment:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Extract deployment info
id: deploy-info
run: |
COMMIT_SHA=${{ github.event.workflow_run.head_sha }}
COMMIT_MSG=$(git log --format=%B -n 1 $COMMIT_SHA)
AUTHOR=$(git log --format="%an <%ae>" -n 1 $COMMIT_SHA)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "commit=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "message=$COMMIT_MSG" >> $GITHUB_OUTPUT
echo "author=$AUTHOR" >> $GITHUB_OUTPUT
echo "timestamp=$TIMESTAMP" >> $GITHUB_OUTPUT
- name: Send to monitoring system
run: |
curl -X POST "${{ secrets.MONITORING_WEBHOOK }}" \
-H "Content-Type: application/json" \
-d '{
"event_type": "deployment",
"environment": "production",
"commit": "${{ steps.deploy-info.outputs.commit }}",
"message": "${{ steps.deploy-info.outputs.message }}",
"author": "${{ steps.deploy-info.outputs.author }}",
"timestamp": "${{ steps.deploy-info.outputs.timestamp }}"
}'
Best Practices
CI/CD Integration Best Practices:
- Use shallow clones for faster checkout when full history isn't needed
- Cache dependencies and build artifacts between runs
- Run tests in parallel when possible
- Use matrix builds for multiple environments
- Implement proper secret management
- Add meaningful status checks and notifications
- Document deployment procedures and rollback strategies
Pipeline Optimization
# Optimized CI pipeline
name: Optimized CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}
docs: ${{ steps.changes.outputs.docs }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'frontend/**'
- 'package*.json'
backend:
- 'backend/**'
- 'requirements.txt'
docs:
- 'docs/**'
- '*.md'
frontend-tests:
needs: changes
if: ${{ needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- run: npm ci
- run: npm test
backend-tests:
needs: changes
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- run: pip install -r requirements.txt
- run: pytest
Key Takeaways
- Automation: CI/CD pipelines automate testing, building, and deployment based on Git events
- Branch Strategy: Align CI/CD workflows with your Git branching strategy
- Quality Gates: Implement automated quality checks before deployment
- Security: Integrate security scanning into your pipeline
- Monitoring: Track deployments and correlate with Git metadata
- Optimization: Use caching, parallelization, and selective testing for performance
Integrating Git with CI/CD pipelines creates powerful automated workflows that ensure code quality, security, and reliable deployments. Master these integrations to build robust DevOps practices that scale with your team and projects.