BCards/.gitea/workflows/deploy-bcards.yml
Ricardo Carneiro e1c1f38a34
Some checks failed
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Blocked by required conditions
BCards Deployment Pipeline / Deploy to Release Swarm (ARM) (push) Blocked by required conditions
BCards Deployment Pipeline / Cleanup Old Resources (push) Blocked by required conditions
BCards Deployment Pipeline / Deployment Summary (push) Blocked by required conditions
BCards Deployment Pipeline / Run Tests (push) Successful in 2s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Has been cancelled
fix: checkout
2025-10-27 21:18:21 -03:00

614 lines
23 KiB
YAML
Raw Permalink 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 Deployment Pipeline
on:
push:
branches:
- main
- 'Release/*'
# PRs apenas validam, não fazem deploy
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]
env:
REGISTRY: registry.redecarneir.us
IMAGE_NAME: bcards
MONGODB_HOST: 192.168.0.100:27017
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Test info
run: |
echo "🧪 Executando testes para ${{ github.ref_name }}"
echo "🎯 Trigger: ${{ github.event_name }}"
# Verificar se deve pular testes
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"
# Job específico para validação de PRs (sem 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 Branch: ${{ github.base_ref }}"
echo "📂 Source Branch: ${{ github.head_ref }}"
echo "🧪 Tests: ${{ needs.test.result }}"
echo "👤 Author: ${{ github.event.pull_request.user.login }}"
echo "📝 Title: ${{ github.event.pull_request.title }}"
echo ""
echo "✨ PR está pronto para merge!"
build-and-push:
name: Build and Push Image
runs-on: [self-hosted, arm64, bcards]
needs: [test]
# Só faz build/push em push (não em PR)
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
# Main = Produção (ARM64) - usando Dockerfile simples
echo "tag=latest" >> $GITHUB_OUTPUT
echo "platform=linux/arm64" >> $GITHUB_OUTPUT
echo "environment=Production" >> $GITHUB_OUTPUT
echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT
echo "deploy_target=production" >> $GITHUB_OUTPUT
elif [[ "$BRANCH_NAME" == Release/* ]]; then
# Release = Swarm tests (Orange Pi arm64) - usando Dockerfile simples também
VERSION_RAW=${BRANCH_NAME#Release/}
# Only remove V/v if it's at the start and followed by a number (like v1.0.0)
VERSION=$(echo "$VERSION_RAW" | sed 's/^[Vv]\([0-9]\)/\1/')
[ -z "$VERSION" ] && VERSION="0.0.1"
echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "platform=linux/arm64" >> $GITHUB_OUTPUT
echo "environment=Testing" >> $GITHUB_OUTPUT
echo "dockerfile=Dockerfile" >> $GITHUB_OUTPUT
echo "deploy_target=testing" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
fi
COMMIT_SHA=${{ github.sha }}
SHORT_COMMIT=${COMMIT_SHA:0:7}
echo "commit=$SHORT_COMMIT" >> $GITHUB_OUTPUT
echo "📦 Tag: ${{ steps.settings.outputs.tag }}"
echo "🏗️ Platform: ${{ steps.settings.outputs.platform }}"
echo "🌍 Environment: ${{ steps.settings.outputs.environment }}"
echo "🎯 Target: ${{ steps.settings.outputs.deploy_target }}"
- name: Build and push image
run: |
echo "🏗️ Building image for ${{ steps.settings.outputs.deploy_target }}..."
# Debug das variáveis
echo "Platform: ${{ steps.settings.outputs.platform }}"
echo "Dockerfile: ${{ steps.settings.outputs.dockerfile }}"
echo "Tag: ${{ steps.settings.outputs.tag }}"
# Verificar se o Dockerfile existe
if [ ! -f "${{ steps.settings.outputs.dockerfile }}" ]; then
echo "❌ Dockerfile não encontrado: ${{ steps.settings.outputs.dockerfile }}"
echo "📂 Arquivos na raiz:"
ls -la
echo "📂 Arquivos em src/BCards.Web/:"
ls -la src/BCards.Web/ || echo "Diretório não existe"
exit 1
else
echo "✅ Dockerfile encontrado: ${{ steps.settings.outputs.dockerfile }}"
fi
# Build para a plataforma correta
if [ "${{ steps.settings.outputs.deploy_target }}" = "production" ]; then
# Build para produção (main branch)
docker buildx build \
--platform ${{ steps.settings.outputs.platform }} \
--file ${{ steps.settings.outputs.dockerfile }} \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \
--push \
--progress=plain \
.
else
# Build para staging (Release branches)
docker buildx build \
--platform ${{ steps.settings.outputs.platform }} \
--file ${{ steps.settings.outputs.dockerfile }} \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.settings.outputs.tag }} \
--push \
--build-arg VERSION=${{ steps.settings.outputs.version || 'latest' }} \
--build-arg COMMIT=${{ steps.settings.outputs.commit }} \
--progress=plain \
.
fi
deploy-production:
name: Deploy to Production (ARM - OCI)
runs-on: ubuntu-latest
needs: [build-and-push]
if: github.ref_name == 'main'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create Production Configuration
run: |
echo "🔧 Creating appsettings.Production.json with environment variables..."
# Cria o arquivo de configuração para produção
cat > appsettings.Production.json << 'CONFIG_EOF'
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"BCards": "Information",
"BCards.Web.Services.GridFSImageStorage": "Debug"
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Information"
}
},
"File": {
"Path": "/app/logs/bcards-{Date}.log",
"LogLevel": {
"Default": "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": 5.90,
"MaxPages": 3,
"MaxLinks": 8,
"AllowPremiumThemes": false,
"AllowProductLinks": false,
"AllowAnalytics": true,
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ],
"Interval": "month"
},
"Professional": {
"Name": "Profissional",
"PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj",
"Price": 12.90,
"MaxPages": 5,
"MaxLinks": 20,
"AllowPremiumThemes": false,
"AllowProductLinks": false,
"AllowAnalytics": true,
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ],
"Interval": "month"
},
"Premium": {
"Name": "Premium",
"PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu",
"Price": 19.90,
"MaxPages": 15,
"MaxLinks": -1,
"AllowPremiumThemes": true,
"AllowProductLinks": false,
"AllowAnalytics": true,
"SpecialModeration": false,
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ],
"Interval": "month"
},
"PremiumAffiliate": {
"Name": "Premium+Afiliados",
"PriceId": "price_1RycTaBMIadsOxJVeDLseXQq",
"Price": 29.90,
"MaxPages": 15,
"MaxLinks": -1,
"AllowPremiumThemes": true,
"AllowProductLinks": true,
"AllowAnalytics": true,
"SpecialModeration": true,
"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": 59.00,
"MaxPages": 3,
"MaxLinks": 8,
"AllowPremiumThemes": false,
"AllowProductLinks": false,
"AllowAnalytics": true,
"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": 129.00,
"MaxPages": 5,
"MaxLinks": 20,
"AllowPremiumThemes": false,
"AllowProductLinks": false,
"AllowAnalytics": true,
"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": 199.00,
"MaxPages": 15,
"MaxLinks": -1,
"AllowPremiumThemes": true,
"AllowProductLinks": false,
"AllowAnalytics": true,
"SpecialModeration": false,
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ],
"Interval": "year"
},
"PremiumAffiliateYearly": {
"Name": "Premium+Afiliados Anual",
"PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1",
"Price": 299.00,
"MaxPages": 15,
"MaxLinks": -1,
"AllowPremiumThemes": true,
"AllowProductLinks": true,
"AllowAnalytics": true,
"SpecialModeration": true,
"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/BCardsDB?replicaSet=rs0&authSource=admin",
"DatabaseName": "BCardsDB",
"MaxConnectionPoolSize": 100,
"ConnectTimeout": "30s",
"ServerSelectionTimeout": "30s",
"SocketTimeout": "30s"
},
"BaseUrl": "https://bcards.site",
"Environment": {
"Name": "Production",
"IsStagingEnvironment": false,
"AllowTestData": false,
"EnableDetailedErrors": false
},
"Performance": {
"EnableCaching": true,
"CacheExpirationMinutes": 30,
"EnableCompression": true,
"EnableResponseCaching": true
},
"Security": {
"EnableHttpsRedirection": true,
"EnableHsts": true,
"RequireHttpsMetadata": true
},
"HealthChecks": {
"Enabled": true,
"Endpoints": {
"Health": "/health",
"Ready": "/ready",
"Live": "/live"
},
"MongoDb": {
"Enabled": true,
"Timeout": "10s"
}
},
"Features": {
"EnablePreviewMode": true,
"EnableModerationWorkflow": true,
"EnableAnalytics": true,
"EnableFileUploads": true,
"MaxFileUploadSize": "5MB"
},
"Serilog": {
"OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}"
}
}
CONFIG_EOF
echo "✅ Configuration file created!"
echo "🔍 File content (sensitive data masked):"
cat appsettings.Production.json | sed 's/"[^"]*_[0-9A-Za-z_]*"/"***MASKED***"/g'
- name: Deploy to Production Swarm
run: |
echo "🚀 Deploying to production Docker Swarm (ARM64)..."
# Configura SSH
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Adiciona hosts conhecidos
ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts
ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts
# Testa a chave SSH
ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded"
# Upload configuration and stack file to swarm manager
echo "📤 Uploading files to Swarm manager..."
scp -o StrictHostKeyChecking=no appsettings.Production.json ubuntu@141.148.162.114:/tmp/
scp -o StrictHostKeyChecking=no deploy/docker-stack.yml ubuntu@141.148.162.114:/tmp/
# Deploy to Docker Swarm
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 << 'EOF'
set -e
echo "🔄 Updating Docker Swarm stack..."
# Update Docker config with new appsettings
echo "📝 Updating bcards-appsettings config..."
# Remove old config (will fail if in use, that's ok - swarm will use it until update)
docker config rm bcards-appsettings 2>/dev/null || echo "Config in use or doesn't exist, will create new one"
# Create new config with timestamp to force update
CONFIG_NAME="bcards-appsettings-$(date +%s)"
docker config create ${CONFIG_NAME} /tmp/appsettings.Production.json
# Update stack file to use new config name
sed "s/bcards-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack.yml > /tmp/docker-stack-updated.yml
echo "🐳 Deploying stack to Swarm (rolling update, zero downtime)..."
docker stack deploy -c /tmp/docker-stack-updated.yml bcards --with-registry-auth
echo "⏳ Waiting for service to update..."
sleep 10
# Show service status
docker service ls --filter name=bcards_bcards-app
docker service ps bcards_bcards-app --no-trunc --filter "desired-state=running" | head -10
echo "🧹 Cleaning up standalone containers if they exist..."
docker stop bcards-prod 2>/dev/null || echo "No standalone container to stop"
docker rm bcards-prod 2>/dev/null || echo "No standalone container to remove"
# Clean up temp files
rm -f /tmp/appsettings.Production.json /tmp/docker-stack.yml /tmp/docker-stack-updated.yml
echo "✅ Swarm stack updated successfully!"
EOF
- name: Health Check Production
run: |
echo "🏥 Verificando saúde dos servidores de produção..."
sleep 30
# Verifica Servidor 1
echo "Verificando Servidor 1 (ARM)..."
ssh -o StrictHostKeyChecking=no ubuntu@141.148.162.114 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 1 pode não estar respondendo"'
# Verifica Servidor 2
echo "Verificando Servidor 2 (ARM)..."
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 2 pode não estar respondendo"'
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/}
# Only remove V/v if it's at the start and followed by a number (like v1.0.0)
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 }}
# Replace ${BCARDS_IMAGE} with actual image name using sed
sed "s|\${BCARDS_IMAGE}|${BCARDS_IMAGE}|g" deploy/docker-stack.release.yml > artifacts/docker-stack.release.yml
echo "🔧 Generated manifest with image: ${BCARDS_IMAGE}"
echo "📄 Manifest content:"
head -10 artifacts/docker-stack.release.yml
- name: Deploy to release swarm
run: |
echo "🚀 Deploying release stack to Orange Pi swarm..."
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:
name: Cleanup Old Resources
runs-on: ubuntu-latest
needs: [deploy-production, deploy-test]
if: always() && (needs.deploy-production.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 141.148.162.114 >> ~/.ssh/known_hosts
ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts
ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded"
for server in 141.148.162.114 129.146.116.218; do
echo "🧹 Limpando servidor $server..."
ssh -o StrictHostKeyChecking=no ubuntu@$server << 'EOF'
docker container prune -f
docker image prune -f
docker network prune -f
EOF
done
else
echo " Release branch: limpeza remota ignorada (Swarm gerencia recursos)."
fi
echo "✅ Limpeza concluída!"
deployment-summary:
name: Deployment Summary
runs-on: ubuntu-latest
needs: [deploy-production, 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 (Swarm ARM)"
echo "🖥️ Servers: 141.148.162.114, 129.146.116.218"
echo "📦 Tag: latest"
echo "🔗 Status: ${{ needs.deploy-production.result }}"
else
echo "🌍 Environment: Release (Swarm ARM)"
echo "🖥️ Servers: 141.148.162.114, 129.146.116.218"
echo "📦 Branch Tag: ${{ github.ref_name }}"
echo "🔗 Status: ${{ needs.deploy-test.result }}"
fi
echo "===================="
echo "✅ Pipeline completed!"