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.LUSLINKS_FROM_EMAIL (default: noreply@luslinks.site) # vars.LUSLINKS_FROM_NAME (default: LusLinks) 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_1RycPaBMIadsOxJVKioZZofK", "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_1RycQmBMIadsOxJVGqjVMaOj", "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_1RycRUBMIadsOxJVkxGOh3uu", "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_1RycTaBMIadsOxJVeDLseXQq", "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_1RycWgBMIadsOxJVGdtEeoMS", "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_1RycXdBMIadsOxJV5cNX7dHm", "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_1RycYnBMIadsOxJVPdKmzy4m", "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_1RycaEBMIadsOxJVEhsdB2Y1", "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." }, "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' scp -o StrictHostKeyChecking=no -r 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_1RycPaBMIadsOxJVKioZZofK", "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_1RycQmBMIadsOxJVGqjVMaOj", "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_1RycRUBMIadsOxJVkxGOh3uu", "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_1RycTaBMIadsOxJVeDLseXQq", "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_1RycWgBMIadsOxJVGdtEeoMS", "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_1RycXdBMIadsOxJV5cNX7dHm", "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_1RycYnBMIadsOxJVPdKmzy4m", "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_1RycaEBMIadsOxJVEhsdB2Y1", "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." }, "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' scp -o StrictHostKeyChecking=no -r 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: luslinks.site ──────────────────────────────────────────────── deploy-luslinks: name: Deploy luslinks.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 luslinks run: | cat > appsettings.luslinks.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.LUSLINKS_FROM_EMAIL || 'noreply@luslinks.site' }}", "FromName": "${{ vars.LUSLINKS_FROM_NAME || 'LusLinks' }}" }, "Plans": { "Basic": { "Name": "Básico", "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", "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_1RycQmBMIadsOxJVGqjVMaOj", "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_1RycRUBMIadsOxJVkxGOh3uu", "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_1RycTaBMIadsOxJVeDLseXQq", "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_1RycWgBMIadsOxJVGdtEeoMS", "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_1RycXdBMIadsOxJV5cNX7dHm", "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_1RycYnBMIadsOxJVPdKmzy4m", "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_1RycaEBMIadsOxJVEhsdB2Y1", "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/LusLinksDB?replicaSet=rs0&authSource=admin", "DatabaseName": "LusLinksDB" }, "BaseUrl": "https://luslinks.site", "Tenant": { "SiteName": "LusLinks", "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@luslinks.site", "ContentFolder": "luslinks", "AgeGated": false, "UrlExample": "luslinks.site/pastor/seu-nome", "DpoEmail": "dpo@luslinks.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 LusLinks 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." }, "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.luslinks.json gerado" - name: Deploy luslinks 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 luslinks content to $NODE..." ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/luslinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content' scp -o StrictHostKeyChecking=no -r src/BCards.Web/Content/Tenants/luslinks/. ubuntu@$NODE:/opt/bcards-content/luslinks/ done # ── Deploy stack on manager ───────────────────────────────────────── scp -o StrictHostKeyChecking=no appsettings.luslinks.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ scp -o StrictHostKeyChecking=no deploy/docker-stack-luslinks.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/ ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF' set -e echo "🔄 Updating luslinks stack..." docker config rm luslinks-appsettings 2>/dev/null || true CONFIG_NAME="luslinks-appsettings-$(date +%s)" docker config create ${CONFIG_NAME} /tmp/appsettings.luslinks.json sed "s/luslinks-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-luslinks.yml > /tmp/docker-stack-luslinks-final.yml docker stack deploy -c /tmp/docker-stack-luslinks-final.yml luslinks --with-registry-auth rm -f /tmp/appsettings.luslinks.json /tmp/docker-stack-luslinks.yml /tmp/docker-stack-luslinks-final.yml echo "✅ luslinks stack atualizado!" EOF - name: Health Check luslinks run: | sleep 30 ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \ 'curl -sf http://localhost:8083/health && echo "✅ luslinks healthy" || echo "⚠️ luslinks 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-luslinks, deploy-test] if: always() && (needs.deploy-bcards.result == 'success' || needs.deploy-spicylinks.result == 'success' || needs.deploy-luslinks.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 luslinks; 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-luslinks, 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 " luslinks.site → :8083 [${{ needs.deploy-luslinks.result }}]" else echo "🌍 Environment: Release (Test Swarm)" echo "🔗 Status: ${{ needs.deploy-test.result }}" fi echo "====================" echo "✅ Pipeline completed!"