Skip to content

Commit

Permalink
Create separate threads on GitHub Discussions (#5)
Browse files Browse the repository at this point in the history
* create new discussions

* revert after testing is done

* pass PRs as string to the template

* trying to fix: TypeError: sequence item 0: expected str instance, PullRequest found

* converting PRs to str

* debug deletion

* correct variable name in deletion query

* test title

* revert testing tweaks

* requested changes and mypy fixes

* fix linter

* fix: unit tests

* testing: to not unnecessary mention

* testing: change repo name

* testing: final tweaks

* fix: syntax

* revert: testing tweaks
  • Loading branch information
SD-13 authored Dec 5, 2024
1 parent 9765c6a commit 48e428b
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 405 deletions.
4 changes: 2 additions & 2 deletions .github/PENDING_REVIEW_NOTIFICATION_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Hi {{ username }},

It looks like you're assigned to this PR, but haven't taken any action for at least 2 days:
The following PRs are currently blocked on your review:
{{ pr_list }}

Please review and unassign yourself from the pending PRs as soon as possible.
Please review and unassign yourself from the pending PRs as soon as possible, then mark this discussion thread as 'Done'.

To avoid these messages in the future, please bookmark [this link](https://github.com/pulls/assigned) and check it daily. Thanks!
241 changes: 101 additions & 140 deletions src/github_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import datetime
import logging

from typing import Any, Callable, DefaultDict, Dict, List, Optional, Tuple, Union
from typing import Any, Callable, DefaultDict, Dict, List, Optional, Union
from dateutil import parser
import requests
from src import github_domain
Expand Down Expand Up @@ -203,18 +203,51 @@ def get_pull_request_dict_with_timestamp(
assignee['created_at'] = parser.parse(event['created_at'])
return pr_dict


@check_token
def _get_discussion_data(
def _get_repository_id(
org_name: str,
repo_name: str,
discussion_category: str,
discussion_title: str,
) -> Tuple[str, int]:
"""Fetch discussion data from api and return corresponding discussion id and
discussion number.
) -> str:
"""Fetch repository id from given org and repo and return the id."""

query = """
query ($org_name: String!, $repository: String!) {
repository(owner: $org_name, name: $repository) {
id
}
}
"""

variables = {
'org_name': org_name,
'repository': repo_name
}

response = requests.post(
GITHUB_GRAPHQL_URL,
json={'query': query, 'variables': variables},
headers=_get_request_headers(),
timeout=TIMEOUT_SECS
)
data = response.json()

repository_id: str = (
data['data']['repository']['id'])

if repository_id is None:
raise builtins.BaseException(
f'{org_name}/{repo_name} doesn\'t exist.')

return repository_id

@check_token
def _get_category_id(
org_name: str,
repo_name: str,
discussion_category: str
) -> str:
"""Fetch discussion category id from given category name and return the id."""

# The following query is written in GraphQL and is being used to fetch the category
# ids and titles from the GitHub discussions. To learn more, check this out
# https://docs.github.com/en/graphql.
Expand Down Expand Up @@ -244,7 +277,7 @@ def _get_discussion_data(
)
data = response.json()

category_id = None
category_id: Optional[str] = None
discussion_categories = (
data['data']['repository']['discussionCategories']['nodes'])

Expand All @@ -257,6 +290,21 @@ def _get_discussion_data(
raise builtins.BaseException(
f'{discussion_category} category is missing in GitHub Discussion.')

assert category_id is not None
return category_id

@check_token
def _get_discussion_ids(
org_name: str,
repo_name: str,
discussion_category: str,
) -> List[str]:
"""Fetch discussion data from api and return corresponding discussion id and
discussion number.
"""

category_id = _get_category_id(org_name, repo_name, discussion_category)

# The following query is written in GraphQL and is being used to fetch discussions
# from a particular GitHub discussion category. This helps to find out the discussion
# where we want to comment. To learn more, check this out
Expand Down Expand Up @@ -289,62 +337,34 @@ def _get_discussion_data(
timeout=TIMEOUT_SECS
)
data = response.json()
discussion_id = None

discussions = data['data']['repository']['discussions']['nodes']
discussion_ids = [
discussion['id'] for discussion in discussions if discussion['id'] is not None
]

for discussion in discussions:
if discussion['title'] == discussion_title:
discussion_id = discussion['id']
discussion_number = discussion['number']
break

if discussion_id is None:
raise builtins.BaseException(
f'Discussion with title {discussion_title} not found, please create a '
'discussion with that title.')

return discussion_id, discussion_number


def _get_past_time(days: int=60) -> str:
"""Returns the subtraction of current time and the arg passed in days."""
return (
datetime.datetime.now(
datetime.timezone.utc) - datetime.timedelta(days=days)).strftime(
'%Y-%m-%dT%H:%M:%SZ')
if not discussion_ids:
logging.info('No existing discussions found')

return discussion_ids

def _get_old_comment_ids(
org_name: str,
repo_name: str,
discussion_number: int
) -> List[str]:
"""Return the old comment ids."""
def _delete_discussion(discussion_id: str) -> None:
"""Delete the GitHub Discussion comment related to the comment id."""

# The following query is written in GraphQL and is being used to fetch the oldest 50
# comments in a existing GitHub discussions. Assuming that, this workflow will run
# twice every week, we will may not have more than 50 comments to delete. To learn
# more, check this out https://docs.github.com/en/graphql.
query = """
query ($org_name: String!, $repository: String!, $discussion_number: Int!) {
repository(owner: $org_name, name: $repository) {
discussion(number: $discussion_number) {
comments(first: 50) {
nodes {
id
createdAt
}
}
mutation deleteDiscussion($discussion_id: ID!) {
deleteDiscussion(input: {id: $discussion_id}) {
clientMutationId,
discussion {
title
}
}
}
"""

variables = {
'org_name': org_name,
'repository': repo_name,
'discussion_number': discussion_number
'discussion_id': discussion_id
}

response = requests.post(
Expand All @@ -353,76 +373,50 @@ def _get_old_comment_ids(
headers=_get_request_headers(),
timeout=TIMEOUT_SECS
)

response.raise_for_status()
data = response.json()

comment_ids: List[str] = []

discussion_comments = (
data['data']['repository']['discussion']['comments']['nodes']
)

# Delete comments posted before this time.
delete_comments_before_in_days = _get_past_time(DELETE_COMMENTS_BEFORE_IN_DAYS)

for comment in discussion_comments:
if comment['createdAt'] < delete_comments_before_in_days:
comment_ids.append(comment['id'])
else:
break

return comment_ids


def _delete_comment(comment_id: str) -> None:
"""Delete the GitHub Discussion comment related to the comment id."""

query = """
mutation deleteComment($comment_id: ID!) {
deleteDiscussionComment(input: {id: $comment_id}) {
clientMutationId
comment {
bodyText
}
}
}
"""

variables = {
'comment_id': comment_id
}
@check_token
def delete_discussions(
org_name: str,
repo_name: str,
discussion_category: str,
) -> None:
"""Delete all existing discussions in the given discussion category."""

response = requests.post(
GITHUB_GRAPHQL_URL,
json={'query': query, 'variables': variables},
headers=_get_request_headers(),
timeout=TIMEOUT_SECS
)
response.raise_for_status()
discussion_ids = _get_discussion_ids(
org_name, repo_name, discussion_category)

for discussion_id in discussion_ids:
_delete_discussion(discussion_id)

def _post_comment(discussion_id: str, message: str) -> None:
"""Post the given message in an existing discussion."""
@check_token
def create_discussion(
org_name: str,
repo_name: str,
discussion_category: str,
discussion_title: str,
discussion_body: str
) -> None:
"""Create a new discussion with the given title and body in the given discussion category."""

# The following code is written in GraphQL and is being used to perform a mutation
# operation. More specifically, we are using it to comment in GitHub discussion to
# let reviewers know about some of their pending tasks. To learn more, check this out:
# https://docs.github.com/en/graphql.
category_id = _get_category_id(org_name, repo_name, discussion_category)
repo_id = _get_repository_id(org_name, repo_name)
query = """
mutation post_comment($discussion_id: ID!, $comment: String!) {
addDiscussionComment(input: {discussionId: $discussion_id, body: $comment}) {
clientMutationId
comment {
mutation createDiscussion($repo_id: ID!, $category_id: ID!, $title: String!, $body: String!) {
createDiscussion(input: {repositoryId: $repo_id, categoryId: $category_id, title: $title, body: $body}) {
discussion {
id
}
}
}
"""

variables = {
'discussion_id': discussion_id,
'comment': message
'repo_id': repo_id,
'category_id': category_id,
'title': discussion_title,
'body': discussion_body
}

response = requests.post(
Expand All @@ -431,38 +425,5 @@ def _post_comment(discussion_id: str, message: str) -> None:
headers=_get_request_headers(),
timeout=TIMEOUT_SECS
)
response.raise_for_status()


@check_token
def delete_discussion_comments(
org_name: str,
repo_name: str,
discussion_category: str,
discussion_title: str
) -> None:
"""Delete old comments from GitHub Discussion."""

_, discussion_number = _get_discussion_data(
org_name, repo_name, discussion_category, discussion_title)

comment_ids = _get_old_comment_ids(org_name, repo_name, discussion_number)

for comment_id in comment_ids:
_delete_comment(comment_id)


@check_token
def add_discussion_comments(
org_name: str,
repo_name: str,
discussion_category: str,
discussion_title: str,
message: str
) -> None:
"""Add comments in an existing GitHub discussion."""

discussion_id, _ = _get_discussion_data(
org_name, repo_name, discussion_category, discussion_title)

_post_comment(discussion_id, message)
response.raise_for_status()
Loading

0 comments on commit 48e428b

Please sign in to comment.