CI/CD

intermediate ci-cd github-actions automation deployment pipeline

Before CI/CD, deployments were a manual nightmare. Someone runs the tests locally (maybe), builds the app, copies files to the server, restarts things, and prays it works. With CI/CD, all of that happens automatically every time we push code. No manual steps, no human error, no prayers.

CI vs CD

CI (Continuous Integration) — Every time we push code or open a PR, the system automatically:

  • Pulls the latest code
  • Installs dependencies
  • Runs the linter
  • Runs the tests
  • Builds the app

If any step fails, we know immediately. No more “it works on my machine.”

CD (Continuous Delivery / Continuous Deployment) — After CI passes, what happens next?

  • Continuous Delivery — The app is built and ready to deploy, but a human clicks the button. We have a release candidate sitting there, validated and packaged.
  • Continuous Deployment — No human in the loop. CI passes, deploy happens automatically. Merge to main = live in production.

Most teams start with Continuous Delivery and move to Continuous Deployment once they trust their test suite.

The Pipeline Flow

A typical CI/CD pipeline looks like this:

CI/CD Pipeline
Push
trigger
Lint
code style
Test
unit + integration
Build
compile / bundle
Deploy
staging / prod
CI ←——————————————→
CD →

The CI part catches bugs early. The CD part ensures consistent deployments. Together, they give us confidence that what’s in main is always shippable.

GitHub Actions Basics

GitHub Actions is the most popular CI/CD platform for GitHub repos. Workflows are YAML files that live in .github/workflows/. Let’s break down the key concepts:

  • Workflow — A YAML file that defines the automation. Triggered by events.
  • Trigger — What starts the workflow: push, pull_request, schedule, workflow_dispatch (manual).
  • Job — A group of steps that run on the same runner. Jobs run in parallel by default.
  • Step — An individual task: run a command, use an action, etc.
  • Action — A reusable piece of automation (like actions/checkout or actions/setup-node).
  • Runner — The machine that runs the job (GitHub provides ubuntu-latest, macos-latest, etc.).

A Practical GitHub Actions Workflow

Here’s a real-world workflow that lints and tests on PRs, then deploys on merge to main:

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

# When to run
on:
  push:
    branches: [main]             # deploy on merge to main
  pull_request:
    branches: [main]             # lint + test on PRs

jobs:
  # Job 1: Lint and Test (runs on every push and PR)
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4          # clone the repo
      - uses: actions/setup-node@v4        # install Node.js
        with:
          node-version: 20
          cache: "npm"                     # cache node_modules
      - run: npm ci                        # install deps
      - run: npm run lint                  # run linter
      - run: npm test                      # run tests

  # Job 2: Deploy (only on push to main, after CI passes)
  deploy:
    needs: ci                              # wait for CI to pass
    if: github.ref == 'refs/heads/main'    # only on main branch
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy to server
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}  # from GitHub Secrets
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          rsync -avz --delete dist/ user@server:/var/www/app/ -e "ssh -i key.pem"

A few things to notice:

  • needs: ci makes the deploy job wait for CI to pass
  • if: github.ref == 'refs/heads/main' ensures we only deploy from the main branch
  • Secrets are stored in GitHub Settings, never in the workflow file

Branch Strategy

CI/CD works best with a good branching strategy:

  1. main — Always deployable. Protected branch, no direct pushes.
  2. Feature branchesfeature/add-auth, fix/login-bug. Short-lived.
  3. Pull Requests — Feature branch → main. CI runs on every PR. Code review required.
  4. Merge — Once PR is approved and CI passes, merge to main → CD triggers deployment.
# Typical workflow
git checkout -b feature/add-auth    # create feature branch
# ... write code ...
git push -u origin feature/add-auth # push and open PR
# CI runs automatically on the PR
# Teammate reviews and approves
# Merge to main → deploy happens automatically

Build Caching

CI builds can be slow. Caching dependencies between runs saves a lot of time.

# GitHub Actions — cache npm dependencies
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: "npm"                   # built-in caching

# Or manual caching for anything
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
    restore-keys: ${{ runner.os }}-npm-

The hashFiles('package-lock.json') key means the cache is busted only when dependencies change. If package-lock.json hasn’t changed, npm install finishes in seconds.

Why CI/CD Matters

Without CI/CD:

  • “Did anyone run the tests before merging?” — Nobody knows
  • Deployments are scary, manual, and different every time
  • Bugs make it to production because nobody tested that edge case

With CI/CD:

  • Every PR is automatically tested — broken code can’t merge
  • Deployments are consistent and repeatable
  • The team ships faster because they trust the pipeline

The investment in setting up CI/CD pays for itself after the first week.

In simple language, CI/CD automates the entire path from code push to production deployment — CI catches bugs early by running tests on every change, and CD makes sure deployments are consistent and hands-free.