NALU/.gitea/workflows/deploy-nalu.yml
Ricardo Carneiro 72ad41c529
All checks were successful
NALU Deployment Pipeline / Run Tests (push) Successful in 1m29s
NALU Deployment Pipeline / PR Validation (push) Has been skipped
NALU Deployment Pipeline / Build and Push Image (push) Successful in 1m16s
NALU Deployment Pipeline / Deploy naluai.dev (push) Successful in 47s
NALU Deployment Pipeline / Cleanup Old Resources (push) Successful in 12s
fix: health check retry loop instead of fixed 35s sleep
Retry up to 12x with 10s intervals (2 min total).
Also reuse SSH setup in health check step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 22:18:00 -03:00

302 lines
12 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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: NALU Deployment Pipeline
# ─── Required Gitea Secrets ────────────────────────────────────────────────
# secrets.SSH_PRIVATE_KEY SSH key for ubuntu@ on OCI nodes
# secrets.NALU_MONGODB_CONNECTION MongoDB connection string (with password)
# secrets.NALU_GROQ_API_KEY Groq API key
# secrets.NALU_OPENROUTER_API_KEY OpenRouter API key
# secrets.NALU_GOOGLEAI_API_KEY Google AI (Gemini) API key
# secrets.NALU_STRIPE_SECRET_KEY Stripe secret key
# secrets.NALU_STRIPE_WEBHOOK_SECRET Stripe webhook signing secret
# secrets.NALU_OAUTH_GOOGLE_SECRET Google OAuth client secret
# secrets.NALU_OAUTH_MS_SECRET Microsoft OAuth client secret
# secrets.NALU_OAUTH_GITHUB_SECRET GitHub OAuth client secret
#
# ─── Required Gitea Variables (vars.*) ────────────────────────────────────
# vars.NALU_STRIPE_PUBLISHABLE_KEY
# vars.NALU_OAUTH_GOOGLE_CLIENT_ID
# vars.NALU_OAUTH_MS_CLIENT_ID
# vars.NALU_OAUTH_GITHUB_CLIENT_ID
on:
push:
branches:
- main
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]
env:
REGISTRY: registry.redecarneir.us
IMAGE_NAME: nalu
SWARM_MANAGER: 141.148.162.114
SWARM_WORKER: 129.146.116.218
jobs:
# ─── Tests ────────────────────────────────────────────────────────────────
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Cache NuGet packages
uses: actions/cache@v3
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --no-restore --configuration Release
- name: Run tests
run: dotnet test --no-build --configuration Release --verbosity normal --filter "FullyQualifiedName!~PipelineIntegration&FullyQualifiedName!~McpServerTests"
# ─── PR Validation ────────────────────────────────────────────────────────
pr-validation:
name: PR Validation
runs-on: ubuntu-latest
needs: [test]
if: github.event_name == 'pull_request'
steps:
- name: PR ready
run: echo "✅ PR validated — tests passed"
# ─── Build & Push ─────────────────────────────────────────────────────────
build-and-push:
name: Build and Push Image
runs-on: ubuntu-latest
needs: [test]
if: github.event_name == 'push' && github.ref_name == 'main' && needs.test.result == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "${SSH_PRIVATE_KEY}" | base64 -d > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null
- name: Build image on ARM server
run: |
echo "🏗️ Syncing source to ARM builder..."
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} 'rm -rf /tmp/nalu-build && mkdir -p /tmp/nalu-build'
rsync -az --delete \
--exclude='.git' \
--exclude='**/bin/' \
--exclude='**/obj/' \
--exclude='**/.vs/' \
-e "ssh -o StrictHostKeyChecking=no" \
./ ubuntu@${{ env.SWARM_MANAGER }}:/tmp/nalu-build/
echo "🔨 Building Docker image natively on ARM64..."
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF'
set -e
cd /tmp/nalu-build
docker build \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
--progress=plain \
.
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
rm -rf /tmp/nalu-build
echo "✅ Image pushed"
EOF
# ─── Deploy: naluai.dev ───────────────────────────────────────────────────
deploy:
name: Deploy naluai.dev
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 non-sensitive appsettings
run: |
cat > appsettings.nalu.json << 'CONFIG_EOF'
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Hangfire": {
"NameImporterCron": "0 3 1 * *"
},
"Groq": {
"BaseUrl": "https://api.groq.com/openai/v1",
"Model": "llama-3.3-70b-versatile",
"MaxTokens": 500,
"Temperature": 0.1
},
"OpenRouter": {
"BaseUrl": "https://openrouter.ai/api/v1",
"Model": "google/gemma-4-31b-it:free",
"MaxTokens": 500,
"Temperature": 0.1
},
"GoogleAi": {
"BaseUrl": "https://generativelanguage.googleapis.com/v1beta/openai/",
"Model": "gemini-2.0-flash"
},
"Plans": {
"free": { "credits_per_month": 3000, "credits_per_day": 100, "price_brl": 0, "price_usd": 0 },
"starter": { "credits_per_month": 15000, "credits_per_day": 0, "price_brl": 2900, "price_usd": 590 },
"indie": { "credits_per_month": 50000, "credits_per_day": 0, "price_brl": 6900, "price_usd": 1390 },
"pro": { "credits_per_month": 250000, "credits_per_day": 0, "price_brl": 19900, "price_usd": 3990 }
},
"RateLimit": {
"PerIpPerMinute": 60,
"PerIpPerHour": 500
},
"Cache": {
"DefaultTtlMinutes": 60
},
"Stripe": {
"PublishableKey": "${{ vars.NALU_STRIPE_PUBLISHABLE_KEY }}"
},
"OAuth": {
"Google": {
"ClientId": "${{ vars.NALU_OAUTH_GOOGLE_CLIENT_ID }}"
},
"Microsoft": {
"ClientId": "${{ vars.NALU_OAUTH_MS_CLIENT_ID }}",
"TenantId": "common"
},
"GitHub": {
"ClientId": "${{ vars.NALU_OAUTH_GITHUB_CLIENT_ID }}"
}
}
}
CONFIG_EOF
echo "✅ appsettings.nalu.json gerado"
- name: Deploy nalu stack
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "${SSH_PRIVATE_KEY}" | base64 -d > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null
scp -o StrictHostKeyChecking=no appsettings.nalu.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
scp -o StrictHostKeyChecking=no deploy/docker-stack-nalu.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << SSHEOF
set -e
# ── Remove stack so secrets can be rotated ───────────────────────
docker stack rm nalu 2>/dev/null || true
# wait until all nalu services are gone
timeout 60 bash -c 'until ! docker service ls 2>/dev/null | grep -q nalu_; do sleep 2; done' || true
sleep 3
# ── Create/update Docker secrets ─────────────────────────────────
update_secret() {
local name=\$1
local value=\$2
docker secret rm "\$name" 2>/dev/null || true
printf '%s' "\$value" | docker secret create "\$name" -
}
update_secret nalu_mongodb_connection '${{ secrets.NALU_MONGODB_CONNECTION }}'
update_secret nalu_groq_api_key '${{ secrets.NALU_GROQ_API_KEY }}'
update_secret nalu_openrouter_api_key '${{ secrets.NALU_OPENROUTER_API_KEY }}'
update_secret nalu_googleai_api_key '${{ secrets.NALU_GOOGLEAI_API_KEY }}'
update_secret nalu_stripe_secret_key '${{ secrets.NALU_STRIPE_SECRET_KEY }}'
update_secret nalu_stripe_webhook_secret '${{ secrets.NALU_STRIPE_WEBHOOK_SECRET }}'
update_secret nalu_oauth_google_secret '${{ secrets.NALU_OAUTH_GOOGLE_SECRET }}'
update_secret nalu_oauth_ms_secret '${{ secrets.NALU_OAUTH_MS_SECRET }}'
update_secret nalu_oauth_github_secret '${{ secrets.NALU_OAUTH_GITHUB_SECRET }}'
# ── Create docker config ──────────────────────────────────────────
docker config rm nalu-appsettings 2>/dev/null || true
CONFIG_NAME="nalu-appsettings-\$(date +%s)"
docker config create "\${CONFIG_NAME}" /tmp/appsettings.nalu.json
sed "s/nalu-appsettings/\${CONFIG_NAME}/g" /tmp/docker-stack-nalu.yml > /tmp/docker-stack-nalu-final.yml
# ── Deploy stack ──────────────────────────────────────────────────
docker stack deploy -c /tmp/docker-stack-nalu-final.yml nalu --with-registry-auth
rm -f /tmp/appsettings.nalu.json /tmp/docker-stack-nalu.yml /tmp/docker-stack-nalu-final.yml
echo "✅ nalu stack atualizado!"
SSHEOF
- name: Health check
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "${SSH_PRIVATE_KEY}" | base64 -d > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ env.SWARM_MANAGER }} >> ~/.ssh/known_hosts 2>/dev/null
# retry for up to 2 minutes
for i in $(seq 1 12); do
sleep 10
echo "Attempt $i/12..."
if ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} 'curl -sf http://localhost:8084/health'; then
echo "✅ nalu healthy"
exit 0
fi
done
echo "❌ health check failed after 2 minutes"
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} 'docker service ps nalu_app'
exit 1
# ─── Cleanup ──────────────────────────────────────────────────────────────
cleanup:
name: Cleanup Old Resources
runs-on: ubuntu-latest
needs: [deploy]
if: always() && needs.deploy.result == 'success'
steps:
- name: Cleanup
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "${SSH_PRIVATE_KEY}" | base64 -d > ~/.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
ssh -o StrictHostKeyChecking=no ubuntu@$SERVER << 'EOF'
docker container prune -f
docker image prune -f
# Keep last 3 nalu configs
docker config ls --filter "name=nalu-appsettings" --format "{{.ID}} {{.Name}}" \
| sort -k2 | head -n -3 | awk '{print $1}' \
| xargs -r docker config rm 2>/dev/null || true
EOF
done
echo "✅ Cleanup done"