CI vs CD vs Continuous Deployment. Pipeline anatomy. Jenkins vs GitHub Actions vs GitLab CI — three tools that dominate every DevOps job description. Understand all three, be hireable anywhere.
CI
Every commit triggers an automated build + test. The goal is detecting integration issues early — before they compound.
CD (Delivery)
Every successful CI run produces a deployable artifact. Deployment to production is one-click / manual approval.
CD (Deployment)
Every successful CI run is automatically deployed to production. No human gate. Full automation.
on: block. Jenkins: webhooks or poll SCM.docker build + push. Image tagged with commit SHA for traceability.Jenkinsfile.github/workflows/.gitlab-ci.yml in repo rootpipeline { agent { label 'linux' } // which agent to run on environment { NODE_ENV = 'test' APP_PORT = '3000' } triggers { githubPush() // webhook from GitHub } stages { stage('Checkout') { steps { checkout scm // clone repo } } stage('Install') { steps { sh 'npm ci' } } stage('Test') { steps { sh 'npm test' } post { always { junit '**/test-results/*.xml' } } } stage('Build Docker') { steps { sh 'docker build -t myapp:${BUILD_NUMBER} .' } } } post { success { slackSend '✅ Build passed' } failure { slackSend '❌ Build failed' } } }
# === Run Jenkins in Docker (fastest start) === docker run -d \ --name jenkins \ -p 8080:8080 \ -p 50000:50000 \ -v jenkins_home:/var/jenkins_home \ jenkins/jenkins:lts-jdk17 # Jenkins UI → http://localhost:8080 # === Get the initial admin password === docker exec jenkins \ cat /var/jenkins_home/secrets/initialAdminPassword # Copy this → paste in browser Unlock page # === Stop / Start === docker stop jenkins docker start jenkins # Data persists in 'jenkins_home' volume
# Requires Java 17+ sudo apt update sudo apt install -y fontconfig openjdk-17-jre # Add Jenkins repo key + source sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \ https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \ "https://pkg.jenkins.io/debian-stable binary/" \ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null sudo apt update && sudo apt install -y jenkins # Start + enable sudo systemctl enable --now jenkins sudo systemctl status jenkins # Get unlock password sudo cat /var/lib/jenkins/secrets/initialAdminPassword # UI → http://SERVER_IP:8080
# Option A: Windows Installer (.msi) # 1. Download from jenkins.io → LTS .msi # 2. Run installer → installs as Windows Service # 3. Opens browser at http://localhost:8080 # Password: C:\ProgramData\Jenkins\secrets\initialAdminPassword # Option B: WAR file (any OS with Java) java -jar jenkins.war --httpPort=8080 # Data stored in ~/.jenkins/ # Password: ~/.jenkins/secrets/initialAdminPassword # Option C: WSL2 on Windows (recommended) # Run the Ubuntu/Debian steps above inside WSL2 # Access at http://localhost:8080 from Windows browser
NodeJS Plugin — run npm commandsGitHub Integration Plugin — webhooksDocker Pipeline Plugin — Docker agentsBlue Ocean — modern pipeline UI (optional)
NodeJS-20 (exactly this — used in Jenkinsfile)github-credentialsmy-devops-appgithub-credentials*/mainJenkinsfile# Without webhook: Jenkins polls GitHub every N minutes # With webhook: GitHub pushes event to Jenkins instantly # On GitHub: # Repo → Settings → Webhooks → Add webhook # Payload URL: http://YOUR_JENKINS_IP:8080/github-webhook/ # Content type: application/json # Events: Just the push event # Active: ✅ # For local Jenkins (not internet-accessible), # use ngrok to expose it: ngrok http 8080 # ngrok gives: https://abc123.ngrok.io # Use: https://abc123.ngrok.io/github-webhook/ # In Jenkins pipeline job config: # Build Triggers → ✅ GitHub hook trigger for GITScm polling
| Install server | Jenkins: ✋ ~20 min | GHA: ✅ Zero |
| Configure tools | Jenkins: ✋ Plugins | GHA: ✅ In YAML |
| Webhook | Jenkins: ✋ Manual | GHA: ✅ Automatic |
| Credentials | Jenkins: ✋ UI setup | GHA: ✅ GitHub Secrets |
| First pipeline | Jenkins: ~45 min total | GHA: ~5 min total |
docker stop jenkins → laptop close ആക്കൂ. docker start jenkins → everything restored. Volume-ൽ data persist ആകുന്നു.
# ── Workflow name ────────────────────── name: CI Pipeline # ── Triggers ─────────────────────────── on: push: branches: [ main, 'feat/**' ] pull_request: branches: [ main ] # ── Jobs ─────────────────────────────── jobs: build-and-test: # Job name runs-on: ubuntu-latest # Runner (agent) strategy: matrix: node-version: [ 18, 20 ] # Run on both! steps: # Step 1: Clone repo - uses: actions/checkout@v4 # Step 2: Setup Node.js - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: \${{ matrix.node-version }} cache: 'npm' # Step 3: Install dependencies - name: Install dependencies run: npm ci # Step 4: Lint - name: Lint run: npm run lint # Step 5: Run tests - name: Run tests run: npm test # Step 6: Upload coverage - name: Upload coverage uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/
run: (shell) or uses: (marketplace action)uses: actions/checkout@v4 — someone wrote this so you don't have to. 20,000+ community actions: setup-node, setup-python, docker/build-push-action, aws-actions, azure/login. Always pin to a version tag.
pipeline { } → jobs:agent { } → runs-on:stage('x') { } → Job namesh 'cmd' → run: cmdenvironment { } → env:
name: CI Pipeline on: # ← TRIGGER push: branches: [ main ] jobs: # ← PIPELINE build: # ← JOB (= stage) runs-on: ubuntu-latest # ← AGENT env: # ← ENV VARS NODE_ENV: test steps: # ← STEPS - uses: actions/checkout@v4 - name: Install # ← STEP run: npm ci # ← SHELL CMD - name: Test run: npm test - name: Build run: docker build -t app . notify: # ← 2nd JOB needs: [ build ] # ← dependency if: failure() runs-on: ubuntu-latest steps: - run: echo "Build failed!"
// TRIGGER (configured in job, or: // triggers { githubPush() }) pipeline { // ← PIPELINE agent any // ← AGENT environment { // ← ENV VARS NODE_ENV = 'test' } stages { // ← STAGES stage('Checkout') {// ← STAGE steps { // ← STEPS checkout scm } } stage('Install') { steps { sh 'npm ci' // ← SHELL CMD } } stage('Test') { steps { sh 'npm test' } } stage('Build') { steps { sh 'docker build -t app .' } } } post { // ← POST (notify) failure { echo 'Build failed!' } } }
| GitHub Actions | → | Jenkinsfile |
| jobs: | → | pipeline { stages { } } |
| job name | → | stage('name') |
| runs-on: | → | agent { } |
| run: cmd | → | sh 'cmd' |
| if: failure() | → | post { failure { } } |
| needs: | → | stage ordering |
Push a GitHub Actions workflow → watch it run in the Actions tab → your first real CI pipeline in production
mkdir -p .github/workflows in your my-devops-app repo. This is where all GitHub Actions YAML files live..github/workflows/ci.yml — triggers on push to main and feat/** branches. Runs install, lint, test."test": "echo 'Tests passed' && exit 0" to package.json scripts if no test framework yet.git switch -c feat/add-ci && git add . && git commit -m "ci: add github actions pipeline" && git push. Watch GitHub — the pipeline fires immediately.Jenkinsfile in the repo root with the same stages. Note what you'd additionally need: a Jenkins server, credentials, webhook setup.name: CI on: push: branches: [ main, 'feat/**' ] pull_request: branches: [ main ] jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js 20 uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Lint run: npm run lint || echo "No lint script" - name: Test run: npm test - name: Print success run: echo "✅ CI passed on $(date)"
// Equivalent pipeline in Jenkins Declarative syntax pipeline { agent any tools { nodejs 'NodeJS-20' } // plugin needed triggers { githubPush() } stages { stage('Checkout') { steps { checkout scm } } stage('Install') { steps { sh 'npm ci' } } stage('Lint') { steps { sh 'npm run lint || echo "No lint"' } } stage('Test') { steps { sh 'npm test' } } stage('Report') { steps { echo "✅ CI passed" } } } post { always { echo 'Pipeline complete' } success { echo 'All green!' } failure { echo 'Build failed — check logs' } } }
"scripts": {
"test": "echo 'Tests passed' && exit 0",
"lint": "eslint . || true"
}ci: add github actions pipeline
3 questions · 5 minutes · CI/CD definitions, pipeline stages, and tool choice
notes/day11-cicd.md with tool comparison ✓| Term | Definition | Example |
|---|---|---|
| Pipeline | Automated sequence of stages triggered by a git event | .github/workflows/ci.yml, Jenkinsfile |
| Job | A unit of work that runs on one runner/agent (GitHub Actions term) | build, test, deploy-staging |
| Stage | A named phase in a pipeline (Jenkins term, equivalent to job) | stage('Test') { } |
| Step | Individual command or action within a job/stage | run: npm test, sh 'npm test' |
| Runner / Agent | Machine that executes pipeline steps | ubuntu-latest, Jenkins agent |
| Artifact | File produced by a pipeline stage (Docker image, JAR, ZIP) | myapp:abc123 pushed to GHCR |
| Trigger | Event that starts the pipeline | push, pull_request, schedule, manual |
| Secret | Encrypted value (API key, password) injected into pipeline | secrets.DOCKER_PASSWORD |
| Matrix | Run same job with multiple configurations in parallel | Node 18 + 20 simultaneously |
| Cache | Persisted files between runs to speed up builds | node_modules/ cached after npm install |
| SAST | Static Application Security Testing — code analysis without running | ESLint security rules, Semgrep |
| Gate | Approval step that pauses pipeline until human confirms | Prod deployment approval in CD |
# Push to specific branches on: push: branches: [ main, develop ] paths-ignore: [ 'docs/**', '*.md' ] # PRs targeting main pull_request: branches: [ main ] # Scheduled: every weekday at 8am UTC schedule: - cron: '0 8 * * 1-5' # Manual trigger with input workflow_dispatch: inputs: environment: description: 'Deploy target' required: true default: 'staging' # On new git tag (version release) push: tags: [ 'v*.*.*' ] # When another workflow completes workflow_run: workflows: [ "CI" ] types: [ completed ]
# Skip CI with commit message jobs: ci: if: "!contains(github.event.head_commit.message, '[skip ci]')" # Job that needs another to complete first deploy: needs: [ ci ] if: github.ref == 'refs/heads/main' # Secrets usage env: DOCKER_TOKEN: \${{ secrets.DOCKER_TOKEN }} BRANCH: \${{ github.ref_name }} # Upload build artifact - uses: actions/upload-artifact@v4 with: name: dist path: dist/ retention-days: 7 # Download in another job - uses: actions/download-artifact@v4 with: name: dist
paths-ignore: ['docs/**', '*.md'] — skip the workflow when only documentation changes. Saves GitHub Actions free minutes significantly.
| Dimension | Jenkins | GitHub Actions |
|---|---|---|
| Hosting | Self-hosted — you manage the server | GitHub-managed runners (cloud) |
| Pipeline config | Groovy-based Jenkinsfile | YAML in .github/workflows/ |
| Setup time | Hours (install Jenkins, plugins, agents) | Minutes (just push the YAML) |
| Plugins/Marketplace | 1,800+ plugins | 20,000+ marketplace actions |
| Cost | Free software, but server + ops cost | Free for public; 2,000 min/month free for private |
| SCM integration | Webhooks, polling | Native GitHub events (tight integration) |
| Parallel jobs | Parallel stages + multiple agents | Jobs in parallel natively, matrix builds |
| Secrets | Jenkins Credentials Store | GitHub Secrets (repo + org + environment) |
| Docker support | Docker agent, docker plugin | Native — all runners have Docker |
| Audit trail | Jenkins build history | GitHub Actions runs history |
| On-prem / air-gapped | ✅ Yes — self-hosted | ⚠️ Self-hosted runners possible |
| Learning curve | Steep (Groovy, plugins, architecture) | Moderate (YAML, action marketplace) |
| Best for | Enterprise, on-prem, complex pipelines | SaaS, startups, OSS, cloud-native |
Different things happen depending on which branch was pushed:
feat/* → CI only (build + test)main → CI + deploy to stagingv*.*.* tag → CI + deploy to productionif: github.ref == 'refs/heads/main'
Test against multiple environments simultaneously:
strategy:
matrix:
os: [ubuntu, windows, macos]
node: [18, 20, 22]
# Runs 9 jobs in parallel
Run multiple independent jobs in parallel, then collect results:
needs: [unit-tests, lint, security]Code flows through environments with increasing confidence:
dev → staging → prod
node_modules/ — saves 30–60 sec on install per runcache: 'npm' in setup-node@v4 handles this automatically
| Day | Topic | Key Skill | Lab Output |
|---|---|---|---|
| Day 11 ✅ | CI/CD Concepts & Tool Landscape | CI vs CD, pipeline anatomy, Jenkins vs GHA | First GitHub Actions pipeline live |
| Day 12 | GHA + Jenkins Deep Dive | Secrets, matrix, parallel jobs, Docker agent | Multi-job pipeline with test matrix |
| Day 13 | Testing in CI | Jest, coverage thresholds, test reports | Unit tests with 80% coverage gate |
| Day 14 | Artifacts & Package Management | Docker build + push to registry in pipeline | Docker image pushed to GHCR on merge |
| Day 15 | Continuous Deployment | Environment promotion, approval gates | Full CD: staging auto + prod with approval |
name: Workflow Name # shown in Actions tab on: push / pull_request # or full object (see S18) env: # workflow-level env vars NODE_ENV: production defaults: run: working-directory: ./src # default dir for run: jobs: job-name: # alphanumeric + dash runs-on: ubuntu-latest timeout-minutes: 15 # kill if takes too long needs: [ other-job ] # dependencies if: condition # conditional execution environment: production # GitHub Environment concurrency: # prevent simultaneous runs group: prod-deploy cancel-in-progress: true env: # job-level env vars TOKEN: \${{ secrets.TOKEN }} steps: - name: Step description uses: owner/action@v1 # marketplace with: param: value - name: Shell step run: echo "hello" continue-on-error: true # don't fail on error
\$\{{ github.sha }} — commit SHA\$\{{ github.ref_name }} — branch/tag name\$\{{ github.actor }} — who triggered\$\{{ github.event_name }} — push/pr/etc\$\{{ secrets.NAME }} — encrypted secret\$\{{ needs.job.result }} — upstream job result\$\{{ env.VAR }} — environment variableif: success() — run if all previous passedif: failure() — run only on failureif: always() — run regardlessif: cancelled() — run if cancelled
\$\{{ secrets.NAME }}. Never paste tokens directly in the YAML file. They get committed to git history and become public. Store in GitHub → Settings → Secrets → Actions.