BCards/.gitea/workflows/deploy-bcards.yml
Ricardo Carneiro 36163b606d
Some checks failed
BCards Multi-Tenant Deployment Pipeline / Run Tests (push) Successful in 5s
BCards Multi-Tenant Deployment Pipeline / PR Validation (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Build and Push Image (push) Successful in 13m31s
BCards Multi-Tenant Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deploy bcards.site (push) Successful in 1m3s
BCards Multi-Tenant Deployment Pipeline / Deploy spicylinks.site (push) Successful in 1m3s
BCards Multi-Tenant Deployment Pipeline / Deploy luzlinks.site (push) Failing after 31s
BCards Multi-Tenant Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Multi-Tenant Deployment Pipeline / Deployment Summary (push) Successful in 0s
fix: luz links e artigos
2026-04-30 21:28:28 -03:00

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