Back to Blog
Tutorials 10 min read February 20, 2026

GitHub Actions CI/CD: From Zero to Production Pipeline in 30 Minutes

A working CI/CD pipeline catches bugs before production and deploys automatically. Here is how to build one with GitHub Actions in under 30 minutes.

DevForge Team

DevForge Team

AI Development Educators

Developer setting up an automated CI/CD pipeline with GitHub Actions

Every time you push code manually to production, you're making a bet that nothing is broken. A CI/CD pipeline turns that bet into a guarantee: tests run automatically, builds are verified, and deployments happen only when everything passes.

GitHub Actions is the fastest way to build this pipeline if your code is already on GitHub. It's free for public repositories and includes 2,000 minutes per month on the free plan for private repositories.

This guide builds a complete pipeline that runs tests on every pull request and deploys to production on merge to main.

How GitHub Actions Works

A GitHub Actions workflow is a YAML file in .github/workflows/ that defines:

  • Triggers: When the workflow runs (push, pull request, schedule, manual)
  • Jobs: Groups of steps that run on a runner (GitHub-managed virtual machine)
  • Steps: Individual commands or actions that run in sequence within a job

Workflows run on GitHub's servers — you don't need to maintain any infrastructure.

Your First Workflow: Running Tests

Create the file .github/workflows/ci.yml:

yaml
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: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

This workflow:

  1. Triggers on pushes to main and on any pull request targeting main
  2. Checks out your code
  3. Sets up Node.js 20 with npm caching (speeds up subsequent runs)
  4. Installs dependencies with npm ci
  5. Runs your linter
  6. Runs your test suite

Push this file to your repository. The workflow runs immediately, and you'll see the results in the Actions tab on GitHub.

Adding a Build Step

If your application requires a build step (TypeScript compilation, Next.js build, Vite bundle), add it:

yaml
      - name: Build
        run: npm run build

Failing builds fail the workflow and block the PR from being merged — exactly what you want.

Environment Variables and Secrets

Tests that interact with databases or external APIs need credentials. Never put secrets in your workflow file. Use GitHub Secrets instead.

Add secrets in GitHub: Settings → Secrets and variables → Actions → New repository secret.

Reference them in the workflow:

yaml
      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}

Secrets are masked in logs — if a secret value appears in output, GitHub replaces it with ***.

Running Tests Against a Real Database

For integration tests that need a database, use GitHub Actions service containers:

yaml
jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpassword
          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

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

      - name: Install dependencies
        run: npm ci

      - name: Run migrations
        run: npm run db:migrate
        env:
          DATABASE_URL: postgres://postgres:testpassword@localhost:5432/testdb

      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: postgres://postgres:testpassword@localhost:5432/testdb

The service container starts before the job steps and is available at localhost on the specified port.

Adding Deployment

Now add a deployment job that runs only after tests pass, and only on merges to main:

yaml
name: CI/CD

on:
  push:
    branches: [main]
  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
      - run: npm run build

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          # Your deployment command here
          # Examples below

The needs: test line means the deploy job only starts after the test job succeeds. The if condition restricts deployment to push events on main (not pull requests).

Deployment Examples

Deploy to Vercel:

yaml
      - 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'

Deploy to Netlify:

yaml
      - name: Deploy to Netlify
        uses: nwtgck/actions-netlify@v3
        with:
          publish-dir: './dist'
          production-branch: main
          github-token: ${{ secrets.GITHUB_TOKEN }}
          deploy-message: "Deploy from GitHub Actions"
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Deploy via SSH to a VPS:

yaml
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app
            git pull origin main
            npm ci --only=production
            pm2 restart app

Matrix Testing

Test against multiple Node.js versions to ensure compatibility:

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

This runs three parallel test jobs, one for each Node.js version.

Caching Dependencies

npm install is slow. Cache node_modules between workflow runs:

yaml
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

The cache: 'npm' option automatically caches the npm cache directory and restores it on subsequent runs. Cache hits reduce install time from 30-60 seconds to under 5 seconds.

Branch Protection Rules

Once your CI workflow is working, enforce it. In GitHub: Settings → Branches → Add rule → Require status checks to pass before merging.

Add your test job as a required status check. Now pull requests cannot be merged until tests pass. This is the policy that makes CI valuable — the pipeline only prevents bad code from shipping if it's actually enforced.

What a Complete Pipeline Looks Like

A production-ready pipeline for a Node.js application:

  1. On every PR: Run linter, type checker, unit tests, integration tests, build verification
  2. On merge to main: Run all checks again, then deploy to staging
  3. On tag or manual trigger: Deploy to production after staging verification

Start with step one. A pipeline that runs tests on PRs catches most bugs before they reach main. The rest can be added incrementally as your team's confidence in the system grows.

The 30 minutes you spend setting up GitHub Actions pays back in the first bug it catches before deployment.

#GitHub Actions#CI/CD#DevOps#Automation#Deployment#Testing