def should_transition(issue): """ Return a boolean indicating if the given issue should be transitioned automatically from "Needs Triage" to an open status. """ issue_key = to_unicode(issue["key"]) issue_status = to_unicode(issue["fields"]["status"]["name"]) project_key = to_unicode(issue["fields"]["project"]["key"]) if issue_status != "Needs Triage": print( "{key} has status {status}, does not need to be processed".format( key=issue_key, status=issue_status, ), file=sys.stderr, ) return False # Open source pull requests do not skip Needs Triage. # However, if someone creates a subtask on an OSPR issue, that subtasks # might skip Needs Triage (it just follows the rest of the logic in this # function.) is_subtask = issue["fields"]["issuetype"]["subtask"] if project_key == "OSPR" and not is_subtask: print( "{key} is an open source pull request, and does not need to be processed.".format( key=issue_key ), file=sys.stderr, ) return False user_url = URLObject(issue["fields"]["creator"]["self"]) user_url = user_url.set_query_param("expand", "groups") user_resp = jira_get(user_url) if not user_resp.ok: raise requests.exceptions.RequestException(user_resp.text) user = user_resp.json() user_group_map = {g["name"]: g["self"] for g in user["groups"]["items"]} user_groups = set(user_group_map) exempt_groups = { # group name: set of projects that they can create non-triage issues "edx-employees": {"ALL"}, "clarice": {"MOB"}, "bnotions": {"MOB"}, "opencraft": {"SOL"}, } for user_group in user_groups: if user_group not in exempt_groups: continue exempt_projects = exempt_groups[user_group] if "ALL" in exempt_projects: return True if project_key in exempt_projects: return True return False
def issue_opened(issue, bugsnag_context=None): bugsnag_context = bugsnag_context or {} bugsnag_context = {"issue": issue} bugsnag.configure_request(meta_data=bugsnag_context) issue_key = to_unicode(issue["key"]) issue_url = URLObject(issue["self"]) transitioned = False if should_transition(issue): transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) if not transitions_resp.ok: raise requests.exceptions.RequestException(transitions_resp.text) transitions = {t["name"]: t["id"] for t in transitions_resp.json()["transitions"]} if "Open" in transitions: new_status = "Open" elif "Design Backlog" in transitions: new_status = "Design Backlog" else: raise ValueError("No valid transition! Possibilities are {}".format(transitions.keys())) body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, json=body) if not transition_resp.ok: raise requests.exceptions.RequestException(transition_resp.text) transitioned = True # log to stderr action = "Transitioned to Open" if transitioned else "ignored" print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=to_unicode(issue["fields"]["creator"]["displayName"]), username=to_unicode(issue["fields"]["creator"]["name"]), action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return action
def get_jira_issue(key): return jira_get("/rest/api/2/issue/{key}".format(key=key))
def issue_opened(issue): sentry.client.extra_context({"issue": issue}) issue_key = to_unicode(issue["key"]) issue_url = URLObject(issue["self"]) transitioned = False if should_transition(issue): # In JIRA, a "transition" is how an issue changes from one status # to another, like going from "Open" to "In Progress". The workflow # defines what transitions are allowed, and this API will tell us # what transitions are currently allowed by the workflow. # Ref: https://docs.atlassian.com/jira/REST/ondemand/#d2e4954 transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) transitions_resp.raise_for_status() # This transforms the API response into a simple mapping from the # name of the transition (like "In Progress") to the ID of the transition. # Note that a transition may not have the same name as the state that it # goes to, so a transition to go from "Open" to "In Progress" may be # named something like "Start Work". transitions = { t["name"]: t["id"] for t in transitions_resp.json()["transitions"] } # We attempt to transition the issue into the "Open" state for the given project # (some projects use a different name), so look for a transition with the right name new_status = None action = None for state_name in ["Open", "Design Backlog", "To Do"]: if state_name in transitions: new_status = state_name action = "Transitioned to '{}'".format(state_name) if not new_status: # If it's an OSPR subtask (used by teams to manage reviews), transition to team backlog if to_unicode( issue["fields"]["project"] ["key"]) == "OSPR" and issue["fields"]["issuetype"]["subtask"]: new_status = "To Backlog" action = "Transitioned to 'To Backlog'" else: raise ValueError( "No valid transition! Possibilities are {}".format( transitions.keys())) # This creates a new API request to tell JIRA to move the issue from # one status to another using the specified transition. We have to # tell JIRA the transition ID, so we use that mapping we set up earlier. body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, json=body) transition_resp.raise_for_status() transitioned = True # log to stderr if transitioned and not action: action = "Transitioned to Open" else: action = "ignored" print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=to_unicode(issue["fields"]["creator"]["displayName"]), username=to_unicode(issue["fields"]["creator"]["name"]), action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return action
def issue_opened(issue, bugsnag_context=None): bugsnag_context = bugsnag_context or {} bugsnag_context = {"issue": issue} bugsnag.configure_request(meta_data=bugsnag_context) issue_key = to_unicode(issue["key"]) issue_url = URLObject(issue["self"]) transitioned = False if should_transition(issue): # In JIRA, a "transition" is how an issue changes from one status # to another, like going from "Open" to "In Progress". The workflow # defines what transitions are allowed, and this API will tell us # what transitions are currently allowed by the workflow. # Ref: https://docs.atlassian.com/jira/REST/ondemand/#d2e4954 transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) if not transitions_resp.ok: raise requests.exceptions.RequestException(transitions_resp.text) # This transforms the API response into a simple mapping from the # name of the transition (like "In Progress") to the ID of the transition. # Note that a transition may not have the same name as the state that it # goes to, so a transition to go from "Open" to "In Progress" may be # named something like "Start Work". transitions = {t["name"]: t["id"] for t in transitions_resp.json()["transitions"]} # We attempt to transition the issue into the "Open" state for the given project # (some projects use a different name), so look for a transition with the right name new_status = None action = None for state_name in ["Open", "Design Backlog", "To Do"]: if state_name in transitions: new_status = state_name action = "Transitioned to '{}'".format(state_name) if not new_status: # If it's an OSPR subtask (used by teams to manage reviews), transition to team backlog if to_unicode(issue["fields"]["project"]["key"]) == "OSPR" and issue["fields"]["issuetype"]["subtask"]: new_status = "To Backlog" action = "Transitioned to 'To Backlog'" else: raise ValueError("No valid transition! Possibilities are {}".format(transitions.keys())) # This creates a new API request to tell JIRA to move the issue from # one status to another using the specified transition. We have to # tell JIRA the transition ID, so we use that mapping we set up earlier. body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, json=body) if not transition_resp.ok: raise requests.exceptions.RequestException(transition_resp.text) transitioned = True # log to stderr if transitioned and not action: action = "Transitioned to Open" else: action = "ignored" print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=to_unicode(issue["fields"]["creator"]["displayName"]), username=to_unicode(issue["fields"]["creator"]["name"]), action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return action
def jira_issue_created(): """ Received an "issue created" event from JIRA. https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview Ideally, this should be handled in a task queue, but we want to stay within Heroku's free plan, so it will be handled inline instead. (A worker dyno costs money.) """ try: event = request.get_json() except ValueError: raise ValueError( "Invalid JSON from JIRA: {data}".format(data=request.data)) bugsnag.configure_request(meta_data={"event": event}) if app.debug: print(json.dumps(event), file=sys.stderr) issue_key = event["issue"]["key"] issue_status = event["issue"]["fields"]["status"]["name"] project = event["issue"]["fields"]["project"]["key"] if issue_status != "Needs Triage": print( "{key} has status {status}, does not need to be processed".format( key=issue_key, status=issue_status, ), file=sys.stderr, ) return "issue does not need to be triaged" if project == "OSPR": # open source pull requests do not skip Needs Triage print( "{key} is an open source pull request, and does not need to be processed." .format(key=issue_key), file=sys.stderr, ) return "issue is OSPR" issue_url = URLObject(event["issue"]["self"]) user_url = URLObject(event["user"]["self"]) user_url = user_url.set_query_param("expand", "groups") user_resp = jira_get(user_url) if not user_resp.ok: raise requests.exceptions.RequestException(user_resp.text) user = user_resp.json() groups = {g["name"]: g["self"] for g in user["groups"]["items"]} # skip "Needs Triage" if bug was created by edX employee transitioned = False if "edx-employees" in groups: transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) if not transitions_resp.ok: raise requests.exceptions.RequestException(transitions_resp.text) transitions = { t["name"]: t["id"] for t in transitions_resp.json()["transitions"] } if "Open" in transitions: new_status = "Open" elif "Design Backlog" in transitions: new_status = "Design Backlog" else: raise ValueError( "No valid transition! Possibilities are {}".format( transitions.keys())) body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, data=json.dumps(body)) if not transition_resp.ok: raise requests.exceptions.RequestException(transition_resp.text) transitioned = True # log to stderr print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=event["user"]["displayName"], username=event["user"]["name"], action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return "Processed"
def issue_opened(issue, bugsnag_context=None): bugsnag_context = bugsnag_context or {} bugsnag_context = {"issue": issue} bugsnag.configure_request(meta_data=bugsnag_context) issue_key = issue["key"].decode('utf-8') issue_status = issue["fields"]["status"]["name"].decode('utf-8') project = issue["fields"]["project"]["key"].decode('utf-8') if issue_status != "Needs Triage": print( "{key} has status {status}, does not need to be processed".format( key=issue_key, status=issue_status, ), file=sys.stderr, ) return "issue does not need to be triaged" if project == "OSPR": # open source pull requests do not skip Needs Triage print( "{key} is an open source pull request, and does not need to be processed.".format( key=issue_key ), file=sys.stderr, ) return "issue is OSPR" issue_url = URLObject(issue["self"]) user_url = URLObject(issue["fields"]["creator"]["self"]) user_url = user_url.set_query_param("expand", "groups") user_resp = jira_get(user_url) if not user_resp.ok: raise requests.exceptions.RequestException(user_resp.text) user = user_resp.json() groups = {g["name"]: g["self"] for g in user["groups"]["items"]} # skip "Needs Triage" if bug was created by edX employee transitioned = False if "edx-employees" in groups: transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) if not transitions_resp.ok: raise requests.exceptions.RequestException(transitions_resp.text) transitions = {t["name"]: t["id"] for t in transitions_resp.json()["transitions"]} if "Open" in transitions: new_status = "Open" elif "Design Backlog" in transitions: new_status = "Design Backlog" else: raise ValueError("No valid transition! Possibilities are {}".format(transitions.keys())) body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, json=body) if not transition_resp.ok: raise requests.exceptions.RequestException(transition_resp.text) transitioned = True try: name = issue["fields"]["creator"]["displayName"].decode('utf-8') except: bugsnag_context["name_type"] = type(issue["fields"]["creator"]["displayName"]) bugsnag.configure_request(meta_data=bugsnag_context) raise # log to stderr action = "Transitioned to Open" if transitioned else "ignored" print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=name, username=issue["fields"]["creator"]["name"].decode('utf-8'), action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return action
def jira_issue_created(): """ Received an "issue created" event from JIRA. https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview Ideally, this should be handled in a task queue, but we want to stay within Heroku's free plan, so it will be handled inline instead. (A worker dyno costs money.) """ try: event = request.get_json() except ValueError: raise ValueError("Invalid JSON from JIRA: {data}".format(data=request.data)) bugsnag.configure_request(meta_data={"event": event}) if app.debug: print(json.dumps(event), file=sys.stderr) issue_key = event["issue"]["key"] issue_status = event["issue"]["fields"]["status"]["name"] project = event["issue"]["fields"]["project"]["key"] if issue_status != "Needs Triage": print( "{key} has status {status}, does not need to be processed".format( key=issue_key, status=issue_status, ), file=sys.stderr, ) return "issue does not need to be triaged" if project == "OSPR": # open source pull requests do not skip Needs Triage print( "{key} is an open source pull request, and does not need to be processed.".format( key=issue_key ), file=sys.stderr, ) return "issue is OSPR" issue_url = URLObject(event["issue"]["self"]) user_url = URLObject(event["user"]["self"]) user_url = user_url.set_query_param("expand", "groups") user_resp = jira_get(user_url) if not user_resp.ok: raise requests.exceptions.RequestException(user_resp.text) user = user_resp.json() groups = {g["name"]: g["self"] for g in user["groups"]["items"]} # skip "Needs Triage" if bug was created by edX employee transitioned = False if "edx-employees" in groups: transitions_url = issue_url.with_path(issue_url.path + "/transitions") transitions_resp = jira_get(transitions_url) if not transitions_resp.ok: raise requests.exceptions.RequestException(transitions_resp.text) transitions = {t["name"]: t["id"] for t in transitions_resp.json()["transitions"]} if "Open" in transitions: new_status = "Open" elif "Design Backlog" in transitions: new_status = "Design Backlog" else: raise ValueError("No valid transition! Possibilities are {}".format(transitions.keys())) body = { "transition": { "id": transitions[new_status], } } transition_resp = jira.post(transitions_url, data=json.dumps(body)) if not transition_resp.ok: raise requests.exceptions.RequestException(transition_resp.text) transitioned = True # log to stderr print( "{key} created by {name} ({username}), {action}".format( key=issue_key, name=event["user"]["displayName"], username=event["user"]["name"], action="Transitioned to Open" if transitioned else "ignored", ), file=sys.stderr, ) return "Processed"