From dca8cb8973252ad3246d4b379875f32942a37dd2 Mon Sep 17 00:00:00 2001 From: Denshooter Date: Tue, 16 Sep 2025 00:29:23 +0200 Subject: [PATCH] Fix CI/CD: Switch from Gitea Actions to Woodpecker CI workflow - 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 --- ...ci-cd-fast.yml => ci-cd-fast.yml.disabled} | 0 .gitea/workflows/ci-cd-woodpecker.yml | 232 ++++++++++++++++++ sync-env.ps1 | 45 ++-- 3 files changed, 263 insertions(+), 14 deletions(-) rename .gitea/workflows/{ci-cd-fast.yml => ci-cd-fast.yml.disabled} (100%) create mode 100644 .gitea/workflows/ci-cd-woodpecker.yml diff --git a/.gitea/workflows/ci-cd-fast.yml b/.gitea/workflows/ci-cd-fast.yml.disabled similarity index 100% rename from .gitea/workflows/ci-cd-fast.yml rename to .gitea/workflows/ci-cd-fast.yml.disabled diff --git a/.gitea/workflows/ci-cd-woodpecker.yml b/.gitea/workflows/ci-cd-woodpecker.yml new file mode 100644 index 0000000..f4cd42a --- /dev/null +++ b/.gitea/workflows/ci-cd-woodpecker.yml @@ -0,0 +1,232 @@ +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: diff --git a/sync-env.ps1 b/sync-env.ps1 index c1d647b..3fa66be 100644 --- a/sync-env.ps1 +++ b/sync-env.ps1 @@ -11,27 +11,43 @@ if (!(Get-Command bw -ErrorAction SilentlyContinue)) { } # Check status -$status = bw status | ConvertFrom-Json +$statusOutput = bw status 2>$null +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to get Bitwarden status" -ForegroundColor Red + exit 1 +} +$status = $statusOutput | ConvertFrom-Json if ($status.status -eq "unauthenticated") { Write-Host "Please login to Bitwarden:" bw login - $status = bw status | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to login to Bitwarden" -ForegroundColor Red + exit 1 + } + $statusOutput = bw status 2>$null + $status = $statusOutput | ConvertFrom-Json } # Unlock if needed if ($status.status -eq "locked") { Write-Host "Unlocking vault..." - $env:BW_SESSION = bw unlock --raw + Write-Host "Please enter your Bitwarden master password:" + $sessionKey = bw unlock --raw if ($LASTEXITCODE -ne 0) { Write-Host "Error: Failed to unlock vault" -ForegroundColor Red exit 1 } + $env:BW_SESSION = $sessionKey } # Sync Write-Host "Syncing with Bitwarden..." -bw sync | Out-Null +bw sync 2>$null | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Failed to sync with Bitwarden" -ForegroundColor Red + exit 1 +} # CHANGE THIS to your Bitwarden item name $itemName = "portfolio-env" @@ -39,24 +55,25 @@ $itemName = "portfolio-env" Write-Host "Fetching environment variables..." # Get item -$item = bw get item $itemName 2>$null | ConvertFrom-Json - -if (!$item) { +$itemOutput = bw get item $itemName 2>$null +if ($LASTEXITCODE -ne 0) { Write-Host "Error: Could not find item '$itemName' in Bitwarden" -ForegroundColor Red Write-Host "Make sure you have an item with this exact name in your vault" exit 1 } +$item = $itemOutput | ConvertFrom-Json -# Get notes -$notes = $item.notes - -if (!$notes) { +# Get notes and process them +if (!$item.notes) { Write-Host "Error: No notes found in item '$itemName'" -ForegroundColor Red Write-Host "Add your environment variables to the notes field" exit 1 } -# Create .env file -$notes | Out-File -FilePath ".env" -Encoding UTF8 -NoNewline +# Process notes - handle escaped newlines and other JSON formatting +$notes = $item.notes -replace '\\n', "`n" -replace '\\r', "`r" -replace '\\t', "`t" -Write-Host "โœ“ Created .env file successfully!" -ForegroundColor Green \ No newline at end of file +# Create .env file +$envPath = Join-Path (Get-Location) ".env" +$notes | Out-File -FilePath $envPath -Encoding UTF8 -NoNewline +Write-Host "Created .env file successfully at: $envPath" -ForegroundColor Green