GitHub Actions is a powerful CI/CD platform that starts with a generous free tier and is unlimited for public repositories. Run tests on every push, build containers, deploy to production — all in a single YAML file. This guide walks through a complete CI/CD pipeline for a Node.js project from scratch.

Basic Workflow Structure

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

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

jobs:
  test:
    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
      - run: npm test

Matrix Strategy

Run the same job in parallel across multiple Node versions, operating systems or environments.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Docker Image Build + Registry Push

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

Secret Management

Define secrets under Settings → Secrets and variables → Actions. They are automatically masked in logs. For environment-specific secrets, use environments (manual approval can be added for promotion to production).

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: echo ${{ secrets.DATABASE_URL }}
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app
            git pull
            npm ci --production
            pm2 reload ecosystem.config.js

Service Containers (Testing with a DB)

jobs:
  integration-test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5
      redis:
        image: redis:7
        ports: ['6379:6379']
    env:
      DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
      REDIS_URL: redis://localhost:6379
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run migrate
      - run: npm run test:integration

Cache Optimization

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

Reusable Workflows

# .github/workflows/reusable-deploy.yml
name: Deploy
on:
  workflow_call:
    inputs:
      env: { required: true, type: string }
    secrets:
      SSH_KEY: { required: true }

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo Deploying to ${{ inputs.env }}

# Call it from the main workflow
jobs:
  prod:
    uses: ./.github/workflows/reusable-deploy.yml
    with: { env: production }
    secrets: inherit

Workflow Dispatch (Manual Trigger)

on:
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options: [staging, production]
      version:
        type: string
        default: latest

Self-Hosted Runners

The free GitHub-hosted runners have limits (2,000 minutes/month for public repos, more for paid plans). For heavier jobs, run your own self-hosted runner — Settings → Actions → Runners → New self-hosted runner.

Common Pitfalls

  • GITHUB_TOKEN permissions are restricted by default — add permissions: to the workflow when needed
  • Concurrent runs can clash when deploying to the same environment — control with concurrency:
  • actions/checkout@v4 fetches only one commit by default — use fetch-depth: 0 for full history
  • Use npm ci instead of npm install for dependency installs — it honors the lockfile exactly

Conclusion

With GitHub Actions you get production-grade CI/CD from 50 lines of YAML. You can stand up a full test + build + deploy + notification chain in one afternoon. This is the golden age of being able to deploy on every push with confidence.

CI/CD pipeline setup

Reach out for GitHub Actions, GitLab CI or Jenkins pipeline design and implementation. Contact us

WhatsApp