Back to blog
DevOps#devops#ci-cd#jenkins#github-actions#pipeline

DevOps CI/CD Pipeline Tutorial: Jenkins vs GitHub Actions

A hands-on guide to building CI/CD pipelines with Jenkins and GitHub Actions — covering pipeline-as-code, test integration, deployment stages, and how to choose between the two tools.

InnovateBits8 min read

Continuous Integration and Continuous Delivery (CI/CD) is the practice of automating the build, test, and deployment lifecycle so that every code change moves through a consistent, reliable pipeline. Without it, software delivery is slow, error-prone, and dependent on heroic manual effort. With it, teams ship multiple times per day with confidence.

This guide walks through building practical CI/CD pipelines with the two most widely used tools: Jenkins (self-hosted, highly customisable) and GitHub Actions (cloud-native, zero infrastructure). By the end, you'll understand how to choose between them and how to build production-ready pipelines with either.


What a CI/CD Pipeline Does

A CI/CD pipeline is a set of automated steps that runs every time code changes. A typical pipeline:

  1. Triggers on a push, pull request, or tag
  2. Checks out the code
  3. Installs dependencies
  4. Runs linting and static analysis — catch style issues and obvious errors early
  5. Runs unit tests — fast feedback on business logic
  6. Runs integration/API tests — validate service boundaries
  7. Builds the application (compile, bundle, Docker image)
  8. Runs E2E tests against the built artefact
  9. Publishes test reports and artefacts
  10. Deploys to staging (on merge to main) and production (on tag or approval)

The key principle: fail fast, fail clearly. Each stage should catch a specific category of problem and report it clearly so the developer can fix it immediately.


Jenkins

Jenkins is a self-hosted, open-source automation server. It's been the standard CI tool in enterprise environments for over a decade, offering unmatched flexibility and a massive plugin ecosystem (1,800+ plugins).

When to choose Jenkins

  • You need to run tests on on-premise infrastructure (security, compliance, hardware requirements)
  • You have complex, highly customised pipeline logic that GitHub Actions' YAML doesn't express well
  • Your organisation already has Jenkins infrastructure investment
  • You need integration with on-premise tools (Active Directory, internal Artifactory, etc.)

Jenkinsfile basics

Modern Jenkins uses Declarative Pipeline defined in a Jenkinsfile committed to your repository:

pipeline {
  agent any
  
  environment {
    NODE_VERSION = '20'
    STAGING_URL = credentials('staging-url')
  }
  
  options {
    timeout(time: 30, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '10'))
  }
 
  stages {
    stage('Install') {
      steps {
        sh 'node --version'
        sh 'npm ci'
      }
    }
    
    stage('Lint & Type Check') {
      steps {
        sh 'npm run lint'
        sh 'npm run type-check'
      }
    }
    
    stage('Unit Tests') {
      steps {
        sh 'npm test -- --coverage --reporter=junit --outputFile=test-results/unit.xml'
      }
      post {
        always {
          junit 'test-results/unit.xml'
          publishHTML([
            reportDir: 'coverage',
            reportFiles: 'lcov-report/index.html',
            reportName: 'Coverage Report'
          ])
        }
      }
    }
    
    stage('Build') {
      steps {
        sh 'npm run build'
        archiveArtifacts artifacts: 'dist/**', fingerprint: true
      }
    }
    
    stage('E2E Tests') {
      steps {
        sh 'npx playwright install --with-deps chromium'
        sh 'npx playwright test --reporter=junit'
      }
      post {
        always {
          junit 'test-results/e2e.xml'
          publishHTML([
            reportDir: 'playwright-report',
            reportFiles: 'index.html',
            reportName: 'Playwright Report'
          ])
        }
      }
    }
    
    stage('Deploy to Staging') {
      when {
        branch 'main'
      }
      steps {
        sh './scripts/deploy.sh staging'
        echo "Deployed to ${STAGING_URL}"
      }
    }
  }
  
  post {
    failure {
      // Notify Slack on failure
      slackSend(
        channel: '#ci-alerts',
        color: 'danger',
        message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} - ${env.BUILD_URL}"
      )
    }
    success {
      slackSend(
        channel: '#ci-alerts',
        color: 'good',
        message: "Build PASSED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
      )
    }
  }
}

Parallel stages in Jenkins

Running test stages in parallel dramatically reduces pipeline time:

stage('Tests') {
  parallel {
    stage('Unit Tests') {
      steps { sh 'npm test' }
    }
    stage('API Tests') {
      steps { sh 'npx playwright test tests/api/' }
    }
    stage('Lint') {
      steps { sh 'npm run lint' }
    }
  }
}

GitHub Actions

GitHub Actions is GitHub's native CI/CD system. Workflows are defined in YAML files in .github/workflows/. There's no server to maintain — GitHub provides compute on demand.

When to choose GitHub Actions

  • Your code is on GitHub (the integration is seamless)
  • You want zero infrastructure to manage
  • Your team wants to move fast with minimal DevOps overhead
  • You need access to GitHub's marketplace of pre-built Actions

A complete workflow

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
 
env:
  NODE_VERSION: '20'
 
jobs:
  lint-and-typecheck:
    name: Lint & Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
 
  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint-and-typecheck
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
      - run: npm ci
      - run: npm test -- --coverage
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
 
  e2e-tests:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run build
      - name: Run Playwright tests
        run: npx playwright test
        env:
          BASE_URL: http://localhost:3000
      - name: Upload Playwright report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14
 
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./scripts/deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
          STAGING_HOST: ${{ secrets.STAGING_HOST }}

Matrix builds — test across multiple environments

jobs:
  test:
    name: Test on Node ${{ matrix.node-version }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Caching to speed up workflows

Cache node_modules and Playwright browsers to avoid re-downloading on every run:

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm       # Caches npm cache automatically
 
- name: Cache Playwright browsers
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

Jenkins vs GitHub Actions Comparison

JenkinsGitHub Actions
InfrastructureSelf-hosted (you manage)Cloud (GitHub manages)
CostServer costs + maintenanceFree for public repos; minutes-based for private
Setup timeHours-daysMinutes
FlexibilityExtremely highHigh (with limitations)
Plugin ecosystem1,800+ plugins20,000+ marketplace Actions
ScriptingGroovy (Declarative or Scripted)YAML
Secret managementJenkins credentials storeGitHub secrets
On-prem support❌ (cloud only, or GitHub Enterprise)
GitHub integrationVia pluginNative

Pipeline Best Practices

Fail fast. Put the fastest, most likely-to-fail checks (lint, type-check) first. Don't run a 20-minute E2E suite before catching a syntax error.

Cache aggressively. Dependency installation is often the slowest step. Cache node_modules, Maven's .m2, pip's package cache, and browser binaries between runs.

Keep secrets out of logs. Never echo a secret variable. Use GitHub's masked secrets or Jenkins credentials to prevent accidental exposure in logs.

Use environments for deployment gates. GitHub's Environment protection rules (require approval before deploying to production) and Jenkins's input step provide manual gates where automation isn't appropriate.

Notify on failure immediately. Teams that discover CI failures hours after they happened move slower. Set up Slack, Teams, or email notifications on pipeline failure so the responsible developer sees it within minutes.

Archive test artefacts. Always upload test reports, coverage reports, and Playwright traces even on failure. You need these to debug what went wrong.


Integrating Quality Gates

A CI pipeline without quality gates is just a build server. Quality gates enforce standards:

# Fail the build if coverage drops below threshold
- name: Check coverage threshold
  run: |
    COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
    if (( $(echo "$COVERAGE < 70" | bc -l) )); then
      echo "Coverage $COVERAGE% is below threshold of 70%"
      exit 1
    fi

Common quality gates to enforce:

  • Unit test pass rate: 100% (no failing tests merged)
  • Code coverage: minimum threshold per repository
  • E2E test pass rate: 100% on main branch merges
  • Linting: zero warnings on new code
  • Security scanning: no new high/critical vulnerabilities

What's Next

With a CI/CD pipeline in place, the next level is:

  • Deployment automation — blue/green deployments, canary releases, automatic rollback on failure metrics
  • Test environment automation — provision ephemeral test environments per PR using Docker Compose or Kubernetes
  • Observability in production — feed production error rates back into your quality metrics

For more on the DevOps principles behind CI/CD, see our DevOps guide. For test automation to run in your pipeline, see our Playwright and API Testing guides.