[BACKEND] Initialize Go backend structure and dependencies (#23)
This commit is contained in:
355
.gitea/workflows/ci.yaml
Normal file
355
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,355 @@
|
||||
# .gitea/workflows/ci.yaml
|
||||
# ============================================
|
||||
# Chat Switchboard - CI/CD Pipeline
|
||||
# ============================================
|
||||
# Cluster deployments use SEPARATE FE + BE images.
|
||||
# Unified image is for Docker Hub only (Docker Desktop use).
|
||||
#
|
||||
# Deployment mapping:
|
||||
# PR → FE + BE :dev → wipe/migrate → dev.DOMAIN
|
||||
# Push to main → FE + BE :test → migrate → test.DOMAIN
|
||||
# Push to main → FE-only :lite → (no DB) → lite.DOMAIN
|
||||
# Tag v* → FE + BE :latest → migrate → www.DOMAIN
|
||||
# → Unified :latest → Docker Hub only (not deployed)
|
||||
#
|
||||
# Required Gitea Variables:
|
||||
# REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST
|
||||
#
|
||||
# Required Gitea Secrets:
|
||||
# POSTGRES_USER, POSTGRES_PASSWORD
|
||||
# POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD
|
||||
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional)
|
||||
#
|
||||
# Global Variables (Gitea org-level):
|
||||
# CERT_ISSUER_PROD
|
||||
# ============================================
|
||||
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ gitea.workflow }}-${{ gitea.event.pull_request.number || gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ vars.REGISTRY }}
|
||||
NAMESPACE: ${{ vars.NAMESPACE || 'gobha-ai' }}
|
||||
DOMAIN: ${{ vars.DOMAIN || 'gobha.ai' }}
|
||||
CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }}
|
||||
POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }}
|
||||
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
|
||||
FE_IMAGE: ${{ vars.REGISTRY }}/xcaliber/switchboard-fe
|
||||
BE_IMAGE: ${{ vars.REGISTRY }}/xcaliber/switchboard-be
|
||||
UNIFIED_IMAGE: ${{ vars.REGISTRY }}/xcaliber/switchboard
|
||||
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/chat-switchboard' }}
|
||||
|
||||
jobs:
|
||||
# ── Stage 1: Go Build & Test ─────────────────
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Download and tidy
|
||||
working-directory: server
|
||||
run: |
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
- name: Run tests
|
||||
working-directory: server
|
||||
run: go test -v -race ./...
|
||||
|
||||
- name: Build check
|
||||
working-directory: server
|
||||
run: CGO_ENABLED=0 go build -o /dev/null .
|
||||
|
||||
# ── Stage 2: Build, Database, Deploy ─────────
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add /tools to PATH
|
||||
run: echo "/tools" >> $GITHUB_PATH
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq gettext-base postgresql-client > /dev/null
|
||||
|
||||
# ── Determine environment ──────────────────
|
||||
- name: Determine environment
|
||||
id: env
|
||||
run: |
|
||||
if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then
|
||||
echo "ENVIRONMENT=dev" >> "$GITHUB_OUTPUT"
|
||||
echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_HOST=dev.${DOMAIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_NAME=chat_switchboard_dev" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_WIPE=true" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_REQUEST=64Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_LIMIT=128Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_REQUEST=25m" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_LIMIT=100m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_REQUEST=128Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_LIMIT=256Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_REQUEST=50m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_LIMIT=250m" >> "$GITHUB_OUTPUT"
|
||||
echo "env_label=dev (PR #${{ gitea.event.pull_request.number }})" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${{ gitea.ref }}" == refs/tags/v* ]]; then
|
||||
VERSION="${{ gitea.ref_name }}"
|
||||
echo "ENVIRONMENT=production" >> "$GITHUB_OUTPUT"
|
||||
echo "IMAGE_TAG=latest" >> "$GITHUB_OUTPUT"
|
||||
echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_HOST=www.${DOMAIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_NAME=chat_switchboard" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_REPLICAS=2" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_REPLICAS=2" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_REQUEST=64Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_LIMIT=128Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_REQUEST=50m" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_LIMIT=200m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_LIMIT=512Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_REQUEST=100m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_LIMIT=500m" >> "$GITHUB_OUTPUT"
|
||||
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
||||
echo "env_label=production (${VERSION})" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ENVIRONMENT=test" >> "$GITHUB_OUTPUT"
|
||||
echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_HOST=test.${DOMAIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_NAME=chat_switchboard_test" >> "$GITHUB_OUTPUT"
|
||||
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_REQUEST=64Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_MEMORY_LIMIT=128Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_REQUEST=25m" >> "$GITHUB_OUTPUT"
|
||||
echo "FE_CPU_LIMIT=100m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_REQUEST=128Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_MEMORY_LIMIT=256Mi" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_REQUEST=50m" >> "$GITHUB_OUTPUT"
|
||||
echo "BE_CPU_LIMIT=250m" >> "$GITHUB_OUTPUT"
|
||||
echo "env_label=test (main)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ── Database Bootstrap ───────────────────────
|
||||
- name: Bootstrap database
|
||||
env:
|
||||
PGHOST: ${{ env.POSTGRES_HOST }}
|
||||
PGPORT: ${{ env.POSTGRES_PORT }}
|
||||
PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }}
|
||||
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
|
||||
APP_USER: ${{ secrets.POSTGRES_USER }}
|
||||
APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
DB_NAME: ${{ steps.env.outputs.DB_NAME }}
|
||||
run: |
|
||||
chmod +x scripts/db-bootstrap.sh
|
||||
scripts/db-bootstrap.sh
|
||||
|
||||
# ── Database Migration ───────────────────────
|
||||
- name: Run migrations
|
||||
env:
|
||||
PGHOST: ${{ env.POSTGRES_HOST }}
|
||||
PGPORT: ${{ env.POSTGRES_PORT }}
|
||||
PGUSER: ${{ secrets.POSTGRES_USER }}
|
||||
PGPASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
DB_NAME: ${{ steps.env.outputs.DB_NAME }}
|
||||
DB_WIPE: ${{ steps.env.outputs.DB_WIPE }}
|
||||
run: |
|
||||
chmod +x scripts/db-migrate.sh
|
||||
scripts/db-migrate.sh
|
||||
|
||||
# ── Build Backend Image ──────────────────────
|
||||
- name: Build backend image
|
||||
run: |
|
||||
docker build -f server/Dockerfile \
|
||||
-t ${BE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }} \
|
||||
./server
|
||||
|
||||
# ── Build Frontend Image (cluster mode) ──────
|
||||
- name: Build frontend image
|
||||
run: |
|
||||
docker build -f Dockerfile.frontend \
|
||||
-t ${FE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }} \
|
||||
.
|
||||
|
||||
# ── Tag release versions ─────────────────────
|
||||
- name: Tag release versions
|
||||
if: steps.env.outputs.EXTRA_TAG != ''
|
||||
run: |
|
||||
docker tag ${BE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }} ${BE_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
docker tag ${FE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }} ${FE_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
|
||||
# ── Push to Internal Registry ────────────────
|
||||
- name: Push images
|
||||
run: |
|
||||
docker push ${BE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }}
|
||||
docker push ${FE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }}
|
||||
if [[ -n "${{ steps.env.outputs.EXTRA_TAG }}" ]]; then
|
||||
docker push ${BE_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
docker push ${FE_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
fi
|
||||
|
||||
# ── Build + Push Unified (release only) ──────
|
||||
- name: Build unified image
|
||||
if: steps.env.outputs.is_release == 'true'
|
||||
run: |
|
||||
docker build -f Dockerfile.unified \
|
||||
-t ${UNIFIED_IMAGE}:latest \
|
||||
-t ${UNIFIED_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }} \
|
||||
.
|
||||
docker push ${UNIFIED_IMAGE}:latest
|
||||
docker push ${UNIFIED_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
|
||||
- name: Push unified to Docker Hub
|
||||
if: steps.env.outputs.is_release == 'true' && secrets.DOCKERHUB_TOKEN != ''
|
||||
run: |
|
||||
echo "${{ secrets.DOCKERHUB_TOKEN }}" | \
|
||||
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
||||
docker tag ${UNIFIED_IMAGE}:latest ${DOCKERHUB_IMAGE}:latest
|
||||
docker tag ${UNIFIED_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
docker push ${DOCKERHUB_IMAGE}:latest
|
||||
docker push ${DOCKERHUB_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}
|
||||
|
||||
# ── Deploy to Kubernetes ─────────────────────
|
||||
- name: Ensure namespace
|
||||
run: |
|
||||
kubectl create namespace ${NAMESPACE} 2>/dev/null || true
|
||||
|
||||
- name: Sync secrets
|
||||
run: |
|
||||
kubectl create secret generic switchboard-db-credentials \
|
||||
--namespace=${NAMESPACE} \
|
||||
--from-literal=POSTGRES_USER=${{ secrets.POSTGRES_USER }} \
|
||||
--from-literal=POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Render and apply manifests
|
||||
env:
|
||||
ENVIRONMENT: ${{ steps.env.outputs.ENVIRONMENT }}
|
||||
IMAGE_TAG: ${{ steps.env.outputs.IMAGE_TAG }}
|
||||
DEPLOY_SUFFIX: ${{ steps.env.outputs.DEPLOY_SUFFIX }}
|
||||
DEPLOY_HOST: ${{ steps.env.outputs.DEPLOY_HOST }}
|
||||
DB_NAME: ${{ steps.env.outputs.DB_NAME }}
|
||||
FE_REPLICAS: ${{ steps.env.outputs.FE_REPLICAS }}
|
||||
BE_REPLICAS: ${{ steps.env.outputs.BE_REPLICAS }}
|
||||
FE_MEMORY_REQUEST: ${{ steps.env.outputs.FE_MEMORY_REQUEST }}
|
||||
FE_MEMORY_LIMIT: ${{ steps.env.outputs.FE_MEMORY_LIMIT }}
|
||||
FE_CPU_REQUEST: ${{ steps.env.outputs.FE_CPU_REQUEST }}
|
||||
FE_CPU_LIMIT: ${{ steps.env.outputs.FE_CPU_LIMIT }}
|
||||
BE_MEMORY_REQUEST: ${{ steps.env.outputs.BE_MEMORY_REQUEST }}
|
||||
BE_MEMORY_LIMIT: ${{ steps.env.outputs.BE_MEMORY_LIMIT }}
|
||||
BE_CPU_REQUEST: ${{ steps.env.outputs.BE_CPU_REQUEST }}
|
||||
BE_CPU_LIMIT: ${{ steps.env.outputs.BE_CPU_LIMIT }}
|
||||
run: |
|
||||
envsubst < k8s/backend.yaml > /tmp/backend.yaml
|
||||
envsubst < k8s/frontend.yaml > /tmp/frontend.yaml
|
||||
envsubst < k8s/ingress.yaml > /tmp/ingress.yaml
|
||||
|
||||
echo "━━━ Applying manifests for ${DEPLOY_HOST} ━━━"
|
||||
kubectl apply -f /tmp/backend.yaml
|
||||
kubectl apply -f /tmp/frontend.yaml
|
||||
kubectl apply -f /tmp/ingress.yaml
|
||||
|
||||
- name: Rollout and verify
|
||||
run: |
|
||||
SUFFIX="${{ steps.env.outputs.DEPLOY_SUFFIX }}"
|
||||
ENV="${{ steps.env.outputs.ENVIRONMENT }}"
|
||||
|
||||
kubectl rollout restart deployment/switchboard-be${SUFFIX} -n ${NAMESPACE}
|
||||
kubectl rollout restart deployment/switchboard-fe${SUFFIX} -n ${NAMESPACE}
|
||||
|
||||
kubectl rollout status deployment/switchboard-be${SUFFIX} -n ${NAMESPACE} --timeout=180s
|
||||
kubectl rollout status deployment/switchboard-fe${SUFFIX} -n ${NAMESPACE} --timeout=180s
|
||||
|
||||
echo ""
|
||||
echo "━━━ Pod Status ━━━"
|
||||
kubectl get pods -n ${NAMESPACE} -l app=switchboard,env=${ENV}
|
||||
|
||||
BE_READY=$(kubectl get deployment switchboard-be${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
|
||||
FE_READY=$(kubectl get deployment switchboard-fe${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
|
||||
|
||||
if [[ "${BE_READY:-0}" -gt 0 ]] && [[ "${FE_READY:-0}" -gt 0 ]]; then
|
||||
echo "✅ Healthy: BE=${BE_READY} FE=${FE_READY} pods ready"
|
||||
else
|
||||
echo "❌ Unhealthy: BE=${BE_READY:-0} FE=${FE_READY:-0}"
|
||||
kubectl logs -n ${NAMESPACE} -l app=switchboard,env=${ENV} --tail=30
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
HOST="${{ steps.env.outputs.DEPLOY_HOST }}"
|
||||
echo "## ✅ Deployed to ${{ steps.env.outputs.env_label }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| | |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **URL** | https://${HOST} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Backend** | \`${BE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Frontend** | \`${FE_IMAGE}:${{ steps.env.outputs.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Database** | \`${{ steps.env.outputs.DB_NAME }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ steps.env.outputs.is_release }}" == "true" ]]; then
|
||||
echo "| **Docker Hub** | \`${DOCKERHUB_IMAGE}:${{ steps.env.outputs.EXTRA_TAG }}\` (unified) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# ── Stage 3: FE-Only Lite Deploy (main only) ─
|
||||
deploy-lite:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add /tools to PATH
|
||||
run: echo "/tools" >> $GITHUB_PATH
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq gettext-base > /dev/null
|
||||
|
||||
- name: Build lite image
|
||||
run: |
|
||||
docker build -f Dockerfile.frontend \
|
||||
-t ${FE_IMAGE}:lite \
|
||||
.
|
||||
|
||||
- name: Push lite image
|
||||
run: docker push ${FE_IMAGE}:lite
|
||||
|
||||
- name: Deploy lite
|
||||
env:
|
||||
ENVIRONMENT: lite
|
||||
IMAGE_TAG: lite
|
||||
DEPLOY_HOST: "lite.${{ vars.DOMAIN || 'gobha.ai' }}"
|
||||
run: |
|
||||
export NAMESPACE="${NAMESPACE}"
|
||||
export FE_IMAGE="${FE_IMAGE}"
|
||||
export CERT_ISSUER="${CERT_ISSUER}"
|
||||
envsubst < k8s/lite.yaml > /tmp/lite.yaml
|
||||
kubectl create namespace ${NAMESPACE} 2>/dev/null || true
|
||||
kubectl apply -f /tmp/lite.yaml
|
||||
kubectl rollout restart deployment/switchboard-lite -n ${NAMESPACE}
|
||||
kubectl rollout status deployment/switchboard-lite -n ${NAMESPACE} --timeout=120s
|
||||
echo "✅ Lite deployed to https://${DEPLOY_HOST}"
|
||||
Reference in New Issue
Block a user