Source code for octomachinery.routing.webhooks_dispatcher
"""GitHub webhook events dispatching logic."""
from __future__ import annotations
import contextlib
import logging
from typing import Any, Iterable
from anyio import get_cancelled_exc_class
from anyio import sleep as async_sleep
import sentry_sdk
# pylint: disable=relative-beyond-top-level,import-error
from ..github.api.app_client import GitHubApp
# pylint: disable=relative-beyond-top-level,import-error
from ..github.entities.action import GitHubAction
# pylint: disable=relative-beyond-top-level,import-error
from ..github.errors import GitHubActionError
# pylint: disable=relative-beyond-top-level,import-error
from ..github.models.events import GitHubEvent
# pylint: disable=relative-beyond-top-level,import-error
from ..runtime.context import RUNTIME_CONTEXT
__all__ = ('route_github_event',)
logger = logging.getLogger(__name__)
# pylint: disable=fixme
[docs]async def route_github_event( # type: ignore[return] # FIXME
*,
github_event: GitHubEvent,
github_app: GitHubApp,
) -> Iterable[Any]:
"""Dispatch GitHub event to corresponding handlers.
Set up ``RUNTIME_CONTEXT`` before doing that. This is so
the concrete event handlers have access to the API client
and flags in runtime.
"""
is_gh_action = isinstance(github_app, GitHubAction)
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.IS_GITHUB_ACTION = is_gh_action
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.IS_GITHUB_APP = not is_gh_action
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.github_app = github_app
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.github_event = github_event
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.app_installation = None
if is_gh_action:
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.app_installation_client = github_app.api_client
else:
with contextlib.suppress(LookupError):
# pylint: disable=pointless-string-statement
"""Provision an installation API client if possible.
Some events (like `ping`) are
happening application/GitHub-wide and are not bound to
a specific installation. The webhook payloads of such events
don't contain any reference to an installation.
Some events don't even refer to a GitHub App
(e.g. `security_advisory`).
"""
github_install = await github_app.get_installation(github_event)
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.app_installation = github_install
# pylint: disable=assigning-non-slot
RUNTIME_CONTEXT.app_installation_client = github_install.api_client
# Give GitHub a sec to deal w/ eventual consistency.
# This is only needed for events that arrive over HTTP.
# If the dispatcher is invoked from GitHub Actions,
# by the time it's invoked the action must be already consistently
# distributed within GitHub's systems because spawning VMs takes time
# and actions are executed in workflows that rely on those VMs.
await async_sleep(1)
try:
return await github_app.dispatch_event(github_event)
except GitHubActionError:
# Bypass GitHub Actions errors as they are supposed to be a
# mechanism for communicating outcomes and are expected.
raise
except get_cancelled_exc_class():
raise
except Exception as exc: # pylint: disable=broad-except
# NOTE: It's probably better to wrap each event handler with
# NOTE: try/except and call `capture_exception()` there instead.
# NOTE: We'll also need to figure out the magic of associating
# NOTE: breadcrumbs with event handlers.
sentry_sdk.capture_exception(exc)
# NOTE: Framework-wise, these exceptions are meaningless because they
# NOTE: can be anything random that the webhook author (octomachinery
# NOTE: end-user) forgot to handle. There's nothing we can do about
# NOTE: them except put in the log so that the end-user would be able
# NOTE: to properly debug their problem by inspecting the logs.
# NOTE: P.S. This is also where we'd inject Sentry
if isinstance(exc.__context__, get_cancelled_exc_class()):
# The CancelledError context is irrelevant to the
# user-defined webhook event handler workflow so we're
# dropping it from the logs:
exc.__context__ = None
logger.exception(
'An unhandled exception happened while running webhook '
'event handlers for "%s"...',
github_event.name,
)
delivery_id_msg = (
'' if is_gh_action
else ' (Delivery ID: ' # type: ignore[attr-defined]
# FIXME: # pylint: disable=fixme
f'{github_event.delivery_id!s})'
)
logger.debug(
'The payload of "%s" event%s is: %r',
github_event.name, delivery_id_msg, github_event.payload,
)
if is_gh_action:
# NOTE: In GitHub Actions env, the app is supposed to run as
# NOTE: a foreground single event process rather than a
# NOTE: server for multiple events. It's okay to propagate
# NOTE: unhandled errors so that they are spit out to the
# NOTE: console.
raise
except BaseException: # SystemExit + KeyboardInterrupt + GeneratorExit
raise