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.
nfyio Team
Talya Smart & Technoplatz JV
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
kubectlaccess (for deploy stage) - GitHub repository secrets configured
Required Secrets
| Secret | Description |
|---|---|
DOCKER_USERNAME | Docker Hub or GHCR username |
DOCKER_PASSWORD | Docker Hub token or GHCR PAT |
KUBE_CONFIG | Base64-encoded kubeconfig |
OPENAI_API_KEY | For integration tests with embeddings |
SLACK_WEBHOOK | Optional — 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.
Written by
nfyio Team
Talya Smart & Technoplatz JV
Building the future of web design at Anti-Gravity. Passionate about creating beautiful, accessible experiences.