McTaba Labs logo
By Bonaventure Ogeto|

What Is CI/CD? Automated Testing and Deployment Explained

CI (Continuous Integration) automatically tests your code every time you push a change, catching bugs before they reach production. CD (Continuous Deployment) automatically deploys your code to production after those tests pass. Together, they eliminate the manual steps between writing code and shipping it to users. GitHub Actions is the most common CI/CD tool for open-source and small team projects.

What happens without CI/CD

Without CI/CD, the deployment process looks like this:

  1. Developer finishes a feature and pushes code.
  2. Someone (maybe the same developer) manually runs the tests on their machine.
  3. Tests pass locally. Developer opens a pull request.
  4. A teammate reviews the code, but does not run the tests because "they probably pass."
  5. The PR is merged.
  6. Someone SSH-es into the production server, pulls the code, runs the build, and restarts the app.
  7. The app crashes because the developer had Node 20 locally but the server runs Node 18.
  8. It is 11 PM and everyone is tired.

Every step that depends on a human remembering to do it is a step that will eventually be skipped. CI/CD replaces these manual steps with automated ones that run every time, without fail.

Continuous Integration: test every push automatically

CI means running your tests and checks automatically every time someone pushes code. A typical CI pipeline:

  1. Developer pushes code to a branch or opens a pull request.
  2. CI server (GitHub Actions, for example) spins up a fresh virtual machine.
  3. It installs dependencies (npm ci).
  4. It runs linting (npm run lint) to catch style issues.
  5. It runs type checking (npx tsc --noEmit) to catch type errors.
  6. It runs tests (npm test) to verify behavior.
  7. It runs the build (npm run build) to make sure the project compiles.
  8. If any step fails, the pull request is marked with a red X. If everything passes, green checkmark.

The key benefit: you catch problems before they reach the main branch. A developer cannot accidentally merge code that breaks the build because CI blocks the merge.

Continuous Deployment: ship automatically after tests pass

CD extends CI by automatically deploying to production when tests pass on the main branch.

The flow:

  1. Developer opens a pull request. CI runs tests. Tests pass.
  2. A teammate reviews the code and approves the PR.
  3. The PR is merged into the main branch.
  4. CI runs tests again on the main branch. Tests pass.
  5. CD automatically deploys the new code to production.
  6. Users see the new feature within minutes of the merge.

If you use Vercel with GitHub, you already have CD. Every push to main triggers a production deployment automatically. That is Continuous Deployment in action.

Some teams prefer Continuous Delivery (note: Delivery, not Deployment). The difference: Continuous Delivery automates everything up to a manual approval step. A human clicks "Deploy" after reviewing the staged changes. This is common in regulated industries or when deploying payment systems where you want an extra pair of eyes.

A real GitHub Actions CI workflow

GitHub Actions is free for public repositories and has generous free tier minutes for private ones. Here is a minimal but complete CI workflow for a Next.js project:

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npx tsc --noEmit

      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}

Save this file at .github/workflows/ci.yml in your repository. GitHub detects it automatically and runs the workflow on every push and pull request.

What each step does:

  • actions/checkout@v4: Downloads your code.
  • actions/setup-node@v4: Installs Node.js 20 and caches npm packages for faster runs.
  • npm ci: Clean install of dependencies from package-lock.json.
  • npm run lint: Catches code style and potential bugs.
  • npx tsc --noEmit: Catches TypeScript errors without generating files.
  • npm run build: Verifies the project compiles for production.

Handling secrets in CI

Your CI environment needs environment variables to build and test, but you cannot put secrets in your workflow file (it is committed to Git). GitHub provides encrypted secrets for this.

To add secrets:

  1. Go to your GitHub repository.
  2. Click Settings, then Secrets and variables, then Actions.
  3. Click "New repository secret" and add each variable.

In your workflow file, access them with the ${{ secrets.NAME }} syntax:

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  DARAJA_CONSUMER_KEY: ${{ secrets.DARAJA_CONSUMER_KEY }}

GitHub masks these values in logs. If a secret accidentally appears in log output, GitHub replaces it with ***.

Some teams use a separate set of credentials for CI: a test database, sandbox API keys, and a dedicated service account. This is good practice because CI runs can access different resources than production.

Practical tips for getting started

Start simple. A CI pipeline that only runs npm run build catches most problems. You can add linting, type checking, and tests later.

Make the CI fast. Developers avoid opening PRs if CI takes 20 minutes. Cache dependencies (the setup-node action has built-in caching), skip unnecessary steps, and run jobs in parallel when possible.

Block merges on CI. In your GitHub repository settings, enable branch protection rules for the main branch. Require that CI passes before a PR can be merged. This is the single most valuable CI setting.

# Example: parallel jobs for faster CI
jobs:
  lint:
    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

  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: npx tsc --noEmit

  build:
    runs-on: ubuntu-latest
    needs: [lint, typecheck]  # Only build if lint and typecheck pass
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npm run build

Write tests. CI is most powerful when you have automated tests. Even a handful of tests for critical flows (user signup, payment processing) catch regressions that a build step alone would miss.

Frequently Asked Questions

Do I need CI/CD for personal projects?
For learning, no. For anything you deploy and share with users, yes. Even a minimal CI pipeline that runs the build catches errors before they go live. Once you set it up once, it runs forever with no maintenance.
Is GitHub Actions the only CI/CD option?
No. Other popular options include CircleCI, Travis CI, GitLab CI/CD, Jenkins, and Buildkite. GitHub Actions is the most convenient if your code is on GitHub because it requires no external setup. The core concepts (triggers, jobs, steps, secrets) are the same across all tools.
How much does GitHub Actions cost?
It is free for public repositories. For private repositories, the free tier includes 2,000 minutes per month, which is enough for most small projects. A typical CI run takes 2 to 5 minutes, so 2,000 minutes covers hundreds of runs per month.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.

See Programs