name: ParentFlow CI/CD Pipeline on: push: branches: - main - develop - 'feature/**' pull_request: branches: - main - develop env: NODE_VERSION: '20' DOCKER_REGISTRY: ghcr.io IMAGE_PREFIX: ${{ github.repository_owner }}/parentflow jobs: # ============================================ # Testing & Quality Checks # ============================================ backend-tests: name: Backend Tests runs-on: ubuntu-latest services: postgres: image: postgres:15-alpine env: POSTGRES_USER: test_user POSTGRES_PASSWORD: test_password POSTGRES_DB: test_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 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: maternal-app/maternal-app-backend/package-lock.json - name: Install dependencies working-directory: maternal-app/maternal-app-backend run: npm ci - name: Run linter working-directory: maternal-app/maternal-app-backend run: npm run lint - name: Run unit tests working-directory: maternal-app/maternal-app-backend env: DATABASE_HOST: localhost DATABASE_PORT: 5432 DATABASE_NAME: test_db DATABASE_USER: test_user DATABASE_PASSWORD: test_password REDIS_HOST: localhost REDIS_PORT: 6379 run: npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./maternal-app/maternal-app-backend/coverage/lcov.info flags: backend frontend-tests: name: Frontend Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' cache-dependency-path: maternal-web/package-lock.json - name: Install dependencies working-directory: maternal-web run: npm ci - name: Run linter working-directory: maternal-web run: npm run lint - name: Type checking working-directory: maternal-web run: npm run type-check - name: Run unit tests working-directory: maternal-web run: npm test -- --coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: files: ./maternal-web/coverage/lcov.info flags: frontend # ============================================ # Security Scanning # ============================================ security-scan: name: Security Scanning runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy results to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' - name: Check for dependency vulnerabilities - Backend working-directory: maternal-app/maternal-app-backend run: npm audit --audit-level=moderate - name: Check for dependency vulnerabilities - Frontend working-directory: maternal-web run: npm audit --audit-level=moderate # ============================================ # Build Docker Images # ============================================ build-images: name: Build Docker Images runs-on: ubuntu-latest needs: [backend-tests, frontend-tests] if: github.event_name == 'push' strategy: matrix: service: - name: backend context: maternal-app/maternal-app-backend dockerfile: Dockerfile.production - name: frontend context: maternal-web dockerfile: Dockerfile.production steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-${{ matrix.service.name }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: ${{ matrix.service.context }} file: ${{ matrix.service.context }}/${{ matrix.service.dockerfile }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | APP_VERSION=${{ github.sha }} # ============================================ # Deploy to Development # ============================================ deploy-dev: name: Deploy to Development runs-on: ubuntu-latest needs: [build-images, security-scan] if: github.ref == 'refs/heads/develop' environment: name: development url: https://maternal.noru1.ro steps: - uses: actions/checkout@v4 - name: Deploy to Development Server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.DEV_HOST }} username: ${{ secrets.DEV_USER }} key: ${{ secrets.DEV_SSH_KEY }} script: | cd /opt/parentflow git pull origin develop docker-compose -f docker-compose.dev.yml pull docker-compose -f docker-compose.dev.yml up -d --force-recreate docker system prune -f # ============================================ # Deploy to Production # ============================================ deploy-production: name: Deploy to Production runs-on: ubuntu-latest needs: [build-images, security-scan] if: github.ref == 'refs/heads/main' environment: name: production url: https://parentflowapp.com steps: - uses: actions/checkout@v4 - name: Run database migrations uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USER }} key: ${{ secrets.PROD_SSH_KEY }} script: | cd /opt/parentflow docker-compose -f docker-compose.production.yml exec -T backend npm run migration:run - name: Deploy to Production Server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USER }} key: ${{ secrets.PROD_SSH_KEY }} script: | cd /opt/parentflow # Backup current version docker tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:latest \ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:backup docker tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend:latest \ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend:backup # Pull new images docker-compose -f docker-compose.production.yml pull # Deploy with zero downtime docker-compose -f docker-compose.production.yml up -d --no-deps --scale backend=2 backend sleep 30 docker-compose -f docker-compose.production.yml up -d --no-deps backend docker-compose -f docker-compose.production.yml up -d --no-deps --scale frontend=2 frontend sleep 30 docker-compose -f docker-compose.production.yml up -d --no-deps frontend # Clean up docker system prune -f - name: Health Check run: | for i in {1..10}; do if curl -f https://api.parentflowapp.com/health; then echo "Backend is healthy" break fi echo "Waiting for backend to be healthy..." sleep 10 done for i in {1..10}; do if curl -f https://parentflowapp.com; then echo "Frontend is healthy" break fi echo "Waiting for frontend to be healthy..." sleep 10 done - name: Rollback on Failure if: failure() uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.PROD_HOST }} username: ${{ secrets.PROD_USER }} key: ${{ secrets.PROD_SSH_KEY }} script: | cd /opt/parentflow # Rollback to backup images docker tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:backup \ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:latest docker tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend:backup \ ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend:latest docker-compose -f docker-compose.production.yml up -d --force-recreate # Notify team of rollback echo "Deployment failed and rolled back" | mail -s "ParentFlow Deployment Failure" team@parentflowapp.com - name: Notify Deployment Success if: success() uses: 8398a7/action-slack@v3 with: status: success text: 'Production deployment successful! 🚀' webhook_url: ${{ secrets.SLACK_WEBHOOK }} - name: Create Sentry Release if: success() uses: getsentry/action-release@v1 env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: parentflow SENTRY_PROJECT: backend,frontend with: environment: production version: ${{ github.sha }}