Back to Blog

CI/CD for nfyio with GitHub Actions

Automate testing, building, and deploying nfyio using GitHub Actions — from lint and integration tests to rolling updates on Kubernetes.

n

nfyio Team

Talya Smart & Technoplatz JV

CI/CD for nfyio with GitHub Actions

Automating your nfyio deployment pipeline eliminates manual errors and gives your team confidence that every change is tested before it hits production. This guide builds a complete CI/CD pipeline using GitHub Actions.

Pipeline Overview

Push to main ──► Lint & Type Check ──► Unit Tests ──► Integration Tests ──► Build Images ──► Deploy

                                                    nfyio + PostgreSQL
                                                    + Redis (service containers)

Prerequisites

  • nfyio source code in a GitHub repository
  • Docker Hub or GitHub Container Registry (GHCR) credentials
  • Kubernetes cluster with kubectl access (for deploy stage)
  • GitHub repository secrets configured

Required Secrets

SecretDescription
DOCKER_USERNAMEDocker Hub or GHCR username
DOCKER_PASSWORDDocker Hub token or GHCR PAT
KUBE_CONFIGBase64-encoded kubeconfig
OPENAI_API_KEYFor integration tests with embeddings
SLACK_WEBHOOKOptional — deployment notifications

Workflow: CI Pipeline

Create .github/workflows/ci.yml:

name: CI

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

env:
  NODE_VERSION: '20'
  POSTGRES_USER: nfyio_test
  POSTGRES_PASSWORD: test_password
  POSTGRES_DB: nfyio_test

jobs:
  lint:
    name: Lint & Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  test-unit:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm test -- --coverage

      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  test-integration:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: lint
    services:
      postgres:
        image: pgvector/pgvector:pg16
        env:
          POSTGRES_USER: ${{ env.POSTGRES_USER }}
          POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
          POSTGRES_DB: ${{ env.POSTGRES_DB }}
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci

      - name: Run migrations
        run: npm run db:migrate
        env:
          DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }}

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }}
          REDIS_URL: redis://localhost:6379
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          JWT_SECRET: test-jwt-secret-for-ci

  build:
    name: Build & Push Image
    runs-on: ubuntu-latest
    needs: [test-unit, test-integration]
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write
    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 }}/gateway:${{ github.sha }}
            ghcr.io/${{ github.repository }}/gateway:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Workflow: Deployment

Create .github/workflows/deploy.yml:

name: Deploy

on:
  workflow_run:
    workflows: ["CI"]
    types: [completed]
    branches: [main]

jobs:
  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config

      - name: Update image tag
        run: |
          kubectl -n nfyio set image deployment/nfyio-gateway \
            gateway=ghcr.io/${{ github.repository }}/gateway:${{ github.sha }}

      - name: Wait for rollout
        run: |
          kubectl -n nfyio rollout status deployment/nfyio-gateway --timeout=300s

      - name: Verify health
        run: |
          sleep 10
          HEALTH=$(kubectl -n nfyio exec deploy/nfyio-gateway -- \
            curl -sf http://localhost:3000/health)
          echo "$HEALTH" | jq
          echo "$HEALTH" | jq -e '.status == "healthy"'

      - name: Notify Slack
        if: always()
        run: |
          STATUS="${{ job.status }}"
          COLOR=$([[ "$STATUS" == "success" ]] && echo "#d4ff00" || echo "#ff0000")
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            -d "{
              \"attachments\": [{
                \"color\": \"$COLOR\",
                \"title\": \"nfyio Deploy: $STATUS\",
                \"text\": \"Commit: ${{ github.sha }}\nActor: ${{ github.actor }}\"
              }]
            }"

Database Migration Workflow

Create .github/workflows/migrate.yml:

name: Database Migration

on:
  push:
    branches: [main]
    paths:
      - 'supabase/migrations/**'
      - 'migrations/**'

jobs:
  migrate:
    name: Run Migrations
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Run migrations
        run: |
          PGPASSWORD=${{ secrets.DB_PASSWORD }} psql \
            -h ${{ secrets.DB_HOST }} \
            -U ${{ secrets.DB_USER }} \
            -d ${{ secrets.DB_NAME }} \
            -f migrations/latest.sql

      - name: Verify schema
        run: |
          PGPASSWORD=${{ secrets.DB_PASSWORD }} psql \
            -h ${{ secrets.DB_HOST }} \
            -U ${{ secrets.DB_USER }} \
            -d ${{ secrets.DB_NAME }} \
            -c "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;"

Branch Protection Rules

Configure your main branch with these protection rules:

# Via GitHub CLI
gh api repos/:owner/:repo/branches/main/protection -X PUT \
  -f 'required_status_checks[strict]=true' \
  -f 'required_status_checks[contexts][]=Lint & Type Check' \
  -f 'required_status_checks[contexts][]=Unit Tests' \
  -f 'required_status_checks[contexts][]=Integration Tests' \
  -f 'required_pull_request_reviews[required_approving_review_count]=1'

Rollback with GitHub Actions

Create a manual rollback trigger:

name: Rollback

on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: 'Image tag to rollback to (e.g., abc1234)'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config

      - name: Rollback
        run: |
          kubectl -n nfyio set image deployment/nfyio-gateway \
            gateway=ghcr.io/${{ github.repository }}/gateway:${{ github.event.inputs.image_tag }}
          kubectl -n nfyio rollout status deployment/nfyio-gateway --timeout=300s

      - name: Verify
        run: |
          kubectl -n nfyio exec deploy/nfyio-gateway -- \
            curl -sf http://localhost:3000/health | jq

Key Takeaways

  • GitHub Actions service containers spin up PostgreSQL (with pgvector) and Redis alongside integration tests — no external test infrastructure needed
  • The CI pipeline gates deployment on lint, unit tests, and integration tests passing
  • Docker images are built with BuildKit layer caching for fast builds
  • Kubernetes rolling updates ensure zero downtime during deployments
  • A health check after deploy catches broken releases before they affect users
  • Manual rollback workflow provides a safety net for critical issues

For deployment setup, see the Kubernetes guide. For monitoring your CI/CD pipeline, see the Prometheus & Grafana guide.

n

Written by

nfyio Team

Talya Smart & Technoplatz JV

Building the future of web design at Anti-Gravity. Passionate about creating beautiful, accessible experiences.