Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 52 additions & 15 deletions .github/workflows/azure-webapps-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -22,37 +22,45 @@ 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:
node-version: '18'
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
export CI=false
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 }}
Expand Down Expand Up @@ -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
30 changes: 21 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"]
# 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"]
21 changes: 21 additions & 0 deletions azure-deploy/startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
77 changes: 42 additions & 35 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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('/<path:path>')
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/<path:filename>')
Expand Down Expand Up @@ -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)
Expand Down
Loading