name: CI/CD Pipeline (Zero Downtime) on: push: branches: [ production ] env: NODE_VERSION: '20' DOCKER_IMAGE: portfolio-app CONTAINER_NAME: portfolio-app NEW_CONTAINER_NAME: portfolio-app-new jobs: production: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run linting run: npm run lint - name: Run tests run: npm run test - name: Build application run: npm run build - name: Run security scan run: | echo "๐Ÿ” Running npm audit..." npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." - name: Build Docker image run: | docker build -t ${{ env.DOCKER_IMAGE }}:latest . docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:$(date +%Y%m%d-%H%M%S) - name: Verify secrets and variables before deployment run: | echo "๐Ÿ” Verifying secrets and variables..." # Check Variables if [ -z "${{ vars.NEXT_PUBLIC_BASE_URL }}" ]; then echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" exit 1 fi if [ -z "${{ vars.MY_EMAIL }}" ]; then echo "โŒ MY_EMAIL variable is missing!" exit 1 fi if [ -z "${{ vars.MY_INFO_EMAIL }}" ]; then echo "โŒ MY_INFO_EMAIL variable is missing!" exit 1 fi # Check Secrets if [ -z "${{ secrets.MY_PASSWORD }}" ]; then echo "โŒ MY_PASSWORD secret is missing!" exit 1 fi if [ -z "${{ secrets.MY_INFO_PASSWORD }}" ]; then echo "โŒ MY_INFO_PASSWORD secret is missing!" exit 1 fi if [ -z "${{ secrets.ADMIN_BASIC_AUTH }}" ]; then echo "โŒ ADMIN_BASIC_AUTH secret is missing!" exit 1 fi echo "โœ… All required secrets and variables are present" - name: Start new container (zero downtime) run: | echo "๐Ÿš€ Starting new container for zero-downtime deployment..." # Start new container with different name docker run -d \ --name ${{ env.NEW_CONTAINER_NAME }} \ --restart unless-stopped \ --network portfolio_net \ -p 3001:3000 \ -e NODE_ENV=${{ vars.NODE_ENV }} \ -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ -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 NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ -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 }}" \ ${{ env.DOCKER_IMAGE }}:latest echo "โœ… New container started on port 3001" - name: Health check new container run: | echo "๐Ÿ” Health checking new container..." sleep 10 # Health check on new container for i in {1..30}; do if curl -f http://localhost:3001/api/health > /dev/null 2>&1; then echo "โœ… New container is healthy!" break fi echo "โณ Waiting for new container to be ready... ($i/30)" sleep 2 done # Final health check if ! curl -f http://localhost:3001/api/health > /dev/null 2>&1; then echo "โŒ New container failed health check!" docker logs ${{ env.NEW_CONTAINER_NAME }} exit 1 fi - name: Switch traffic to new container (zero downtime) run: | echo "๐Ÿ”„ Switching traffic to new container..." # Stop old container docker stop ${{ env.CONTAINER_NAME }} || true # Remove old container docker rm ${{ env.CONTAINER_NAME }} || true # Rename new container to production name docker rename ${{ env.NEW_CONTAINER_NAME }} ${{ env.CONTAINER_NAME }} # Update port mapping (requires container restart) docker stop ${{ env.CONTAINER_NAME }} docker rm ${{ env.CONTAINER_NAME }} # Start with correct port docker run -d \ --name ${{ env.CONTAINER_NAME }} \ --restart unless-stopped \ --network portfolio_net \ -p 3000:3000 \ -e NODE_ENV=${{ vars.NODE_ENV }} \ -e LOG_LEVEL=${{ vars.LOG_LEVEL }} \ -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 NEXT_PUBLIC_UMAMI_URL="${{ vars.NEXT_PUBLIC_UMAMI_URL }}" \ -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}" \ -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 }}" \ ${{ env.DOCKER_IMAGE }}:latest echo "โœ… Traffic switched successfully!" - name: Final health check run: | echo "๐Ÿ” Final health check..." sleep 5 for i in {1..10}; do if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then echo "โœ… Deployment successful! Zero downtime achieved!" break fi echo "โณ Final health check... ($i/10)" sleep 2 done if ! curl -f http://localhost:3000/api/health > /dev/null 2>&1; then echo "โŒ Final health check failed!" docker logs ${{ env.CONTAINER_NAME }} exit 1 fi - name: Cleanup old images run: | echo "๐Ÿงน Cleaning up old images..." docker image prune -f docker system prune -f echo "โœ… Cleanup completed"