- Add specific removal of problematic container afa9a70588844b06e17d5e0527119d589a7a3fde8a17608447cf7d8d448cf261 - Force remove portfolio-app-new container before deployment - Add container listing for debugging after cleanup - Upgrade setup-node to v4 for better performance - Add cache-dependency-path for more efficient caching - Create fast workflow alternative with manual cache management
244 lines
9.5 KiB
YAML
244 lines
9.5 KiB
YAML
name: CI/CD Pipeline (Simple)
|
|
|
|
on:
|
|
push:
|
|
branches: [ main, production ]
|
|
pull_request:
|
|
branches: [ main, production ]
|
|
|
|
env:
|
|
NODE_VERSION: '20'
|
|
DOCKER_IMAGE: portfolio-app
|
|
CONTAINER_NAME: portfolio-app
|
|
|
|
jobs:
|
|
# Production deployment pipeline
|
|
production:
|
|
runs-on: ubuntu-latest
|
|
if: github.ref == 'refs/heads/production'
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
cache: 'npm'
|
|
cache-dependency-path: 'package-lock.json'
|
|
|
|
- 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: Prepare for zero-downtime deployment
|
|
run: |
|
|
echo "🚀 Preparing zero-downtime deployment..."
|
|
|
|
# FORCE REMOVE the problematic container
|
|
echo "🧹 FORCE removing problematic container portfolio-app-new..."
|
|
docker rm -f portfolio-app-new || true
|
|
docker rm -f afa9a70588844b06e17d5e0527119d589a7a3fde8a17608447cf7d8d448cf261 || true
|
|
|
|
# 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
|
|
|
|
# Wait for services to be ready
|
|
sleep 10
|
|
|
|
- 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: Deploy with zero downtime
|
|
run: |
|
|
echo "🚀 Deploying 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..."
|
|
|
|
# Remove specific known problematic containers
|
|
docker rm -f portfolio-app-new portfolio-app-temp-* portfolio-app-backup || true
|
|
|
|
# FORCE remove the specific problematic container by ID
|
|
docker rm -f afa9a70588844b06e17d5e0527119d589a7a3fde8a17608447cf7d8d448cf261 || 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
|
|
|
|
# Double-check: list all containers to see what's left
|
|
echo "📋 Current containers after cleanup:"
|
|
docker ps -a --format "table {{.Names}}\t{{.Status}}" | grep portfolio || echo "No portfolio containers found"
|
|
|
|
# Start new container with unique temporary name (no port mapping needed for health check)
|
|
docker run -d \
|
|
--name $TEMP_CONTAINER_NAME \
|
|
--restart unless-stopped \
|
|
--network portfolio_net \
|
|
-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
|
|
|
|
# Wait for new container to be ready
|
|
echo "⏳ Waiting for new container to be ready..."
|
|
sleep 15
|
|
|
|
# Health check new container using docker exec
|
|
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
|
|
|
|
# Remove old container
|
|
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=${{ 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 "✅ Rolling update completed!"
|
|
else
|
|
echo "🆕 Fresh deployment..."
|
|
docker compose up -d
|
|
fi
|
|
env:
|
|
NODE_ENV: ${{ vars.NODE_ENV }}
|
|
LOG_LEVEL: ${{ vars.LOG_LEVEL }}
|
|
NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }}
|
|
NEXT_PUBLIC_UMAMI_URL: ${{ vars.NEXT_PUBLIC_UMAMI_URL }}
|
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${{ vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
|
|
MY_EMAIL: ${{ vars.MY_EMAIL }}
|
|
MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }}
|
|
MY_PASSWORD: ${{ secrets.MY_PASSWORD }}
|
|
MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }}
|
|
ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }}
|
|
|
|
- name: Wait for container to be ready
|
|
run: |
|
|
sleep 10
|
|
timeout 60 bash -c 'until curl -f http://localhost:3000/api/health; do sleep 2; done'
|
|
|
|
- name: Health check
|
|
run: |
|
|
curl -f http://localhost:3000/api/health
|
|
echo "✅ Deployment successful!"
|
|
|
|
- name: Cleanup old images
|
|
run: |
|
|
docker image prune -f
|
|
docker system prune -f |