Skip to content

Instantly share code, notes, and snippets.

@jlaura
Created August 19, 2020 14:08
Show Gist options
  • Save jlaura/a0c2a456bb2f2d9c31f178ae287fe870 to your computer and use it in GitHub Desktop.
Save jlaura/a0c2a456bb2f2d9c31f178ae287fe870 to your computer and use it in GitHub Desktop.
A bot to manage issues on the USGS-Astrogeology ISIS3 repository.
import requests
from datetime import datetime
### Config ###
# A mapping between the label id needed by the API and the human readable name
labelids = {'inactive': 'MDU6TGFiZWwyMjg1Mjc0MTc2',
'automatically_closed': 'MDU6TGFiZWwyMjg1Mjc2MjI3'}
# Headers necessary for authentication
APIKEY = "The ASC Bot's API Key"
headers = {"Authorization": f"Bearer {APIKEY}"}
# Messages to be posted by the bot at given intervals.
first_message = """
I am a bot that cleans up old issues that do not have activity.
This issue has not received feedback in the last six months. I am going to add the `inactive` label to
this issue. If this is still a pertinent issue, please add a comment or add an emoji to an existing comment.
I will post again in 11 months with another reminder and will close this issue on it's birthday unless it has
some activity.
"""
second_message = """
I am a bot that cleans up old issues that do not have activity.
This issue has not received feedback in the last eleven months! If this is still a pertinent issue, please add a comment or add an emoji to an existing comment.
In one month will close this issue on it's birthday unless it has some activity.
"""
final_message = """
I am a bot that cleans up old issues that do not have activity.
Happy Birthday to this issue! :birthday:
Unfortunately, this issue has not received much attention in the last 12 months. Therefore, I am going to close it. Please feel free to reopen this issue or open a new issue sometime in the future. If this issue is a bug report, please check that the issue still exists in our newest version before reopening.
"""
def run_query(query):
"""
Runs a GraphQL query against the GitHub API.
Parameters
----------
query : str
The GraphQL string query to be passed to the API
Returns
-------
: dict
The JSON (dict) response from the API
"""
request = requests.post('https://api.github.com/graphql', json={'query':query}, headers=headers)
if request.status_code == 200:
return request.json()
else:
raise Exception("Query failed to run by returning code of {}. {}".format(request.status_code, query))
def get_issues():
"""
Get all of the open issues in a repository.
Returns
-------
: dict
GitHub API response parsed to the individual issues (through
edges and nodes in the response).
"""
# Query to get issues, comments, and reactions to comments
query = f"""
query {{
repository(owner:"USGS-Astrogeology", name:"ISIS3") {{
openIssues: issues(states: OPEN, last:1) {{
edges {{
node {{
id
title
updatedAt
createdAt
url
comments(last:100) {{
edges {{
node {{
author {{login}}
updatedAt
createdAt
reactions(last:100) {{
edges {{
node {{
createdAt
}}
}}
}}
}}
}}
}}
}}
}}
}}
}}
}}
"""
response = run_query(query) # Execute the query
result = response['data']['repository']['openIssues']['edges']
return [issue['node'] for issue in result]
def find_most_recent_activity(issue):
"""
This func finds the most recent activity on an issue by iterating over all of
the content, omitting any posts by the bot, and finding the most recent UTC
timestamp.
Parameters
----------
issue : dict
The JSON (dict) response from the github API for a single issue. We
assume that the 'node' key has been omitted.
Returns
-------
age : obj
A datetime.timedelta object
"""
dates = []
# Intentionally skip the last updated at key because this could be the bot talking.
dates.append(issue['createdAt'])
# Step over all the comments; ignore the bot and reactions to the bot.
for comment in issue['comments']['edges']:
comment = comment['node']
if comment['author']['login'] == 'ascbot':
continue
dates.append(comment['updatedAt'])
dates.append(comment['createdAt'])
# Step over any reactions
for reaction in comment['reactions']['edges']:
reaction = reaction['node']
dates.append(reaction['createdAt'])
newest_activity = max(dates)
age = datetime.utcnow() - datetime.strptime(newest_activity, '%Y-%m-%dT%H:%M:%SZ')
return age
def update_with_message(issueid, msg):
"""
Using the Github V4 GraphQL API, update an issue
with a given message.
Parameters
----------
issueid : str
The GitHub hashed issue identifier
msg : str
The string message to be posted by the API key holder
Returns
-------
: dict
The JSON (dict) response from the GitHub API
"""
# Add a comment query
query = f"""
mutation {{
addComment(input:{{
subjectId: "{issueid}",
body:"{msg}"
}}) {{
clientMutationId
}}
}}
"""
return run_query(query)
def add_label(issueid, labelid):
"""
Using the Github V4 GraphQL API, add a
label to an issue.
Parameters
----------
issueid : str
The GitHub hashed issue identifier
labelid : str
The GitHub hashed label identifier
Returns
-------
: dict
The JSON (dict) response from the GitHub API
"""
query = f"""mutation {{
addLabelsToLabelable(input:{{
labelableId:"{issueid}",
labelIds:["{labelid}"]
}}) {{
clientMutationId
}}
}}"""
return run_query(query)
def close_issue(issueid):
"""
Using the Github V4 GraphQL API, close
an issue.
Parameters
----------
issueid : str
The GitHub hashed issue identifier
Returns
-------
: dict
The JSON (dict) response from the GitHub API
"""
query = f"""mutation {{
closeIssue(input:{{
issueId:"{issueid}"
}}) {{
clientMutationId
}}
}}"""
return run_query(query)
def find_and_update_inactive_issues(issues):
"""
Parse a list of of GitHub API response issues and
update those issue which meet the inactivity criteria.
Issues with no activity in the last 182 are updated with
the `inactive` label and a message.
Issues with no activity in 335 days are updated with a
nudge message.
Issues with no activity after 365 days are closed with
a message and an `automatically_closed` label.
Parameters
----------
issues : list
of JSON (dict) issues from the GitHub API parsed
down to the individual nodes (issues)
"""
for issue in open_issues:
age = find_most_recent_activity(issue)
if age.days > 365:
resp = update_with_message(issue['id'], final_message)
resp = add_label(issue['id'], labelids['automatically_closed'])
resp = close_issue[issue['id']]
elif age.days > 335:
resp = update_with_message(issue['id'], second_message)
elif age.days > 182:
resp = update_with_message(issue['id'], first_message)
resp = add_label(issue['id'], labelids['inactive'])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment