Add best model artifacts for deployment #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Model API | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| paths: | |
| - 'api/**' | |
| - 'mlruns/best_model_artifacts/**' | |
| - 'Data/preprocessor.pkl' | |
| - '.github/workflows/cd.yml' | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'api/**' | |
| - '.github/workflows/cd.yml' | |
| workflow_dispatch: | |
| inputs: | |
| deployment_target: | |
| description: 'Choose deployment target' | |
| required: true | |
| default: 'local' | |
| type: choice | |
| options: | |
| - local | |
| - staging | |
| - production | |
| jobs: | |
| # Verify prerequisites | |
| check-prerequisites: | |
| runs-on: self-hosted | |
| outputs: | |
| model-exists: ${{ steps.check.outputs.model }} | |
| preprocessor-exists: ${{ steps.check.outputs.preprocessor }} | |
| defaults: | |
| run: | |
| shell: powershell | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Check required artifacts | |
| id: check | |
| run: | | |
| $modelExists = $false | |
| $preprocessorExists = $false | |
| # Check for best model artifacts | |
| if (Test-Path "mlruns\best_model_artifacts\customer_churn_optimization") { | |
| $modelDirs = Get-ChildItem "mlruns\best_model_artifacts\customer_churn_optimization" -Directory | |
| if ($modelDirs.Count -gt 0) { | |
| Write-Host "✅ Model artifacts found: $($modelDirs[0].Name)" | |
| $modelExists = $true | |
| } | |
| } | |
| # Check for preprocessor | |
| if (Test-Path "Data\preprocessor.pkl") { | |
| Write-Host "✅ Preprocessor found" | |
| $preprocessorExists = $true | |
| } | |
| # Set outputs | |
| "model=$modelExists" >> $env:GITHUB_OUTPUT | |
| "preprocessor=$preprocessorExists" >> $env:GITHUB_OUTPUT | |
| # Fail if required artifacts are missing | |
| if (-not $modelExists) { | |
| Write-Host "❌ No model artifacts found. Run CI pipeline first to train a model." | |
| exit 1 | |
| } | |
| if (-not $preprocessorExists) { | |
| Write-Host "❌ Preprocessor not found. Run data preprocessing first." | |
| exit 1 | |
| } | |
| Write-Host "✅ All required artifacts are present" | |
| # Test API locally (without Docker for Windows) | |
| test-api: | |
| needs: check-prerequisites | |
| runs-on: self-hosted | |
| defaults: | |
| run: | |
| shell: powershell | |
| env: | |
| BEST_MODEL_METRIC: f1 | |
| MLFLOW_EXPERIMENT_NAME: customer_churn_optimization | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python environment | |
| run: | | |
| if (Test-Path ".venv\Scripts\Activate.ps1") { | |
| Write-Host "✅ Virtual environment found" | |
| & ".venv\Scripts\Activate.ps1" | |
| } else { | |
| Write-Host "⚠️ No virtual environment found, using system Python" | |
| } | |
| # Install API dependencies | |
| Write-Host "📦 Installing API dependencies..." | |
| python -m pip install -q -r api/requirements.txt | |
| - name: Start API server in background | |
| run: | | |
| Write-Host "🚀 Starting API server..." | |
| $job = Start-Job -ScriptBlock { | |
| Set-Location "d:\ML 101\ML_101_BootCamp" | |
| python -m uvicorn api.main:app --host 127.0.0.1 --port 8000 | |
| } | |
| Write-Host "⏳ Waiting for API to start..." | |
| Start-Sleep -Seconds 10 | |
| # Check if job is still running | |
| if ($job.State -eq "Running") { | |
| Write-Host "✅ API server started (Job ID: $($job.Id))" | |
| "API_JOB_ID=$($job.Id)" >> $env:GITHUB_ENV | |
| } else { | |
| Write-Host "❌ API server failed to start" | |
| Receive-Job -Job $job | |
| exit 1 | |
| } | |
| - name: Test API endpoints | |
| run: | | |
| Write-Host "🧪 Testing API endpoints..." | |
| # Test health endpoint | |
| Write-Host "Testing /health..." | |
| $health = Invoke-RestMethod -Uri "http://localhost:8000/health" -Method Get | |
| if ($health.status -eq "ok") { | |
| Write-Host "✅ Health check passed" | |
| } else { | |
| Write-Host "❌ Health check failed" | |
| exit 1 | |
| } | |
| # Test schema endpoint | |
| Write-Host "Testing /schema..." | |
| $schema = Invoke-RestMethod -Uri "http://localhost:8000/schema" -Method Get | |
| if ($schema.required_columns) { | |
| Write-Host "✅ Schema endpoint working" | |
| Write-Host " Required columns: $($schema.required_columns.Count)" | |
| } else { | |
| Write-Host "❌ Schema endpoint failed" | |
| exit 1 | |
| } | |
| # Test prediction endpoint | |
| Write-Host "Testing /predict..." | |
| $body = @{ | |
| features = @{ | |
| Age = 30 | |
| Gender = "Female" | |
| Tenure = 39 | |
| "Usage Frequency" = 14 | |
| "Support Calls" = 5 | |
| "Payment Delay" = 18 | |
| "Subscription Type" = "Standard" | |
| "Contract Length" = "Annual" | |
| "Total Spend" = 932 | |
| "Last Interaction" = 17 | |
| } | |
| } | ConvertTo-Json | |
| $prediction = Invoke-RestMethod -Uri "http://localhost:8000/predict" -Method Post -ContentType "application/json" -Body $body | |
| if ($null -ne $prediction.prediction) { | |
| Write-Host "✅ Prediction endpoint working" | |
| Write-Host " Prediction: $($prediction.prediction)" | |
| Write-Host " Probability: $($prediction.probability)" | |
| } else { | |
| Write-Host "❌ Prediction failed" | |
| exit 1 | |
| } | |
| Write-Host "🎉 All API tests passed!" | |
| - name: Stop API server | |
| if: always() | |
| run: | | |
| if ($env:API_JOB_ID) { | |
| Write-Host "🛑 Stopping API server..." | |
| Stop-Job -Id $env:API_JOB_ID -ErrorAction SilentlyContinue | |
| Remove-Job -Id $env:API_JOB_ID -ErrorAction SilentlyContinue | |
| Write-Host "✅ API server stopped" | |
| } | |
| # Deploy to selected environment | |
| deploy: | |
| needs: [check-prerequisites, test-api] | |
| runs-on: self-hosted | |
| if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' | |
| defaults: | |
| run: | |
| shell: powershell | |
| env: | |
| DEPLOYMENT_TARGET: ${{ inputs.deployment_target || 'local' }} | |
| API_PORT_LOCAL: 8000 | |
| API_PORT_STAGING: 8001 | |
| API_PORT_PRODUCTION: 8002 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Deploy to Local | |
| if: env.DEPLOYMENT_TARGET == 'local' | |
| run: | | |
| Write-Host "🚀 Deploying to local environment..." | |
| # Check if API is already running on port 8000 | |
| $running = Get-Process -Name "python" -ErrorAction SilentlyContinue | Where-Object { | |
| $_.CommandLine -like "*uvicorn*api.main*8000*" | |
| } | |
| if ($running) { | |
| Write-Host "⚠️ Stopping existing API server..." | |
| Stop-Process -Id $running.Id -Force | |
| Start-Sleep -Seconds 2 | |
| } | |
| # Start API server as a background service | |
| Write-Host "Starting API server on port ${{ env.API_PORT_LOCAL }}..." | |
| Start-Process powershell -ArgumentList @( | |
| "-NoExit", | |
| "-Command", | |
| "cd 'd:\ML 101\ML_101_BootCamp'; python -m uvicorn api.main:app --host 127.0.0.1 --port ${{ env.API_PORT_LOCAL }}" | |
| ) -WindowStyle Minimized | |
| Write-Host "⏳ Waiting for server to start..." | |
| Start-Sleep -Seconds 10 | |
| # Verify deployment | |
| try { | |
| $health = Invoke-RestMethod -Uri "http://localhost:${{ env.API_PORT_LOCAL }}/health" -Method Get | |
| if ($health.status -eq "ok") { | |
| Write-Host "✅ Local deployment successful!" | |
| Write-Host "🌐 API available at: http://localhost:${{ env.API_PORT_LOCAL }}" | |
| Write-Host "📚 API Docs: http://localhost:${{ env.API_PORT_LOCAL }}/docs" | |
| } | |
| } catch { | |
| Write-Host "❌ Health check failed: $_" | |
| exit 1 | |
| } | |
| - name: Deploy to Staging | |
| if: env.DEPLOYMENT_TARGET == 'staging' | |
| run: | | |
| Write-Host "🚀 Deploying to staging environment..." | |
| # Similar to local but on different port | |
| $running = Get-Process -Name "python" -ErrorAction SilentlyContinue | Where-Object { | |
| $_.CommandLine -like "*uvicorn*api.main*${{ env.API_PORT_STAGING }}*" | |
| } | |
| if ($running) { | |
| Write-Host "⚠️ Stopping existing staging server..." | |
| Stop-Process -Id $running.Id -Force | |
| Start-Sleep -Seconds 2 | |
| } | |
| Write-Host "Starting staging server on port ${{ env.API_PORT_STAGING }}..." | |
| Start-Process powershell -ArgumentList @( | |
| "-NoExit", | |
| "-Command", | |
| "cd 'd:\ML 101\ML_101_BootCamp'; python -m uvicorn api.main:app --host 127.0.0.1 --port ${{ env.API_PORT_STAGING }}" | |
| ) -WindowStyle Minimized | |
| Start-Sleep -Seconds 10 | |
| $health = Invoke-RestMethod -Uri "http://localhost:${{ env.API_PORT_STAGING }}/health" -Method Get | |
| if ($health.status -eq "ok") { | |
| Write-Host "✅ Staging deployment successful!" | |
| Write-Host "🌐 Staging API: http://localhost:${{ env.API_PORT_STAGING }}" | |
| } | |
| - name: Deploy to Production | |
| if: env.DEPLOYMENT_TARGET == 'production' && github.ref == 'refs/heads/main' | |
| run: | | |
| Write-Host "🚀 Deploying to production environment..." | |
| Write-Host "⚠️ Production deployment configured for manual approval only" | |
| # Production deployment steps would include: | |
| # - Deploy to cloud service (Azure/AWS/GCP) | |
| # - Update container registry | |
| # - Configure load balancer | |
| # - Set up monitoring and alerts | |
| # - Run smoke tests | |
| # - Gradual rollout with canary deployment | |
| Write-Host "✅ Production deployment would happen here" | |
| Write-Host "🔧 Configure cloud provider credentials and deployment scripts" | |
| # Post-deployment verification | |
| verify-deployment: | |
| needs: deploy | |
| runs-on: self-hosted | |
| if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | |
| defaults: | |
| run: | |
| shell: powershell | |
| env: | |
| DEPLOYMENT_TARGET: ${{ inputs.deployment_target || 'local' }} | |
| API_PORT_LOCAL: 8000 | |
| API_PORT_STAGING: 8001 | |
| steps: | |
| - name: Verify Deployment | |
| run: | | |
| Write-Host "🔍 Verifying deployment..." | |
| # Determine port based on environment | |
| $port = if ($env:DEPLOYMENT_TARGET -eq "staging") { ${{ env.API_PORT_STAGING }} } else { ${{ env.API_PORT_LOCAL }} } | |
| $baseUrl = "http://localhost:$port" | |
| Write-Host "Testing $env:DEPLOYMENT_TARGET environment at $baseUrl" | |
| # Extended health checks | |
| $successCount = 0 | |
| for ($i = 1; $i -le 5; $i++) { | |
| try { | |
| $health = Invoke-RestMethod -Uri "$baseUrl/health" -Method Get -TimeoutSec 5 | |
| if ($health.status -eq "ok") { | |
| Write-Host "✅ Health check $i/5 passed" | |
| $successCount++ | |
| } | |
| } catch { | |
| Write-Host "⚠️ Health check $i/5 failed: $_" | |
| } | |
| Start-Sleep -Seconds 3 | |
| } | |
| if ($successCount -ge 4) { | |
| Write-Host "🎉 Deployment verified successfully! ($successCount/5 checks passed)" | |
| } else { | |
| Write-Host "❌ Deployment verification failed (only $successCount/5 checks passed)" | |
| exit 1 | |
| } | |
| # Test a prediction to ensure model is working | |
| Write-Host "`n🧪 Testing prediction endpoint..." | |
| $body = @{ | |
| features = @{ | |
| Age = 35 | |
| Gender = "Male" | |
| Tenure = 24 | |
| "Usage Frequency" = 15 | |
| "Support Calls" = 2 | |
| "Payment Delay" = 5 | |
| "Subscription Type" = "Standard" | |
| "Contract Length" = "Annual" | |
| "Total Spend" = 1200 | |
| "Last Interaction" = 30 | |
| } | |
| } | ConvertTo-Json | |
| try { | |
| $prediction = Invoke-RestMethod -Uri "$baseUrl/predict" -Method Post -ContentType "application/json" -Body $body | |
| Write-Host "✅ Model prediction working" | |
| Write-Host " Result: $($prediction.prediction) (probability: $($prediction.probability))" | |
| } catch { | |
| Write-Host "❌ Prediction test failed: $_" | |
| exit 1 | |
| } | |
| - name: Deployment Summary | |
| if: always() | |
| run: | | |
| $target = if ($env:DEPLOYMENT_TARGET) { $env:DEPLOYMENT_TARGET } else { "local" } | |
| $port = if ($target -eq "staging") { ${{ env.API_PORT_STAGING }} } else { ${{ env.API_PORT_LOCAL }} } | |
| Write-Host "`n# 🚀 Deployment Summary" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "**Target:** $target" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "**Commit:** ${{ github.sha }}" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "**Branch:** ${{ github.ref_name }}" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "**API URL:** http://localhost:$port" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "**Docs URL:** http://localhost:${port}/docs" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "### 🧪 Quick Test Commands:" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host '```powershell' >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "# Health check" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "Invoke-RestMethod http://localhost:${port}/health" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "# Get schema" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "Invoke-RestMethod http://localhost:${port}/schema" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "# Open API documentation" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host "Start-Process http://localhost:${port}/docs" >> $env:GITHUB_STEP_SUMMARY | |
| Write-Host '```' >> $env:GITHUB_STEP_SUMMARY |