diff --git a/.github/workflows/azure-webapps-python.yml b/.github/workflows/azure-webapps-python.yml index 62fe0300..dd648dfa 100644 --- a/.github/workflows/azure-webapps-python.yml +++ b/.github/workflows/azure-webapps-python.yml @@ -4,7 +4,7 @@ env: AZURE_WEBAPP_NAME: instantapply # set this to the name of your Azure Web App PYTHON_VERSION: '3.11' DOCKER_REGISTRY: ghcr.io - DOCKER_IMAGE_NAME: lifee77/instantapply + DOCKER_IMAGE_NAME: jeevanbhatta/instantapply on: push: @@ -22,18 +22,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python version - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - cache: 'pip' - cache-dependency-path: '**/requirements.txt' - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -r backend/requirements.txt - - name: Set up Node.js uses: actions/setup-node@v3 with: @@ -41,7 +29,7 @@ jobs: cache: 'npm' cache-dependency-path: '**/package-lock.json' - - name: Build React frontend with CI=false to prevent warnings as errors + - name: Build React frontend run: | cd react-frontend npm ci @@ -49,10 +37,30 @@ jobs: npm run build echo "React build completed successfully" + - name: Copy React build to backend/static + run: | + mkdir -p backend/static + rm -rf backend/static/* + cp -r react-frontend/build/* backend/static/ + echo "React build files copied to backend/static" + ls -la backend/static + + - name: Set up Python version + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: '**/requirements.txt' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r backend/requirements.txt + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Log in to GitHub Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v2 with: registry: ${{ env.DOCKER_REGISTRY }} @@ -119,3 +127,32 @@ jobs: SECRET_KEY="${{ secrets.SECRET_KEY }}" \ DATABASE_URL="${{ secrets.DATABASE_URL }}" \ GEMINI_API_KEY="${{ secrets.GEMINI_API_KEY }}" + + - name: 'Configure Health Check' + uses: azure/cli@v1 + with: + azcliversion: latest + inlineScript: | + # Configure health check settings using generic-configurations + az webapp config set \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --name ${{ env.AZURE_WEBAPP_NAME }} \ + --generic-configurations '{"healthCheckPath": "/health"}' \ + --always-on true + + # Configure container settings + az webapp config container set \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --name ${{ env.AZURE_WEBAPP_NAME }} \ + --enable-app-service-storage true \ + --docker-registry-server-url https://${{ env.DOCKER_REGISTRY }} + + # Set application settings for health check + az webapp config appsettings set \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --name ${{ env.AZURE_WEBAPP_NAME }} \ + --settings \ + WEBSITES_CONTAINER_START_TIME_LIMIT=1800 \ + WEBSITES_ENABLE_APP_SERVICE_STORAGE=true \ + WEBSITE_HEALTHCHECK_MAXPINGFAILURES=3 \ + WEBSITE_HEALTHCHECK_PATH=/health diff --git a/Dockerfile b/Dockerfile index 5c55611b..5194b0f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,12 @@ FROM python:3.11-slim # Add Azure compatibility label LABEL "com.microsoft.azure.webapp"="true" +# Add health check label +LABEL "health.check.path"="/health" +LABEL "health.check.interval"="30s" +LABEL "health.check.timeout"="10s" +LABEL "health.check.retries"="3" + WORKDIR /app # Install system dependencies for psycopg2, playwright, and other packages @@ -56,20 +62,26 @@ RUN mkdir -p backend/uploads RUN mkdir -p instance && \ chmod 777 instance -# Copy the React frontend build directly to backend/static - this is the main location -COPY react-frontend/build/ backend/static/ +# Ensure static directory exists and is writable +RUN mkdir -p backend/static && \ + chmod 777 backend/static + +# Copy the React frontend build files +COPY backend/static/ backend/static/ -# No need to create nested static structure - use the files as they are -# This is important: do NOT move files around, leave them where React put them +# Create a symbolic link for backwards compatibility +RUN ln -sf /app/backend/static /app/static # Display detailed debug information about static files RUN echo "=== DEBUG: Static File Structure ===" && \ - echo "--- React frontend files ---" && \ + echo "--- Backend static files ---" && \ ls -la /app/backend/static && \ + echo "--- Root static files ---" && \ + ls -la /app/static && \ echo "--- JS files ---" && \ - find /app/backend/static -name "*.js" | grep -v "node_modules" && \ + find /app/backend/static -name "*.js" | grep -v "node_modules" || true && \ echo "--- CSS files ---" && \ - find /app/backend/static -name "*.css" | grep -v "node_modules" && \ + find /app/backend/static -name "*.css" | grep -v "node_modules" || true && \ echo "=== END DEBUG ===" # Set environment variables @@ -82,5 +94,5 @@ ENV DATABASE_URL="sqlite:////app/instance/instant_apply.db" # Expose the port the app runs on EXPOSE 8000 -# Command to run the application -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--log-level=debug", "app:app"] \ No newline at end of file +# Add health check configuration to gunicorn +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--log-level=debug", "--timeout", "120", "--workers", "4", "--threads", "2", "--worker-class", "gthread", "--worker-tmp-dir", "/dev/shm", "--access-logfile", "-", "--error-logfile", "-", "app:app"] \ No newline at end of file diff --git a/azure-deploy/startup.sh b/azure-deploy/startup.sh index fcda7dd0..5a9fa5fc 100644 --- a/azure-deploy/startup.sh +++ b/azure-deploy/startup.sh @@ -22,6 +22,27 @@ echo "Installed Python packages:" pip freeze | head -n 20 echo "(package list truncated)..." +echo "Running database migrations..." +# Run database migrations (continue even if migrations fail to avoid breaking the app) +cd $SITE_ROOT +python -c " +import sys +sys.path.insert(0, '$SITE_ROOT') +sys.path.insert(0, '$SITE_ROOT/backend') +try: + from backend.app import create_app + from flask_migrate import upgrade + print('Creating app for migrations...') + app = create_app() + with app.app_context(): + print('Running migrations...') + upgrade() + print('✅ Database migrations completed successfully!') +except Exception as e: + print('⚠️ Migration warning (app will still start):', str(e)) + # Don't exit - continue to start the app even if migrations fail +" || echo "⚠️ Migrations failed but continuing startup..." + echo "Starting Gunicorn server..." # Start the Gunicorn server exec gunicorn --bind=0.0.0.0:8000 --timeout 600 app:app \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index e8a66a5b..37cf1b3e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -79,11 +79,10 @@ def create_app(config_class=Config): # Create Flask app with explicit instance path and React build directory # Point Flask to serve React build files directly - react_build_path = os.path.join(os.path.dirname(__file__), '..', 'react-frontend', 'build') app = Flask(__name__, instance_relative_config=True, instance_path=instance_path, - static_folder=react_build_path, + static_folder='static', # Changed to use the correct static folder static_url_path='') # Load configuration @@ -332,42 +331,16 @@ def serve_static(filename): # Simple 404 handler for static files that logs path for debugging # Add route for root to serve index.html (React app entry point) - @app.route('/') - def index(): - """Serve the React app entry point""" - return app.send_static_file('index.html') - - # Custom 404 handler for non-static routes - @app.errorhandler(404) - def not_found(e): - path = request.path - - # For API routes, return JSON - if path.startswith('/api/'): - return jsonify({"error": "API endpoint not found"}), 404 - - # For static file requests, let Flask handle them naturally (don't override) - if path.startswith('/static/'): - # Re-raise the 404 to let Flask's static file handler try - raise e - - # For everything else, serve the React app (SPA routing) - return app.send_static_file('index.html') - - # Serve React App for all non-static, non-API routes + @app.route('/', defaults={'path': ''}) @app.route('/') def serve(path): - # Skip API routes - let them be handled by their blueprints if path.startswith('api/'): - return jsonify({'error': 'API route not found'}), 404 - - # Skip static routes completely - Flask will handle them with static_folder config - if path.startswith('static/'): - from flask import abort - abort(404) # Let Flask's native static handling take over - - # For all other paths, serve the React app (SPA routing) - return app.send_static_file('index.html') + return {'error': 'Not Found'}, 404 + try: + # First try to serve from static directory + return app.send_static_file('index.html') + except: + return app.send_static_file('index.html') # Serve markdown files from react-frontend/public/content for React to fetch @app.route('/content/') @@ -482,6 +455,40 @@ def teardown_db(error): import traceback print(traceback.format_exc()) + # Add a health check endpoint + @app.route('/health') + def health_check(): + """Enhanced health check endpoint that checks various system components""" + health_status = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "components": { + "app": "healthy", + "database": "unknown" + } + } + + # Check database connection + try: + from utils.db_utils import check_db_connection + db_healthy = check_db_connection() + health_status["components"]["database"] = "healthy" if db_healthy else "unhealthy" + if not db_healthy: + health_status["status"] = "unhealthy" + except Exception as e: + app.logger.error(f"Database health check failed: {str(e)}") + health_status["components"]["database"] = "unhealthy" + health_status["status"] = "unhealthy" + health_status["database_error"] = str(e) + + # Set response status code based on health + status_code = 200 if health_status["status"] == "healthy" else 503 + + # Log health check result + app.logger.info(f"Health check status: {health_status['status']}, Database: {health_status['components']['database']}") + + return health_status, status_code + return app # Create the app instance for deployment (needed for gunicorn/Azure)