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
14 changes: 14 additions & 0 deletions src/event_processors/check_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,30 @@ class CheckRunProcessor(BaseEventProcessor):
"""Processor for check run events using hybrid agentic rule evaluation."""

def __init__(self) -> None:
"""Initialize check run processor with hybrid rule engine agent."""
# Call super class __init__ first
super().__init__()

# Create instance of hybrid RuleEngineAgent
self.engine_agent = get_agent("engine")

def get_event_type(self) -> str:
"""Return the event type this processor handles."""
return "check_run"

async def process(self, task: Task) -> ProcessingResult:
"""Process check_run event with hybrid rule evaluation.

Handles check_run events (rerequested, completed) to re-evaluate rules
when checks are re-run. Ignores Watchflow's own check runs to prevent
infinite loops.

Args:
task: Task containing check_run event payload

Returns:
ProcessingResult with evaluation results
"""
start_time = time.time()
payload = task.payload
check_run = payload.get("check_run", {})
Expand Down
14 changes: 14 additions & 0 deletions src/event_processors/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,27 @@ class DeploymentProcessor(BaseEventProcessor):
"""Processor for deployment events - for logging only."""

def __init__(self) -> None:
"""Initialize deployment processor for logging purposes."""
# Call super class __init__ first
super().__init__()

def get_event_type(self) -> str:
"""Return the event type this processor handles."""
return "deployment"

async def process(self, task: Task) -> ProcessingResult:
"""Process deployment event for logging purposes only.

This processor does not enforce rules - it only logs deployment creation
events for observability. Rule evaluation is handled by
deployment_protection_rule events.

Args:
task: Task containing deployment event payload

Returns:
ProcessingResult with success=True (always succeeds)
"""
start_time = time.time()
payload = task.payload
deployment = payload.get("deployment", {})
Expand Down
63 changes: 63 additions & 0 deletions src/event_processors/deployment_protection_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ class DeploymentProtectionRuleProcessor(BaseEventProcessor):
"""Processor for deployment protection rule events using hybrid agentic rule evaluation."""

def __init__(self):
"""Initialize deployment protection rule processor with hybrid rule engine agent."""
# Call super class __init__ first
super().__init__()

# Create instance of hybrid RuleEngineAgent
self.engine_agent = get_agent("engine")

def get_event_type(self) -> str:
"""Return the event type this processor handles."""
return "deployment_protection_rule"

@staticmethod
Expand All @@ -36,6 +38,45 @@ def _is_valid_environment(env: str | None) -> bool:
return bool(env and isinstance(env, str) and env.strip())

async def process(self, task: Task) -> ProcessingResult:
"""Process deployment protection rule event with hybrid rule evaluation.

This method orchestrates the deployment approval/rejection workflow:
1. Validates callback URL and environment from webhook payload
2. Loads deployment rules from repository configuration
3. Enriches event data with commit/deployment metadata
4. Evaluates rules using hybrid agent (deterministic + LLM fallback)
5. Handles time-based scheduling for delayed deployment windows
6. Approves/rejects deployment via GitHub API callback
7. Posts check run with evaluation results

Args:
task: Task containing deployment_protection_rule event payload with:
- deployment: Deployment metadata (id, sha, ref, environment)
- deployment_callback_url: GitHub API endpoint for approval/rejection
- environment: Target deployment environment name
- installation_id: GitHub App installation identifier
- repo_full_name: Repository in owner/name format

Returns:
ProcessingResult with:
- success: True if deployment was approved/rejected successfully
- violations: List of rule violations that blocked deployment
- api_calls_made: Count of GitHub API calls (approx)
- processing_time_ms: Total processing time in milliseconds
- error: Error message if processing failed

Side Effects:
- Calls GitHub deployment approval/rejection API
- Creates check run with evaluation details
- Schedules delayed deployment approval via deployment scheduler
- Logs structured events at decision boundaries

Error Handling:
- Retries approval API calls with exponential backoff (3 attempts)
- Falls back to LLM if deterministic evaluation fails
- Returns success=False with error message on unrecoverable failures
- Gracefully degrades if rules file is missing or malformed
"""
start_time = time.time()

try:
Expand Down Expand Up @@ -385,9 +426,31 @@ def _format_violations_comment(violations):
return text

async def prepare_webhook_data(self, task: Task) -> dict[str, Any]:
"""Extract data from webhook payload for rule evaluation.

Returns the raw payload as-is since deployment_protection_rule events
contain all necessary data (deployment, environment, callback URL).

Args:
task: Task with deployment_protection_rule payload

Returns:
Dictionary with deployment event data from webhook
"""
return task.payload

async def prepare_api_data(self, task: Task) -> dict[str, Any]:
"""Fetch additional data via GitHub API for rule evaluation.

For deployment_protection_rule events, all necessary data is already
in the webhook payload, so no additional API calls are needed.

Args:
task: Task with deployment_protection_rule payload

Returns:
Empty dictionary (no additional API data required)
"""
return {}

def _get_rule_provider(self):
Expand Down
13 changes: 13 additions & 0 deletions src/event_processors/deployment_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,29 @@ class DeploymentReviewProcessor(BaseEventProcessor):
"""Processor for deployment review events using hybrid agentic rule evaluation."""

def __init__(self) -> None:
"""Initialize deployment review processor with hybrid rule engine agent."""
# Call super class __init__ first
super().__init__()

# Create instance of hybrid RuleEngineAgent
self.engine_agent = get_agent("engine")

def get_event_type(self) -> str:
"""Return the event type this processor handles."""
return "deployment_review"

async def process(self, task: Task) -> ProcessingResult:
"""Process deployment_review event with hybrid rule evaluation.

Handles deployment review approvals/rejections from reviewers after
a deployment protection rule has requested human review.

Args:
task: Task containing deployment_review event payload

Returns:
ProcessingResult with evaluation results
"""
start_time = time.time()
payload = task.payload
deployment_review = payload.get("deployment_review", {})
Expand Down
13 changes: 13 additions & 0 deletions src/event_processors/deployment_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,26 @@ class DeploymentStatusProcessor(BaseEventProcessor):
"""Processor for deployment_status events - for logging and monitoring only."""

def __init__(self) -> None:
"""Initialize deployment status processor for logging and monitoring."""
# Call super class __init__ first
super().__init__()

def get_event_type(self) -> str:
"""Return the event type this processor handles."""
return "deployment_status"

async def process(self, task: Task) -> ProcessingResult:
"""Process deployment_status event for logging and monitoring purposes.

This processor does not enforce rules - it only logs deployment status
transitions (waiting, success, failure, error) for observability.

Args:
task: Task containing deployment_status event payload

Returns:
ProcessingResult with success=True (always succeeds)
"""
start_time = time.time()
payload = task.payload
deployment_status = payload.get("deployment_status", {})
Expand Down
26 changes: 26 additions & 0 deletions src/integrations/github/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class ReviewNode(BaseModel):


class ReviewConnection(BaseModel):
"""Wrapper for list of PR review nodes from GraphQL API."""

nodes: list[ReviewNode]


Expand All @@ -26,10 +28,14 @@ class IssueNode(BaseModel):


class IssueConnection(BaseModel):
"""Wrapper for list of linked issue nodes from GraphQL API."""

nodes: list[IssueNode]


class CommitMessage(BaseModel):
"""Container for a single commit message."""

message: str


Expand All @@ -40,43 +46,61 @@ class CommitNode(BaseModel):


class CommitConnection(BaseModel):
"""Wrapper for list of commit nodes from GraphQL API."""

nodes: list[CommitNode]


class FileNode(BaseModel):
"""Single file path node in GraphQL response."""

path: str


class FileEdge(BaseModel):
"""GraphQL edge wrapper for file node."""

node: FileNode


class FileConnection(BaseModel):
"""Wrapper for list of file edges from GraphQL API."""

edges: list[FileEdge]


class CommentConnection(BaseModel):
"""Wrapper for PR comment count from GraphQL API."""

model_config = ConfigDict(populate_by_name=True)
total_count: int = Field(alias="totalCount")


class ThreadCommentNode(BaseModel):
"""Single review thread comment from GraphQL API."""

author: Actor | None
body: str
createdAt: str


class ThreadCommentConnection(BaseModel):
"""Wrapper for list of review thread comments from GraphQL API."""

nodes: list[ThreadCommentNode]


class ReviewThreadNode(BaseModel):
"""Single review thread with resolution status and comments."""

isResolved: bool
isOutdated: bool
comments: ThreadCommentConnection


class ReviewThreadConnection(BaseModel):
"""Wrapper for list of review thread nodes from GraphQL API."""

nodes: list[ReviewThreadNode]


Expand Down Expand Up @@ -110,6 +134,8 @@ class Repository(BaseModel):


class GraphQLResponseData(BaseModel):
"""GraphQL response data container with repository field."""

repository: Repository | None


Expand Down