From 72e2a220e503d6e791e34af89346a4f2d0843e56 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Mon, 27 Apr 2026 23:21:07 -0300 Subject: [PATCH] feat: atualiza price_ids Stripe para nova conta (acct_1PvIzkBk8jHwC3c0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migra todos os price_ids de teste (conta BMIadsOxJV) para preços live na conta MCP oficial. 12 produtos e 24 preços criados (4 planos × mensal/anual × 3 tenants: BCards, SpicyLinks, LusLinks). Co-Authored-By: Claude Sonnet 4.6 --- .gitea/workflows/deploy-bcards.yml | 1452 ++++++++++++++-------------- src/BCards.Web/appsettings.json | 16 +- 2 files changed, 734 insertions(+), 734 deletions(-) diff --git a/.gitea/workflows/deploy-bcards.yml b/.gitea/workflows/deploy-bcards.yml index 5003c2d..22be7c3 100644 --- a/.gitea/workflows/deploy-bcards.yml +++ b/.gitea/workflows/deploy-bcards.yml @@ -1,726 +1,726 @@ -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' - 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_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' - 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: 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' - rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" 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!" +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_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." + }, + "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." + }, + "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: 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_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/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' + rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" 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!" diff --git a/src/BCards.Web/appsettings.json b/src/BCards.Web/appsettings.json index b9df020..de9befd 100644 --- a/src/BCards.Web/appsettings.json +++ b/src/BCards.Web/appsettings.json @@ -21,7 +21,7 @@ "Plans": { "Basic": { "Name": "Básico", - "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", + "PriceId": "price_1TR10MBk8jHwC3c0iey23Ghb", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, @@ -35,7 +35,7 @@ }, "Professional": { "Name": "Profissional", - "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", + "PriceId": "price_1TR10NBk8jHwC3c0yqmy8soD", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, @@ -49,7 +49,7 @@ }, "Premium": { "Name": "Premium", - "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", + "PriceId": "price_1TR10OBk8jHwC3c0eZa77y31", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, @@ -64,7 +64,7 @@ }, "PremiumAffiliate": { "Name": "Premium+Afiliados", - "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", + "PriceId": "price_1TR10PBk8jHwC3c0B1oIvvYY", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, @@ -79,7 +79,7 @@ }, "BasicYearly": { "Name": "Básico Anual", - "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", + "PriceId": "price_1TR10NBk8jHwC3c0L4SDaWe9", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, @@ -93,7 +93,7 @@ }, "ProfessionalYearly": { "Name": "Profissional Anual", - "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", + "PriceId": "price_1TR10OBk8jHwC3c0IuyvrvRf", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, @@ -107,7 +107,7 @@ }, "PremiumYearly": { "Name": "Premium Anual", - "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", + "PriceId": "price_1TR10PBk8jHwC3c0qngPYMUN", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, @@ -122,7 +122,7 @@ }, "PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", - "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", + "PriceId": "price_1TR10QBk8jHwC3c0f8CBaD1n", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1,