diff --git a/.gitea/workflows/ci-cd-fast.yml b/.gitea/workflows/ci-cd-fast.yml new file mode 100644 index 0000000..03c7e4f --- /dev/null +++ b/.gitea/workflows/ci-cd-fast.yml @@ -0,0 +1,236 @@ +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: | + 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 \ No newline at end of file diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml index 5daba6e..7a310c2 100644 --- a/.gitea/workflows/ci-cd.yml +++ b/.gitea/workflows/ci-cd.yml @@ -21,10 +21,11 @@ jobs: uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v3 + 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 @@ -52,6 +53,11 @@ jobs: 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" @@ -120,6 +126,9 @@ jobs: # 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 @@ -131,6 +140,10 @@ jobs: # 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 \ diff --git a/.gitea/workflows/test-and-build.yml b/.gitea/workflows/test-and-build.yml index 5beade7..8a1db70 100644 --- a/.gitea/workflows/test-and-build.yml +++ b/.gitea/workflows/test-and-build.yml @@ -17,10 +17,11 @@ jobs: uses: actions/checkout@v3 - name: Setup Node.js - uses: actions/setup-node@v3 + 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