DevOps #ci-cd #github-actions

Setting Up CI/CD for Modern Web Apps

Sun Podder
Sun Podder
10 min read
Setting Up CI/CD for Modern Web Apps

Manual deployments are a thing of the past. In this guide, I’ll walk you through setting up a production-grade CI/CD pipeline that will have your code tested and deployed in minutes.

What We’re Building

By the end of this guide, you’ll have:

  • Automated testing on every pull request
  • Preview deployments for feature branches
  • Automatic production deployments on merge to main
  • Slack notifications for deployment status

Prerequisites

  • A GitHub repository
  • A hosting platform (we’ll use Vercel, but the concepts apply anywhere)
  • Basic familiarity with YAML

Step 1: The Testing Pipeline

First, let’s ensure every PR is tested before it can be merged:

# .github/workflows/test.yml
name: Test

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linter
        run: npm run lint
      
      - name: Run type check
        run: npm run type-check
      
      - name: Run tests
        run: npm test -- --coverage
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Step 2: Preview Deployments

Preview deployments let reviewers see changes before merging:

# .github/workflows/preview.yml
name: Preview Deployment

on:
  pull_request:
    branches: [main]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          scope: ${{ secrets.VERCEL_ORG_ID }}
      
      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.preview-url }}'
            })

Step 3: Production Deployment

When code hits main, it goes straight to production:

# .github/workflows/deploy.yml
name: Production Deployment

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
        env:
          NODE_ENV: production
      
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
      
      - name: Notify Slack
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
        if: always()

Best Practices

1. Use Caching Aggressively

Caching dependencies can cut your pipeline time in half:

- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

2. Run Jobs in Parallel

Independent jobs should run simultaneously:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

3. Use Environment Protection

For production deployments, add manual approval:

deploy:
  environment:
    name: production
    url: https://yourapp.com
  runs-on: ubuntu-latest

Common Pitfalls

  1. Not caching - Slow pipelines lead to ignored pipelines
  2. Too many jobs - Parallelization has diminishing returns
  3. Secrets in code - Always use GitHub Secrets
  4. No rollback plan - Always know how to revert

Monitoring Your Pipeline

Track these metrics:

MetricGoodNeeds Work
Average pipeline time< 5 min> 10 min
Success rate> 95%< 90%
Time to rollback< 5 min> 15 min

Conclusion

A well-designed CI/CD pipeline is like having a tireless QA engineer and deployment specialist working 24/7. Invest the time to set it up right, and it will pay dividends for years.

Want me to review your current pipeline? Get in touch.

Share this article:
#ci-cd #github-actions #automation #deployment