Add a base class and registry for GitHub App webhook handlers.
Review Request #15334 — Created Sept. 24, 2026 and updated
A GitHub App has a single, app-wide webhook URL that receives events from
every account the app is installed on. We have theGitHubAppWebhookViewfor
this, which currently only handles app installation events. We should have an
easy way for us and extension authors to handle GitHub webhook events too.This change introduces the
BaseGitHubAppWebhookHandler, which can be
subclassed to list the events the handler supports and the required GitHub App
permissions. Events will be passed to handlers as aGitHubAppWebhookEvent,
containing the event's raw payload, delivery ID, and the account(s) and
repository it may apply to. The actual dispatching to handlers will come in
a later change.This also adds a registry for the handlers. The registry has a helper for
merging all the events and required pemissions of the registered handlers,
so that they can all be shown and requested when creating a GitHub App. The
hook for registering handlers will come in a later change.
- Ran unit tests.
- Used in the Pull Request integration work.
| Summary | ID |
|---|---|
| d3317ad8b4747b74e8e46902a7689955d9aa16b0 |
| Description | From | Last Updated |
|---|---|---|
|
Needs "Version Added" |
|
|
|
Can we call this name or event_name? So we avoid having code that says event.event? |
|
|
|
typo: this is value -> this is the value |
|
|
|
These don't have defaults, and nothing checks them at registration. They'll only fail with an AttributeError later, inside get_events() or … |
|
|
|
This line is missing a : |
|
|
|
Parens aren't necessary here. |
|
|
|
OrderedRegistry.__init__ doesn't take any args. I don't think we need *args or **kwargs passthrough. |
|
|
|
This isn't validating that events is a set. It seems like probably an easy mistake to write: events = 'pull_requests' … |
|
|
|
It seems like this exception will get caught by the extension hook, which will also log. Can we check and … |
|
|
|
This should probably return a Sequence |
|
|
|
Do we want to return this as a frozenset so that callers can't potentially mutate the cache? |
|
|
|
Should this return Mapping? |
|
|
|
Mapping |
|
|
|
self._events is still typed as set[str], and we're returning that internal cache directly. Can we type that as frozenset, and … |
|
|
|
Mapping |
|
|
|
This doesn't use self at all. Can we make it module-level like merge_github_app_permissions? |
|
|
|
Should be Mapping[str, GitHubAppPermissionLevel] |
|
|
|
Missing a "with" in here, and the closing """ should go on its own line. |
|
|
|
Might want to pin what logs we capture here so unrelated things don't cause the test to break: with self.assertLogs('reviewboard.hostingsvcs.github.webhooks', … |
|
|
|
no newline at end of file Column: 15 Error code: W292 |
|
-
-
-
-
These don't have defaults, and nothing checks them at registration. They'll only fail with an
AttributeErrorlater, insideget_events()orget_handlers_for_event(). That would cause every webhook delivery to fail, not just the broken one.Same for an invalid level set in
required_permissions.We should probably override
on_item_registeringand fail loudly if the handler is broken. The SCMTools registry does something like this for reference. -
-
OrderedRegistry.__init__doesn't take any args. I don't think we need*argsor**kwargspassthrough. -
-
-
-
-
-
-
-
Might want to pin what logs we capture here so unrelated things don't cause the test to break:
with self.assertLogs('reviewboard.hostingsvcs.github.webhooks', level='WARNING') as logs:
- Change Summary:
-
- Update
GitHubAppWebhookEvent.eventtoevent_name. - Validate webhook handlers before registering them.
- Update
- Commits:
-
Summary ID 89b3df9fa22bd8a85990587f90633bc976faa54e d80a8f7e357a8cc5ba2cb2abe6b6784a680efc12
Checks run (2 succeeded)
-
-
-
-
This isn't validating that
eventsis a set. It seems like probably an easy mistake to write:events = 'pull_requests'Which will end up iterating over single characters, sticking 'p', 'u', 'l', ... into the github app manifest.
-
It seems like this exception will get caught by the extension hook, which will also log. Can we check and make sure we're not adding extra noise by logging here?
-
self._eventsis still typed asset[str], and we're returning that internal cache directly. Can we type that asfrozenset, and then assemble it here:events = frozenset( event for handler in self for event in handler.events ) -