BytePane

CI/CD Pipeline Guide: GitHub Actions, Jenkins & GitLab CI Compared

DevOps15 min read

What CI/CD Solves

Without CI/CD, deploying software involves manual steps: running tests locally (maybe), building artifacts by hand, uploading files to servers, and hoping nothing breaks. This process is slow, error-prone, and does not scale beyond a single developer.

CI/CD automates the entire pipeline from code push to production deployment. Every change is built, tested, and optionally deployed automatically, giving teams confidence that their main branch is always deployable and bugs are caught within minutes of introduction.

# Typical CI/CD pipeline stages:
#
# 1. TRIGGER     → Push to branch or PR created
# 2. INSTALL     → Install dependencies (npm ci, pip install)
# 3. LINT        → Code style checks (ESLint, Prettier)
# 4. TYPE CHECK  → Static analysis (TypeScript, mypy)
# 5. UNIT TESTS  → Fast isolated tests
# 6. BUILD       → Compile/bundle the application
# 7. INT. TESTS  → Integration tests (API, database)
# 8. E2E TESTS   → End-to-end browser tests (Cypress, Playwright)
# 9. DEPLOY      → Ship to staging or production
# 10. NOTIFY     → Slack/email notification

GitHub Actions: Full Pipeline Example

GitHub Actions is the most popular CI/CD platform for open-source and GitHub-hosted projects. Workflows are defined in YAML files inside .github/workflows/.

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

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

jobs:
  lint-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck

  test:
    runs-on: ubuntu-latest
    needs: lint-and-typecheck
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.node-version }}
          path: coverage/

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy to server
        env:
          SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          rsync -avz -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
            dist/ [email protected]:/var/www/app/

GitLab CI: Pipeline Configuration

GitLab CI uses a single .gitlab-ci.yml file at the repository root. It has built-in support for environments, container registries, and security scanning.

# .gitlab-ci.yml
stages:
  - lint
  - test
  - build
  - deploy

variables:
  NODE_ENV: production

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/

lint:
  stage: lint
  image: node:20-alpine
  script:
    - npm ci --cache .npm
    - npm run lint
    - npm run typecheck

test:
  stage: test
  image: node:20-alpine
  script:
    - npm ci --cache .npm
    - npm test -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  image: node:20-alpine
  script:
    - npm ci --cache .npm
    - npm run build
  artifacts:
    paths:
      - dist/

deploy_production:
  stage: deploy
  environment:
    name: production
    url: https://myapp.com
  script:
    - apt-get update && apt-get install -y rsync
    - rsync -avz dist/ [email protected]:/var/www/app/
  only:
    - main
  when: manual  # Requires manual click to deploy

Jenkins: Declarative Pipeline

Jenkins is the most flexible CI/CD tool, with over 1,800 plugins. It runs on your own servers, giving you full control over the build environment. Jenkins pipelines are defined in a Jenkinsfile.

// Jenkinsfile (Declarative Pipeline)
pipeline {
    agent {
        docker { image 'node:20-alpine' }
    }

    environment {
        NODE_ENV = 'production'
        NPM_TOKEN = credentials('npm-token')
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        stage('Lint & Type Check') {
            parallel {
                stage('Lint') {
                    steps { sh 'npm run lint' }
                }
                stage('Type Check') {
                    steps { sh 'npm run typecheck' }
                }
            }
        }
        stage('Test') {
            steps {
                sh 'npm test -- --coverage'
            }
            post {
                always {
                    junit 'test-results/*.xml'
                    publishHTML(target: [
                        reportDir: 'coverage/lcov-report',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }
        stage('Build') {
            steps { sh 'npm run build' }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps {
                sshagent(['deploy-key']) {
                    sh 'rsync -avz dist/ deploy@server:/var/www/app/'
                }
            }
        }
    }

    post {
        failure {
            slackSend channel: '#deploys',
                      message: "Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
        }
    }
}

Platform Comparison

FeatureGitHub ActionsGitLab CIJenkins
HostingCloud (self-hosted runners available)Cloud + self-hostedSelf-hosted only
Config formatYAMLYAMLGroovy (Jenkinsfile)
Free tier2,000 min/month400 min/monthFree (self-hosted)
Marketplace20,000+ actionsTemplates + components1,800+ plugins
Container registryGHCR (separate)Built-inPlugin required
Learning curveLowMediumHigh

Caching and Speed Optimization

Pipeline speed directly impacts developer productivity. Caching dependencies is the single biggest optimization you can make. Without caching, every build downloads all packages from scratch.

# GitHub Actions: Cache node_modules
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # Automatically caches ~/.npm

# For Docker builds: Cache layers
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# Parallel jobs: Run independent steps simultaneously
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]  # Runs in parallel with 'test'
  test:
    runs-on: ubuntu-latest
    steps: [...]  # Runs in parallel with 'lint'
  build:
    needs: [lint, test]  # Waits for both to pass
    steps: [...]

# Skip unnecessary work with path filters
on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - 'package-lock.json'
    paths-ignore:
      - 'docs/**'
      - '*.md'

CI/CD pipelines generate configuration in YAML and JSON formats. Validate your pipeline configuration files with our YAML to JSON Converter to catch syntax errors before pushing.

Deployment Strategies

How you deploy is as important as what you deploy. Different strategies offer different trade-offs between safety, speed, and complexity.

StrategyHow It WorksRisk Level
RollingReplace instances one by oneLow
Blue/GreenFull parallel environment, instant switchVery Low
CanaryRoute 5-10% traffic to new version firstVery Low
Feature FlagsDeploy code, toggle features independentlyLow
Big BangReplace everything at onceHigh

Secrets Management in Pipelines

Never hardcode secrets in pipeline configuration files. Every CI/CD platform provides secure secret storage that injects values at runtime and masks them in logs.

# GitHub Actions: Repository Secrets
# Settings → Secrets and Variables → Actions → New repository secret

# Use in workflow:
env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  API_KEY: ${{ secrets.API_KEY }}

# Environment-specific secrets (staging vs production):
deploy:
  environment: production  # Uses production secrets
  env:
    DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}

# OIDC: Passwordless cloud auth (no stored secrets!)
permissions:
  id-token: write
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/deploy
      aws-region: us-east-1
  # No AWS keys stored — uses short-lived OIDC tokens

For a comprehensive guide on managing secrets across environments, see our environment variables guide.

Pipeline Best Practices

  1. Fail fast -- Run the quickest checks first (lint, type check). If code style fails, skip the 10-minute test suite.
  2. Make pipelines deterministic -- Use npm ci not npm install, pin action versions with SHA, and use exact Docker image tags.
  3. Keep the main branch deployable -- Every merge to main should produce a shippable artifact. Use feature flags for incomplete features.
  4. Test in production-like environments -- Use the same Node version, OS, and dependency versions as production.
  5. Monitor pipeline health -- Track build success rate, average duration, and flaky test frequency. Alert when build times regress.
  6. Use branch protection rules -- Require all CI checks to pass before merging. No exceptions.

Validate Your Pipeline Config with BytePane

Convert and validate CI/CD configuration files with our YAML to JSON Converter. Schedule cron expressions for timed pipeline triggers. Compare deployment configs with the Diff Checker.

Open YAML to JSON Converter

Related Articles