-
Notifications
You must be signed in to change notification settings - Fork 5
/
webhook.py
202 lines (169 loc) · 9.05 KB
/
webhook.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import time
from typing import Optional
from operator import itemgetter
import src.github.controller as github_controller
import src.github.graphql.client as graphql_client
import src.github.logic as github_logic
from src.aws.lock import dynamodb_lock_client
from src.github.models import PullRequestReviewComment, Review
from src.http import HttpResponse
from src.logger import logger
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request
def _handle_pull_request_webhook(payload: dict) -> HttpResponse:
pull_request_id = payload["pull_request"]["node_id"]
org_name = payload["organization"]["login"]
with dynamodb_lock_client.acquire_lock(pull_request_id, sort_key=pull_request_id):
pull_request = graphql_client.get_pull_request(org_name, pull_request_id)
# a label change will trigger this webhook, so it may trigger automerge
github_logic.maybe_automerge_pull_request(pull_request)
github_logic.maybe_add_automerge_warning_comment(pull_request)
github_controller.upsert_pull_request(pull_request)
return HttpResponse("200")
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment
def _handle_issue_comment_webhook(payload: dict) -> HttpResponse:
action, issue, comment = itemgetter("action", "issue", "comment")(payload)
issue_id = issue["node_id"] # issue_id can be pull_request_id
comment_id = comment["node_id"]
org_name = payload["organization"]["login"]
logger.info(f"issue: {issue_id}, comment: {comment_id}")
if action == "created" or action == "edited":
with dynamodb_lock_client.acquire_lock(comment_id, sort_key=comment_id):
pull_request, comment = graphql_client.get_pull_request_and_comment(
org_name, issue_id, comment_id
)
github_controller.upsert_comment(pull_request, comment)
return HttpResponse("200")
if action == "deleted":
logger.info(f"Deleting comment {comment_id}")
with dynamodb_lock_client.acquire_lock(comment_id, sort_key=comment_id):
github_controller.delete_comment(comment_id)
return HttpResponse("200")
error_text = f"Unknown action for issue_comment: {action}"
logger.error(error_text)
return HttpResponse("400", error_text)
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request_review
def _handle_pull_request_review_webhook(payload: dict) -> HttpResponse:
pull_request_id = payload["pull_request"]["node_id"]
review_id = payload["review"]["node_id"]
org_name = payload["organization"]["login"]
with dynamodb_lock_client.acquire_lock(pull_request_id, sort_key=pull_request_id):
pull_request, review = graphql_client.get_pull_request_and_review(
org_name, pull_request_id, review_id
)
github_logic.maybe_automerge_pull_request(pull_request)
github_controller.upsert_review(pull_request, review)
return HttpResponse("200")
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request_review_comment
def _handle_pull_request_review_comment(payload: dict):
"""Handle when a pull request review comment is edited or removed.
When comments are added it either hits:
1 _handle_issue_comment_webhook (if the comment is on PR itself)
2 _handle_pull_request_review_webhook (if the comment is on the "Files Changed" tab)
Note that it hits (2) even if the comment is inline, and doesn't contain a review;
in those cases Github still creates a review object for it.
Unfortunately, this payload doesn't contain the node id of the review.
Instead, it includes a separate, numeric id
which is stored as `databaseId` on each GraphQL object.
To get the review, we either:
(1) query for the comment, and use the `review` edge in GraphQL.
(2) Iterate through all reviews on the pull request, and find the one whose databaseId matches.
See get_review_for_database_id()
We do (1) for comments that were added or edited, but if a comment was just deleted, we have to do (2).
See https://developer.github.com/v4/object/repository/#fields.
"""
action = payload["action"]
comment_id = payload["comment"]["node_id"]
pull_request_id = payload["pull_request"]["node_id"]
org_name = payload["organization"]["login"]
if action == "created" or action == "edited":
with dynamodb_lock_client.acquire_lock(
pull_request_id, sort_key=pull_request_id
):
pull_request, comment = graphql_client.get_pull_request_and_comment(
org_name, pull_request_id, comment_id
)
if not isinstance(comment, PullRequestReviewComment):
raise Exception(
f"Unexpected comment type {type(PullRequestReviewComment)} for pull"
" request review"
)
review = Review.from_comment(comment)
github_controller.upsert_review(pull_request, review)
return HttpResponse("200")
if action == "deleted":
maybe_review: Optional[Review] = None
with dynamodb_lock_client.acquire_lock(
pull_request_id, sort_key=pull_request_id
):
# This is NOT the node_id, but is a numeric string (the databaseId field).
review_database_id = payload["comment"]["pull_request_review_id"]
maybe_review = graphql_client.get_review_for_database_id(
org_name, pull_request_id, review_database_id
)
if maybe_review is None:
github_controller.delete_comment(comment_id)
else:
pull_request = graphql_client.get_pull_request(
org_name, pull_request_id
)
github_controller.upsert_review(pull_request, maybe_review)
return HttpResponse("200")
error_text = f"Unknown action for review_comment: {action}"
logger.error(error_text)
return HttpResponse("400", error_text)
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#status
def _handle_status_webhook(payload: dict) -> HttpResponse:
commit_id = payload["commit"]["node_id"]
org_name = payload["organization"]["login"]
pull_request = graphql_client.get_pull_request_for_commit_id(org_name, commit_id)
if pull_request is None:
# This could happen for commits that get pushed outside of the normal
# pull request flow. These should just be silently ignored.
logger.warning(f"No pull request found for commit id {commit_id}")
return HttpResponse("200")
with dynamodb_lock_client.acquire_lock(
pull_request.id(), sort_key=pull_request.id()
):
github_logic.maybe_automerge_pull_request(pull_request)
github_controller.upsert_pull_request(pull_request)
return HttpResponse("200")
# https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#check_suite
def _handle_check_suite_webhook(payload: dict) -> HttpResponse:
pull_requests = payload["check_suite"]["pull_requests"]
org_name = payload["organization"]["login"]
if len(pull_requests) == 0:
return HttpResponse("400", "No Pull Request Found")
# TODO: How to handle multiple PRs?
pull_request_number = pull_requests[0]["number"]
repository_node_id = payload["repository"]["node_id"]
pull_request = graphql_client.get_pull_request_by_repository_and_number(
org_name, repository_node_id, pull_request_number
)
with dynamodb_lock_client.acquire_lock(
pull_request.id(), sort_key=pull_request.id()
):
github_logic.maybe_automerge_pull_request(pull_request)
github_controller.upsert_pull_request(pull_request)
return HttpResponse("200")
_events_map = {
"pull_request": _handle_pull_request_webhook,
"issue_comment": _handle_issue_comment_webhook,
"pull_request_review": _handle_pull_request_review_webhook,
"status": _handle_status_webhook,
"pull_request_review_comment": _handle_pull_request_review_comment,
"check_suite": _handle_check_suite_webhook,
}
def handle_github_webhook(event_type, payload) -> HttpResponse:
if event_type not in _events_map:
logger.info(f"No handler for event type {event_type}")
return HttpResponse("501", f"No handler for event type {event_type}")
logger.info(f"Received event type {event_type}!")
# TEMPORARY: sleep for 2 seconds before handling any webhook. We're running
# into an issue where the Github Webhook sends us a node_id, but when we
# immediately query that id using the GraphQL API, we get back an error
# "Could not resolve to a node with the global id of '<node_id>'". This is
# an attempt to mitigate this issue temporarily by waiting a second to see
# if Github's data consistency needs a bit of time (does not have
# read-after-write consistency)
time.sleep(2)
return _events_map[event_type](payload)