Repository: Zeeeepa/pr-agent
Pull request #35: Implement robust API key validations and database migration scheme
Author: login: codegen-sh[bot]
Labels: name: Review effort 3/5
Created: 2025-04-26T11:47:22Z | Merged: 2025-04-26T11:51:36Z

### **User description**
This PR implements robust API key validations and a proper database migration scheme as requested. The changes include:

## API Key Validations
- Enhanced validation for Supabase URL and API key in the settings service
- Added validation for GitHub token using the GitHub API
- Improved error handling in the settings validation endpoint
- Added masking of sensitive information in settings responses

## Database Migration Scheme
- Created a database migration system with versioning
- Implemented an initial migration for creating the database schema
- Added automatic database initialization on application startup
- Improved error handling for database operations

## Error Handling Improvements
- Added structured error responses with error codes
- Enhanced error handling in API routes
- Improved logging for database and API operations

These changes ensure that the application properly validates API keys before using them and provides clear error messages when validation fails. The database migration scheme allows for easier management of database schema changes in the future.

Fixes the error: `ValueError: Supabase is not initialized. Please provide Supabase URL and API key in settings.`

---

[💻 View my work](https://codegen.sh/agent/trace/10942) • [About Codegen](https://codegen.com)

## Summary by Sourcery

Introduce API key validation for Supabase and GitHub, and implement a database migration system. Enhance API error handling with structured responses and specific error codes.

New Features:
- Implement a database migration framework to manage schema changes.
- Add automatic database initialization and migration execution at startup.
- Add validation for Supabase credentials by attempting a connection.
- Add validation for GitHub tokens using the GitHub API.
- Mask sensitive API keys and tokens in settings API responses.
- Return structured error responses (message, code) from API endpoints.
- Improve error messages for missing database or GitHub configuration.


___

### **PR Type**
Enhancement, Bug fix


___

### **Description**
- Implement robust API key and settings validation, including GitHub token checks.
  - Validate Supabase URL and API key format and connectivity.
  - Validate GitHub token by querying the GitHub API.
  - Return structured error responses with error codes for validation failures.
  - Mask sensitive values (tokens, keys) in settings API responses.

- Introduce a database migration system with versioning.
  - Add initial migration for schema creation and migration tracking.
  - Run automatic database initialization and migrations on application startup.

- Enhance error handling and logging across API endpoints and database operations.
  - Return specific error codes for configuration and connection issues.
  - Improve Supabase client initialization and connection testing.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
      <summary><strong>routes.py</strong><dd><code>Structured API error handling and robust settings validation</code></dd></summary>
<hr>

pr_agent/execserver/api/routes.py

<li>Add structured error handling and error codes to all API endpoints.<br> <li> Validate settings and mask sensitive info in responses.<br> <li> Return specific error messages for missing/invalid Supabase or GitHub <br>credentials.<br> <li> Set global settings service instance for consistent usage.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-e07921e05611e2e5ac04e38cf4e489a30417254f1dd744efb28a4e84b78937f6">+171/-59</a></td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>app.py</strong><dd><code>Automatic database initialization and improved error reporting</code></dd></summary>
<hr>

pr_agent/execserver/app.py

<li>Add database initialization and migration execution on app startup.<br> <li> Improve logging for database initialization.<br> <li> Add error code to general exception handler response.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-c7797f348a7596d2e99b198c3f5047b740b937f4c1e3c524f2d3fe04438b24d8">+34/-1</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>__init__.py</strong><dd><code>Database package initialization and migration exports</code>&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pr_agent/execserver/db/__init__.py

<li>Add database package initialization and export migration <br>functions/constants.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-95478b58ccc447864b6aab6fff4e78546f39b504e482819b205a51ea552ddae3">+14/-0</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>migrations.py</strong><dd><code>Database migration system and initial schema migration</code>&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pr_agent/execserver/db/migrations.py

<li>Implement migration system with versioning and initial schema <br>creation.<br> <li> Add migration tracking and up/down migration logic.<br> <li> Provide functions to initialize database and run migrations.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-200a2df453ee9917c80fbc092299d87b461c8afe32bbb8480fd00d495b5b2853">+298/-0</a>&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>db_service.py</strong><dd><code>Enhanced Supabase client initialization and error handling</code></dd></summary>
<hr>

pr_agent/execserver/services/db_service.py

<li>Improve Supabase client initialization and connection testing.<br> <li> Add is_connected method and detailed error handling.<br> <li> Add docstrings and error raising for uninitialized database.<br> <li> Enhance logging for database operations.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-f8f2288053863740335130a4347f7b3e12357508f6f5796769026fc020ab05ef">+99/-4</a>&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
      <summary><strong>settings_service.py</strong><dd><code>Robust settings validation and sensitive info masking</code>&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pr_agent/execserver/services/settings_service.py

<li>Add robust validation for Supabase and GitHub credentials.<br> <li> Mask sensitive values in settings API responses.<br> <li> Return detailed error messages for validation failures.<br> <li> Use tuple return for validation status and error message.


</details>


  </td>
  <td><a href="https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-a7389591c0e25a9a469b1de7e3f5e7fef5f7745ac39449085222ca7141811c64">+41/-15</a>&nbsp; </td>

</tr>
</table></td></tr></tr></tbody></table>

___

> <details> <summary>  Need help?</summary><li>Type <code>/help how to ...</code> in the comments thread for any questions about Qodo Merge usage.</li><li>Check out the <a href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a> for more information.</li></details>


Diff:
diff --git a/pr_agent/execserver/api/routes.py b/pr_agent/execserver/api/routes.py
index 6a00d62783..d9166103c6 100644
--- a/pr_agent/execserver/api/routes.py
+++ b/pr_agent/execserver/api/routes.py
@@ -14,13 +14,14 @@
 from ..services.settings_service import SettingsService
 from pr_agent.servers.utils import verify_signature
 from pr_agent.servers.github_app import handle_github_webhooks as pr_agent_handle_github_webhooks
-from ..config import get_setting_or_env
+from ..config import get_setting_or_env, set_settings_service
 
 # Create router
 router = APIRouter()
 
 # Service instances
 settings_service = SettingsService()
+set_settings_service(settings_service)  # Set the global settings service
 db_service = DatabaseService(settings_service)
 github_service = GitHubService()
 workflow_service = WorkflowService()
@@ -61,37 +62,58 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
 async def validate_settings(settings: Dict[str, str]):
     """
     Validate settings
+    
+    Returns:
+        JSON response with validation result and message
     """
     try:
         # Validate the settings
-        valid = await settings_service.validate_settings(settings)
-        return {"valid": valid}
+        valid, error_message = await settings_service.validate_settings(settings)
+        
+        if not valid:
+            return {"valid": False, "message": error_message, "code": "validation_error"}
+        
+        return {"valid": True, "message": "Settings validated successfully"}
     except Exception as e:
-        raise HTTPException(status_code=400, detail=str(e))
+        return {"valid": False, "message": str(e), "code": "validation_error"}
 
 @router.post("/api/v1/settings")
 async def save_settings(settings: Dict[str, str]):
     """
     Save settings
+    
+    Returns:
+        JSON response with save result
     """
     try:
+        # Validate settings before saving
+        valid, error_message = await settings_service.validate_settings(settings)
+        
+        if not valid:
+            raise HTTPException(status_code=400, detail={"message": error_message, "code": "validation_error"})
+        
         # Save the settings
         await settings_service.save_settings(settings)
-        return {"status": "success"}
+        return {"status": "success", "message": "Settings saved successfully"}
+    except HTTPException:
+        raise
     except Exception as e:
-        raise HTTPException(status_code=400, detail=str(e))
+        raise HTTPException(status_code=500, detail={"message": f"Failed to save settings: {str(e)}", "code": "server_error"})
 
 @router.get("/api/v1/settings")
 async def get_settings():
     """
     Get settings
+    
+    Returns:
+        JSON response with settings
     """
     try:
         # Get the settings
         settings = await settings_service.get_settings()
         return settings
     except Exception as e:
-        raise HTTPException(status_code=400, detail=str(e))
+        raise HTTPException(status_code=500, detail={"message": f"Failed to get settings: {str(e)}", "code": "server_error"})
 
 # Projects endpoints
 @router.get("/api/v1/projects")
@@ -99,24 +121,47 @@ async def get_projects() -> List[Project]:
     """
     Get all projects
     """
-    return await db_service.get_projects()
+    try:
+        return await db_service.get_projects()
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.get("/api/v1/projects/github")
 async def get_github_projects() -> List[Project]:
     """
     Get all GitHub repositories
     """
-    return await github_service.get_repositories()
+    try:
+        return await github_service.get_repositories()
+    except ValueError as e:
+        if "GitHub token" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "GitHub token not configured. Please set up GitHub token in settings.", "code": "github_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.get("/api/v1/projects/{project_id}")
 async def get_project(project_id: str) -> Project:
     """
     Get a project by ID
     """
-    project = await db_service.get_project(project_id)
-    if not project:
-        raise HTTPException(status_code=404, detail="Project not found")
-    return project
+    try:
+        project = await db_service.get_project(project_id)
+        if not project:
+            raise HTTPException(status_code=404, detail={"message": "Project not found", "code": "not_found"})
+        return project
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except HTTPException:
+        raise
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 # Triggers endpoints
 @router.get("/api/v1/triggers")
@@ -124,52 +169,86 @@ async def get_triggers(project_id: Optional[str] = None) -> List[Trigger]:
     """
     Get all triggers, optionally filtered by project
     """
-    if project_id:
-        return await db_service.get_triggers_for_project(project_id)
-    
-    # Get all projects
-    projects = await db_service.get_projects()
-    
-    # Get triggers for each project
-    all_triggers = []
-    for project in projects:
-        triggers = await db_service.get_triggers_for_project(project.id)
-        all_triggers.extend(triggers)
-    
-    return all_triggers
+    try:
+        if project_id:
+            return await db_service.get_triggers_for_project(project_id)
+        
+        # Get all projects
+        projects = await db_service.get_projects()
+        
+        # Get triggers for each project
+        all_triggers = []
+        for project in projects:
+            triggers = await db_service.get_triggers_for_project(project.id)
+            all_triggers.extend(triggers)
+        
+        return all_triggers
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.post("/api/v1/triggers")
 async def create_trigger(trigger: Trigger) -> Trigger:
     """
     Create a new trigger
     """
-    # Validate project exists
-    project = await db_service.get_project(trigger.project_id)
-    if not project:
-        raise HTTPException(status_code=404, detail="Project not found")
-    
-    # Create the trigger
-    return await db_service.create_trigger(trigger)
+    try:
+        # Validate project exists
+        project = await db_service.get_project(trigger.project_id)
+        if not project:
+            raise HTTPException(status_code=404, detail={"message": "Project not found", "code": "not_found"})
+        
+        # Create the trigger
+        return await db_service.create_trigger(trigger)
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except HTTPException:
+        raise
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.get("/api/v1/triggers/{trigger_id}")
 async def get_trigger(trigger_id: str) -> Trigger:
     """
     Get a trigger by ID
     """
-    trigger = await db_service.get_trigger(trigger_id)
-    if not trigger:
-        raise HTTPException(status_code=404, detail="Trigger not found")
-    return trigger
+    try:
+        trigger = await db_service.get_trigger(trigger_id)
+        if not trigger:
+            raise HTTPException(status_code=404, detail={"message": "Trigger not found", "code": "not_found"})
+        return trigger
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except HTTPException:
+        raise
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.patch("/api/v1/triggers/{trigger_id}")
 async def update_trigger(trigger_id: str, data: Dict[str, Any]) -> Trigger:
     """
     Update a trigger
     """
-    trigger = await db_service.update_trigger(trigger_id, data)
-    if not trigger:
-        raise HTTPException(status_code=404, detail="Trigger not found")
-    return trigger
+    try:
+        trigger = await db_service.update_trigger(trigger_id, data)
+        if not trigger:
+            raise HTTPException(status_code=404, detail={"message": "Trigger not found", "code": "not_found"})
+        return trigger
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except HTTPException:
+        raise
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 # Workflows endpoints
 @router.get("/api/v1/workflows")
@@ -177,29 +256,52 @@ async def get_workflows(repository: str) -> List[Workflow]:
     """
     Get all workflows for a repository
     """
-    owner, repo = repository.split("/")
-    return await github_service.get_workflows(owner, repo)
+    try:
+        owner, repo = repository.split("/")
+        return await github_service.get_workflows(owner, repo)
+    except ValueError as e:
+        if "GitHub token" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "GitHub token not configured. Please set up GitHub token in settings.", "code": "github_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.get("/api/v1/workflow_runs")
 async def get_workflow_runs(repository: str, workflow_id: Optional[str] = None, limit: int = 10) -> List[WorkflowRun]:
     """
     Get workflow runs for a repository
     """
-    owner, repo = repository.split("/")
-    return await github_service.get_workflow_runs(owner, repo, workflow_id, limit)
+    try:
+        owner, repo = repository.split("/")
+        return await github_service.get_workflow_runs(owner, repo, workflow_id, limit)
+    except ValueError as e:
+        if "GitHub token" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "GitHub token not configured. Please set up GitHub token in settings.", "code": "github_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 @router.post("/api/v1/workflows/{workflow_id}/dispatch")
 async def trigger_workflow(workflow_id: str, repository: str, ref: Optional[str] = None, inputs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
     """
     Trigger a workflow
     """
-    owner, repo = repository.split("/")
-    success = await github_service.trigger_workflow(owner, repo, workflow_id, ref, inputs)
-    
-    if not success:
-        raise HTTPException(status_code=500, detail="Failed to trigger workflow")
-    
-    return {"status": "triggered"}
+    try:
+        owner, repo = repository.split("/")
+        success = await github_service.trigger_workflow(owner, repo, workflow_id, ref, inputs)
+        
+        if not success:
+            raise HTTPException(status_code=500, detail={"message": "Failed to trigger workflow", "code": "workflow_error"})
+        
+        return {"status": "triggered"}
+    except ValueError as e:
+        if "GitHub token" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "GitHub token not configured. Please set up GitHub token in settings.", "code": "github_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except HTTPException:
+        raise
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 # Events endpoints
 @router.get("/api/v1/events")
@@ -207,7 +309,14 @@ async def get_events(repository: Optional[str] = None, limit: int = 10) -> List[
     """
     Get recent events
     """
-    return await event_service.get_recent_events(repository, limit)
+    try:
+        return await event_service.get_recent_events(repository, limit)
+    except ValueError as e:
+        if "Supabase is not initialized" in str(e):
+            raise HTTPException(status_code=400, detail={"message": "Database connection not configured. Please set up Supabase URL and API key in settings.", "code": "db_not_configured"})
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
 
 # Execute code endpoint
 @router.post("/api/v1/execute_code")
@@ -215,9 +324,12 @@ async def execute_code(code: str, repository: str, event_data: Dict[str, Any]) -
     """
     Execute Python code
     """
-    success = await workflow_service.execute_python_code(code, repository, event_data)
-    
-    if not success:
-        raise HTTPException(status_code=500, detail="Failed to execute code")
-    
-    return {"status": "executed"}
+    try:
+        success = await workflow_service.execute_python_code(code, repository, event_data)
+        
+        if not success:
+            raise HTTPException(status_code=500, detail={"message": "Failed to execute code", "code": "execution_error"})
+        
+        return {"status": "executed"}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail={"message": str(e), "code": "server_error"})
diff --git a/pr_agent/execserver/app.py b/pr_agent/execserver/app.py
index b33ce9de2d..7a5c65190c 100644
--- a/pr_agent/execserver/app.py
+++ b/pr_agent/execserver/app.py
@@ -6,6 +6,7 @@
 """
 
 import os
+import asyncio
 from pathlib import Path
 from fastapi import FastAPI, Request, Response
 from fastapi.middleware.cors import CORSMiddleware
@@ -13,8 +14,12 @@
 from fastapi.responses import JSONResponse
 
 from pr_agent.execserver.api.routes import router as api_router
-from pr_agent.execserver.config import get_cors_origins, get_log_level, get_log_format, set_settings_service
+from pr_agent.execserver.config import (
+    get_cors_origins, get_log_level, get_log_format, 
+    set_settings_service, get_supabase_url, get_supabase_anon_key
+)
 from pr_agent.execserver.services.settings_service import SettingsService
+from pr_agent.execserver.db import initialize_database
 from pr_agent.log import setup_logger, LoggingFormat
 from pr_agent.error_handler import PRAgentError, handle_exceptions
 from pr_agent.log.enhanced_logging import RequestContext, structured_log
@@ -115,6 +120,7 @@ async def general_exception_handler(request: Request, exc: Exception):
         content={
             "error": "InternalServerError",
             "message": "An unexpected error occurred",
+            "code": "server_error"
         },
     )
 
@@ -132,6 +138,33 @@ async def health_check():
     """Health check endpoint for monitoring."""
     return {"status": "ok", "version": "0.1.0"}
 
+# Database initialization
+@app.on_event("startup")
+async def startup_db_client():
+    """Initialize database on startup."""
+    try:
+        # Try to get Supabase credentials from settings service first
+        supabase_url = settings_service.get_setting('SUPABASE_URL')
+        supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
+        
+        # If not found in settings service, try config
+        if not supabase_url or not supabase_anon_key:
+            supabase_url = get_supabase_url()
+            supabase_anon_key = get_supabase_anon_key()
+        
+        # Initialize database if credentials are available
+        if supabase_url and supabase_anon_key:
+            logger.info("Initializing database...")
+            success = await initialize_database(supabase_url, supabase_anon_key)
+            if success:
+                logger.info("Database initialized successfully")
+            else:
+                logger.warning("Database initialization failed")
+        else:
+            logger.warning("Supabase credentials not found, skipping database initialization")
+    except Exception as e:
+        logger.error(f"Error initializing database: {str(e)}")
+
 # Main entry point
 if __name__ == "__main__":
     import uvicorn
diff --git a/pr_agent/execserver/db/__init__.py b/pr_agent/execserver/db/__init__.py
new file mode 100644
index 0000000000..98ea5d140e
--- /dev/null
+++ b/pr_agent/execserver/db/__init__.py
@@ -0,0 +1,14 @@
+"""
+Database package for ExeServer.
+
+This package provides database-related functionality for the ExeServer module.
+"""
+
+from .migrations import initialize_database, run_migrations, get_current_db_version, CURRENT_SCHEMA_VERSION
+
+__all__ = [
+    'initialize_database',
+    'run_migrations',
+    'get_current_db_version',
+    'CURRENT_SCHEMA_VERSION',
+]
diff --git a/pr_agent/execserver/db/migrations.py b/pr_agent/execserver/db/migrations.py
new file mode 100644
index 0000000000..8aa473499c
--- /dev/null
+++ b/pr_agent/execserver/db/migrations.py
@@ -0,0 +1,298 @@
+"""
+Database migration module for ExeServer.
+
+This module provides functionality for managing database schema migrations.
+"""
+
+import logging
+import os
+import json
+from datetime import datetime
+from typing import List, Dict, Any, Optional
+
+from supabase import create_client, Client
+
+logger = logging.getLogger(__name__)
+
+# Migration schema version
+CURRENT_SCHEMA_VERSION = 1
+
+class Migration:
+    """
+    Base class for database migrations
+    """
+    def __init__(self, version: int, description: str):
+        self.version = version
+        self.description = description
+    
+    async def up(self, supabase: Client) -> bool:
+        """
+        Apply the migration
+        
+        Args:
+            supabase: Supabase client
+            
+        Returns:
+            True if successful
+        """
+        raise NotImplementedError("Subclasses must implement up()")
+    
+    async def down(self, supabase: Client) -> bool:
+        """
+        Revert the migration
+        
+        Args:
+            supabase: Supabase client
+            
+        Returns:
+            True if successful
+        """
+        raise NotImplementedError("Subclasses must implement down()")
+
+
+class InitialMigration(Migration):
+    """
+    Initial database migration to create the schema
+    """
+    def __init__(self):
+        super().__init__(1, "Initial schema creation")
+    
+    async def up(self, supabase: Client) -> bool:
+        """
+        Create the initial schema
+        
+        Args:
+            supabase: Supabase client
+            
+        Returns:
+            True if successful
+        """
+        try:
+            # Create events table
+            supabase.table("events").select("*").limit(1).execute()
+            logger.info("Events table already exists")
+        except Exception:
+            logger.info("Creating events table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "events",
+                "columns": """
+                    id uuid PRIMARY KEY,
+                    event_type text NOT NULL,
+                    repository text NOT NULL,
+                    payload jsonb NOT NULL,
+                    created_at timestamp NOT NULL,
+                    processed boolean NOT NULL DEFAULT false,
+                    processed_at timestamp
+                """
+            }).execute()
+        
+        try:
+            # Create projects table
+            supabase.table("projects").select("*").limit(1).execute()
+            logger.info("Projects table already exists")
+        except Exception:
+            logger.info("Creating projects table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "projects",
+                "columns": """
+                    id uuid PRIMARY KEY,
+                    name text NOT NULL,
+                    full_name text NOT NULL,
+                    description text,
+                    created_at timestamp NOT NULL
+                """
+            }).execute()
+        
+        try:
+            # Create triggers table
+            supabase.table("triggers").select("*").limit(1).execute()
+            logger.info("Triggers table already exists")
+        except Exception:
+            logger.info("Creating triggers table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "triggers",
+                "columns": """
+                    id uuid PRIMARY KEY,
+                    project_id uuid NOT NULL REFERENCES projects(id),
+                    name text NOT NULL,
+                    description text,
+                    conditions jsonb NOT NULL,
+                    actions jsonb NOT NULL,
+                    enabled boolean NOT NULL DEFAULT true,
+                    created_at timestamp NOT NULL
+                """
+            }).execute()
+        
+        try:
+            # Create workflows table
+            supabase.table("workflows").select("*").limit(1).execute()
+            logger.info("Workflows table already exists")
+        except Exception:
+            logger.info("Creating workflows table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "workflows",
+                "columns": """
+                    id uuid PRIMARY KEY,
+                    repository text NOT NULL,
+                    name text NOT NULL,
+                    description text,
+                    created_at timestamp NOT NULL
+                """
+            }).execute()
+        
+        try:
+            # Create workflow_runs table
+            supabase.table("workflow_runs").select("*").limit(1).execute()
+            logger.info("Workflow runs table already exists")
+        except Exception:
+            logger.info("Creating workflow_runs table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "workflow_runs",
+                "columns": """
+                    id uuid PRIMARY KEY,
+                    workflow_id uuid NOT NULL REFERENCES workflows(id),
+                    repository text NOT NULL,
+                    status text NOT NULL,
+                    created_at timestamp NOT NULL,
+                    completed_at timestamp
+                """
+            }).execute()
+        
+        try:
+            # Create migrations table
+            supabase.table("migrations").select("*").limit(1).execute()
+            logger.info("Migrations table already exists")
+        except Exception:
+            logger.info("Creating migrations table")
+            supabase.rpc("create_table_if_not_exists", {
+                "table_name": "migrations",
+                "columns": """
+                    id serial PRIMARY KEY,
+                    version integer NOT NULL,
+                    description text NOT NULL,
+                    applied_at timestamp NOT NULL
+                """
+            }).execute()
+            
+            # Insert this migration
+            supabase.table("migrations").insert({
+                "version": self.version,
+                "description": self.description,
+                "applied_at": datetime.utcnow().isoformat()
+            }).execute()
+        
+        return True
+    
+    async def down(self, supabase: Client) -> bool:
+        """
+        Revert the initial schema
+        
+        Args:
+            supabase: Supabase client
+            
+        Returns:
+            True if successful
+        """
+        try:
+            # Drop tables in reverse order
+            supabase.rpc("drop_table_if_exists", {"table_name": "workflow_runs"}).execute()
+            supabase.rpc("drop_table_if_exists", {"table_name": "workflows"}).execute()
+            supabase.rpc("drop_table_if_exists", {"table_name": "triggers"}).execute()
+            supabase.rpc("drop_table_if_exists", {"table_name": "projects"}).execute()
+            supabase.rpc("drop_table_if_exists", {"table_name": "events"}).execute()
+            supabase.rpc("drop_table_if_exists", {"table_name": "migrations"}).execute()
+            
+            return True
+        except Exception as e:
+            logger.error(f"Error reverting migration: {str(e)}")
+            return False
+
+
+# List of all migrations in order
+MIGRATIONS = [
+    InitialMigration(),
+    # Add new migrations here
+]
+
+
+async def get_current_db_version(supabase: Client) -> int:
+    """
+    Get the current database schema version
+    
+    Args:
+        supabase: Supabase client
+        
+    Returns:
+        Current schema version
+    """
+    try:
+        result = supabase.table("migrations").select("version").order("version", desc=True).limit(1).execute()
+        if result.data:
+            return result.data[0]["version"]
+        return 0
+    except Exception:
+        return 0
+
+
+async def run_migrations(supabase: Client) -> bool:
+    """
+    Run all pending migrations
+    
+    Args:
+        supabase: Supabase client
+        
+    Returns:
+        True if all migrations were successful
+    """
+    try:
+        current_version = await get_current_db_version(supabase)
+        logger.info(f"Current database schema version: {current_version}")
+        
+        # Apply pending migrations
+        for migration in MIGRATIONS:
+            if migration.version > current_version:
+                logger.info(f"Applying migration {migration.version}: {migration.description}")
+                success = await migration.up(supabase)
+                
+                if not success:
+                    logger.error(f"Migration {migration.version} failed")
+                    return False
+                
+                # Record the migration
+                if migration.version > 1:  # Skip for initial migration (it records itself)
+                    supabase.table("migrations").insert({
+                        "version": migration.version,
+                        "description": migration.description,
+                        "applied_at": datetime.utcnow().isoformat()
+                    }).execute()
+                
+                logger.info(f"Migration {migration.version} applied successfully")
+        
+        return True
+    except Exception as e:
+        logger.error(f"Error running migrations: {str(e)}")
+        return False
+
+
+async def initialize_database(supabase_url: str, supabase_anon_key: str) -> bool:
+    """
+    Initialize the database and run migrations
+    
+    Args:
+        supabase_url: Supabase URL
+        supabase_anon_key: Supabase anonymous key
+        
+    Returns:
+        True if initialization was successful
+    """
+    try:
+        # Create Supabase client
+        supabase = create_client(supabase_url, supabase_anon_key)
+        
+        # Run migrations
+        success = await run_migrations(supabase)
+        
+        return success
+    except Exception as e:
+        logger.error(f"Error initializing database: {str(e)}")
+        return False
diff --git a/pr_agent/execserver/services/db_service.py b/pr_agent/execserver/services/db_service.py
index 44a2a4773b..ea99ff41df 100644
--- a/pr_agent/execserver/services/db_service.py
+++ b/pr_agent/execserver/services/db_service.py
@@ -3,6 +3,7 @@
 from typing import Dict, Any, List, Optional
 from datetime import datetime
 import json
+import logging
 
 from supabase import create_client, Client
 
@@ -12,6 +13,8 @@
 from ..models.workflow import Workflow, WorkflowRun
 from ..config import get_supabase_url, get_supabase_anon_key
 
+# Set up logging
+logger = logging.getLogger(__name__)
 
 class DatabaseService:
     """
@@ -24,7 +27,12 @@ def __init__(self, settings_service=None):
         self._initialize_supabase()
         
     def _initialize_supabase(self):
-        """Initialize the Supabase client"""
+        """
+        Initialize the Supabase client
+        
+        This method attempts to initialize the Supabase client using settings
+        from the settings service or from the config.
+        """
         try:
             # Try to get settings from settings service first
             if self.settings_service:
@@ -32,22 +40,61 @@ def _initialize_supabase(self):
                 supabase_anon_key = self.settings_service.get_setting('SUPABASE_ANON_KEY')
                 
                 if supabase_url and supabase_anon_key:
+                    logger.info("Initializing Supabase from settings service")
                     self.supabase = create_client(supabase_url, supabase_anon_key)
+                    # Test connection with a simple query
+                    self.supabase.table('events').select('*').limit(1).execute()
+                    logger.info("Supabase initialized successfully from settings service")
                     return
             
             # Fall back to config if settings service doesn't have the values
-            self.supabase = create_client(get_supabase_url(), get_supabase_anon_key())
+            supabase_url = get_supabase_url()
+            supabase_anon_key = get_supabase_anon_key()
+            
+            if supabase_url and supabase_anon_key:
+                logger.info("Initializing Supabase from config")
+                self.supabase = create_client(supabase_url, supabase_anon_key)
+                # Test connection with a simple query
+                self.supabase.table('events').select('*').limit(1).execute()
+                logger.info("Supabase initialized successfully from config")
+                return
+                
+            logger.warning("Supabase URL and API key not found in settings or config")
         except Exception as e:
             # Log the error but don't raise it - we'll handle missing Supabase in each method
-            print(f"Failed to initialize Supabase: {str(e)}")
+            logger.error(f"Failed to initialize Supabase: {str(e)}")
+            self.supabase = None
     
     def _ensure_supabase(self):
-        """Ensure Supabase is initialized"""
+        """
+        Ensure Supabase is initialized
+        
+        Raises:
+            ValueError: If Supabase is not initialized
+        """
         if not self.supabase:
+            # Try to initialize again in case settings were updated
             self._initialize_supabase()
             
         if not self.supabase:
             raise ValueError("Supabase is not initialized. Please provide Supabase URL and API key in settings.")
+    
+    def is_connected(self) -> bool:
+        """
+        Check if the database connection is established
+        
+        Returns:
+            bool: True if connected, False otherwise
+        """
+        try:
+            if not self.supabase:
+                return False
+                
+            # Test connection with a simple query
+            self.supabase.table('events').select('*').limit(1).execute()
+            return True
+        except Exception:
+            return False
         
     # Event methods
     async def log_event(self, event_type: str, repository: str, payload: Dict[str, Any]) -> Event:
@@ -61,6 +108,9 @@ async def log_event(self, event_type: str, repository: str, payload: Dict[str, A
             
         Returns:
             The created Event object
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -89,6 +139,9 @@ async def mark_event_processed(self, event_id: str) -> Event:
             
         Returns:
             The updated Event object
+            
+        Raises:
+            ValueError: If Supabase is not initialized or event not found
         """
         self._ensure_supabase()
         
@@ -116,6 +169,9 @@ async def get_event(self, event_id: str) -> Optional[Event]:
             
         Returns:
             The Event object or None if not found
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -140,6 +196,9 @@ async def get_events(self, repository: Optional[str] = None, processed: Optional
             
         Returns:
             List of Event objects
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -165,6 +224,9 @@ async def create_project(self, project: Project) -> Project:
             
         Returns:
             The created Project object
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -180,6 +242,9 @@ async def get_project(self, project_id: str) -> Optional[Project]:
             
         Returns:
             The Project object or None if not found
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -197,6 +262,9 @@ async def get_projects(self) -> List[Project]:
         
         Returns:
             List of Project objects
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -213,6 +281,9 @@ async def create_trigger(self, trigger: Trigger) -> Trigger:
             
         Returns:
             The created Trigger object
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -228,6 +299,9 @@ async def get_trigger(self, trigger_id: str) -> Optional[Trigger]:
             
         Returns:
             The Trigger object or None if not found
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -248,6 +322,9 @@ async def get_triggers_for_project(self, project_id: str) -> List[Trigger]:
             
         Returns:
             List of Trigger objects
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -264,6 +341,9 @@ async def update_trigger(self, trigger_id: str, data: Dict[str, Any]) -> Optiona
             
         Returns:
             The updated Trigger object or None if not found
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -285,6 +365,9 @@ async def create_workflow(self, workflow: Workflow) -> Workflow:
             
         Returns:
             The created Workflow object
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -300,6 +383,9 @@ async def get_workflow(self, workflow_id: str) -> Optional[Workflow]:
             
         Returns:
             The Workflow object or None if not found
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -320,6 +406,9 @@ async def get_workflows_for_repository(self, repository: str) -> List[Workflow]:
             
         Returns:
             List of Workflow objects
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -335,6 +424,9 @@ async def create_workflow_run(self, workflow_run: WorkflowRun) -> WorkflowRun:
             
         Returns:
             The created WorkflowRun object
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
@@ -354,6 +446,9 @@ async def get_workflow_runs(self, workflow_id: Optional[str] = None, repository:
             
         Returns:
             List of WorkflowRun objects
+            
+        Raises:
+            ValueError: If Supabase is not initialized
         """
         self._ensure_supabase()
         
diff --git a/pr_agent/execserver/services/settings_service.py b/pr_agent/execserver/services/settings_service.py
index 6b72ccd5ea..8c1d0e12f2 100644
--- a/pr_agent/execserver/services/settings_service.py
+++ b/pr_agent/execserver/services/settings_service.py
@@ -6,8 +6,10 @@
 
 import os
 import json
-from typing import Dict, Any, Optional
+import re
+from typing import Dict, Any, Optional, Tuple
 from supabase import create_client, Client
+import requests
 
 class SettingsService:
     """
@@ -43,7 +45,13 @@ async def get_settings(self) -> Dict[str, str]:
         Returns:
             Dict of settings
         """
-        return self.settings
+        # Return a copy of settings with sensitive information masked
+        masked_settings = self.settings.copy()
+        for key in masked_settings:
+            if any(sensitive in key.upper() for sensitive in ['TOKEN', 'KEY', 'SECRET', 'PASSWORD']):
+                if masked_settings[key]:
+                    masked_settings[key] = f"{masked_settings[key][:4]}{'*' * (len(masked_settings[key]) - 8)}{masked_settings[key][-4:]}" if len(masked_settings[key]) > 8 else "********"
+        return masked_settings
     
     async def save_settings(self, settings: Dict[str, str]) -> bool:
         """
@@ -63,7 +71,7 @@ async def save_settings(self, settings: Dict[str, str]) -> bool:
         
         return True
     
-    async def validate_settings(self, settings: Dict[str, str]) -> bool:
+    async def validate_settings(self, settings: Dict[str, str]) -> Tuple[bool, Optional[str]]:
         """
         Validate settings
         
@@ -71,18 +79,24 @@ async def validate_settings(self, settings: Dict[str, str]) -> bool:
             settings: Dict of settings to validate
             
         Returns:
-            True if valid
-            
-        Raises:
-            Exception: If validation fails
+            Tuple of (is_valid, error_message)
         """
         # Validate Supabase settings if provided
-        if 'SUPABASE_URL' in settings and 'SUPABASE_ANON_KEY' in settings:
+        if 'SUPABASE_URL' in settings or 'SUPABASE_ANON_KEY' in settings:
+            # Both URL and key must be provided together
+            if 'SUPABASE_URL' not in settings or 'SUPABASE_ANON_KEY' not in settings:
+                return False, "Both Supabase URL and API key must be provided together"
+            
             supabase_url = settings['SUPABASE_URL']
             supabase_anon_key = settings['SUPABASE_ANON_KEY']
             
-            if not supabase_url or not supabase_anon_key:
-                raise Exception("Supabase URL and API key are required")
+            # Validate URL format
+            if not supabase_url or not supabase_url.startswith(('http://', 'https://')):
+                return False, "Supabase URL must be a valid URL starting with http:// or https://"
+            
+            # Validate API key format (should be a non-empty string)
+            if not supabase_anon_key or len(supabase_anon_key) < 10:
+                return False, "Supabase API key appears to be invalid (too short)"
             
             try:
                 # Try to connect to Supabase
@@ -90,18 +104,30 @@ async def validate_settings(self, settings: Dict[str, str]) -> bool:
                 # Test a simple query
                 supabase.table('events').select('*').limit(1).execute()
             except Exception as e:
-                raise Exception(f"Failed to connect to Supabase: {str(e)}")
+                return False, f"Failed to connect to Supabase: {str(e)}"
         
         # Validate GitHub token if provided
         if 'GITHUB_TOKEN' in settings:
             github_token = settings['GITHUB_TOKEN']
             
-            if not github_token:
-                raise Exception("GitHub token is required")
+            # Validate token format (should be a non-empty string)
+            if not github_token or len(github_token) < 10:
+                return False, "GitHub token appears to be invalid (too short)"
             
-            # Additional GitHub token validation could be added here
+            # Validate token with GitHub API
+            try:
+                headers = {
+                    'Authorization': f'token {github_token}',
+                    'Accept': 'application/vnd.github.v3+json'
+                }
+                response = requests.get('https://api.github.com/user', headers=headers)
+                
+                if response.status_code != 200:
+                    return False, f"GitHub token validation failed: {response.json().get('message', 'Unknown error')}"
+            except Exception as e:
+                return False, f"GitHub token validation failed: {str(e)}"
         
-        return True
+        return True, None
     
     def get_setting(self, key: str, default: Any = None) -> Any:
         """


Reviews:
user: login: sourcery-ai[bot]
body: We have skipped reviewing this pull request. It seems to have been created by a bot (hey, codegen-sh[bot]!). We assume it knows what it's doing!
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-26T11:52:38Z
user: login: qodo-code-review[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-26T12:00:23Z
user: login: codiumai-pr-agent-free[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-26T12:00:23Z

Review comments:
diff_hunk: @@ -132,6 +138,33 @@ async def health_check():
     """Health check endpoint for monitoring."""
     return {"status": "ok", "version": "0.1.0"}
 
+# Database initialization
+@app.on_event("startup")
+async def startup_db_client():
+    """Initialize database on startup."""
+    try:
+        # Try to get Supabase credentials from settings service first
+        supabase_url = settings_service.get_setting('SUPABASE_URL')
+        supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
path: pr_agent/execserver/app.py
user: login: qodo-code-review[bot]
body: **Suggestion:** Fix undefined variable usage
```suggestion
        # Check if settings_service is initialized
        if 'settings_service' in globals() and settings_service:
            supabase_url = settings_service.get_setting('SUPABASE_URL')
            supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
        else:
            supabase_url = None
            supabase_anon_key = None
```

<!-- manually_applied -->
created_at: 2025-04-26T12:00:23Z
author_association: NONE
diff_hunk: @@ -132,6 +138,33 @@ async def health_check():
     """Health check endpoint for monitoring."""
     return {"status": "ok", "version": "0.1.0"}
 
+# Database initialization
+@app.on_event("startup")
+async def startup_db_client():
+    """Initialize database on startup."""
+    try:
+        # Try to get Supabase credentials from settings service first
+        supabase_url = settings_service.get_setting('SUPABASE_URL')
+        supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
path: pr_agent/execserver/app.py
user: login: codiumai-pr-agent-free[bot]
body: **Suggestion:** Fix undefined variable usage
```suggestion
        # Check if settings_service is initialized
        if 'settings_service' in globals() and settings_service:
            supabase_url = settings_service.get_setting('SUPABASE_URL')
            supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
        else:
            supabase_url = None
            supabase_anon_key = None
```

<!-- manually_applied -->
created_at: 2025-04-26T12:00:23Z
author_association: NONE

PR comments:
user: login: korbit-ai[bot]
created_at: 2025-04-26T11:47:25Z
body: By default, I don't review pull requests opened by bots. If you would like me to review this pull request anyway, you can request a review via the `/korbit-review` command in a comment.

[](695139a1-e4ad-4e64-b80a-cc551ec72f79)
author_association: NONE
user: login: sourcery-ai[bot]
created_at: 2025-04-26T11:47:26Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request implements robust API key validation for Supabase and GitHub credentials, ensuring they are valid and connected before use. It also introduces a database migration framework with an initial schema creation and automatic execution on application startup. Error handling across API routes has been enhanced to provide structured responses with specific error codes, and sensitive information in settings responses is now masked.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented comprehensive validation for Supabase URL/Key and GitHub Token. | <ul><li>Validate Supabase URL format and key presence</li><li>Test Supabase connection during validation</li><li>Validate GitHub token via GitHub API call</li><li>Return specific error messages for validation failures</li></ul> | `pr_agent/execserver/services/settings_service.py`<br/>`pr_agent/execserver/api/routes.py` |
| Introduced a versioned database migration framework. | <ul><li>Created a versioned migration framework</li><li>Defined migration classes</li><li>Implemented initial schema creation migration</li><li>Added functions for managing and running migrations</li></ul> | `pr_agent/execserver/db/migrations.py`<br/>`pr_agent/execserver/db/__init__.py` |
| Configured automatic database initialization and migration execution on application startup. | <ul><li>Added application startup event handler</li><li>Attempted initialization from settings service then config</li><li>Executed database initialization and migrations on startup</li></ul> | `pr_agent/execserver/app.py`<br/>`pr_agent/execserver/services/db_service.py` |
| Enhanced API error responses to include specific error codes and structured details. | <ul><li>Modified API endpoints to return consistent error response format</li><li>Included specific error codes for different failure types (validation, config, server, etc.)</li></ul> | `pr_agent/execserver/api/routes.py`<br/>`pr_agent/execserver/app.py`<br/>`pr_agent/execserver/services/db_service.py` |
| Implemented masking of sensitive information in the settings API response. | <ul><li>Masked sensitive values (like tokens and keys) in the settings API response</li></ul> | `pr_agent/execserver/services/settings_service.py` |
| Refined Supabase client initialization and added connection testing. | <ul><li>Prioritized settings service for credentials</li><li>Added connection testing after initialization</li><li>Provided a connection status check method</li><li>Added logging for database operations</li></ul> | `pr_agent/execserver/services/db_service.py` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: korbit-ai[bot]
created_at: 2025-04-26T11:51:36Z
body: By default, I don't review pull requests opened by bots. If you would like me to review this pull request anyway, you can request a review via the `/korbit-review` command in a comment.

[](76217b22-add8-4167-baf8-9f819be4a67f)
author_association: NONE
user: login: qodo-code-review[bot]
created_at: 2025-04-26T11:52:01Z
body: ## PR Reviewer Guide 🔍

#### (Review updated until commit https://github.com/Zeeeepa/pr-agent/commit/37b5af9f2e62b6be3df8adcc90a33d55c3039fb8)


Here are some key observations to aid the review process:

<table>
<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr>
<tr><td>🧪&nbsp;<strong>No relevant tests</strong></td></tr>
<tr><td>🔒&nbsp;<strong>Security concerns</strong><br><br>

<strong>Sensitive information exposure:</strong><br> The settings service in pr_agent/execserver/services/settings_service.py implements masking for sensitive values (lines 49-53), but the implementation has limitations. It only masks values whose keys contain 'TOKEN', 'KEY', 'SECRET', or 'PASSWORD' (case-insensitive). This approach could miss sensitive values with non-standard key names. Additionally, the masking still shows the first and last 4 characters of the sensitive value, which might reveal enough information for short tokens or keys. A more comprehensive approach would be to use a whitelist of safe keys rather than trying to detect sensitive keys.</td></tr>
<tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br>

<details><summary><a href='https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-c7797f348a7596d2e99b198c3f5047b740b937f4c1e3c524f2d3fe04438b24d8R165-R166'><strong>Error Handling</strong></a>

The database initialization in startup_db_client() catches all exceptions but only logs them. This could lead to the application starting with an uninitialized database without proper error notification to users.
</summary>

```python
except Exception as e:
    logger.error(f"Error initializing database: {str(e)}")
```

</details>

<details><summary><a href='https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-a7389591c0e25a9a469b1de7e3f5e7fef5f7745ac39449085222ca7141811c64R49-R53'><strong>Security Concern</strong></a>

The masking of sensitive information in get_settings() may not be comprehensive. The current implementation only masks values if the key contains certain keywords, which could miss sensitive data with non-standard key names.
</summary>

```python
masked_settings = self.settings.copy()
for key in masked_settings:
    if any(sensitive in key.upper() for sensitive in ['TOKEN', 'KEY', 'SECRET', 'PASSWORD']):
        if masked_settings[key]:
            masked_settings[key] = f"{masked_settings[key][:4]}{'*' * (len(masked_settings[key]) - 8)}{masked_settings[key][-4:]}" if len(masked_settings[key]) > 8 else "********"
```

</details>

<details><summary><a href='https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-200a2df453ee9917c80fbc092299d87b461c8afe32bbb8480fd00d495b5b2853R272-R274'><strong>Error Handling</strong></a>

The migration system catches exceptions but doesn't provide a mechanism to roll back partially applied migrations when one fails, which could leave the database in an inconsistent state.
</summary>

```python
except Exception as e:
    logger.error(f"Error running migrations: {str(e)}")
    return False
```

</details>

</td></tr>
</table>

author_association: NONE
user: login: qodo-code-review[bot]
created_at: 2025-04-26T11:52:26Z
body: ## PR Code Suggestions ✨
<!-- 37b5af9 -->

Latest suggestions up to 37b5af9
Explore these optional code suggestions:

<table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=3>Possible issue</td>
<td>



<details><summary>✅ 
Fix undefined variable usage
</summary>

___


**The <code>settings_service</code> variable is used before it's defined. The function is <br>trying to access settings from the settings_service before it's properly <br>initialized, which will cause an error.**

[pr_agent/execserver/app.py [146-148]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-c7797f348a7596d2e99b198c3f5047b740b937f4c1e3c524f2d3fe04438b24d8R146-R148)

```diff
-# Try to get Supabase credentials from settings service first
-supabase_url = settings_service.get_setting('SUPABASE_URL')
-supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
+# Check if settings_service is initialized
+if 'settings_service' in globals() and settings_service:
+    supabase_url = settings_service.get_setting('SUPABASE_URL')
+    supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
+else:
+    supabase_url = None
+    supabase_anon_key = None
```

`[Suggestion has been applied]`

<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: This suggestion addresses a critical issue where the code attempts to use the settings_service variable in the startup_db_client function before ensuring it's properly defined, which could cause application startup failures.


</details></details></td><td align=center>High

</td></tr><tr><td>



<details><summary>Fix JSON parsing error</summary>

___


**The code attempts to parse the JSON response even when the response might not be <br>JSON (e.g., if GitHub returns an HTML error page). This could raise an exception <br>and prevent proper error handling.**

[pr_agent/execserver/services/settings_service.py [119-126]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-a7389591c0e25a9a469b1de7e3f5e7fef5f7745ac39449085222ca7141811c64R119-R126)

```diff
 # Validate token with GitHub API
 try:
     headers = {
         'Authorization': f'token {github_token}',
         'Accept': 'application/vnd.github.v3+json'
     }
     response = requests.get('https://api.github.com/user', headers=headers)
     
     if response.status_code != 200:
-        return False, f"GitHub token validation failed: {response.json().get('message', 'Unknown error')}"
+        error_message = "Unknown error"
+        try:
+            error_message = response.json().get('message', 'Unknown error')
+        except:
+            pass
+        return False, f"GitHub token validation failed: {error_message}"
```


`[To ensure code accuracy, apply this suggestion manually]`

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: The suggestion fixes a potential exception that could occur when trying to parse a non-JSON response from GitHub's API, which would prevent proper error handling and potentially crash the validation process.


</details></details></td><td align=center>Medium

</td></tr><tr><td>



<details><summary>Fix table drop order</summary>

___


**The migrations table should be dropped last since it's being used to track the <br>migration status. If it's dropped before other tables and an error occurs, the <br>system won't be able to track which tables were successfully dropped.**

[pr_agent/execserver/db/migrations.py [197-203]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-200a2df453ee9917c80fbc092299d87b461c8afe32bbb8480fd00d495b5b2853R197-R203)

```diff
 # Drop tables in reverse order
 supabase.rpc("drop_table_if_exists", {"table_name": "workflow_runs"}).execute()
 supabase.rpc("drop_table_if_exists", {"table_name": "workflows"}).execute()
 supabase.rpc("drop_table_if_exists", {"table_name": "triggers"}).execute()
 supabase.rpc("drop_table_if_exists", {"table_name": "projects"}).execute()
 supabase.rpc("drop_table_if_exists", {"table_name": "events"}).execute()
+# Drop migrations table last
 supabase.rpc("drop_table_if_exists", {"table_name": "migrations"}).execute()
```

- [ ] **Apply this suggestion** <!-- /improve --apply_suggestion=2 -->

<details><summary>Suggestion importance[1-10]: 7</summary>

__

Why: The suggestion correctly identifies a logical issue in the migration system. Dropping the migrations table before other tables could lead to inconsistent state if an error occurs during the process, as the system would lose track of which tables were successfully dropped.


</details></details></td><td align=center>Medium

</td></tr>
<tr><td align="center" colspan="2">

- [ ] More <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

___

#### Previous suggestions
<details><summary>Suggestions up to commit 37b5af9</summary>
<br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=3>Possible issue</td>
<td>



<details><summary>Add null check</summary>

___


**The code is trying to access <code>settings_service</code> without checking if it's properly <br>initialized. If the settings service isn't available at startup, this will cause <br>an application crash. Add a null check before attempting to access the settings <br>service.**

[pr_agent/execserver/app.py [146-153]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-c7797f348a7596d2e99b198c3f5047b740b937f4c1e3c524f2d3fe04438b24d8R146-R153)

```diff
 # Try to get Supabase credentials from settings service first
-supabase_url = settings_service.get_setting('SUPABASE_URL')
-supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
+supabase_url = None
+supabase_anon_key = None
+
+if settings_service:
+    supabase_url = settings_service.get_setting('SUPABASE_URL')
+    supabase_anon_key = settings_service.get_setting('SUPABASE_ANON_KEY')
 
 # If not found in settings service, try config
 if not supabase_url or not supabase_anon_key:
     supabase_url = get_supabase_url()
     supabase_anon_key = get_supabase_anon_key()
```

 <!-- /improve --apply_suggestion=0 -->

<details><summary>Suggestion importance[1-10]: 9</summary>

__

Why: This suggestion prevents a potential application crash by adding a null check before accessing the settings_service. This is a critical defensive programming practice that improves application stability during startup.


</details></details></td><td align=center>High

</td></tr><tr><td>



<details><summary>Add request timeout</summary>

___


**The GitHub API request is missing a timeout parameter, which could cause the <br>application to hang indefinitely if GitHub's API is unresponsive. Always set a <br>reasonable timeout for external API calls.**

[pr_agent/execserver/services/settings_service.py [117-128]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-a7389591c0e25a9a469b1de7e3f5e7fef5f7745ac39449085222ca7141811c64R117-R128)

```diff
 # Validate token with GitHub API
 try:
     headers = {
         'Authorization': f'token {github_token}',
         'Accept': 'application/vnd.github.v3+json'
     }
-    response = requests.get('https://api.github.com/user', headers=headers)
+    response = requests.get('https://api.github.com/user', headers=headers, timeout=10)
     
     if response.status_code != 200:
         return False, f"GitHub token validation failed: {response.json().get('message', 'Unknown error')}"
 except Exception as e:
     return False, f"GitHub token validation failed: {str(e)}"
```

 <!-- /improve --apply_suggestion=1 -->

<details><summary>Suggestion importance[1-10]: 8</summary>

__

Why: Adding a timeout to the GitHub API request is a critical improvement that prevents potential application hangs if GitHub's API is unresponsive. This is an important defensive programming practice for external API calls.


</details></details></td><td align=center>Medium

</td></tr><tr><td>



<details><summary>Verify migration record insertion</summary>

___


**The migration system is inserting a record but not checking if the insert was <br>successful. This could lead to a situation where the migration is recorded as <br>successful even if it failed. Add error handling to verify the insert operation.**

[pr_agent/execserver/db/migrations.py [177-182]](https://github.com/Zeeeepa/pr-agent/pull/35/files#diff-200a2df453ee9917c80fbc092299d87b461c8afe32bbb8480fd00d495b5b2853R177-R182)

```diff
 # Insert this migration
-supabase.table("migrations").insert({
+result = supabase.table("migrations").insert({
     "version": self.version,
     "description": self.description,
     "applied_at": datetime.utcnow().isoformat()
 }).execute()
 
+if not result.data or len(result.data) == 0:
+    logger.error("Failed to record migration in migrations table")
+    return False
+
```

 <!-- /improve --apply_suggestion=2 -->

<details><summary>Suggestion importance[1-10]: 7</summary>

__

Why: The suggestion adds important error handling to verify that the migration record was successfully inserted. Without this check, the system might incorrectly assume a migration succeeded when it actually failed, leading to database inconsistencies.


</details></details></td><td align=center>Medium

</td></tr>
<tr><td align="center" colspan="2">

 <!-- /improve --more_suggestions=true -->

</td><td></td></tr></tbody></table>

</details>

author_association: NONE