From bb661909134152b4ae6da43618bd3c57c30a07a1 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 14 Feb 2026 14:29:15 +0000 Subject: [PATCH] Suppress routine GitHub CI webhook deliveries --- backend/.env.example | 2 + backend/app/core/config.py | 3 + backend/app/services/webhooks/dispatch.py | 112 ++++++++++++++++++ .../tests/test_webhook_routine_suppression.py | 71 +++++++++++ 4 files changed, 188 insertions(+) create mode 100644 backend/tests/test_webhook_routine_suppression.py diff --git a/backend/.env.example b/backend/.env.example index 6f4b190f..a9193df4 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -26,3 +26,5 @@ WEBHOOK_DISPATCH_THROTTLE_SECONDS=2.0 WEBHOOK_DISPATCH_SCHEDULE_ID=webhook-dispatch-batch WEBHOOK_DISPATCH_SCHEDULE_INTERVAL_SECONDS=900 WEBHOOK_DISPATCH_MAX_RETRIES=3 +# Suppress routine GitHub CI telemetry events from lead notifications (still persisted to DB/memory). +WEBHOOK_DISPATCH_SUPPRESS_ROUTINE_EVENTS=true diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 91a5603d..5a1837f1 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -60,6 +60,9 @@ class Settings(BaseSettings): webhook_dispatch_throttle_seconds: float = 2.0 webhook_dispatch_schedule_interval_seconds: int = 900 webhook_dispatch_max_retries: int = 3 + # If true, suppress high-volume routine CI telemetry events (e.g. GitHub check_run success) + # from lead notifications. Payloads are still persisted and recorded in board memory. + webhook_dispatch_suppress_routine_events: bool = True # Logging log_level: str = "INFO" diff --git a/backend/app/services/webhooks/dispatch.py b/backend/app/services/webhooks/dispatch.py index 9fe8632b..50850fae 100644 --- a/backend/app/services/webhooks/dispatch.py +++ b/backend/app/services/webhooks/dispatch.py @@ -24,6 +24,102 @@ from app.services.webhooks.queue import ( logger = get_logger(__name__) +_ROUTINE_GITHUB_EVENTS = frozenset({"check_run", "check_suite", "workflow_run"}) +_SUCCESS_GITHUB_CONCLUSIONS = frozenset({None, "success", "neutral", "skipped"}) +# Consider these actionable enough to page the lead / surface in task threads. +_ACTIONABLE_GITHUB_CONCLUSIONS = frozenset( + { + "failure", + "cancelled", + "timed_out", + "action_required", + "stale", + "startup_failure", + }, +) + + +def _as_dict(value: object) -> dict[str, object] | None: + if isinstance(value, dict): + # Keep only string keys; payloads can include non-str keys in edge cases. + normalized: dict[str, object] = {} + for k, v in value.items(): + if isinstance(k, str): + normalized[k] = v + return normalized + return None + + +def _str_or_none(value: object) -> str | None: + if value is None: + return None + if isinstance(value, str): + return value + return str(value) + + +def _extract_github_conclusion(payload: dict[str, object], *, key: str) -> str | None: + container = _as_dict(payload.get(key)) + if not container: + return None + return _str_or_none(container.get("conclusion")) + + +def _extract_github_status(payload: dict[str, object], *, key: str) -> str | None: + container = _as_dict(payload.get(key)) + if not container: + return None + return _str_or_none(container.get("status")) + + +def _should_suppress_routine_delivery( + *, + payload_event: str | None, + payload_value: object, +) -> bool: + """Return True if this delivery is routine noise and should not notify leads. + + This intentionally only targets high-volume GitHub CI telemetry events. + We still persist the webhook payload + board memory entry for audit/debug. + """ + + if not settings.webhook_dispatch_suppress_routine_events: + return False + + if payload_event not in _ROUTINE_GITHUB_EVENTS: + return False + + payload = _as_dict(payload_value) + if payload is None: + return False + + action = _str_or_none(payload.get("action")) + # If GitHub hasn't marked it completed, it's almost always noise. + if action and action != "completed": + return True + + if payload_event == "workflow_run": + status = _extract_github_status(payload, key="workflow_run") + if status and status != "completed": + return True + conclusion = _extract_github_conclusion(payload, key="workflow_run") + elif payload_event == "check_run": + status = _extract_github_status(payload, key="check_run") + if status and status != "completed": + return True + conclusion = _extract_github_conclusion(payload, key="check_run") + else: # check_suite + status = _extract_github_status(payload, key="check_suite") + if status and status != "completed": + return True + conclusion = _extract_github_conclusion(payload, key="check_suite") + + if conclusion in _SUCCESS_GITHUB_CONCLUSIONS: + return True + + # Only page on explicitly non-success conclusions. + return conclusion not in _ACTIONABLE_GITHUB_CONCLUSIONS + def _build_payload_preview(payload_value: object) -> str: if isinstance(payload_value, str): @@ -165,6 +261,22 @@ async def _process_single_item(item: QueuedWebhookDelivery) -> None: return board, webhook, payload = loaded + if _should_suppress_routine_delivery( + payload_event=item.payload_event, + payload_value=payload.payload, + ): + logger.info( + "webhook.dispatch.suppressed_routine", + extra={ + "payload_id": str(item.payload_id), + "webhook_id": str(item.webhook_id), + "board_id": str(item.board_id), + "event": item.payload_event, + "attempt": item.attempts, + }, + ) + return + await _notify_lead(session=session, board=board, webhook=webhook, payload=payload) await session.commit() diff --git a/backend/tests/test_webhook_routine_suppression.py b/backend/tests/test_webhook_routine_suppression.py new file mode 100644 index 00000000..383fc435 --- /dev/null +++ b/backend/tests/test_webhook_routine_suppression.py @@ -0,0 +1,71 @@ +# ruff: noqa: INP001 + +from __future__ import annotations + +import pytest + +from app.services.webhooks import dispatch + + +@pytest.mark.parametrize( + ("payload_event", "payload_value", "expected"), + [ + ("check_run", {"action": "completed", "check_run": {"status": "completed", "conclusion": "success"}}, True), + ("check_run", {"action": "completed", "check_run": {"status": "completed", "conclusion": None}}, True), + ("check_run", {"action": "created", "check_run": {"status": "queued"}}, True), + ("check_run", {"action": "completed", "check_run": {"status": "completed", "conclusion": "failure"}}, False), + ( + "workflow_run", + {"action": "completed", "workflow_run": {"status": "completed", "conclusion": "success"}}, + True, + ), + ( + "workflow_run", + {"action": "completed", "workflow_run": {"status": "completed", "conclusion": "cancelled"}}, + False, + ), + ( + "check_suite", + {"action": "completed", "check_suite": {"status": "completed", "conclusion": "timed_out"}}, + False, + ), + ( + "check_suite", + {"action": "completed", "check_suite": {"status": "completed", "conclusion": "neutral"}}, + True, + ), + # Non-target events should not be suppressed by this helper. + ("pull_request", {"action": "opened"}, False), + (None, {"action": "opened"}, False), + # Non-dict payloads: don't suppress (we can't reason about it). + ("check_run", "raw", False), + ], +) +def test_should_suppress_routine_delivery( + monkeypatch: pytest.MonkeyPatch, + payload_event: str | None, + payload_value: object, + expected: bool, +) -> None: + monkeypatch.setattr(dispatch.settings, "webhook_dispatch_suppress_routine_events", True) + assert ( + dispatch._should_suppress_routine_delivery( + payload_event=payload_event, + payload_value=payload_value, + ) + is expected + ) + + +def test_suppression_disabled_via_settings(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(dispatch.settings, "webhook_dispatch_suppress_routine_events", False) + assert ( + dispatch._should_suppress_routine_delivery( + payload_event="check_run", + payload_value={ + "action": "completed", + "check_run": {"status": "completed", "conclusion": "success"}, + }, + ) + is False + )