BCards/.gitea/workflows/deploy-bcards.yml
Ricardo Carneiro d5cdbda5f4
Some checks failed
BCards Multi-Tenant Deployment Pipeline / Run Tests (push) Successful in 6s
BCards Multi-Tenant Deployment Pipeline / PR Validation (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Build and Push Image (push) Successful in 12m33s
BCards Multi-Tenant Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy bcards.site (push) Failing after 31s
BCards Multi-Tenant Deployment Pipeline / Deploy spicylinks.site (push) Successful in 1m3s
BCards Multi-Tenant Deployment Pipeline / Deploy luslinks.site (push) Successful in 1m3s
BCards Multi-Tenant Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deployment Summary (push) Successful in 0s
fix: deploy
2026-03-29 21:06:23 -03:00

727 lines
44 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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!"