Setting Up CI/CD Pipelines with GitHub Actions: A Practical Walkthrough

I remember the first time I set up a CI/CD pipeline. It took me an entire weekend of reading docs, copying YAML files, and wondering why my builds kept failing. I’m writing this so you don’t have to go through that.

What Are We Building?

By the end of this article, you’ll have a GitHub Actions workflow that automatically runs your tests on every push, checks code quality, and deploys to production when you merge to main. We’ll use a Node.js project as our example, but the concepts apply to any language.

Your First Workflow File

Create .github/workflows/ci.yml in your repository:

name: CI Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm run lint

That’s it for a basic pipeline. Push this file and GitHub will automatically run your tests on Node 18 and 20 every time you push code or open a pull request.

Adding Deployment

Let’s add automatic deployment when code is merged to main:

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          npm ci --production
          npm run build
          # Your deployment command here

The needs: test line ensures deployment only happens after all tests pass. The if condition ensures it only deploys from the main branch.

Caching Dependencies

One of the biggest time saves is caching. Without it, every build downloads all your npm packages from scratch:

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

This typically cuts build times by 30-50%.

Managing Secrets

Never put API keys or tokens directly in your workflow files. Use GitHub’s encrypted secrets:

  1. Go to your repo Settings > Secrets and variables > Actions
  2. Click “New repository secret”
  3. Add your secret (e.g., DEPLOY_TOKEN)
  4. Reference it in workflows as ${{ secrets.DEPLOY_TOKEN }}

Common Gotchas

  • YAML indentation: Use spaces, never tabs. A single wrong indent breaks everything
  • Branch names: Make sure your workflow triggers match your actual branch names
  • Timeouts: Add timeout-minutes: 10 to jobs so hung processes don’t burn your free minutes
  • Concurrency: Use the concurrency key to cancel redundant builds when you push multiple times quickly

Start simple, get it working, then add complexity. A basic pipeline that runs your tests is infinitely better than a complex one that you never finish setting up.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top