karthik.dev
Back to blogDevOps

CI/CD with GitHub Actions: From Zero to Production-Ready Pipeline

2026-03-15 · 7 min read

A good CI/CD pipeline is the difference between "I think this works" and "I know this works." After setting up GitHub Actions pipelines for NEXORA, CareerByte AI, and CodeMyFYP Academy, here's the pattern I've converged on.

The pipeline philosophy

Every commit to main should either be confidently deployable or fail loudly. That means automating:

  1. Code quality (lint + type check)
  2. Tests (unit + integration)
  3. Build verification
  4. Docker image build
  5. Deployment

Each step is a gate. Failure at any gate blocks the pipeline and notifies the developer before bad code reaches production.

The base workflow

name: CI/CD Pipeline

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

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      
      - run: npm ci
      
      - name: Type check
        run: npx tsc --noEmit
      
      - name: Lint
        run: npm run lint
      
      - name: Tests
        run: npm test -- --coverage
        env:
          NODE_ENV: test

Caching dependencies correctly

actions/setup-node with cache: npm caches the npm store based on package-lock.json. On a cache hit, npm ci takes seconds instead of minutes. For a 200-package project, this saves 2–3 minutes per run.

The key insight: use npm ci (not npm install) in CI. npm ci is deterministic, respects lockfile exactly, and fails if the lockfile is out of sync — catching the "works on my machine" class of bugs.

Running integration tests against a real database

For tests that need PostgreSQL, use the services block to spin up a container:

jobs:
  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run db:migrate
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
      - run: npm run test:integration
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb

Docker build and push

  docker:
    needs: [quality, integration]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The cache-from/cache-to: type=gha uses GitHub's Actions cache for Docker layer caching. First build: slow. Every subsequent build that doesn't change layers: fast.

Secrets management

Never hardcode secrets in workflow files. Use GitHub repository secrets (Settings → Secrets → Actions) and reference them as ${{ secrets.SECRET_NAME }}.

For environment-specific config, use GitHub Environments with protection rules — require a manual approval before deploying to production.

What I'd add to every pipeline

  1. Dependency audit: npm audit --audit-level=high catches known vulnerabilities in your dependency tree
  2. Bundle size check: For Next.js apps, next build output shows bundle sizes — add a step that fails if they exceed a threshold
  3. Lighthouse CI: For portfolio or marketing sites, automated performance checks on every PR

The last one is particularly satisfying — it's how I catch accidental performance regressions before they reach users.