Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enables github webhooks #518

Merged
merged 1 commit into from
Feb 16, 2025
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
3 changes: 3 additions & 0 deletions src/codegen/extensions/events/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import modal # deptry: ignore

from codegen.extensions.events.github import GitHub
from codegen.extensions.events.linear import Linear
from codegen.extensions.events.slack import Slack

Expand All @@ -11,6 +12,7 @@
class CodegenApp(modal.App):
linear: Linear
slack: Slack
github: GitHub

def __init__(self, name: str, modal_api_key: str, image: modal.Image):
self._modal_api_key = modal_api_key
Expand All @@ -21,4 +23,5 @@

# Expose attributes that provide event decorators for different providers.
self.linear = Linear(self)
self.slack = Slack(self)

Check failure on line 26 in src/codegen/extensions/events/app.py

View workflow job for this annotation

GitHub Actions / mypy

error: Cannot instantiate abstract class "Slack" with abstract attributes "subscribe_handler_to_webhook" and "unsubscribe_handler_to_webhook" [abstract]
self.github = GitHub(self)

Check failure on line 27 in src/codegen/extensions/events/app.py

View workflow job for this annotation

GitHub Actions / mypy

error: Cannot instantiate abstract class "GitHub" with abstract attributes "subscribe_handler_to_webhook" and "unsubscribe_handler_to_webhook" [abstract]
123 changes: 123 additions & 0 deletions src/codegen/extensions/events/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import logging
from typing import Any, Callable, TypeVar

from fastapi import Request
from pydantic import BaseModel

from codegen.extensions.events.interface import EventHandlerManagerProtocol
from codegen.extensions.github.types.base import GitHubInstallation, GitHubWebhookPayload

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


# Type variable for event types
T = TypeVar("T", bound=BaseModel)


class GitHub(EventHandlerManagerProtocol):
def __init__(self, app):
self.app = app
self.registered_handlers = {}

# TODO - add in client info
# @property
# def client(self) -> Github:
# if not self._client:
# self._client = Github(os.environ["GITHUB_TOKEN"])
# return self._client

def unsubscribe_all_handlers(self):
logger.info("[HANDLERS] Clearing all handlers")
self.registered_handlers.clear()

def event(self, event_name: str):
"""Decorator for registering a GitHub event handler.

Example:
@app.github.event('push')
def handle_push(event: PushEvent): # Can be typed with Pydantic model
logger.info(f"Received push to {event.ref}")

@app.github.event('pull_request:opened')
def handle_pr(event: dict): # Or just use dict for raw event
logger.info(f"Received PR")
"""
logger.info(f"[EVENT] Registering handler for {event_name}")

def register_handler(func: Callable[[T], Any]):
# Get the type annotation from the first parameter
event_type = func.__annotations__.get("event")
func_name = func.__qualname__
logger.info(f"[EVENT] Registering function {func_name} for {event_name}")

def new_func(raw_event: dict):
# Only validate if a Pydantic model was specified
if event_type and issubclass(event_type, BaseModel):
try:
parsed_event = event_type.model_validate(raw_event)
return func(parsed_event)
except Exception as e:
logger.exception(f"Error parsing event: {e}")
raise
else:
# Pass through raw dict if no type validation needed
return func(raw_event)

Check failure on line 65 in src/codegen/extensions/events/github.py

View workflow job for this annotation

GitHub Actions / mypy

error: Argument 1 has incompatible type "dict[Any, Any]"; expected "T" [arg-type]

self.registered_handlers[event_name] = new_func
return new_func

return register_handler

def handle(self, event: dict, request: Request):
"""Handle both webhook events and installation callbacks."""
logger.info("[HANDLER] Handling GitHub event")

# Check if this is an installation event
if "installation_id" in event and "code" in event:
installation = GitHubInstallation.model_validate(event)
logger.info("=====[GITHUB APP INSTALLATION]=====")
logger.info(f"Code: {installation.code}")
logger.info(f"Installation ID: {installation.installation_id}")
logger.info(f"Setup Action: {installation.setup_action}")
return {
"message": "GitHub app installation details received",
"details": {
"code": installation.code,
"installation_id": installation.installation_id,
"setup_action": installation.setup_action,
},
}

# Extract headers for webhook events
headers = {
"x-github-event": request.headers.get("x-github-event"),
"x-github-delivery": request.headers.get("x-github-delivery"),
"x-github-hook-id": request.headers.get("x-github-hook-id"),
"x-github-hook-installation-target-id": request.headers.get("x-github-hook-installation-target-id"),
"x-github-hook-installation-target-type": request.headers.get("x-github-hook-installation-target-type"),
}
print(headers)

# Handle webhook events
try:
webhook = GitHubWebhookPayload.model_validate({"headers": headers, "event": event})

# Get base event type and action
event_type = webhook.headers.event_type
action = webhook.event.action

# Combine event type and action if both exist
full_event_type = f"{event_type}:{action}" if action else event_type

if full_event_type not in self.registered_handlers:
logger.info(f"[HANDLER] No handler found for event type: {full_event_type}")
return {"message": "Event type not handled"}

else:
logger.info(f"[HANDLER] Handling event: {full_event_type}")
handler = self.registered_handlers[full_event_type]
return handler(event) # TODO - pass through typed values
except Exception as e:
logger.exception(f"Error handling webhook: {e}")
raise
62 changes: 62 additions & 0 deletions src/codegen/extensions/events/github_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import datetime
from typing import Optional


class GitHubRepository:
id: int
node_id: str
name: str
full_name: str
private: bool


class GitHubAccount:
login: str
id: int
node_id: str
avatar_url: str
type: str
site_admin: bool
# Other URL fields omitted for brevity
user_view_type: str


class GitHubInstallation:
id: int
client_id: str
account: GitHubAccount
repository_selection: str
access_tokens_url: str
repositories_url: str
html_url: str
app_id: int
app_slug: str
target_id: int
target_type: str
permissions: dict[str, str] # e.g. {'actions': 'write', 'checks': 'read', ...}
events: list[str]
created_at: datetime
updated_at: datetime
single_file_name: Optional[str]
has_multiple_single_files: bool
single_file_paths: list[str]
suspended_by: Optional[str]
suspended_at: Optional[datetime]


class GitHubUser:
login: str
id: int
node_id: str
avatar_url: str
type: str
site_admin: bool
# Other URL fields omitted for brevity


class GitHubInstallationEvent:
action: str
installation: GitHubInstallation
repositories: list[GitHubRepository]
requester: Optional[dict]
sender: GitHubUser
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions src/codegen/extensions/github/types/author.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class GitHubAuthor(BaseModel):
name: str
email: str
username: str
68 changes: 68 additions & 0 deletions src/codegen/extensions/github/types/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from pydantic import BaseModel, Field


class GitHubUser(BaseModel):
login: str
id: int
node_id: str
type: str


class GitHubRepository(BaseModel):
id: int
node_id: str
name: str
full_name: str
private: bool
owner: GitHubUser


class GitHubIssue(BaseModel):
id: int
node_id: str
number: int
title: str
body: str | None
user: GitHubUser
state: str
comments: int


class GitHubPullRequest(BaseModel):
id: int
node_id: str
number: int
title: str
body: str | None
user: GitHubUser
state: str
head: dict
base: dict
merged: bool | None = None


class GitHubEvent(BaseModel):
action: str | None = None
issue: GitHubIssue | None = None
pull_request: GitHubPullRequest | None = None
repository: GitHubRepository
sender: GitHubUser


class GitHubWebhookHeaders(BaseModel):
event_type: str = Field(..., alias="x-github-event")
delivery_id: str = Field(..., alias="x-github-delivery")
hook_id: str = Field(..., alias="x-github-hook-id")
installation_target_id: str = Field(..., alias="x-github-hook-installation-target-id")
installation_target_type: str = Field(..., alias="x-github-hook-installation-target-type")


class GitHubWebhookPayload(BaseModel):
headers: GitHubWebhookHeaders
event: GitHubEvent


class GitHubInstallation(BaseModel):
code: str
installation_id: str
setup_action: str = "install"
17 changes: 17 additions & 0 deletions src/codegen/extensions/github/types/commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pydantic import BaseModel

from .author import GitHubAuthor


class GitHubCommit(BaseModel):
id: str
tree_id: str
distinct: bool
message: str
timestamp: str
url: str
author: GitHubAuthor
committer: GitHubAuthor
added: list[str]
removed: list[str]
modified: list[str]
14 changes: 14 additions & 0 deletions src/codegen/extensions/github/types/enterprise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel


class GitHubEnterprise(BaseModel):
id: int
slug: str
name: str
node_id: str
avatar_url: str
description: str
website_url: str
html_url: str
created_at: str
updated_at: str
31 changes: 31 additions & 0 deletions src/codegen/extensions/github/types/events/pull_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pydantic import BaseModel

from ..base import GitHubRepository, GitHubUser
from ..enterprise import GitHubEnterprise
from ..installation import GitHubInstallation
from ..label import GitHubLabel
from ..organization import GitHubOrganization
from ..pull_request import PullRequest


class PullRequestLabeledEvent(BaseModel):
action: str # Will be "labeled"
number: int
pull_request: PullRequest
label: GitHubLabel
repository: GitHubRepository
organization: GitHubOrganization
enterprise: GitHubEnterprise
sender: GitHubUser
installation: GitHubInstallation


class PullRequestOpenedEvent(BaseModel):
action: str = "opened" # Always "opened" for this event
number: int
pull_request: PullRequest
repository: GitHubRepository
organization: GitHubOrganization
enterprise: GitHubEnterprise
sender: GitHubUser
installation: GitHubInstallation
27 changes: 27 additions & 0 deletions src/codegen/extensions/github/types/events/push.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from pydantic import BaseModel

from ..base import GitHubRepository, GitHubUser
from ..commit import GitHubCommit
from ..enterprise import GitHubEnterprise
from ..installation import GitHubInstallation
from ..organization import GitHubOrganization
from ..pusher import GitHubPusher


class PushEvent(BaseModel):
ref: str
before: str
after: str
repository: GitHubRepository
pusher: GitHubPusher
organization: GitHubOrganization
enterprise: GitHubEnterprise
sender: GitHubUser
installation: GitHubInstallation
created: bool
deleted: bool
forced: bool
base_ref: str | None = None
compare: str
commits: list[GitHubCommit]
head_commit: GitHubCommit
6 changes: 6 additions & 0 deletions src/codegen/extensions/github/types/installation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class GitHubInstallation(BaseModel):
id: int
node_id: str
11 changes: 11 additions & 0 deletions src/codegen/extensions/github/types/label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel


class GitHubLabel(BaseModel):
id: int
node_id: str
url: str
name: str
color: str
default: bool
description: str | None
Loading
Loading