name: CI/CD Pipeline (Woodpecker) when: event: push branch: production steps: build: image: node:20-alpine commands: - echo "๐Ÿš€ Starting CI/CD Pipeline" - echo "๐Ÿ“‹ Step 1: Installing dependencies..." - npm ci --prefer-offline --no-audit - echo "๐Ÿ” Step 2: Running linting..." - npm run lint - echo "๐Ÿงช Step 3: Running tests..." - npm run test - echo "๐Ÿ—๏ธ Step 4: Building application..." - npm run build - echo "๐Ÿ”’ Step 5: Running security scan..." - npm audit --audit-level=high || echo "โš ๏ธ Some vulnerabilities found, but continuing..." volumes: - node_modules:/app/node_modules docker-build: image: docker:latest commands: - echo "๐Ÿณ Building Docker image..." - docker build -t portfolio-app:latest . - docker tag portfolio-app:latest portfolio-app:$(date +%Y%m%d-%H%M%S) volumes: - /var/run/docker.sock:/var/run/docker.sock deploy: image: docker:latest commands: - echo "๐Ÿš€ Deploying application..." # Verify secrets and variables - echo "๐Ÿ” Verifying secrets and variables..." - | if [ -z "$NEXT_PUBLIC_BASE_URL" ]; then echo "โŒ NEXT_PUBLIC_BASE_URL variable is missing!" exit 1 fi if [ -z "$MY_EMAIL" ]; then echo "โŒ MY_EMAIL variable is missing!" exit 1 fi if [ -z "$MY_INFO_EMAIL" ]; then echo "โŒ MY_INFO_EMAIL variable is missing!" exit 1 fi if [ -z "$MY_PASSWORD" ]; then echo "โŒ MY_PASSWORD secret is missing!" exit 1 fi if [ -z "$MY_INFO_PASSWORD" ]; then echo "โŒ MY_INFO_PASSWORD secret is missing!" exit 1 fi if [ -z "$ADMIN_BASIC_AUTH" ]; then echo "โŒ ADMIN_BASIC_AUTH secret is missing!" exit 1 fi echo "โœ… All required secrets and variables are present" # Check if current container is running - | if docker ps -q -f name=portfolio-app | grep -q .; then echo "๐Ÿ“Š Current container is running, proceeding with zero-downtime update" CURRENT_CONTAINER_RUNNING=true else echo "๐Ÿ“Š No current container running, doing fresh deployment" CURRENT_CONTAINER_RUNNING=false fi # Ensure database and redis are running - echo "๐Ÿ”ง Ensuring database and redis are running..." - docker compose up -d postgres redis - sleep 10 # Deploy with zero downtime - | if [ "$CURRENT_CONTAINER_RUNNING" = "true" ]; then echo "๐Ÿ”„ Performing rolling update..." # Generate unique container name TIMESTAMP=$(date +%s) TEMP_CONTAINER_NAME="portfolio-app-temp-$TIMESTAMP" echo "๐Ÿ”ง Using temporary container name: $TEMP_CONTAINER_NAME" # Clean up any existing temporary containers echo "๐Ÿงน Cleaning up any existing temporary containers..." docker rm -f portfolio-app-new portfolio-app-temp-* portfolio-app-backup || true # Find and remove any containers with portfolio-app in the name (except the main one) EXISTING_CONTAINERS=$(docker ps -a --format "table {{.Names}}" | grep "portfolio-app" | grep -v "^portfolio-app$" || true) if [ -n "$EXISTING_CONTAINERS" ]; then echo "๐Ÿ—‘๏ธ Removing existing portfolio-app containers:" echo "$EXISTING_CONTAINERS" echo "$EXISTING_CONTAINERS" | xargs -r docker rm -f || true fi # Also clean up any stopped containers docker container prune -f || true # Start new container with unique temporary name docker run -d \ --name $TEMP_CONTAINER_NAME \ --restart unless-stopped \ --network portfolio_net \ -e NODE_ENV=$NODE_ENV \ -e LOG_LEVEL=$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="$NEXT_PUBLIC_BASE_URL" \ -e NEXT_PUBLIC_UMAMI_URL="$NEXT_PUBLIC_UMAMI_URL" \ -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ -e MY_EMAIL="$MY_EMAIL" \ -e MY_INFO_EMAIL="$MY_INFO_EMAIL" \ -e MY_PASSWORD="$MY_PASSWORD" \ -e MY_INFO_PASSWORD="$MY_INFO_PASSWORD" \ -e ADMIN_BASIC_AUTH="$ADMIN_BASIC_AUTH" \ portfolio-app:latest # Wait for new container to be ready echo "โณ Waiting for new container to be ready..." sleep 15 # Health check new container for i in {1..20}; do if docker exec $TEMP_CONTAINER_NAME curl -f http://localhost:3000/api/health > /dev/null 2>&1; then echo "โœ… New container is healthy!" break fi echo "โณ Health check attempt $i/20..." sleep 3 done # Stop old container echo "๐Ÿ›‘ Stopping old container..." docker stop portfolio-app || true docker rm portfolio-app || true # Rename new container docker rename $TEMP_CONTAINER_NAME portfolio-app # Update port mapping docker stop portfolio-app docker rm portfolio-app # Start with correct port docker run -d \ --name portfolio-app \ --restart unless-stopped \ --network portfolio_net \ -p 3000:3000 \ -e NODE_ENV=$NODE_ENV \ -e LOG_LEVEL=$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="$NEXT_PUBLIC_BASE_URL" \ -e NEXT_PUBLIC_UMAMI_URL="$NEXT_PUBLIC_UMAMI_URL" \ -e NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \ -e MY_EMAIL="$MY_EMAIL" \ -e MY_INFO_EMAIL="$MY_INFO_EMAIL" \ -e MY_PASSWORD="$MY_PASSWORD" \ -e MY_INFO_PASSWORD="$MY_INFO_PASSWORD" \ -e ADMIN_BASIC_AUTH="$ADMIN_BASIC_AUTH" \ portfolio-app:latest echo "โœ… Rolling update completed!" else echo "๐Ÿ†• Fresh deployment..." docker compose up -d fi # Wait for container to be ready - echo "โณ Waiting for container to be ready..." - sleep 15 # Health check - | echo "๐Ÿฅ Performing health check..." for i in {1..40}; do if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then echo "โœ… Application is healthy!" break fi echo "โณ Health check attempt $i/40..." sleep 3 done # Final verification - echo "๐Ÿ” Final health verification..." - docker ps --filter "name=portfolio-app" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - | if curl -f http://localhost:3000/api/health; then echo "โœ… Health endpoint accessible" else echo "โŒ Health endpoint not accessible" exit 1 fi - | if curl -f http://localhost:3000/ > /dev/null; then echo "โœ… Main page is accessible" else echo "โŒ Main page is not accessible" exit 1 fi - echo "โœ… Deployment successful!" # Cleanup - docker image prune -f - docker system prune -f volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - NODE_ENV - LOG_LEVEL - NEXT_PUBLIC_BASE_URL - NEXT_PUBLIC_UMAMI_URL - NEXT_PUBLIC_UMAMI_WEBSITE_ID - MY_EMAIL - MY_INFO_EMAIL - MY_PASSWORD - MY_INFO_PASSWORD - ADMIN_BASIC_AUTH volumes: node_modules: