- Disabled ci-cd-fast.yml (Gitea Actions syntax) - Added ci-cd-woodpecker.yml (proper Woodpecker CI syntax) - Fixed environment variable and secret access - Should resolve deployment issues with missing variables
318 lines
12 KiB
Plaintext
318 lines
12 KiB
Plaintext
name: CI/CD Pipeline (Fast)
|
|
|
|
on:
|
|
push:
|
|
branches: [ production ]
|
|
|
|
env:
|
|
NODE_VERSION: '20'
|
|
DOCKER_IMAGE: portfolio-app
|
|
CONTAINER_NAME: portfolio-app
|
|
|
|
jobs:
|
|
production:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js (Fast)
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
# Disable cache to avoid slow validation
|
|
cache: ''
|
|
|
|
- name: Cache npm dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.npm
|
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node-
|
|
|
|
- name: Install dependencies
|
|
run: npm ci --prefer-offline --no-audit
|
|
|
|
- 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..."
|
|
|
|
# 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
|
|
|
|
# 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 (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: |
|
|
echo "⏳ Waiting for container to be ready..."
|
|
sleep 15
|
|
|
|
# Check if container is actually running
|
|
if ! docker ps --filter "name=portfolio-app" --format "{{.Names}}" | grep -q "portfolio-app"; then
|
|
echo "❌ Container failed to start"
|
|
echo "Container logs:"
|
|
docker logs portfolio-app --tail=50
|
|
exit 1
|
|
fi
|
|
|
|
# Wait for health check with better error handling
|
|
echo "🏥 Performing health check..."
|
|
for i in {1..40}; do
|
|
# First try direct access to port 3000
|
|
if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then
|
|
echo "✅ Application is healthy (direct access)!"
|
|
break
|
|
fi
|
|
|
|
# If direct access fails, try through docker exec (internal container check)
|
|
if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then
|
|
echo "✅ Application is healthy (internal check)!"
|
|
# Check if port is properly exposed
|
|
if ! curl -f http://localhost:3000/api/health > /dev/null 2>&1; then
|
|
echo "⚠️ Application is running but port 3000 is not exposed to host"
|
|
echo "This might be expected in some deployment configurations"
|
|
break
|
|
fi
|
|
fi
|
|
|
|
# Check if container is still running
|
|
if ! docker ps --filter "name=portfolio-app" --format "{{.Names}}" | grep -q "portfolio-app"; then
|
|
echo "❌ Container stopped during health check"
|
|
echo "Container logs:"
|
|
docker logs portfolio-app --tail=50
|
|
exit 1
|
|
fi
|
|
|
|
echo "⏳ Health check attempt $i/40..."
|
|
sleep 3
|
|
done
|
|
|
|
# Final health check - try both methods
|
|
if docker exec portfolio-app curl -f http://localhost:3000/api/health > /dev/null 2>&1; then
|
|
echo "✅ Final health check passed (internal)"
|
|
# Try external access if possible
|
|
if curl -f http://localhost:3000/api/health > /dev/null 2>&1; then
|
|
echo "✅ External access also working"
|
|
else
|
|
echo "⚠️ External access not available (port not exposed)"
|
|
fi
|
|
else
|
|
echo "❌ Health check timeout - application not responding"
|
|
echo "Container logs:"
|
|
docker logs portfolio-app --tail=100
|
|
exit 1
|
|
fi
|
|
|
|
- name: Health check
|
|
run: |
|
|
echo "🔍 Final health verification..."
|
|
|
|
# Check container status
|
|
docker ps --filter "name=portfolio-app" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
|
|
|
# Test health endpoint - try both methods
|
|
echo "🏥 Testing health endpoint..."
|
|
if curl -f http://localhost:3000/api/health; then
|
|
echo "✅ Health endpoint accessible externally"
|
|
elif docker exec portfolio-app curl -f http://localhost:3000/api/health; then
|
|
echo "✅ Health endpoint accessible internally (external port not exposed)"
|
|
else
|
|
echo "❌ Health endpoint not accessible"
|
|
exit 1
|
|
fi
|
|
|
|
# Test main page - try both methods
|
|
echo "🌐 Testing main page..."
|
|
if curl -f http://localhost:3000/ > /dev/null; then
|
|
echo "✅ Main page is accessible externally"
|
|
elif docker exec portfolio-app curl -f http://localhost:3000/ > /dev/null; then
|
|
echo "✅ Main page is accessible internally (external port not exposed)"
|
|
else
|
|
echo "❌ Main page is not accessible"
|
|
exit 1
|
|
fi
|
|
|
|
echo "✅ Deployment successful!"
|
|
|
|
- name: Cleanup old images
|
|
run: |
|
|
docker image prune -f
|
|
docker system prune -f |