From e1c1f38a347c27fa9b24794b7a0a9633a2ab1043 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Mon, 27 Oct 2025 21:18:21 -0300 Subject: [PATCH] fix: checkout --- .gitea/workflows/deploy-bcards.yml | 1223 ++++++++++++++-------------- 1 file changed, 613 insertions(+), 610 deletions(-) diff --git a/.gitea/workflows/deploy-bcards.yml b/.gitea/workflows/deploy-bcards.yml index 16aa8ea..728e12a 100644 --- a/.gitea/workflows/deploy-bcards.yml +++ b/.gitea/workflows/deploy-bcards.yml @@ -1,610 +1,613 @@ -name: BCards Deployment Pipeline - -on: - push: - branches: - - main - - 'Release/*' - # PRs apenas validam, não fazem deploy - pull_request: - branches: [ main ] - types: [opened, synchronize, reopened] - -env: - REGISTRY: registry.redecarneir.us - IMAGE_NAME: bcards - MONGODB_HOST: 192.168.0.100:27017 - -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - - steps: - - name: Test info - run: | - echo "🧪 Executando testes para ${{ github.ref_name }}" - echo "🎯 Trigger: ${{ github.event_name }}" - - # Verificar se deve pular testes - SKIP_TESTS="${{ github.event.inputs.skip_tests || vars.SKIP_TESTS }}" - - if [ "$SKIP_TESTS" == "true" ]; then - echo "⚠️ Testes PULADOS" - echo "TESTS_SKIPPED=true" >> $GITHUB_ENV - else - echo "✅ Executando testes" - echo "TESTS_SKIPPED=false" >> $GITHUB_ENV - fi - - - name: Checkout code - if: env.TESTS_SKIPPED == 'false' - uses: actions/checkout@v4 - - - name: Setup .NET 8 - if: env.TESTS_SKIPPED == 'false' - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '8.0.x' - - - name: Cache dependencies - if: env.TESTS_SKIPPED == 'false' - uses: actions/cache@v3 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Restore dependencies - if: env.TESTS_SKIPPED == 'false' - run: dotnet restore - - - name: Build solution - if: env.TESTS_SKIPPED == 'false' - run: dotnet build --no-restore --configuration Release - - - name: Run unit tests - if: env.TESTS_SKIPPED == 'false' - run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage" - - # Job específico para validação de PRs (sem deploy) - pr-validation: - name: PR Validation - runs-on: ubuntu-latest - needs: [test] - if: github.event_name == 'pull_request' - - steps: - - name: PR Validation Summary - run: | - echo "✅ Pull Request Validation Summary" - echo "🎯 Target Branch: ${{ github.base_ref }}" - echo "📂 Source Branch: ${{ github.head_ref }}" - echo "🧪 Tests: ${{ needs.test.result }}" - echo "👤 Author: ${{ github.event.pull_request.user.login }}" - echo "📝 Title: ${{ github.event.pull_request.title }}" - echo "" - echo "✨ PR está pronto para merge!" - - build-and-push: - name: Build and Push Image - runs-on: [self-hosted, arm64, bcards] - needs: [test] - # Só faz build/push em push (não em PR) - if: github.event_name == 'push' && (needs.test.result == 'success' || needs.test.result == 'skipped') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/arm64 - - - name: Determine build settings - id: settings - run: | - BRANCH_NAME="${{ github.ref_name }}" - - if [ "$BRANCH_NAME" = "main" ]; then - # Main = Produção (ARM64) - usando Dockerfile simples - echo "tag=latest" >> $GITHUB_OUTPUT - echo "platform=linux/arm64" >> $GITHUB_OUTPUT - echo "environment=Production" >> $GITHUB_OUTPUT - echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT - echo "deploy_target=production" >> $GITHUB_OUTPUT - elif [[ "$BRANCH_NAME" == Release/* ]]; then - # Release = Swarm tests (Orange Pi arm64) - usando Dockerfile simples também - VERSION_RAW=${BRANCH_NAME#Release/} - # Only remove V/v if it's at the start and followed by a number (like v1.0.0) - VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/') - [ -z "$VERSION" ] && VERSION="0.0.1" - - echo "tag=$VERSION" >> $GITHUB_OUTPUT - echo "platform=linux/arm64" >> $GITHUB_OUTPUT - echo "environment=Testing" >> $GITHUB_OUTPUT - echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT - echo "deploy_target=testing" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - fi - - COMMIT_SHA=${{ github.sha }} - SHORT_COMMIT=${COMMIT_SHA:0:7} - echo "commit=$SHORT_COMMIT" >> $GITHUB_OUTPUT - - echo "📦 Tag: ${{ steps.settings.outputs.tag }}" - echo "🏗️ Platform: ${{ steps.settings.outputs.platform }}" - echo "🌍 Environment: ${{ steps.settings.outputs.environment }}" - echo "🎯 Target: ${{ steps.settings.outputs.deploy_target }}" - - - name: Build and push image - run: | - echo "🏗️ Building image for ${{ steps.settings.outputs.deploy_target }}..." - - # Debug das variáveis - echo "Platform: ${{ steps.settings.outputs.platform }}" - echo "Dockerfile: ${{ steps.settings.outputs.dockerfile }}" - echo "Tag: ${{ steps.settings.outputs.tag }}" - - # Verificar se o Dockerfile existe - if [ ! -f "${{ steps.settings.outputs.dockerfile }}" ]; then - echo "❌ Dockerfile não encontrado: ${{ steps.settings.outputs.dockerfile }}" - echo "📂 Arquivos na raiz:" - ls -la - echo "📂 Arquivos em src/BCards.Web/:" - ls -la src/BCards.Web/ || echo "Diretório não existe" - exit 1 - else - echo "✅ Dockerfile encontrado: ${{ steps.settings.outputs.dockerfile }}" - fi - - # Build para a plataforma correta - if [ "${{ steps.settings.outputs.deploy_target }}" = "production" ]; then - # Build para produção (main branch) - docker buildx build \ - --platform ${{ steps.settings.outputs.platform }} \ - --file ${{ steps.settings.outputs.dockerfile }} \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ - --push \ - --progress=plain \ - . - else - # Build para staging (Release branches) - docker buildx build \ - --platform ${{ steps.settings.outputs.platform }} \ - --file ${{ steps.settings.outputs.dockerfile }} \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ - --push \ - --build-arg VERSION=${{ steps.settings.outputs.version || 'latest' }} \ - --build-arg COMMIT=${{ steps.settings.outputs.commit }} \ - --progress=plain \ - . - fi - - deploy-production: - name: Deploy to Production (ARM - OCI) - runs-on: ubuntu-latest - needs: [build-and-push] - if: github.ref_name == 'main' - environment: production - - steps: - - name: Create Production Configuration - run: | - echo "🔧 Creating appsettings.Production.json with environment variables..." - - # Cria o arquivo de configuração para produção - cat > appsettings.Production.json << 'CONFIG_EOF' - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Microsoft.EntityFrameworkCore": "Warning", - "BCards": "Information", - "BCards.Web.Services.GridFSImageStorage": "Debug" - }, - "Console": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Information" - } - }, - "File": { - "Path": "/app/logs/bcards-{Date}.log", - "LogLevel": { - "Default": "Information" - } - } - }, - "AllowedHosts": "*", - "Stripe": { - "PublishableKey": "${{ vars.STRIPE_PUBLISHABLE_KEY }}", - "SecretKey": "${{ secrets.STRIPE_SECRET_KEY }}", - "WebhookSecret": "${{ secrets.STRIPE_WEBHOOK_SECRET }}", - "Environment": "${{ vars.STRIPE_ENVIRONMENT || 'test' }}" - }, - "Authentication": { - "Google": { - "ClientId": "${{ vars.GOOGLE_CLIENT_ID }}", - "ClientSecret": "${{ secrets.GOOGLE_CLIENT_SECRET }}" - }, - "Microsoft": { - "ClientId": "${{ vars.MICROSOFT_CLIENT_ID }}", - "ClientSecret": "${{ secrets.MICROSOFT_CLIENT_SECRET }}" - } - }, - "SendGrid": { - "ApiKey": "${{ secrets.SENDGRID_API_KEY }}", - "FromEmail": "${{ vars.SENDGRID_FROM_EMAIL || 'ricardo.carneiro@jobmaker.com.br' }}", - "FromName": "${{ vars.SENDGRID_FROM_NAME || 'Ricardo Carneiro' }}" - }, - "Plans": { - "Basic": { - "Name": "Básico", - "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", - "Price": 5.90, - "MaxPages": 3, - "MaxLinks": 8, - "AllowPremiumThemes": false, - "AllowProductLinks": false, - "AllowAnalytics": true, - "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], - "Interval": "month" - }, - "Professional": { - "Name": "Profissional", - "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", - "Price": 12.90, - "MaxPages": 5, - "MaxLinks": 20, - "AllowPremiumThemes": false, - "AllowProductLinks": false, - "AllowAnalytics": true, - "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], - "Interval": "month" - }, - "Premium": { - "Name": "Premium", - "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", - "Price": 19.90, - "MaxPages": 15, - "MaxLinks": -1, - "AllowPremiumThemes": true, - "AllowProductLinks": false, - "AllowAnalytics": true, - "SpecialModeration": false, - "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ], - "Interval": "month" - }, - "PremiumAffiliate": { - "Name": "Premium+Afiliados", - "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", - "Price": 29.90, - "MaxPages": 15, - "MaxLinks": -1, - "AllowPremiumThemes": true, - "AllowProductLinks": true, - "AllowAnalytics": true, - "SpecialModeration": true, - "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], - "Interval": "month" - }, - "BasicYearly": { - "Name": "Básico Anual", - "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", - "Price": 59.00, - "MaxPages": 3, - "MaxLinks": 8, - "AllowPremiumThemes": false, - "AllowProductLinks": false, - "AllowAnalytics": true, - "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], - "Interval": "year" - }, - "ProfessionalYearly": { - "Name": "Profissional Anual", - "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", - "Price": 129.00, - "MaxPages": 5, - "MaxLinks": 20, - "AllowPremiumThemes": false, - "AllowProductLinks": false, - "AllowAnalytics": true, - "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], - "Interval": "year" - }, - "PremiumYearly": { - "Name": "Premium Anual", - "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", - "Price": 199.00, - "MaxPages": 15, - "MaxLinks": -1, - "AllowPremiumThemes": true, - "AllowProductLinks": false, - "AllowAnalytics": true, - "SpecialModeration": false, - "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], - "Interval": "year" - }, - "PremiumAffiliateYearly": { - "Name": "Premium+Afiliados Anual", - "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", - "Price": 299.00, - "MaxPages": 15, - "MaxLinks": -1, - "AllowPremiumThemes": true, - "AllowProductLinks": true, - "AllowAnalytics": true, - "SpecialModeration": true, - "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], - "Interval": "year" - } - }, - "Moderation": { - "PriorityTimeframes": { - "Trial": "7.00:00:00", - "Basic": "7.00:00:00", - "Professional": "3.00:00:00", - "Premium": "1.00:00:00" - }, - "MaxAttempts": 3, - "ModeratorEmail": "${{ vars.MODERATOR_EMAIL || 'ricardo.carneiro@jobmaker.com.br' }}", - "ModeratorEmails": [ - "${{ vars.MODERATOR_EMAIL_1 || 'rrcgoncalves@gmail.com' }}", - "${{ vars.MODERATOR_EMAIL_2 || 'rirocarneiro@gmail.com' }}" - ] - }, - "MongoDb": { - "ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/BCardsDB?replicaSet=rs0&authSource=admin", - "DatabaseName": "BCardsDB", - "MaxConnectionPoolSize": 100, - "ConnectTimeout": "30s", - "ServerSelectionTimeout": "30s", - "SocketTimeout": "30s" - }, - "BaseUrl": "https://bcards.site", - "Environment": { - "Name": "Production", - "IsStagingEnvironment": false, - "AllowTestData": false, - "EnableDetailedErrors": false - }, - "Performance": { - "EnableCaching": true, - "CacheExpirationMinutes": 30, - "EnableCompression": true, - "EnableResponseCaching": true - }, - "Security": { - "EnableHttpsRedirection": true, - "EnableHsts": true, - "RequireHttpsMetadata": true - }, - "HealthChecks": { - "Enabled": true, - "Endpoints": { - "Health": "/health", - "Ready": "/ready", - "Live": "/live" - }, - "MongoDb": { - "Enabled": true, - "Timeout": "10s" - } - }, - "Features": { - "EnablePreviewMode": true, - "EnableModerationWorkflow": true, - "EnableAnalytics": true, - "EnableFileUploads": true, - "MaxFileUploadSize": "5MB" - }, - "Serilog": { - "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}" - } - } - CONFIG_EOF - - echo "✅ Configuration file created!" - echo "🔍 File content (sensitive data masked):" - cat appsettings.Production.json | sed 's/"[^"]*_[0-9A-Za-z_]*"/"***MASKED***"/g' - - - name: Deploy to Production Swarm - run: | - echo "🚀 Deploying to production Docker Swarm (ARM64)..." - - # Configura SSH - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - - # Adiciona hosts conhecidos - ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts - ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts - - # Testa a chave SSH - ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded" - - # Upload configuration and stack file to swarm manager - echo "📤 Uploading files to Swarm manager..." - scp -o StrictHostKeyChecking=no appsettings.Production.json ubuntu@141.148.162.114:/tmp/ - scp -o StrictHostKeyChecking=no deploy/docker-stack.yml ubuntu@141.148.162.114:/tmp/ - - # Deploy to Docker Swarm - ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF' - set -e - - echo "🔄 Updating Docker Swarm stack..." - - # Update Docker config with new appsettings - echo "📝 Updating bcards-appsettings config..." - - # Remove old config (will fail if in use, that's ok - swarm will use it until update) - docker config rm bcards-appsettings 2>/dev/null || echo "Config in use or doesn't exist, will create new one" - - # Create new config with timestamp to force update - CONFIG_NAME="bcards-appsettings-$(date +%s)" - docker config create ${CONFIG_NAME} /tmp/appsettings.Production.json - - # Update stack file to use new config name - sed "s/bcards-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack.yml > /tmp/docker-stack-updated.yml - - echo "🐳 Deploying stack to Swarm (rolling update, zero downtime)..." - docker stack deploy -c /tmp/docker-stack-updated.yml bcards --with-registry-auth - - echo "⏳ Waiting for service to update..." - sleep 10 - - # Show service status - docker service ls --filter name=bcards_bcards-app - docker service ps bcards_bcards-app --no-trunc --filter "desired-state=running" | head -10 - - echo "🧹 Cleaning up standalone containers if they exist..." - docker stop bcards-prod 2>/dev/null || echo "No standalone container to stop" - docker rm bcards-prod 2>/dev/null || echo "No standalone container to remove" - - # Clean up temp files - rm -f /tmp/appsettings.Production.json /tmp/docker-stack.yml /tmp/docker-stack-updated.yml - - echo "✅ Swarm stack updated successfully!" - EOF - - - name: Health Check Production - run: | - echo "🏥 Verificando saúde dos servidores de produção..." - sleep 30 - - # Verifica Servidor 1 - echo "Verificando Servidor 1 (ARM)..." - ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 1 pode não estar respondendo"' - - # Verifica Servidor 2 - echo "Verificando Servidor 2 (ARM)..." - ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 2 pode não estar respondendo"' - - deploy-test: - name: Deploy to Release Swarm (ARM) - runs-on: [self-hosted, arm64, bcards] - needs: [build-and-push] - if: startsWith(github.ref_name, 'Release/') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Extract version - id: version - run: | - BRANCH_NAME="${{ github.ref_name }}" - VERSION_RAW=${BRANCH_NAME#Release/} - # Only remove V/v if it's at the start and followed by a number (like v1.0.0) - VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/') - [ -z "$VERSION" ] && VERSION="0.0.1" - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "📦 Deploying version: $VERSION" - - - name: Prepare release stack manifest - run: | - mkdir -p artifacts - BCARDS_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} - - # Replace ${BCARDS_IMAGE} with actual image name using sed - sed "s|\${BCARDS_IMAGE}|${BCARDS_IMAGE}|g" deploy/docker-stack.release.yml > artifacts/docker-stack.release.yml - - echo "🔧 Generated manifest with image: ${BCARDS_IMAGE}" - echo "📄 Manifest content:" - head -10 artifacts/docker-stack.release.yml - - - name: Deploy to release swarm - run: | - echo "🚀 Deploying release stack to Orange Pi swarm..." - docker stack deploy -c artifacts/docker-stack.release.yml bcards-release - - - name: Await release service readiness - run: | - echo "⏳ Aguardando serviço bcards-release estabilizar..." - ATTEMPTS=30 - while [ $ATTEMPTS -gt 0 ]; do - REPLICAS=$(docker service ls --filter name=bcards-release_bcards-release --format '{{.Replicas}}') - if [ "$REPLICAS" = "1/1" ]; then - echo "✅ Serviço com $REPLICAS réplica" - break - fi - echo "Atual: ${REPLICAS:-N/A}; aguardando..." - sleep 5 - ATTEMPTS=$((ATTEMPTS-1)) - done - - if [ "$REPLICAS" != "1/1" ]; then - echo "❌ Serviço não atingiu 1/1 réplica" - docker service ps bcards-release_bcards-release - exit 1 - fi - - docker service ps bcards-release_bcards-release - - cleanup: - name: Cleanup Old Resources - runs-on: ubuntu-latest - needs: [deploy-production, deploy-test] - if: always() && (needs.deploy-production.result == 'success' || needs.deploy-test.result == 'success') - - steps: - - name: Cleanup containers and images - run: | - echo "🧹 Limpando recursos antigos..." - - if [ "${{ github.ref_name }}" = "main" ]; then - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts - ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts - ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded" - - for server in 141.148.162.114 129.146.116.218; do - echo "🧹 Limpando servidor $server..." - ssh -o StrictHostKeyChecking=no ubuntu@$server << 'EOF' - docker container prune -f - docker image prune -f - docker network prune -f - EOF - done - else - echo "ℹ️ Release branch: limpeza remota ignorada (Swarm gerencia recursos)." - fi - - echo "✅ Limpeza concluída!" - - deployment-summary: - name: Deployment Summary - runs-on: ubuntu-latest - needs: [deploy-production, deploy-test] - if: always() - - steps: - - name: Summary - run: | - echo "📋 DEPLOYMENT SUMMARY" - echo "====================" - echo "🎯 Branch: ${{ github.ref_name }}" - echo "🔑 Commit: ${{ github.sha }}" - echo "🏗️ Registry: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" - - if [ "${{ github.ref_name }}" = "main" ]; then - echo "🌍 Environment: Production (Swarm ARM)" - echo "🖥️ Servers: 141.148.162.114, 129.146.116.218" - echo "📦 Tag: latest" - echo "🔗 Status: ${{ needs.deploy-production.result }}" - else - echo "🌍 Environment: Release (Swarm ARM)" - echo "🖥️ Servers: 141.148.162.114, 129.146.116.218" - echo "📦 Branch Tag: ${{ github.ref_name }}" - echo "🔗 Status: ${{ needs.deploy-test.result }}" - fi - - echo "====================" - echo "✅ Pipeline completed!" +name: BCards Deployment Pipeline + +on: + push: + branches: + - main + - 'Release/*' + # PRs apenas validam, não fazem deploy + pull_request: + branches: [ main ] + types: [opened, synchronize, reopened] + +env: + REGISTRY: registry.redecarneir.us + IMAGE_NAME: bcards + MONGODB_HOST: 192.168.0.100:27017 + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + steps: + - name: Test info + run: | + echo "🧪 Executando testes para ${{ github.ref_name }}" + echo "🎯 Trigger: ${{ github.event_name }}" + + # Verificar se deve pular testes + SKIP_TESTS="${{ github.event.inputs.skip_tests || vars.SKIP_TESTS }}" + + if [ "$SKIP_TESTS" == "true" ]; then + echo "⚠️ Testes PULADOS" + echo "TESTS_SKIPPED=true" >> $GITHUB_ENV + else + echo "✅ Executando testes" + echo "TESTS_SKIPPED=false" >> $GITHUB_ENV + fi + + - name: Checkout code + if: env.TESTS_SKIPPED == 'false' + uses: actions/checkout@v4 + + - name: Setup .NET 8 + if: env.TESTS_SKIPPED == 'false' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Cache dependencies + if: env.TESTS_SKIPPED == 'false' + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + if: env.TESTS_SKIPPED == 'false' + run: dotnet restore + + - name: Build solution + if: env.TESTS_SKIPPED == 'false' + run: dotnet build --no-restore --configuration Release + + - name: Run unit tests + if: env.TESTS_SKIPPED == 'false' + run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage" + + # Job específico para validação de PRs (sem deploy) + pr-validation: + name: PR Validation + runs-on: ubuntu-latest + needs: [test] + if: github.event_name == 'pull_request' + + steps: + - name: PR Validation Summary + run: | + echo "✅ Pull Request Validation Summary" + echo "🎯 Target Branch: ${{ github.base_ref }}" + echo "📂 Source Branch: ${{ github.head_ref }}" + echo "🧪 Tests: ${{ needs.test.result }}" + echo "👤 Author: ${{ github.event.pull_request.user.login }}" + echo "📝 Title: ${{ github.event.pull_request.title }}" + echo "" + echo "✨ PR está pronto para merge!" + + build-and-push: + name: Build and Push Image + runs-on: [self-hosted, arm64, bcards] + needs: [test] + # Só faz build/push em push (não em PR) + if: github.event_name == 'push' && (needs.test.result == 'success' || needs.test.result == 'skipped') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/arm64 + + - name: Determine build settings + id: settings + run: | + BRANCH_NAME="${{ github.ref_name }}" + + if [ "$BRANCH_NAME" = "main" ]; then + # Main = Produção (ARM64) - usando Dockerfile simples + echo "tag=latest" >> $GITHUB_OUTPUT + echo "platform=linux/arm64" >> $GITHUB_OUTPUT + echo "environment=Production" >> $GITHUB_OUTPUT + echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT + echo "deploy_target=production" >> $GITHUB_OUTPUT + elif [[ "$BRANCH_NAME" == Release/* ]]; then + # Release = Swarm tests (Orange Pi arm64) - usando Dockerfile simples também + VERSION_RAW=${BRANCH_NAME#Release/} + # Only remove V/v if it's at the start and followed by a number (like v1.0.0) + VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/') + [ -z "$VERSION" ] && VERSION="0.0.1" + + echo "tag=$VERSION" >> $GITHUB_OUTPUT + echo "platform=linux/arm64" >> $GITHUB_OUTPUT + echo "environment=Testing" >> $GITHUB_OUTPUT + echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT + echo "deploy_target=testing" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi + + COMMIT_SHA=${{ github.sha }} + SHORT_COMMIT=${COMMIT_SHA:0:7} + echo "commit=$SHORT_COMMIT" >> $GITHUB_OUTPUT + + echo "📦 Tag: ${{ steps.settings.outputs.tag }}" + echo "🏗️ Platform: ${{ steps.settings.outputs.platform }}" + echo "🌍 Environment: ${{ steps.settings.outputs.environment }}" + echo "🎯 Target: ${{ steps.settings.outputs.deploy_target }}" + + - name: Build and push image + run: | + echo "🏗️ Building image for ${{ steps.settings.outputs.deploy_target }}..." + + # Debug das variáveis + echo "Platform: ${{ steps.settings.outputs.platform }}" + echo "Dockerfile: ${{ steps.settings.outputs.dockerfile }}" + echo "Tag: ${{ steps.settings.outputs.tag }}" + + # Verificar se o Dockerfile existe + if [ ! -f "${{ steps.settings.outputs.dockerfile }}" ]; then + echo "❌ Dockerfile não encontrado: ${{ steps.settings.outputs.dockerfile }}" + echo "📂 Arquivos na raiz:" + ls -la + echo "📂 Arquivos em src/BCards.Web/:" + ls -la src/BCards.Web/ || echo "Diretório não existe" + exit 1 + else + echo "✅ Dockerfile encontrado: ${{ steps.settings.outputs.dockerfile }}" + fi + + # Build para a plataforma correta + if [ "${{ steps.settings.outputs.deploy_target }}" = "production" ]; then + # Build para produção (main branch) + docker buildx build \ + --platform ${{ steps.settings.outputs.platform }} \ + --file ${{ steps.settings.outputs.dockerfile }} \ + --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ + --push \ + --progress=plain \ + . + else + # Build para staging (Release branches) + docker buildx build \ + --platform ${{ steps.settings.outputs.platform }} \ + --file ${{ steps.settings.outputs.dockerfile }} \ + --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ + --push \ + --build-arg VERSION=${{ steps.settings.outputs.version || 'latest' }} \ + --build-arg COMMIT=${{ steps.settings.outputs.commit }} \ + --progress=plain \ + . + fi + + deploy-production: + name: Deploy to Production (ARM - OCI) + runs-on: ubuntu-latest + needs: [build-and-push] + if: github.ref_name == 'main' + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Create Production Configuration + run: | + echo "🔧 Creating appsettings.Production.json with environment variables..." + + # Cria o arquivo de configuração para produção + cat > appsettings.Production.json << 'CONFIG_EOF' + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning", + "BCards": "Information", + "BCards.Web.Services.GridFSImageStorage": "Debug" + }, + "Console": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information" + } + }, + "File": { + "Path": "/app/logs/bcards-{Date}.log", + "LogLevel": { + "Default": "Information" + } + } + }, + "AllowedHosts": "*", + "Stripe": { + "PublishableKey": "${{ vars.STRIPE_PUBLISHABLE_KEY }}", + "SecretKey": "${{ secrets.STRIPE_SECRET_KEY }}", + "WebhookSecret": "${{ secrets.STRIPE_WEBHOOK_SECRET }}", + "Environment": "${{ vars.STRIPE_ENVIRONMENT || 'test' }}" + }, + "Authentication": { + "Google": { + "ClientId": "${{ vars.GOOGLE_CLIENT_ID }}", + "ClientSecret": "${{ secrets.GOOGLE_CLIENT_SECRET }}" + }, + "Microsoft": { + "ClientId": "${{ vars.MICROSOFT_CLIENT_ID }}", + "ClientSecret": "${{ secrets.MICROSOFT_CLIENT_SECRET }}" + } + }, + "SendGrid": { + "ApiKey": "${{ secrets.SENDGRID_API_KEY }}", + "FromEmail": "${{ vars.SENDGRID_FROM_EMAIL || 'ricardo.carneiro@jobmaker.com.br' }}", + "FromName": "${{ vars.SENDGRID_FROM_NAME || 'Ricardo Carneiro' }}" + }, + "Plans": { + "Basic": { + "Name": "Básico", + "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", + "Price": 5.90, + "MaxPages": 3, + "MaxLinks": 8, + "AllowPremiumThemes": false, + "AllowProductLinks": false, + "AllowAnalytics": true, + "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], + "Interval": "month" + }, + "Professional": { + "Name": "Profissional", + "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", + "Price": 12.90, + "MaxPages": 5, + "MaxLinks": 20, + "AllowPremiumThemes": false, + "AllowProductLinks": false, + "AllowAnalytics": true, + "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], + "Interval": "month" + }, + "Premium": { + "Name": "Premium", + "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", + "Price": 19.90, + "MaxPages": 15, + "MaxLinks": -1, + "AllowPremiumThemes": true, + "AllowProductLinks": false, + "AllowAnalytics": true, + "SpecialModeration": false, + "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ], + "Interval": "month" + }, + "PremiumAffiliate": { + "Name": "Premium+Afiliados", + "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", + "Price": 29.90, + "MaxPages": 15, + "MaxLinks": -1, + "AllowPremiumThemes": true, + "AllowProductLinks": true, + "AllowAnalytics": true, + "SpecialModeration": true, + "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], + "Interval": "month" + }, + "BasicYearly": { + "Name": "Básico Anual", + "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", + "Price": 59.00, + "MaxPages": 3, + "MaxLinks": 8, + "AllowPremiumThemes": false, + "AllowProductLinks": false, + "AllowAnalytics": true, + "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], + "Interval": "year" + }, + "ProfessionalYearly": { + "Name": "Profissional Anual", + "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", + "Price": 129.00, + "MaxPages": 5, + "MaxLinks": 20, + "AllowPremiumThemes": false, + "AllowProductLinks": false, + "AllowAnalytics": true, + "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], + "Interval": "year" + }, + "PremiumYearly": { + "Name": "Premium Anual", + "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", + "Price": 199.00, + "MaxPages": 15, + "MaxLinks": -1, + "AllowPremiumThemes": true, + "AllowProductLinks": false, + "AllowAnalytics": true, + "SpecialModeration": false, + "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], + "Interval": "year" + }, + "PremiumAffiliateYearly": { + "Name": "Premium+Afiliados Anual", + "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", + "Price": 299.00, + "MaxPages": 15, + "MaxLinks": -1, + "AllowPremiumThemes": true, + "AllowProductLinks": true, + "AllowAnalytics": true, + "SpecialModeration": true, + "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], + "Interval": "year" + } + }, + "Moderation": { + "PriorityTimeframes": { + "Trial": "7.00:00:00", + "Basic": "7.00:00:00", + "Professional": "3.00:00:00", + "Premium": "1.00:00:00" + }, + "MaxAttempts": 3, + "ModeratorEmail": "${{ vars.MODERATOR_EMAIL || 'ricardo.carneiro@jobmaker.com.br' }}", + "ModeratorEmails": [ + "${{ vars.MODERATOR_EMAIL_1 || 'rrcgoncalves@gmail.com' }}", + "${{ vars.MODERATOR_EMAIL_2 || 'rirocarneiro@gmail.com' }}" + ] + }, + "MongoDb": { + "ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/BCardsDB?replicaSet=rs0&authSource=admin", + "DatabaseName": "BCardsDB", + "MaxConnectionPoolSize": 100, + "ConnectTimeout": "30s", + "ServerSelectionTimeout": "30s", + "SocketTimeout": "30s" + }, + "BaseUrl": "https://bcards.site", + "Environment": { + "Name": "Production", + "IsStagingEnvironment": false, + "AllowTestData": false, + "EnableDetailedErrors": false + }, + "Performance": { + "EnableCaching": true, + "CacheExpirationMinutes": 30, + "EnableCompression": true, + "EnableResponseCaching": true + }, + "Security": { + "EnableHttpsRedirection": true, + "EnableHsts": true, + "RequireHttpsMetadata": true + }, + "HealthChecks": { + "Enabled": true, + "Endpoints": { + "Health": "/health", + "Ready": "/ready", + "Live": "/live" + }, + "MongoDb": { + "Enabled": true, + "Timeout": "10s" + } + }, + "Features": { + "EnablePreviewMode": true, + "EnableModerationWorkflow": true, + "EnableAnalytics": true, + "EnableFileUploads": true, + "MaxFileUploadSize": "5MB" + }, + "Serilog": { + "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}" + } + } + CONFIG_EOF + + echo "✅ Configuration file created!" + echo "🔍 File content (sensitive data masked):" + cat appsettings.Production.json | sed 's/"[^"]*_[0-9A-Za-z_]*"/"***MASKED***"/g' + + - name: Deploy to Production Swarm + run: | + echo "🚀 Deploying to production Docker Swarm (ARM64)..." + + # Configura SSH + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + + # Adiciona hosts conhecidos + ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts + ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts + + # Testa a chave SSH + ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded" + + # Upload configuration and stack file to swarm manager + echo "📤 Uploading files to Swarm manager..." + scp -o StrictHostKeyChecking=no appsettings.Production.json ubuntu@141.148.162.114:/tmp/ + scp -o StrictHostKeyChecking=no deploy/docker-stack.yml ubuntu@141.148.162.114:/tmp/ + + # Deploy to Docker Swarm + ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF' + set -e + + echo "🔄 Updating Docker Swarm stack..." + + # Update Docker config with new appsettings + echo "📝 Updating bcards-appsettings config..." + + # Remove old config (will fail if in use, that's ok - swarm will use it until update) + docker config rm bcards-appsettings 2>/dev/null || echo "Config in use or doesn't exist, will create new one" + + # Create new config with timestamp to force update + CONFIG_NAME="bcards-appsettings-$(date +%s)" + docker config create ${CONFIG_NAME} /tmp/appsettings.Production.json + + # Update stack file to use new config name + sed "s/bcards-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack.yml > /tmp/docker-stack-updated.yml + + echo "🐳 Deploying stack to Swarm (rolling update, zero downtime)..." + docker stack deploy -c /tmp/docker-stack-updated.yml bcards --with-registry-auth + + echo "⏳ Waiting for service to update..." + sleep 10 + + # Show service status + docker service ls --filter name=bcards_bcards-app + docker service ps bcards_bcards-app --no-trunc --filter "desired-state=running" | head -10 + + echo "🧹 Cleaning up standalone containers if they exist..." + docker stop bcards-prod 2>/dev/null || echo "No standalone container to stop" + docker rm bcards-prod 2>/dev/null || echo "No standalone container to remove" + + # Clean up temp files + rm -f /tmp/appsettings.Production.json /tmp/docker-stack.yml /tmp/docker-stack-updated.yml + + echo "✅ Swarm stack updated successfully!" + EOF + + - name: Health Check Production + run: | + echo "🏥 Verificando saúde dos servidores de produção..." + sleep 30 + + # Verifica Servidor 1 + echo "Verificando Servidor 1 (ARM)..." + ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 1 pode não estar respondendo"' + + # Verifica Servidor 2 + echo "Verificando Servidor 2 (ARM)..." + ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 2 pode não estar respondendo"' + + deploy-test: + name: Deploy to Release Swarm (ARM) + runs-on: [self-hosted, arm64, bcards] + needs: [build-and-push] + if: startsWith(github.ref_name, 'Release/') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version + id: version + run: | + BRANCH_NAME="${{ github.ref_name }}" + VERSION_RAW=${BRANCH_NAME#Release/} + # Only remove V/v if it's at the start and followed by a number (like v1.0.0) + VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/') + [ -z "$VERSION" ] && VERSION="0.0.1" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "📦 Deploying version: $VERSION" + + - name: Prepare release stack manifest + run: | + mkdir -p artifacts + BCARDS_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} + + # Replace ${BCARDS_IMAGE} with actual image name using sed + sed "s|\${BCARDS_IMAGE}|${BCARDS_IMAGE}|g" deploy/docker-stack.release.yml > artifacts/docker-stack.release.yml + + echo "🔧 Generated manifest with image: ${BCARDS_IMAGE}" + echo "📄 Manifest content:" + head -10 artifacts/docker-stack.release.yml + + - name: Deploy to release swarm + run: | + echo "🚀 Deploying release stack to Orange Pi swarm..." + docker stack deploy -c artifacts/docker-stack.release.yml bcards-release + + - name: Await release service readiness + run: | + echo "⏳ Aguardando serviço bcards-release estabilizar..." + ATTEMPTS=30 + while [ $ATTEMPTS -gt 0 ]; do + REPLICAS=$(docker service ls --filter name=bcards-release_bcards-release --format '{{.Replicas}}') + if [ "$REPLICAS" = "1/1" ]; then + echo "✅ Serviço com $REPLICAS réplica" + break + fi + echo "Atual: ${REPLICAS:-N/A}; aguardando..." + sleep 5 + ATTEMPTS=$((ATTEMPTS-1)) + done + + if [ "$REPLICAS" != "1/1" ]; then + echo "❌ Serviço não atingiu 1/1 réplica" + docker service ps bcards-release_bcards-release + exit 1 + fi + + docker service ps bcards-release_bcards-release + + cleanup: + name: Cleanup Old Resources + runs-on: ubuntu-latest + needs: [deploy-production, deploy-test] + if: always() && (needs.deploy-production.result == 'success' || needs.deploy-test.result == 'success') + + steps: + - name: Cleanup containers and images + run: | + echo "🧹 Limpando recursos antigos..." + + if [ "${{ github.ref_name }}" = "main" ]; then + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts + ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts + ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded" + + for server in 141.148.162.114 129.146.116.218; do + echo "🧹 Limpando servidor $server..." + ssh -o StrictHostKeyChecking=no ubuntu@$server << 'EOF' + docker container prune -f + docker image prune -f + docker network prune -f + EOF + done + else + echo "ℹ️ Release branch: limpeza remota ignorada (Swarm gerencia recursos)." + fi + + echo "✅ Limpeza concluída!" + + deployment-summary: + name: Deployment Summary + runs-on: ubuntu-latest + needs: [deploy-production, deploy-test] + if: always() + + steps: + - name: Summary + run: | + echo "📋 DEPLOYMENT SUMMARY" + echo "====================" + echo "🎯 Branch: ${{ github.ref_name }}" + echo "🔑 Commit: ${{ github.sha }}" + echo "🏗️ Registry: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + + if [ "${{ github.ref_name }}" = "main" ]; then + echo "🌍 Environment: Production (Swarm ARM)" + echo "🖥️ Servers: 141.148.162.114, 129.146.116.218" + echo "📦 Tag: latest" + echo "🔗 Status: ${{ needs.deploy-production.result }}" + else + echo "🌍 Environment: Release (Swarm ARM)" + echo "🖥️ Servers: 141.148.162.114, 129.146.116.218" + echo "📦 Branch Tag: ${{ github.ref_name }}" + echo "🔗 Status: ${{ needs.deploy-test.result }}" + fi + + echo "====================" + echo "✅ Pipeline completed!"