name: CI/CD Pipeline on: push: branches: [main, dev, production] pull_request: branches: [main, dev, production] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: # Test Job (parallel) test: name: Run Tests runs-on: self-hosted # Use your own server for speed! steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.npm node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: npm ci - name: Create test environment file run: | cat > .env < .env < /dev/null 2>&1; then echo "✅ Staging deployment successful!" break fi sleep 2 done # Verify deployment if curl -f http://localhost:3001/api/health; then echo "✅ Staging deployment verified!" else echo "⚠️ Staging health check failed, but container is running" docker compose -f $COMPOSE_FILE logs --tail=50 fi # Deploy to production deploy: name: Deploy to Production runs-on: self-hosted needs: build if: github.event_name == 'push' && github.ref == 'refs/heads/production' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Deploy to production (zero-downtime) run: | # Set deployment variables export IMAGE_NAME="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:production" export CONTAINER_NAME="portfolio-app" export COMPOSE_FILE="docker-compose.production.yml" export BACKUP_CONTAINER="portfolio-app-backup" # Set environment variables for docker-compose export NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" export MY_EMAIL="${{ vars.MY_EMAIL }}" export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" # Pull latest production image echo "📦 Pulling latest production image..." docker pull $IMAGE_NAME # Check if production container is running if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then echo "🔄 Production container is running - performing zero-downtime deployment..." # Start new container with different name first (blue-green) echo "🚀 Starting new container (green)..." docker run -d \ --name ${BACKUP_CONTAINER} \ --network portfolio_net \ -p 3002:3000 \ -e NODE_ENV=production \ -e DATABASE_URL=postgresql://portfolio_user:portfolio_pass@postgres:5432/portfolio_db?schema=public \ -e REDIS_URL=redis://redis:6379 \ -e NEXT_PUBLIC_BASE_URL="${{ vars.NEXT_PUBLIC_BASE_URL }}" \ -e MY_EMAIL="${{ vars.MY_EMAIL }}" \ -e MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" \ -e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \ -e MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" \ -e ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" \ $IMAGE_NAME || true # Wait for new container to be healthy echo "⏳ Waiting for new container to be healthy..." for i in {1..30}; do if curl -f http://localhost:3002/api/health > /dev/null 2>&1; then echo "✅ New container is healthy!" break fi sleep 2 done # Stop old container echo "🛑 Stopping old container..." docker stop ${CONTAINER_NAME} || true # Remove old container docker rm ${CONTAINER_NAME} || true # Rename new container to production name docker rename ${BACKUP_CONTAINER} ${CONTAINER_NAME} # Update port mapping (requires container restart, but it's already healthy) docker stop ${CONTAINER_NAME} docker rm ${CONTAINER_NAME} # Start with correct port using docker-compose docker compose -f $COMPOSE_FILE up -d --force-recreate else echo "🆕 No existing container - starting fresh deployment..." docker compose -f $COMPOSE_FILE up -d --force-recreate fi # Wait for health check echo "⏳ Waiting for production application to be healthy..." for i in {1..30}; do if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then echo "✅ Production deployment successful!" break fi sleep 2 done # Verify deployment if curl -f http://localhost:3000/api/health; then echo "✅ Production deployment verified!" else echo "❌ Production deployment failed!" docker compose -f $COMPOSE_FILE logs --tail=100 exit 1 fi # Cleanup backup container if it exists docker rm -f ${BACKUP_CONTAINER} 2>/dev/null || true - name: Cleanup old images run: | # Remove unused images older than 7 days docker image prune -f --filter "until=168h" # Remove unused containers docker container prune -f