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
16 changes: 16 additions & 0 deletions schemas/supabase/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@ class CreditsInsert(TypedDict):
expires_at: NotRequired[datetime.datetime | None]


class EmailSends(TypedDict):
id: int
owner_id: int
owner_name: str
email_type: str
resend_email_id: str | None
created_at: datetime.datetime


class EmailSendsInsert(TypedDict):
owner_id: int
owner_name: str
email_type: str
resend_email_id: NotRequired[str | None]


class Installations(TypedDict):
created_at: datetime.datetime
installation_id: int
Expand Down
74 changes: 74 additions & 0 deletions services/webhook/test_webhook_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ async def test_handle_webhook_event_pull_request_labeled_dashboard(
"""Test handling of pull request labeled event from dashboard triggers handle_new_pr."""
payload = {
"action": "labeled",
"label": {"name": "gitauto"},
"pull_request": {"head": {"ref": "gitauto/dashboard-20250101-120000-Ab12"}},
"sender": {"login": "test-user", "id": 12345},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"):
Expand All @@ -257,7 +259,9 @@ async def test_handle_webhook_event_pull_request_labeled_schedule(
"""Test handling of pull request labeled event from schedule triggers handle_new_pr."""
payload = {
"action": "labeled",
"label": {"name": "gitauto"},
"pull_request": {"head": {"ref": "gitauto/schedule-20250101-120000-Ab12"}},
"sender": {"login": "test-user", "id": 12345},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"):
Expand All @@ -269,6 +273,76 @@ async def test_handle_webhook_event_pull_request_labeled_schedule(
lambda_info=None,
)

@pytest.mark.asyncio
async def test_handle_webhook_event_pull_request_labeled_non_gitauto_label_ignored(
self, mock_handle_new_pr
):
"""Test that non-gitauto labels (e.g. dependabot's 'dependencies') are ignored."""
payload = {
"action": "labeled",
"label": {"name": "dependencies"},
"pull_request": {"head": {"ref": "dependabot/npm_and_yarn/ajv-6.14.0"}},
"sender": {"login": "dependabot[bot]", "id": 49699333},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"):
await handle_webhook_event(event_name="pull_request", payload=payload)

mock_handle_new_pr.assert_not_called()

@pytest.mark.asyncio
async def test_handle_webhook_event_pull_request_labeled_bot_sender_ignored(
self, mock_handle_new_pr
):
"""Test that bot senders (other than GitAuto) are rejected even with gitauto label."""
payload = {
"action": "labeled",
"label": {"name": "gitauto"},
"pull_request": {"head": {"ref": "dependabot/npm_and_yarn/ajv-6.14.0"}},
"sender": {"login": "dependabot[bot]", "id": 49699333},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"):
await handle_webhook_event(event_name="pull_request", payload=payload)

mock_handle_new_pr.assert_not_called()

@pytest.mark.asyncio
async def test_handle_webhook_event_pull_request_labeled_gitauto_bot_allowed(
self, mock_handle_new_pr
):
"""Test that GitAuto's own bot is allowed (for schedule triggers)."""
payload = {
"action": "labeled",
"label": {"name": "gitauto"},
"pull_request": {"head": {"ref": "gitauto/schedule-20250101-120000-Ab12"}},
"sender": {"login": "gitauto[bot]", "id": 160085510},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"), patch(
"services.webhook.webhook_handler.GITHUB_APP_USER_ID", 160085510
):
await handle_webhook_event(event_name="pull_request", payload=payload)

mock_handle_new_pr.assert_called_once()

@pytest.mark.asyncio
async def test_handle_webhook_event_pull_request_labeled_non_gitauto_branch_ignored(
self, mock_handle_new_pr
):
"""Test that a gitauto label on a non-gitauto branch is ignored."""
payload = {
"action": "labeled",
"label": {"name": "gitauto"},
"pull_request": {"head": {"ref": "feature/some-branch"}},
"sender": {"login": "test-user", "id": 12345},
}

with patch("services.webhook.webhook_handler.PRODUCT_ID", "gitauto"):
await handle_webhook_event(event_name="pull_request", payload=payload)

mock_handle_new_pr.assert_not_called()

@pytest.mark.asyncio
async def test_handle_webhook_event_check_suite_completed_failure(
self, mock_handle_check_suite
Expand Down
23 changes: 21 additions & 2 deletions services/webhook/webhook_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Local imports
from config import (
GITHUB_APP_USER_ID,
GITHUB_CHECK_RUN_FAILURES,
PRODUCT_ID,
)
Expand Down Expand Up @@ -183,11 +184,29 @@ async def handle_webhook_event(
# See https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request
if event_name == "pull_request" and action == "labeled":
typed_payload = cast(PrLabeledPayload, payload)

# Only process when the "gitauto" label is specifically added
label_name = typed_payload["label"]["name"]
if label_name != PRODUCT_ID:
logger.info("Ignoring non-gitauto label: %s", label_name)
return

# Reject bot senders (except GitAuto's own app for schedule triggers)
sender = typed_payload["sender"]
sender_login = sender["login"]
if sender_login.endswith("[bot]") and sender["id"] != GITHUB_APP_USER_ID:
logger.info("Ignoring label event from bot: %s", sender_login)
return

# Determine trigger from branch name: {PRODUCT_ID}/schedule-* vs {PRODUCT_ID}/dashboard-*
head_ref = typed_payload["pull_request"]["head"]["ref"]
prefix = f"{PRODUCT_ID}/"
suffix = head_ref[len(prefix) :] if head_ref.startswith(prefix) else ""
trigger = cast(Trigger, suffix.split("-")[0] if suffix else "dashboard")
if not head_ref.startswith(prefix):
logger.info("Ignoring non-gitauto branch: %s", head_ref)
return

suffix = head_ref[len(prefix) :]
trigger = cast(Trigger, suffix.split("-")[0])
await handle_new_pr(
payload=typed_payload,
trigger=trigger,
Expand Down
Loading