Skip to main content
โšก Calmops

CI/CD Pipeline Automation: GitHub Actions vs Jenkins vs GitLab

Introduction

Continuous Integration and Continuous Deployment (CI/CD) are fundamental practices in modern software development. Automating build, test, and deployment processes reduces errors, accelerates time-to-market, and improves code quality. However, choosing the right CI/CD platform is criticalโ€”the wrong choice can lead to slow pipelines, high maintenance costs, and operational headaches.

This comprehensive guide compares three major CI/CD platformsโ€”GitHub Actions, Jenkins, and GitLab CI/CDโ€”with practical examples and real-world deployment strategies.


Core Concepts & Terminology

Continuous Integration (CI)

Automatically building and testing code changes on every commit to catch issues early.

Continuous Deployment (CD)

Automatically deploying tested code to production without manual intervention.

Continuous Delivery

Automatically preparing code for production but requiring manual approval for deployment.

Pipeline

Sequence of automated steps (build, test, deploy) triggered by code changes.

Workflow

Automated process triggered by events (push, pull request, schedule).

Job

Individual task within a pipeline (e.g., run tests, build Docker image).

Stage

Logical grouping of jobs that run in sequence or parallel.

Artifact

Output from a job (e.g., compiled binary, test reports, Docker image).

Secret

Sensitive data (API keys, credentials) stored securely and injected at runtime.

Webhook

Mechanism for triggering pipelines based on repository events.

Trigger

Event that initiates a pipeline (push, pull request, schedule, manual).

Rollback

Reverting to a previous version if deployment fails.


CI/CD Platform Comparison

Feature Comparison Matrix

Feature GitHub Actions Jenkins GitLab CI/CD
Pricing Free (public), $21/month (private) Free (self-hosted) Free (SaaS), $228/year (premium)
Hosting Cloud only Self-hosted or cloud Cloud or self-hosted
Setup Complexity Very easy Complex Easy
Learning Curve Gentle Steep Moderate
Scalability Excellent Excellent Excellent
Ecosystem 10,000+ actions 1,000+ plugins 500+ integrations
Container Support Native Via plugins Native
Kubernetes Support Excellent Excellent Excellent
Cost (100 jobs/month) $0-21 $0 (self-hosted) $0-228
Best For GitHub projects Enterprise GitLab projects

GitHub Actions

Architecture Overview

GitHub Repository
โ”œโ”€โ”€ .github/workflows/
โ”‚   โ”œโ”€โ”€ ci.yml
โ”‚   โ”œโ”€โ”€ deploy.yml
โ”‚   โ””โ”€โ”€ scheduled-tasks.yml
โ””โ”€โ”€ Code

Workflow Triggers:
- push
- pull_request
- schedule
- workflow_dispatch
- repository_dispatch

Execution:
GitHub-hosted runners (Ubuntu, Windows, macOS)
or Self-hosted runners

Basic Workflow Example

name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    
    - name: Run linting
      run: |
        flake8 src/
        black --check src/
    
    - name: Run tests
      run: |
        pytest tests/ --cov=src --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.xml
    
    - name: Build Docker image
      run: |
        docker build -t myapp:${{ github.sha }} .
        docker tag myapp:${{ github.sha }} myapp:latest
    
    - name: Push to registry
      if: github.ref == 'refs/heads/main'
      run: |
        echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
        docker push myapp:${{ github.sha }}
        docker push myapp:latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to production
      run: |
        mkdir -p ~/.ssh
        echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
        chmod 600 ~/.ssh/deploy_key
        ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
        ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \
          "cd /app && docker pull myapp:${{ github.sha }} && docker-compose up -d"

Advanced Workflow with Matrix

name: Multi-Version Testing

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.9', '3.10', '3.11']
        exclude:
          - os: macos-latest
            python-version: '3.9'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    
    - name: Run tests
      run: pytest tests/

Deployment to Kubernetes

name: Deploy to Kubernetes

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Build and push Docker image
      run: |
        docker build -t myregistry.azurecr.io/myapp:${{ github.sha }} .
        docker login -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }} myregistry.azurecr.io
        docker push myregistry.azurecr.io/myapp:${{ github.sha }}
    
    - name: Deploy to Kubernetes
      run: |
        mkdir -p ~/.kube
        echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
        
        kubectl set image deployment/myapp \
          myapp=myregistry.azurecr.io/myapp:${{ github.sha }} \
          -n production
        
        kubectl rollout status deployment/myapp -n production

Jenkins

Architecture Overview

Jenkins Master
โ”œโ”€โ”€ Job Configuration
โ”œโ”€โ”€ Pipeline Definition
โ””โ”€โ”€ Credential Management

Jenkins Agents
โ”œโ”€โ”€ Agent 1 (Linux)
โ”œโ”€โ”€ Agent 2 (Windows)
โ””โ”€โ”€ Agent 3 (macOS)

Plugins:
- Pipeline
- Docker
- Kubernetes
- Git
- GitHub
- Slack

Declarative Pipeline Example

pipeline {
    agent any
    
    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 1, unit: 'HOURS')
        timestamps()
    }
    
    environment {
        DOCKER_REGISTRY = 'myregistry.azurecr.io'
        DOCKER_IMAGE = "${DOCKER_REGISTRY}/myapp"
        DOCKER_TAG = "${BUILD_NUMBER}"
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                script {
                    sh '''
                        python -m pip install --upgrade pip
                        pip install -r requirements.txt
                    '''
                }
            }
        }
        
        stage('Lint') {
            steps {
                script {
                    sh '''
                        flake8 src/
                        black --check src/
                    '''
                }
            }
        }
        
        stage('Test') {
            steps {
                script {
                    sh '''
                        pytest tests/ --cov=src --cov-report=xml
                    '''
                }
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    sh '''
                        docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
                        docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
                    '''
                }
            }
        }
        
        stage('Push to Registry') {
            when {
                branch 'main'
            }
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
                        sh '''
                            echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin ${DOCKER_REGISTRY}
                            docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
                            docker push ${DOCKER_IMAGE}:latest
                        '''
                    }
                }
            }
        }
        
        stage('Deploy to Kubernetes') {
            when {
                branch 'main'
            }
            steps {
                script {
                    withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
                        sh '''
                            kubectl set image deployment/myapp \
                              myapp=${DOCKER_IMAGE}:${DOCKER_TAG} \
                              -n production
                            
                            kubectl rollout status deployment/myapp -n production
                        '''
                    }
                }
            }
        }
    }
    
    post {
        always {
            junit 'test-results.xml'
            publishHTML([
                reportDir: 'htmlcov',
                reportFiles: 'index.html',
                reportName: 'Coverage Report'
            ])
        }
        
        failure {
            emailext(
                subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Build failed. Check console output at ${env.BUILD_URL}",
                to: "${env.CHANGE_AUTHOR_EMAIL}"
            )
        }
        
        success {
            slackSend(
                color: 'good',
                message: "Build Successful: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
            )
        }
    }
}

Scripted Pipeline Example

node {
    try {
        stage('Checkout') {
            checkout scm
        }
        
        stage('Build') {
            sh 'python -m pip install -r requirements.txt'
        }
        
        stage('Test') {
            sh 'pytest tests/'
        }
        
        stage('Build Docker') {
            sh 'docker build -t myapp:${BUILD_NUMBER} .'
        }
        
        if (env.BRANCH_NAME == 'main') {
            stage('Deploy') {
                sh 'docker push myapp:${BUILD_NUMBER}'
                sh 'kubectl set image deployment/myapp myapp=myapp:${BUILD_NUMBER} -n production'
            }
        }
    }
    catch (Exception e) {
        currentBuild.result = 'FAILURE'
        throw e
    }
    finally {
        junit 'test-results.xml'
    }
}

Kubernetes Plugin Configuration

pipeline {
    agent {
        kubernetes {
            yaml '''
                apiVersion: v1
                kind: Pod
                spec:
                  serviceAccountName: jenkins
                  containers:
                  - name: docker
                    image: docker:latest
                    command:
                    - cat
                    tty: true
                    volumeMounts:
                    - name: docker-sock
                      mountPath: /var/run/docker.sock
                  - name: kubectl
                    image: bitnami/kubectl:latest
                    command:
                    - cat
                    tty: true
                  volumes:
                  - name: docker-sock
                    hostPath:
                      path: /var/run/docker.sock
            '''
        }
    }
    
    stages {
        stage('Build') {
            steps {
                container('docker') {
                    sh 'docker build -t myapp:${BUILD_NUMBER} .'
                }
            }
        }
        
        stage('Deploy') {
            steps {
                container('kubectl') {
                    sh 'kubectl set image deployment/myapp myapp=myapp:${BUILD_NUMBER} -n production'
                }
            }
        }
    }
}

GitLab CI/CD

Architecture Overview

GitLab Repository
โ”œโ”€โ”€ .gitlab-ci.yml
โ””โ”€โ”€ Code

GitLab Runner
โ”œโ”€โ”€ Docker executor
โ”œโ”€โ”€ Shell executor
โ”œโ”€โ”€ Kubernetes executor
โ””โ”€โ”€ Machine executor

Pipeline Stages:
- build
- test
- deploy

Basic Pipeline Example

stages:
  - build
  - test
  - deploy

variables:
  DOCKER_REGISTRY: myregistry.azurecr.io
  DOCKER_IMAGE: $DOCKER_REGISTRY/myapp

build:
  stage: build
  image: python:3.11
  script:
    - pip install -r requirements.txt
    - python setup.py build
  artifacts:
    paths:
      - build/
    expire_in: 1 day

test:
  stage: test
  image: python:3.11
  script:
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
    - pytest tests/ --cov=src --cov-report=xml
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    paths:
      - htmlcov/
    expire_in: 30 days

lint:
  stage: test
  image: python:3.11
  script:
    - pip install flake8 black
    - flake8 src/
    - black --check src/

docker_build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
    - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
  only:
    - main

docker_push:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
    - docker push $DOCKER_IMAGE:latest
  only:
    - main

deploy_production:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context production
    - kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
    - kubectl rollout status deployment/myapp -n production
  only:
    - main
  environment:
    name: production
    kubernetes:
      namespace: production

Advanced Pipeline with Environments

stages:
  - build
  - test
  - deploy_staging
  - deploy_production

variables:
  DOCKER_REGISTRY: myregistry.azurecr.io
  DOCKER_IMAGE: $DOCKER_REGISTRY/myapp

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
    - docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
  artifacts:
    paths:
      - docker-image.tar
    expire_in: 1 day

test:
  stage: test
  image: python:3.11
  script:
    - pip install -r requirements.txt
    - pytest tests/ --cov=src
  coverage: '/TOTAL.*\s+(\d+%)$/'

deploy_staging:
  stage: deploy_staging
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n staging
    - kubectl rollout status deployment/myapp -n staging
  environment:
    name: staging
    kubernetes:
      namespace: staging
    url: https://staging.example.com
  only:
    - develop

deploy_production:
  stage: deploy_production
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context production
    - kubectl set image deployment/myapp myapp=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
    - kubectl rollout status deployment/myapp -n production
  environment:
    name: production
    kubernetes:
      namespace: production
    url: https://example.com
  only:
    - main
  when: manual  # Require manual approval

Real-World Pipeline Comparison

Scenario: Microservices Deployment

GitHub Actions Implementation

name: Deploy Microservices

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [api, web, worker]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Build ${{ matrix.service }}
      run: |
        cd services/${{ matrix.service }}
        docker build -t myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }} .
    
    - name: Push to registry
      run: |
        echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} myregistry.azurecr.io
        docker push myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }}
    
    - name: Deploy to Kubernetes
      run: |
        mkdir -p ~/.kube
        echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
        kubectl set image deployment/${{ matrix.service }} \
          ${{ matrix.service }}=myregistry.azurecr.io/${{ matrix.service }}:${{ github.sha }} \
          -n production

Jenkins Implementation

pipeline {
    agent any
    
    parameters {
        choice(name: 'SERVICE', choices: ['api', 'web', 'worker'], description: 'Service to deploy')
    }
    
    stages {
        stage('Build') {
            steps {
                dir("services/${params.SERVICE}") {
                    sh 'docker build -t myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER} .'
                }
            }
        }
        
        stage('Push') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'docker-registry', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
                    sh '''
                        echo $PASS | docker login -u $USER myregistry.azurecr.io
                        docker push myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER}
                    '''
                }
            }
        }
        
        stage('Deploy') {
            steps {
                withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
                    sh '''
                        kubectl set image deployment/${SERVICE} \
                          ${SERVICE}=myregistry.azurecr.io/${SERVICE}:${BUILD_NUMBER} \
                          -n production
                    '''
                }
            }
        }
    }
}

GitLab CI/CD Implementation

stages:
  - build
  - deploy

variables:
  DOCKER_REGISTRY: myregistry.azurecr.io

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - cd services/$SERVICE
    - docker build -t $DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA .
    - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
    - docker push $DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA

deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/$SERVICE $SERVICE=$DOCKER_REGISTRY/$SERVICE:$CI_COMMIT_SHA -n production
  only:
    - main

Best Practices & Common Pitfalls

Best Practices

  1. Keep Pipelines Fast: Optimize build times, parallelize jobs
  2. Fail Fast: Run quick checks (lint, unit tests) before slow tests
  3. Artifact Management: Clean up old artifacts to save storage
  4. Secret Management: Never commit secrets, use secure storage
  5. Idempotent Deployments: Deployments should be safe to repeat
  6. Rollback Strategy: Always have a way to revert deployments
  7. Monitoring: Monitor pipeline performance and failures
  8. Documentation: Document pipeline configuration and processes
  9. Testing: Comprehensive testing before production deployment
  10. Gradual Rollout: Use canary or blue-green deployments

Common Pitfalls

  1. Slow Pipelines: Inefficient builds, unnecessary steps
  2. Flaky Tests: Tests that fail intermittently
  3. Secrets in Logs: Accidentally exposing sensitive data
  4. No Rollback Plan: Unable to revert failed deployments
  5. Inadequate Testing: Deploying untested code
  6. Manual Approvals: Too many manual steps slow down deployment
  7. Poor Monitoring: Can’t detect deployment issues
  8. Artifact Bloat: Old artifacts consuming storage
  9. Tight Coupling: Pipeline tightly coupled to infrastructure
  10. Lack of Documentation: Team can’t maintain pipeline

External Resources

GitHub Actions

Jenkins

GitLab CI/CD

Learning Resources


Conclusion

Choosing the right CI/CD platform depends on your specific needs, existing infrastructure, and team expertise. GitHub Actions excels for GitHub-hosted projects with its simplicity and tight integration. Jenkins provides maximum flexibility and control for complex enterprise environments. GitLab CI/CD offers a balanced approach with strong Kubernetes support.

Regardless of platform choice, focus on building fast, reliable pipelines that enable rapid, safe deployments. Invest in proper testing, monitoring, and rollback strategies to ensure production stability.

Start with a simple pipeline, gradually add complexity, and continuously optimize based on real-world metrics and feedback.

Comments