name: BCards Multi-Tenant Deployment Pipeline # ─── Required Gitea Secrets ──────────────────────────────────────────────── # secrets.SSH_PRIVATE_KEY – SSH key for ubuntu@ on both OCI nodes # secrets.STRIPE_SECRET_KEY – Shared Stripe secret key # secrets.STRIPE_WEBHOOK_SECRET – Shared Stripe webhook secret # secrets.GOOGLE_CLIENT_SECRET – Google OAuth client secret # secrets.MICROSOFT_CLIENT_SECRET – Microsoft OAuth client secret # secrets.SENDGRID_API_KEY – SendGrid API key # # ─── Required Gitea Variables (vars.*) ──────────────────────────────────── # vars.STRIPE_PUBLISHABLE_KEY # vars.STRIPE_ENVIRONMENT (default: test) # vars.GOOGLE_CLIENT_ID # vars.MICROSOFT_CLIENT_ID # vars.OPENSEARCH_URL (default: http://localhost:9201) # vars.MODERATOR_EMAIL # vars.MODERATOR_EMAIL_1 # vars.MODERATOR_EMAIL_2 # # ─── Per-Tenant Variables (optional, have defaults) ─────────────────────── # vars.SPICYLINKS_FROM_EMAIL (default: noreply@spicylinks.site) # vars.SPICYLINKS_FROM_NAME (default: SpicyLinks) # vars.LUZLINKS_FROM_EMAIL (default: noreply@luzlinks.site) # vars.LUZLINKS_FROM_NAME (default: LuzLinks) on: push: branches: - main - 'Release/*' pull_request: branches: [ main ] types: [opened, synchronize, reopened] env: REGISTRY: registry.redecarneir.us IMAGE_NAME: bcards SWARM_MANAGER: 141.148.162.114 SWARM_WORKER: 129.146.116.218 jobs: # ─── Tests ──────────────────────────────────────────────────────────────── test: name: Run Tests runs-on: ubuntu-latest steps: - name: Test info run: | echo "🧪 Executando testes para ${{ github.ref_name }}" 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" # ─── PR Validation (no 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: ${{ github.base_ref }}" echo "📂 Source: ${{ github.head_ref }}" echo "🧪 Tests: ${{ needs.test.result }}" echo "✨ PR está pronto para merge!" # ─── Build & Push (single image, all tenants share it) ─────────────────── build-and-push: name: Build and Push Image runs-on: [self-hosted, arm64, bcards] needs: [test] 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 echo "tag=latest" >> $GITHUB_OUTPUT echo "environment=Production" >> $GITHUB_OUTPUT echo "deploy_target=production" >> $GITHUB_OUTPUT elif [[ "$BRANCH_NAME" == Release/* ]]; then VERSION_RAW=${BRANCH_NAME#Release/} VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/') [ -z "$VERSION" ] && VERSION="0.0.1" echo "tag=$VERSION" >> $GITHUB_OUTPUT echo "environment=Testing" >> $GITHUB_OUTPUT echo "deploy_target=testing" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT fi SHORT_COMMIT=${GITHUB_SHA:0:7} echo "commit=$SHORT_COMMIT" >> $GITHUB_OUTPUT - name: Build and push image run: | echo "🏗️ Building image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }}" if [ "${{ steps.settings.outputs.deploy_target }}" = "production" ]; then docker buildx build \ --platform linux/arm64 \ --file Dockerfile \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ --push \ --progress=plain \ . else docker buildx build \ --platform linux/arm64 \ --file Dockerfile \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \ --push \ --build-arg BUILD_CONFIGURATION=Testing \ --build-arg VERSION=${{ steps.settings.outputs.version || 'latest' }} \ --build-arg COMMIT=${{ steps.settings.outputs.commit }} \ --progress=plain \ . fi # ─── Deploy: bcards.site ────────────────────────────────────────────────── deploy-bcards: name: Deploy bcards.site runs-on: ubuntu-latest needs: [build-and-push] if: github.ref_name == 'main' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Generate appsettings for bcards run: | cat > appsettings.bcards.json << 'CONFIG_EOF' { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "BCards": "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_1TR10MBk8jHwC3c0iey23Ghb", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" }, "Professional": { "Name": "Profissional", "PriceId": "price_1TR10NBk8jHwC3c0yqmy8soD", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" }, "Premium": { "Name": "Premium", "PriceId": "price_1TR10OBk8jHwC3c0eZa77y31", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)" ], "Interval": "month" }, "PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10PBk8jHwC3c0B1oIvvYY", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)" ], "Interval": "month" }, "BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10NBk8jHwC3c0L4SDaWe9", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10OBk8jHwC3c0IuyvrvRf", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10PBk8jHwC3c0qngPYMUN", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" }, "PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10QBk8jHwC3c0f8CBaD1n", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)", "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" }, "BaseUrl": "https://bcards.site", "Tenant": { "SiteName": "BCards", "SiteDescription": "Crie sua página profissional com links organizados. A melhor alternativa para ter sua bio / links. Criada para profissionais e empresas no Brasil.", "Tagline": "Sua bio de links profissional", "SupportEmail": "suporte@bcards.site", "ContentFolder": "bcards", "AgeGated": false, "UrlExample": "bcards.site/corretor/seu-nome", "DpoEmail": "dpo@bcards.site", "HeroHeadline": "Sua presença digital profissional em um só lugar", "HeroDescription": "Organize seus links, portfólio, contatos e redes sociais em uma página única e elegante. Criado para profissionais e empresas brasileiras que querem ser encontrados.", "HeroCtaText": "Criar Minha Página Grátis", "FeaturesHeadline": "Por que profissionais escolhem o {SiteName}?", "Features": [ { "Icon": "🎨", "Title": "Temas Profissionais", "Description": "Mais de 40 temas para sua área: corretores, advogados, médicos, consultores e muito mais." }, { "Icon": "📊", "Title": "Analytics de Verdade", "Description": "Saiba quantas pessoas acessaram sua página, quais links clicaram e de onde vieram." }, { "Icon": "🔗", "Title": "URLs com Credibilidade", "Description": "Sua URL tem contexto profissional: bcards.site/corretor/seu-nome — transmite autoridade instantânea." } ], "CtaHeadline": "Pronto para se destacar?", "CtaDescription": "Junte-se a milhares de profissionais que já têm sua presença digital organizada no BCards.", "CtaButtonText": "Criar Minha Página Grátis", "MetaKeywords": "cartão digital, página de links, bio links, linktree brasil, página profissional, corretor, advogado, médico, consultor", "FooterTagline": "Sua presença digital profissional, simplificada.", "HeroGradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", "PrimaryColor": "#667eea", "PrimaryColorDark": "#5a6fd6" }, "Support": { "TelegramUrl": "https://t.me/jobmakerbr", "FormspreeUrl": "https://formspree.io/f/xpwynqpj", "EnableTelegramForPlans": [ "Premium", "PremiumAffiliate" ], "EnableFormForPlans": [ "Basic", "Professional", "Premium", "PremiumAffiliate" ], "EnableRatingForAllUsers": true }, "Serilog": { "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://141.148.162.114:19201' }}" } } CONFIG_EOF echo "✅ appsettings.bcards.json gerado" - name: Deploy bcards stack run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H ${{ env.SWARM_WORKER }} >> ~/.ssh/known_hosts 2>/dev/null # ── Sync Content to both nodes ────────────────────────────────────── for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do echo "📂 Syncing content to $NODE..." ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/bcards && sudo chown -R ubuntu:ubuntu /opt/bcards-content' rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/bcards/ ubuntu@$NODE:/opt/bcards-content/bcards/ done # ── Deploy stack on manager ───────────────────────────────────────── scp -o StrictHostKeyChecking=no appsettings.bcards.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ scp -o StrictHostKeyChecking=no deploy/docker-stack-bcards.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF' set -e echo "🔄 Updating bcards stack..." docker config rm bcards-appsettings 2>/dev/null || true CONFIG_NAME="bcards-appsettings-$(date +%s)" docker config create ${CONFIG_NAME} /tmp/appsettings.bcards.json sed "s/bcards-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-bcards.yml > /tmp/docker-stack-bcards-final.yml docker stack deploy -c /tmp/docker-stack-bcards-final.yml bcards --with-registry-auth rm -f /tmp/appsettings.bcards.json /tmp/docker-stack-bcards.yml /tmp/docker-stack-bcards-final.yml echo "✅ bcards stack atualizado!" EOF - name: Health Check bcards run: | sleep 30 ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \ 'curl -sf http://localhost:8080/health && echo "✅ bcards healthy" || echo "⚠️ bcards health check failed"' # ─── Deploy: spicylinks.site ────────────────────────────────────────────── deploy-spicylinks: name: Deploy spicylinks.site runs-on: ubuntu-latest needs: [build-and-push] if: github.ref_name == 'main' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Generate appsettings for spicylinks run: | cat > appsettings.spicylinks.json << 'CONFIG_EOF' { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "BCards": "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.SPICYLINKS_FROM_EMAIL || 'noreply@spicylinks.site' }}", "FromName": "${{ vars.SPICYLINKS_FROM_NAME || 'SpicyLinks' }}" }, "Plans": { "Basic": { "Name": "Básico", "PriceId": "price_1TR10QBk8jHwC3c06u8j4XVY", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" }, "Professional": { "Name": "Profissional", "PriceId": "price_1TR10RBk8jHwC3c0KfunnYYn", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" }, "Premium": { "Name": "Premium", "PriceId": "price_1TR10SBk8jHwC3c0gMqUEp7m", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" }, "PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10TBk8jHwC3c0uPOZVZ4P", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" }, "BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10RBk8jHwC3c0C8aOMAYE", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10SBk8jHwC3c0X7LBy3UU", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10TBk8jHwC3c0TaDIA6bD", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" }, "PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10UBk8jHwC3c0NF66MzC7", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "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/SpicyLinksDB?replicaSet=rs0&authSource=admin", "DatabaseName": "SpicyLinksDB" }, "BaseUrl": "https://spicylinks.site", "Tenant": { "SiteName": "SpicyLinks", "SiteDescription": "A plataforma discreta e segura para criadores de conteúdo adulto. Reúna suas assinaturas, lista de desejos, redes sociais e conteúdo exclusivo em uma única bio.", "Tagline": "Seu conteúdo exclusivo, um link só", "SupportEmail": "suporte@spicylinks.site", "ContentFolder": "spicylinks", "AgeGated": true, "UrlExample": "spicylinks.site/modelo/seu-nome", "DpoEmail": "dpo@spicylinks.site", "HeroHeadline": "Seu conteúdo exclusivo, um link só", "HeroDescription": "A plataforma discreta e segura para criadores de conteúdo adulto. Reúna suas assinaturas, lista de desejos, redes e conteúdo exclusivo em uma bio elegante.", "HeroCtaText": "Criar Minha Bio", "FeaturesHeadline": "Por que criadores escolhem o {SiteName}?", "Features": [ { "Icon": "❤️", "Title": "Tudo num Só Link", "Description": "Instagram, Twitter/X, OnlyFans, lista de desejos e mais — tudo em uma bio única, elegante e fácil de compartilhar." }, { "Icon": "🔒", "Title": "Verificação de Idade", "Description": "Acesso protegido com verificação de idade automática. Plataforma segura, discreta e responsável." }, { "Icon": "📊", "Title": "Saiba Quem Te Visita", "Description": "Analytics detalhado de cliques, visualizações e origem do tráfego para otimizar suas conversões." } ], "CtaHeadline": "Pronta para monetizar seu conteúdo?", "CtaDescription": "Milhares de criadoras já centralizam seus links e aumentam suas conversões com o SpicyLinks.", "CtaButtonText": "Criar Minha Bio", "MetaKeywords": "bio links criadora, creator bio, linktree conteudo adulto, links onlyfans, bio instagram criadora", "FooterTagline": "Seu conteúdo, sua identidade.", "HeroGradient": "linear-gradient(135deg, #ff416c 0%, #c0392b 100%)", "PrimaryColor": "#e63946", "PrimaryColorDark": "#c1121f", "DefaultCategories": [ { "Icon": "📸", "Name": "Modelos", "Slug": "modelos", "Description": "Modelos e criadores de conteúdo visual", "SeoKeywords": [ "modelo", "fotografia", "conteúdo", "criadora" ] }, { "Icon": "⭐", "Name": "Influencers", "Slug": "influencers","Description": "Influencers e personalidades digitais", "SeoKeywords": [ "influencer", "digital", "social media" ] }, { "Icon": "💪", "Name": "Fitness", "Slug": "fitness", "Description": "Criadores de conteúdo fitness e lifestyle", "SeoKeywords": [ "fitness", "academia", "saúde", "corpo" ] }, { "Icon": "🎨", "Name": "Arte", "Slug": "arte", "Description": "Artistas e criadores de conteúdo visual", "SeoKeywords": [ "arte", "ilustração", "design", "criativo" ] }, { "Icon": "🎵", "Name": "Música", "Slug": "musica", "Description": "Músicos e cantores independentes", "SeoKeywords": [ "música", "cantor", "artista", "show" ] }, { "Icon": "🎮", "Name": "Gaming", "Slug": "gaming", "Description": "Streamers e criadores de conteúdo gamer", "SeoKeywords": [ "gaming", "streamer", "games", "twitch" ] }, { "Icon": "🦸", "Name": "Cosplay", "Slug": "cosplay", "Description": "Cosplayers e criadores de fantasia", "SeoKeywords": [ "cosplay", "anime", "fantasia", "cosplayer" ] }, { "Icon": "💋", "Name": "Lifestyle", "Slug": "lifestyle", "Description": "Criadores de conteúdo lifestyle e entretenimento", "SeoKeywords": [ "lifestyle", "entretenimento", "diversão" ] } ], "AllowedLinkTypes": [ { "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite o domínio e caminho", "Color": "bg-primary" }, { "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "seuemail@exemplo.com", "Instructions": "Digite apenas o email", "Color": "bg-success" }, { "Icon": "fas fa-phone", "Label": "📞 Telefone", "Prefix": "tel:", "Placeholder": "5511999999999", "Instructions": "Número com código do país", "Color": "bg-success" }, { "Icon": "fab fa-instagram", "Label": "📸 Instagram", "Prefix": "https://instagram.com/","Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-danger" }, { "Icon": "fab fa-twitter", "Label": "🐦 Twitter/X", "Prefix": "https://x.com/", "Placeholder": "seu_usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" }, { "Icon": "fab fa-tiktok", "Label": "🎵 TikTok", "Prefix": "https://tiktok.com/@", "Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" }, { "Icon": "fas fa-shopping-cart","Label": "🛒 Lista de Desejos","Prefix": "https://", "Placeholder": "wishlist.com/...", "Instructions": "Link para lista de desejos", "Color": "bg-warning" }, { "Icon": "fas fa-heart", "Label": "❤️ Assinatura", "Prefix": "https://", "Placeholder": "plataforma.com/...", "Instructions": "Link para plataforma paga", "Color": "bg-danger" } ] }, "Support": { "TelegramUrl": "https://t.me/jobmakerbr", "FormspreeUrl": "https://formspree.io/f/xpwynqpj", "EnableTelegramForPlans": [ "Premium", "PremiumAffiliate" ], "EnableFormForPlans": [ "Basic", "Professional", "Premium", "PremiumAffiliate" ], "EnableRatingForAllUsers": true }, "Serilog": { "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://141.148.162.114:19201' }}" } } CONFIG_EOF echo "✅ appsettings.spicylinks.json gerado" - name: Deploy spicylinks stack run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H ${{ env.SWARM_WORKER }} >> ~/.ssh/known_hosts 2>/dev/null # ── Sync Content to both nodes ────────────────────────────────────── for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do echo "📂 Syncing spicylinks content to $NODE..." ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/spicylinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content' rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/spicylinks/ ubuntu@$NODE:/opt/bcards-content/spicylinks/ done # ── Deploy stack on manager ───────────────────────────────────────── scp -o StrictHostKeyChecking=no appsettings.spicylinks.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ scp -o StrictHostKeyChecking=no deploy/docker-stack-spicylinks.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF' set -e echo "🔄 Updating spicylinks stack..." docker config rm spicylinks-appsettings 2>/dev/null || true CONFIG_NAME="spicylinks-appsettings-$(date +%s)" docker config create ${CONFIG_NAME} /tmp/appsettings.spicylinks.json sed "s/spicylinks-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-spicylinks.yml > /tmp/docker-stack-spicylinks-final.yml docker stack deploy -c /tmp/docker-stack-spicylinks-final.yml spicylinks --with-registry-auth rm -f /tmp/appsettings.spicylinks.json /tmp/docker-stack-spicylinks.yml /tmp/docker-stack-spicylinks-final.yml echo "✅ spicylinks stack atualizado!" EOF - name: Health Check spicylinks run: | sleep 30 ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \ 'curl -sf http://localhost:8082/health && echo "✅ spicylinks healthy" || echo "⚠️ spicylinks health check failed"' # ─── Deploy: luzlinks.site ──────────────────────────────────────────────── deploy-luzlinks: name: Deploy luzlinks.site runs-on: ubuntu-latest needs: [build-and-push] if: github.ref_name == 'main' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Generate appsettings for luzlinks run: | cat > appsettings.luzlinks.json << 'CONFIG_EOF' { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "BCards": "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.LUZLINKS_FROM_EMAIL || 'noreply@luzlinks.site' }}", "FromName": "${{ vars.LUZLINKS_FROM_NAME || 'LuzLinks' }}" }, "Plans": { "Basic": { "Name": "Básico", "PriceId": "price_1TR10UBk8jHwC3c0C9UJTNYg", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" }, "Professional": { "Name": "Profissional", "PriceId": "price_1TR10VBk8jHwC3c0v6nlFB0R", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" }, "Premium": { "Name": "Premium", "PriceId": "price_1TR10WBk8jHwC3c0QigqeR7b", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" }, "PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10XBk8jHwC3c03SfR3Z4v", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" }, "BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10VBk8jHwC3c000wYtGxR", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10WBk8jHwC3c0zCitaA4j", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "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_1TR10XBk8jHwC3c0MSZFXh7x", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" }, "PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10YBk8jHwC3c0EzYrA0PJ", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "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/LuzLinksDB?replicaSet=rs0&authSource=admin", "DatabaseName": "LuzLinksDB" }, "BaseUrl": "https://luzlinks.site", "Tenant": { "SiteName": "LuzLinks", "SiteDescription": "A plataforma para pastores, padres, líderes religiosos e ministérios. Reúna seus estudos bíblicos, eventos, lives e canal em uma única página de fé.", "Tagline": "Conecte sua comunidade em um único link", "SupportEmail": "suporte@luzlinks.site", "ContentFolder": "luzlinks", "AgeGated": false, "UrlExample": "luzlinks.site/pastor/seu-nome", "DpoEmail": "dpo@luzlinks.site", "HeroHeadline": "Conecte sua comunidade em um único link", "HeroDescription": "A plataforma ideal para pastores, padres, líderes e ministérios. Reúna seus estudos bíblicos, agenda de cultos, canal e dízimos em uma só página.", "HeroCtaText": "Criar Minha Bio de Fé", "FeaturesHeadline": "Por que líderes religiosos usam o {SiteName}?", "Features": [ { "Icon": "📖", "Title": "Conteúdo Espiritual Organizado", "Description": "Concentre seus estudos bíblicos, séries de pregações, agenda de cultos e canal do YouTube em um só link." }, { "Icon": "🙏", "Title": "Facilite Dízimos e Ofertas", "Description": "Link direto para doações do seu ministério. Simplifique as ofertas e dízimos da sua congregação." }, { "Icon": "📅", "Title": "Agenda e Eventos", "Description": "Compartilhe retiros, cultos especiais e eventos com toda a comunidade de forma simples e organizada." } ], "CtaHeadline": "Compartilhe sua mensagem com o mundo", "CtaDescription": "Líderes de toda denominação já usam o LuzLinks para alcançar mais pessoas com sua mensagem de fé.", "CtaButtonText": "Criar Minha Bio de Fé", "MetaKeywords": "bio links pastor, página ministério, linktree cristão, links religiosos, página iglesia, bio pastor, links igreja", "FooterTagline": "Conectando fé e comunidade.", "HeroGradient": "linear-gradient(135deg, #5b9bd5 0%, #1a5276 100%)", "PrimaryColor": "#2471a3", "PrimaryColorDark": "#1a5276", "DefaultCategories": [ { "Icon": "🙏", "Name": "Pastores", "Slug": "pastor", "Description": "Pastores evangélicos, protestantes e pentecostais", "SeoKeywords": [ "pastor", "evangélico", "protestante", "pentecostal", "pregador" ] }, { "Icon": "✝️", "Name": "Padres", "Slug": "padre", "Description": "Sacerdotes, padres e religiosos da Igreja Católica", "SeoKeywords": [ "padre", "sacerdote", "católico", "pároco", "religioso" ] }, { "Icon": "⛪", "Name": "Igrejas", "Slug": "igreja", "Description": "Congregações, comunidades de fé e denominações", "SeoKeywords": [ "igreja", "congregação", "comunidade", "denominação", "templo" ] }, { "Icon": "🌟", "Name": "Ministérios", "Slug": "ministerio", "Description": "Ministérios, organizações e missões cristãs", "SeoKeywords": [ "ministério", "missão", "organização cristã", "obra" ] }, { "Icon": "🎵", "Name": "Louvor e Adoração", "Slug": "louvor", "Description": "Ministérios de louvor, bandas gospel e cantores cristãos", "SeoKeywords": [ "louvor", "adoração", "gospel", "banda", "música cristã" ] }, { "Icon": "👨‍👩‍👧", "Name": "Família e Jovens", "Slug": "familia", "Description": "Líderes de grupos de jovens, casais e família", "SeoKeywords": [ "jovens", "família", "casais", "célula", "grupo" ] }, { "Icon": "📖", "Name": "Estudos Bíblicos", "Slug": "estudos", "Description": "Mestres, professores bíblicos e teólogos", "SeoKeywords": [ "estudo bíblico", "teologia", "mestre", "professor", "bíblia" ] }, { "Icon": "🌍", "Name": "Missionários", "Slug": "missionario","Description": "Missionários e evangelistas nacionais e internacionais", "SeoKeywords": [ "missionário", "evangelista", "evangelismo", "missões" ] }, { "Icon": "📻", "Name": "Mídia Cristã", "Slug": "midia", "Description": "Podcasts, canais, rádios e comunicação cristã", "SeoKeywords": [ "podcast", "canal cristão", "rádio evangélica", "mídia" ] }, { "Icon": "🤝", "Name": "Assistência Social", "Slug": "assistencia","Description": "Projetos sociais, pastorais de assistência e ONGs cristãs","SeoKeywords": [ "assistência social", "projeto social", "ONG", "pastoral" ] } ], "AllowedLinkTypes": [ { "Icon": "fas fa-globe", "Label": "🌐 Site / Ministério", "Prefix": "https://", "Placeholder": "ministerio.com.br", "Instructions": "Digite o domínio do site", "Color": "bg-primary" }, { "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "contato@ministerio.com", "Instructions": "Digite apenas o email", "Color": "bg-success" }, { "Icon": "fas fa-phone", "Label": "📞 Telefone / WhatsApp", "Prefix": "tel:", "Placeholder": "5511999999999", "Instructions": "Número com código do país", "Color": "bg-success" }, { "Icon": "fab fa-youtube", "Label": "📺 YouTube", "Prefix": "https://youtube.com/", "Placeholder": "@canal ou c/CANAL", "Instructions": "Digite o canal ou @usuário", "Color": "bg-danger" }, { "Icon": "fab fa-instagram", "Label": "📸 Instagram", "Prefix": "https://instagram.com/", "Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-danger" }, { "Icon": "fas fa-book", "Label": "📖 Estudo / Série", "Prefix": "https://", "Placeholder": "link-do-estudo.com", "Instructions": "Link para estudo bíblico ou série", "Color": "bg-info" }, { "Icon": "fas fa-calendar", "Label": "📅 Agenda / Eventos", "Prefix": "https://", "Placeholder": "calendly.com/seunome", "Instructions": "Link para agenda ou evento", "Color": "bg-warning" }, { "Icon": "fas fa-donate", "Label": "🙏 Dízimos / Ofertas", "Prefix": "https://", "Placeholder": "pix.com.br/ministerio", "Instructions": "Link para doações ou dízimos", "Color": "bg-success" }, { "Icon": "fas fa-map-marker-alt","Label": "📍 Localização", "Prefix": "https://maps.google.com/?q=", "VisualPrefix": "📍 Maps:", "Placeholder": "Rua da Igreja, 123", "Instructions": "Endereço da igreja/ministério", "Color": "bg-warning" }, { "Icon": "fas fa-download", "Label": "⬇️ Material / Apostila","Prefix": "https://", "Placeholder": "drive.google.com/...", "Instructions": "Link para download de material", "Color": "bg-secondary" } ] }, "Support": { "TelegramUrl": "https://t.me/jobmakerbr", "FormspreeUrl": "https://formspree.io/f/xpwynqpj", "EnableTelegramForPlans": [ "Premium", "PremiumAffiliate" ], "EnableFormForPlans": [ "Basic", "Professional", "Premium", "PremiumAffiliate" ], "EnableRatingForAllUsers": true }, "Serilog": { "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://141.148.162.114:19201' }}" } } CONFIG_EOF echo "✅ appsettings.luzlinks.json gerado" - name: Deploy luzlinks stack run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H ${{ env.SWARM_WORKER }} >> ~/.ssh/known_hosts 2>/dev/null # ── Sync Content to both nodes ────────────────────────────────────── for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do echo "📂 Syncing luzlinks content to $NODE..." ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/luzlinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content' rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/luzlinks/ ubuntu@$NODE:/opt/bcards-content/luzlinks/ done # ── Deploy stack on manager ───────────────────────────────────────── scp -o StrictHostKeyChecking=no appsettings.luzlinks.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ scp -o StrictHostKeyChecking=no deploy/docker-stack-luzlinks.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF' set -e echo "🔄 Updating luzlinks stack..." docker config rm luzlinks-appsettings 2>/dev/null || true CONFIG_NAME="luzlinks-appsettings-$(date +%s)" docker config create ${CONFIG_NAME} /tmp/appsettings.luzlinks.json sed "s/luzlinks-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-luzlinks.yml > /tmp/docker-stack-luzlinks-final.yml docker stack deploy -c /tmp/docker-stack-luzlinks-final.yml luzlinks --with-registry-auth rm -f /tmp/appsettings.luzlinks.json /tmp/docker-stack-luzlinks.yml /tmp/docker-stack-luzlinks-final.yml echo "✅ luzlinks stack atualizado!" EOF - name: Health Check luzlinks run: | sleep 30 ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \ 'curl -sf http://localhost:8083/health && echo "✅ luzlinks healthy" || echo "⚠️ luzlinks health check failed"' # ─── Release branch deploy (test swarm) ─────────────────────────────────── 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/} 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 }} sed "s|\${BCARDS_IMAGE}|${BCARDS_IMAGE}|g" deploy/docker-stack.release.yml > artifacts/docker-stack.release.yml echo "🔧 Generated manifest with image: ${BCARDS_IMAGE}" - name: Deploy to release swarm run: | 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 ────────────────────────────────────────────────────────────── cleanup: name: Cleanup Old Resources runs-on: ubuntu-latest needs: [deploy-bcards, deploy-spicylinks, deploy-luzlinks, deploy-test] if: always() && (needs.deploy-bcards.result == 'success' || needs.deploy-spicylinks.result == 'success' || needs.deploy-luzlinks.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 ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null ssh-keyscan -H ${{ env.SWARM_WORKER }} >> ~/.ssh/known_hosts 2>/dev/null for SERVER in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do echo "🧹 Limpando $SERVER..." ssh -o StrictHostKeyChecking=no ubuntu@$SERVER << 'EOF' docker container prune -f docker image prune -f docker network prune -f # Remove stale swarm configs (keep last 3 per tenant) for TENANT in bcards spicylinks luzlinks; do docker config ls --filter "name=${TENANT}-appsettings" --format "{{.ID}} {{.Name}}" \ | sort -k2 | head -n -3 | awk '{print $1}' \ | xargs -r docker config rm 2>/dev/null || true done EOF done else echo "ℹ️ Release branch: limpeza remota ignorada." fi echo "✅ Limpeza concluída!" # ─── Summary ────────────────────────────────────────────────────────────── deployment-summary: name: Deployment Summary runs-on: ubuntu-latest needs: [deploy-bcards, deploy-spicylinks, deploy-luzlinks, 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 (Multi-Tenant)" echo "🖥️ Manager: ${{ env.SWARM_MANAGER }}" echo "🖥️ Worker: ${{ env.SWARM_WORKER }}" echo "" echo " bcards.site → :8080 [${{ needs.deploy-bcards.result }}]" echo " spicylinks.site → :8082 [${{ needs.deploy-spicylinks.result }}]" echo " luzlinks.site → :8083 [${{ needs.deploy-luzlinks.result }}]" else echo "🌍 Environment: Release (Test Swarm)" echo "🔗 Status: ${{ needs.deploy-test.result }}" fi echo "====================" echo "✅ Pipeline completed!"