def test_dynamodb_lock_different_pull_request_ids(self): dummy_counter = 0 with dynamodb_lock("pull_request_1"): dummy_counter += 1 with dynamodb_lock("pull_request_2"): dummy_counter += 1 self.assertEqual(dummy_counter, 2)
def test_dynamodb_lock_consecutive_same_lock(self): # Just make sure the lock is released properly after the block lock_name = "pull_request_id" dummy_counter = 0 with dynamodb_lock(lock_name): dummy_counter += 1 with dynamodb_lock(lock_name): dummy_counter += 1 self.assertEqual(dummy_counter, 2)
def test_dynamodb_lock_blocks_others_from_acquiring_lock(self): lock_name = "pull_request_id" dummy_counter = 0 with dynamodb_lock(lock_name): dummy_counter += 1 with self.assertRaises(DynamoDBLockError): with dynamodb_lock(lock_name, retry_timeout=timedelta( milliseconds=0.001)): # same lock name dummy_counter += 1 self.assertEqual(dummy_counter, 1)
def test_dynamodb_lock_raises_exception_thrown_in_block(self): class DemoException(Exception): pass lock_name = "pull_request_id" with self.assertRaises(DemoException): with dynamodb_lock(lock_name): raise DemoException("oops") # Lock should still be released after the exception was raised dummy_counter = 0 with dynamodb_lock(lock_name): dummy_counter += 1 self.assertEqual(dummy_counter, 1)
def _handle_pull_request_webhook(payload: dict) -> HttpResponse: pull_request_id = payload["pull_request"]["node_id"] with dynamodb_lock(pull_request_id): pull_request = graphql_client.get_pull_request(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")
def _handle_pull_request_review_webhook(payload: dict) -> HttpResponse: pull_request_id = payload["pull_request"]["node_id"] review_id = payload["review"]["node_id"] with dynamodb_lock(pull_request_id): pull_request, review = graphql_client.get_pull_request_and_review( pull_request_id, review_id) github_logic.maybe_automerge_pull_request(pull_request) github_controller.upsert_review(pull_request, review) return HttpResponse("200")
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. """ pull_request_id = payload["pull_request"]["node_id"] action = payload["action"] comment_id = payload["comment"]["node_id"] # This is NOT the node_id, but is a numeric string (the databaseId field). review_database_id = payload["comment"]["pull_request_review_id"] with dynamodb_lock(pull_request_id): if action in ("created", "edited"): pull_request, comment = graphql_client.get_pull_request_and_comment( pull_request_id, comment_id) if not isinstance(comment, PullRequestReviewComment): raise Exception( f"Unexpected comment type {type(PullRequestReviewComment)} for pull request review" ) review: Optional[Review] = Review.from_comment(comment) elif action == "deleted": pull_request = graphql_client.get_pull_request(pull_request_id) review = graphql_client.get_review_for_database_id( pull_request_id, review_database_id) if review is None: # If we deleted the last comment from a review, Github might have deleted the review. # If so, we should delete the Asana comment. logger.info( "No review found in Github. Deleting the Asana comment.") github_controller.delete_comment(comment_id) else: raise ValueError(f"Unexpected action: {action}") if review is not None: github_controller.upsert_review(pull_request, review) return HttpResponse("200")
def _handle_status_webhook(payload: dict) -> HttpResponse: commit_id = payload["commit"]["node_id"] pull_request = graphql_client.get_pull_request_for_commit(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.warn(f"No pull request found for commit id {commit_id}") return HttpResponse("200") with dynamodb_lock(pull_request.id()): github_logic.maybe_automerge_pull_request(pull_request) github_controller.upsert_pull_request(pull_request) return HttpResponse("200")
def _handle_issue_comment_webhook(payload: dict) -> HttpResponse: action, issue, comment = itemgetter("action", "issue", "comment")(payload) issue_id = issue["node_id"] comment_id = comment["node_id"] with dynamodb_lock(issue_id): if action in ("created", "edited"): pull_request, comment = graphql_client.get_pull_request_and_comment( issue_id, comment_id) github_controller.upsert_comment(pull_request, comment) return HttpResponse("200") elif action == "deleted": logger.info(f"Deleting comment {comment_id}") github_controller.delete_comment(comment_id) return HttpResponse("200") else: error_text = f"Unknown action for issue_comment: {action}" logger.info(error_text) return HttpResponse("400", error_text)