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.