CI/CD Platforms: Jenkins vs. GitLab CI vs. GitHub Actions
All these platforms solve the same core problem: automating the software delivery lifecycle. They act as the central nervous system for DevOps workflow, connecting code repository to live environment.
Their primary services are Continuous Integration (CI) and Continuous Delivery/Deployment (CD).
CI: Automatically running builds, tests, and static analysis every time a developer pushes code. The main goal is to find and fix bugs faster.
CD: Automatically taking the tested code from the CI stage and releasing it to a staging or production environment.
The Platforms
1. Jenkins
The original, open-source, "do-anything" automation server. It's the classic workhorse.
Core Services:
Unmatched Extensibility: Jenkins's power comes from its massive ecosystem of over 1,800 plugins. If you need to integrate with a tool, there's almost certainly a plugin for it.
Total Control: It's typically self-hosted (though cloud options exist), giving you complete control over the build environment, hardware, and security.
Pipeline as Code: Uses a
Jenkinsfile(written in Groovy) to define your pipeline, allowing you to check your build logic into source control.Flexibility: It can handle everything from simple CI jobs to complex, multi-stage, cross-platform deployment orchestration.
2. GitLab CI/CD
The "all-in-one" platform. CI/CD is just one feature of the complete GitLab DevOps suite.
Core Services:
Deep Integration: This is its biggest strength. The CI/CD is built directly into the same platform that hosts your code (SCM), container registry, security scanning (SAST/DAST), package management, and issue tracking.
Convention over Configuration: It's very easy to get started. Just add a
.gitlab-ci.ymlfile to your repo, and GitLab automatically detects it and runs the pipeline.Integrated Runners: Provides shared, managed runners out of the box, but also makes it easy to register your own self-hosted runners (on VMs, Kubernetes, etc.) for more control.
Auto DevOps: A feature that attempts to automatically build, test, and deploy your application with zero configuration, which is great for standard projects.
3. GitHub Actions
The "event-driven" automation platform, deeply integrated with the GitHub ecosystem.
Core Services:
More than just CI/CD: Actions is designed to trigger "workflows" from any event in GitHub (a new issue, a comment, a PR, a release), not just code pushes. This makes it a powerful general-purpose automation tool.
Community-Powered Marketplace: Its key feature. Instead of plugins, you use "Actions" (reusable units of code) from the GitHub Marketplace. This makes it incredibly fast to build complex workflows by stitching together community-built components.
YAML Configuration: Workflows are defined in YAML files stored in the
.github/workflowsdirectory of your repository.Hosted Runners: Provides managed runners for Linux, Windows, and macOS, which is a major convenience. You can also use self-hosted runners.
Pipeline as Code
The "Pipeline as Code" (PaC) syntax is the heart of modern CI/CD and the place. The most fundamental split is this:
Jenkins uses a Groovy-based Domain Specific Language (DSL).
GitLab CI and GitHub Actions both use YAML.
1. Jenkins (The Jenkinsfile)
Jenkins's PaC is all about power and flexibility. Because its Jenkinsfile is written in Groovy, it's not just a configuration file—it's a full-blown programming language.
Two Flavors:
Scripted Pipeline: The older, more flexible style. It's essentially free-form Groovy scripting. It's powerful but can become complex and hard to maintain.
Declarative Pipeline: The modern, preferred style. It's much more structured and readable, with a clear syntax for defining the pipeline. I'll focus on this one as it's the modern standard.
Core Concepts (Declarative)
pipeline { ... }: The wrapper for the entire pipeline.agent { ... }: Defines where the pipeline (or a specific stage) will run. This could beany(any available agent),none(for a parent stage), or specific labels likedocker,linux, etc.stages { ... }: A container for all the sequentialstageblocks.stage('Name') { ... }: A distinct phase of your pipeline, like "Build", "Test", or "Deploy". These show up as separate columns in the Jenkins UI.steps { ... }: Contains the actual commands to be run within astage. This is where you put shell commands (sh '...'), run plugins, etc.
Example: Declarative Jenkinsfile
This example builds a Node.js app, runs tests, and (if on the main branch) deploys it.
// Jenkinsfile
pipeline {
// 1. Define the execution environment for the whole pipeline
agent any // Use any available Jenkins agent
// 2. Define global environment variables
environment {
NODE_ENV = 'test'
}
// 3. Define the main execution stages
stages {
// --- BUILD STAGE ---
stage('Build') {
steps {
// 'sh' is a built-in step to run a shell command
echo 'Starting the build stage...'
sh 'npm install'
sh 'npm run build'
}
}
// --- TEST STAGE ---
stage('Test') {
steps {
echo 'Starting the test stage...'
sh 'npm test'
}
// 'post' actions run after the steps in a stage
post {
always {
// 'junit' is a plugin step to record test results
junit 'reports/junit.xml'
}
}
}
// --- DEPLOY STAGE ---
stage('Deploy') {
// 'when' adds a condition for running this stage
when {
branch 'main' // Only run this stage on the 'main' branch
}
steps {
echo 'Starting the deploy stage...'
sh './deploy-to-prod.sh'
}
}
}
}The "Escape Hatch": The best part of Declarative is the script step. If you need the full power of Groovy, you can just add a script { ... } block inside your steps and write any programmatic logic you want (loops, conditionals, try/catch blocks, etc.).
2. GitLab CI/CD (The .gitlab-ci.yml)
GitLab's philosophy is "convention over configuration." The syntax is YAML and is job-centric. It's tightly integrated with the GitLab ecosystem.
Core Concepts
stages: [...]: (Optional but recommended) A top-level key that defines the order of your stages. Jobs in the same stage run in parallel. Jobs in later stages wait for jobs in earlier stages to complete.job_name:: (e.g.,build_job:) This is your custom name for a job. Each job is a top-level YAML object.stage: ...: Assigns a job to one of thestagesyou defined at the top.image: ...: A key feature. This specifies the Docker image to use for this specific job, making it incredibly easy to use different environments (e.g.,node:18for building,python:3.10for testing).script: [...]: The list of shell commands the job will run. This is the "what to do."artifacts: { ... }: Defines files or directories to save from the job and pass to later jobs.rules: ...: The powerful, modern way to define when a job should run (e.g., on which branch, on a merge request, etc.).
Example: .gitlab-ci.yml
This pipeline does the same thing as the Jenkins example.
# .gitlab-ci.yml
# 1. Define the order of stages
stages:
- build
- test
- deploy
# 2. Define a global cache for all jobs
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
# --- BUILD JOB ---
# This is a job. The name is 'build_app'
build_app:
stage: build
image: node:18-alpine # Use a Node.js 18 Docker image
script:
- echo "Starting the build job..."
- npm install
- npm run build
artifacts:
# Save the 'dist' folder for the deploy job
paths:
- dist/
# --- TEST JOB ---
test_app:
stage: test
image: node:18-alpine
script:
- echo "Starting the test job..."
- npm test
artifacts:
# Save test reports
when: always
paths:
- reports/junit.xml
reports:
junit: reports/junit.xml
# --- DEPLOY JOB ---
deploy_to_production:
stage: deploy
image: gcr.io/google.com/cloudsdk:latest # Use a different image for deployment
script:
- echo "Deploying to production..."
- ./deploy-to-prod.sh # Assumes gcloud SDK is configured
rules:
# 3. Only run this job if the commit is on the 'main' branch
- if: $CI_COMMIT_BRANCH == "main"3. GitHub Actions
GitHub Actions is event-driven. The syntax is also YAML, but its philosophy is built around "workflows" that trigger on "events" (like a push, a new issue, a comment, etc.).
Core Concepts
name: ...: The name of your workflow, which appears in the "Actions" tab.on: { ... }: The most important part. This defines the event that triggers the workflow (e.g.,on: [push],on: pull_request,on: schedule: ...).jobs: { ... }: Contains one or more jobs. By default, jobs run in parallel.job_id: { ... }: (e.g.,build-and-test:) The unique ID for a job.runs-on: ...: Defines the runner to use (e.g.,ubuntu-latest,windows-latest,macos-latest). This is GitHub's equivalent of Jenkins'sagent.steps: [...]: A list of steps to run sequentially within a job. This is the heart of the job.uses: ...: The killer feature. This keyword lets you pull in a pre-built "Action" from the GitHub Marketplace (e.g.,uses: actions/checkout@v4to check out code, oruses: aws-actions/configure-aws-credentials@v4to log in to AWS).run: ...: The equivalent of GitLab'sscript:. Runs a shell command.
Example: .github/workflows/ci-pipeline.yml
This workflow does the same build, test, and deploy.
# .github/workflows/ci-pipeline.yml
# 1. Name of the workflow
name: CI/CD Pipeline
# 2. Define the trigger event
on:
push:
branches: [ "main" ] # Run on push to main
pull_request:
branches: [ "*" ] # Also run on all pull requests
# 3. Define the jobs
jobs:
# --- BUILD AND TEST JOB ---
build-and-test:
# The 'build-and-test' name is the job ID
name: Build & Test
runs-on: ubuntu-latest # Use a GitHub-hosted Linux runner
steps:
# Step 1: Check out the code
- name: Check out repository
uses: actions/checkout@v4 # This is a Marketplace Action
# Step 2: Set up Node.js
- name: Set up Node.js 18
uses: actions/setup-node@v4 # Another Marketplace Action
with:
node-version: 18
cache: 'npm' # Automatically caches node_modules
# Step 3: Install dependencies
- name: Install dependencies
run: npm install
# Step 4: Run build
- name: Build project
run: npm run build
# Step 5: Run tests
- name: Run tests
run: npm test
# --- DEPLOY JOB ---
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
# 'needs' creates a dependency. This job won't run until 'build-and-test' succeeds
needs: build-and-test
# 'if' adds a condition. This job will ONLY run on a push to the 'main' branch.
# It will be skipped on pull requests.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy
run: ./deploy-to-prod.shSummary: Key Differences at a Glance
| Feature | Jenkins | GitLab CI | GitHub Actions |
|---|---|---|---|
| File Name | Jenkinsfile | .gitlab-ci.yml | .github/workflows/*.yml |
| Language | Groovy (a programming language) | YAML (a configuration format) | YAML (a configuration format) |
| Core Philosophy | Programmatic & Extensible. A "hub" for automation. | Job-centric & Integrated. A "factory assembly line." | Event-driven & Composable. A set of "reactions." |
| Key Syntactic Feature | script { ... } block for pure Groovy logic. | image: ... per-job Docker image definition. | uses: ... for reusable Marketplace actions. |
As you can see, while they all achieve the same goal, their syntax reflects a very different design philosophy. Jenkins is programmatic, GitLab is structured and job-centric, and GitHub is event-centric and component-based.
