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 printf '%s' "${SSH_PRIVATE_KEY}" | tr -d '\r' | sed 's/\\n/\n/g' > ~/.ssh/id_rsa echo "" >> ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa # debug key format (no content exposed) head -1 ~/.ssh/id_rsa wc -l ~/.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 printf '%s' "${SSH_PRIVATE_KEY}" | tr -d '\r' | sed 's/\\n/\n/g' > ~/.ssh/id_rsa echo "" >> ~/.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 # ── 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 run: | sleep 35 ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \ 'curl -sf http://localhost:8084/health && echo "✅ nalu healthy" || (echo "❌ health check failed"; 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 printf '%s' "${SSH_PRIVATE_KEY}" | tr -d '\r' | sed 's/\\n/\n/g' > ~/.ssh/id_rsa echo "" >> ~/.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"