🔧 DevOps

CI/CD Pipelines

Last updated: 2025-09-25 02:29:54

Continuous Integration/Continuous Deployment

CI/CD automates the process of integrating code changes and deploying applications.

GitHub Actions Workflow

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  release:
    types: [published]

env:
  NODE_VERSION: '18'
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
    
    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 linting
      run: npm run lint
    
    - name: Run tests
      run: npm run test:coverage
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: ./coverage/lcov.info
        flags: unittests
        name: codecov-umbrella
    
    - name: Run security audit
      run: npm audit --audit-level moderate

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' || github.event_name == 'release'
    
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Setup Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.DOCKER_REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha,prefix={{branch}}-
    
    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v4
      with:
        context: .
        file: ./Dockerfile
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          NODE_ENV=production
          BUILD_DATE=${{ github.event.head_commit.timestamp }}
          VCS_REF=${{ github.sha }}

  deploy-staging:
    needs: [test, build]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
    
    steps:
    - name: Deploy to staging
      uses: azure/k8s-deploy@v1
      with:
        manifests: |
          k8s/staging/deployment.yaml
          k8s/staging/service.yaml
          k8s/staging/ingress.yaml
        images: ${{ needs.build.outputs.image-tag }}
        kubeconfig: ${{ secrets.KUBE_CONFIG_STAGING }}
    
    - name: Run integration tests
      run: |
        npm ci
        npm run test:integration
      env:
        TEST_URL: https://staging.myapp.com
        API_KEY: ${{ secrets.STAGING_API_KEY }}
    
    - name: Notify Slack
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        channel: '#deployments'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
      if: always()

  deploy-production:
    needs: [test, build]
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    environment: production
    
    steps:
    - name: Deploy to production
      uses: azure/k8s-deploy@v1
      with:
        manifests: |
          k8s/production/deployment.yaml
          k8s/production/service.yaml
          k8s/production/ingress.yaml
        images: ${{ needs.build.outputs.image-tag }}
        kubeconfig: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
    
    - name: Update deployment status
      uses: bobheadxi/deployments@v1
      with:
        step: finish
        token: ${{ secrets.GITHUB_TOKEN }}
        status: ${{ job.status }}
        env: production
        deployment_id: ${{ steps.deployment.outputs.deployment_id }}
    
    - name: Create deployment
      id: deployment
      uses: bobheadxi/deployments@v1
      with:
        step: start
        token: ${{ secrets.GITHUB_TOKEN }}
        env: production

GitLab CI/CD Pipeline

# .gitlab-ci.yml
stages:
  - test
  - build
  - security
  - deploy-staging
  - deploy-production

variables:
  NODE_VERSION: '18'
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  POSTGRES_DB: testdb
  POSTGRES_USER: testuser
  POSTGRES_PASSWORD: testpass

cache:
  paths:
    - node_modules/
  key:
    files:
      - package-lock.json

# Test stage
test:unit:
  stage: test
  image: node:$NODE_VERSION-alpine
  services:
    - postgres:13-alpine
    - redis:6-alpine
  variables:
    DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
    REDIS_URL: redis://redis:6379
  before_script:
    - npm ci
  script:
    - npm run lint
    - npm run test:unit
    - npm run test:integration
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
      junit: junit.xml
    paths:
      - coverage/
    expire_in: 1 week
  only:
    - merge_requests
    - main
    - develop

test:e2e:
  stage: test
  image: cypress/included:12.0.0
  services:
    - name: postgres:13-alpine
    - name: redis:6-alpine
    - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      alias: app
  variables:
    CYPRESS_baseUrl: http://app:3000
    DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
  before_script:
    - npm ci
  script:
    - npx cypress run --record --key $CYPRESS_RECORD_KEY
  artifacts:
    when: always
    paths:
      - cypress/videos/
      - cypress/screenshots/
    expire_in: 1 week
  only:
    - merge_requests
    - main

# Build stage
build:docker:
  stage: build
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build 
        --build-arg NODE_ENV=production
        --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
        --build-arg VCS_REF=$CI_COMMIT_SHA
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --tag $CI_REGISTRY_IMAGE:latest
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main
    - develop
    - tags

# Security scanning
security:dependency-scan:
  stage: security
  image: node:$NODE_VERSION-alpine
  before_script:
    - npm ci
  script:
    - npm audit --audit-level moderate
    - npx snyk test --severity-threshold=high
  allow_failure: true
  only:
    - merge_requests
    - main

security:container-scan:
  stage: security
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock 
        -v $PWD:/tmp/.cache/ aquasec/trivy:latest 
        image --exit-code 0 --no-progress --format table 
        $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  dependencies:
    - build:docker
  only:
    - main
    - develop

# Deployment stages
deploy:staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.myapp.com
  before_script:
    - echo $KUBE_CONFIG_STAGING | base64 -d > kubeconfig
    - export KUBECONFIG=kubeconfig
  script:
    - kubectl set image deployment/myapp-deployment 
        myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA 
        --namespace=staging
    - kubectl rollout status deployment/myapp-deployment --namespace=staging
    - kubectl get services --namespace=staging
  after_script:
    - rm -f kubeconfig
  dependencies:
    - build:docker
  only:
    - develop
  when: manual

deploy:production:
  stage: deploy-production
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://myapp.com
  before_script:
    - echo $KUBE_CONFIG_PRODUCTION | base64 -d > kubeconfig
    - export KUBECONFIG=kubeconfig
  script:
    - kubectl set image deployment/myapp-deployment 
        myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA 
        --namespace=production
    - kubectl rollout status deployment/myapp-deployment --namespace=production
    - kubectl get services --namespace=production
  after_script:
    - rm -f kubeconfig
    # Send notification
    - 'curl -X POST -H "Content-type: application/json" 
        --data "{\"text\":\"🚀 Deployed $CI_PROJECT_NAME to production: $CI_COMMIT_MESSAGE\"}" 
        $SLACK_WEBHOOK_URL'
  dependencies:
    - build:docker
  only:
    - tags
  when: manual

Jenkins Pipeline

// Jenkinsfile
pipeline {
    agent {
        kubernetes {
            yaml """
                apiVersion: v1
                kind: Pod
                metadata:
                  labels:
                    jenkins: agent
                spec:
                  containers:
                  - name: node
                    image: node:18-alpine
                    command:
                    - cat
                    tty: true
                  - name: docker
                    image: docker:20.10.16
                    command:
                    - cat
                    tty: true
                    volumeMounts:
                    - mountPath: /var/run/docker.sock
                      name: docker-sock
                  - name: kubectl
                    image: bitnami/kubectl:latest
                    command:
                    - cat
                    tty: true
                  volumes:
                  - name: docker-sock
                    hostPath:
                      path: /var/run/docker.sock
            """
        }
    }
    
    environment {
        DOCKER_REGISTRY = 'your-registry.com'
        IMAGE_NAME = 'myapp'
        KUBECONFIG = credentials('kubeconfig')
        SLACK_WEBHOOK = credentials('slack-webhook')
        SONARQUBE_TOKEN = credentials('sonarqube-token')
    }
    
    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 45, unit: 'MINUTES')
        timestamps()
    }
    
    triggers {
        githubPush()
        cron(env.BRANCH_NAME == 'main' ? '0 2 * * *' : '') // Nightly builds on main
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        script: 'git rev-parse --short HEAD',
                        returnStdout: true
                    ).trim()
                    env.BUILD_VERSION = "${env.BRANCH_NAME}-${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                }
            }
        }
        
        stage('Install Dependencies') {
            steps {
                container('node') {
                    sh 'npm ci'
                }
            }
        }
        
        stage('Code Quality') {
            parallel {
                stage('Lint') {
                    steps {
                        container('node') {
                            sh 'npm run lint'
                        }
                    }
                }
                
                stage('Test') {
                    steps {
                        container('node') {
                            sh 'npm run test:coverage'
                        }
                        publishTestResults testResultsPattern: 'junit.xml'
                        publishCoverageGoberturaReport 'coverage/cobertura-coverage.xml'
                    }
                }
                
                stage('Security Scan') {
                    steps {
                        container('node') {
                            sh 'npm audit --audit-level moderate'
                            sh 'npx snyk test --severity-threshold=high || true'
                        }
                    }
                }
            }
        }
        
        stage('SonarQube Analysis') {
            when {
                anyOf {
                    branch 'main'
                    changeRequest()
                }
            }
            steps {
                container('node') {
                    withSonarQubeEnv('SonarQube') {
                        sh """
                            npx sonar-scanner \
                                -Dsonar.projectKey=myapp \
                                -Dsonar.sources=src \
                                -Dsonar.tests=src \
                                -Dsonar.test.inclusions='**/*.test.js' \
                                -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \
                                -Dsonar.testExecutionReportPaths=test-report.xml
                        """
                    }
                }
            }
        }
        
        stage('Build Docker Image') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                    buildingTag()
                }
            }
            steps {
                container('docker') {
                    script {
                        def image = docker.build(
                            "${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION}",
                            "--build-arg NODE_ENV=production ."
                        )
                        
                        docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
                            image.push()
                            image.push('latest')
                        }
                    }
                }
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                container('kubectl') {
                    sh """
                        kubectl set image deployment/myapp-deployment \
                            myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION} \
                            --namespace=staging
                        kubectl rollout status deployment/myapp-deployment --namespace=staging --timeout=300s
                    """
                }
            }
        }
        
        stage('Integration Tests') {
            when {
                branch 'develop'
            }
            steps {
                container('node') {
                    sh 'npm run test:integration'
                }
            }
        }
        
        stage('Deploy to Production') {
            when {
                buildingTag()
            }
            steps {
                input message: 'Deploy to production?', ok: 'Deploy'
                container('kubectl') {
                    sh """
                        kubectl set image deployment/myapp-deployment \
                            myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION} \
                            --namespace=production
                        kubectl rollout status deployment/myapp-deployment --namespace=production --timeout=600s
                    """
                }
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        
        success {
            script {
                if (env.BRANCH_NAME == 'main' || env.TAG_NAME) {
                    sh """
                        curl -X POST -H 'Content-type: application/json' \
                            --data '{"text":"✅ Successfully deployed ${IMAGE_NAME}:${BUILD_VERSION}"}' \
                            ${SLACK_WEBHOOK}
                    """
                }
            }
        }
        
        failure {
            sh """
                curl -X POST -H 'Content-type: application/json' \
                    --data '{"text":"❌ Build failed for ${IMAGE_NAME}:${BUILD_VERSION} - ${BUILD_URL}"}' \
                    ${SLACK_WEBHOOK}
            """
        }
    }
}