Example #1
0
def jira_rescan_users():
    """
    This task goes through all users on JIRA and ensures that they are assigned
    to the correct group based on the user's email address. It's meant to be
    run regularly: once an hour or so.
    """
    # a mapping of group name to email domain
    domain_groups = {
        "edx-employees": "@edx.org",
        "clarice": "@claricetechnologies.com",
        "bnotions": "@bnotions.com",
        "qualcomm": ".qualcomm.com",
        "ubc": "@cs.ubc.ca",
        "ubc": "@ubc.ca",
    }
    if request.method == "GET":
        return render_template("jira_rescan_users.html", domain_groups=domain_groups)

    failures = defaultdict(dict)

    requested_group = request.form.get("group")
    if requested_group:
        if requested_group not in domain_groups:
            resp = jsonify({"error": "Not found", "groups": domain_groups.keys()})
            resp.status_code = 404
            return resp
        requested_groups = {requested_group: domain_groups[requested_group]}
    else:
        requested_groups = domain_groups

    for groupname, domain in requested_groups.items():
        users_in_group = jira_group_members(groupname, session=jira, debug=True)
        usernames_in_group = set(u["name"] for u in users_in_group)
        bugsnag_context = {
            "groupname": groupname,
            "usernames_in_group": usernames_in_group,
        }
        bugsnag.configure_request(meta_data=bugsnag_context)

        for user in jira_users(filter=domain, session=jira, debug=True):
            if not user["email"].endswith(domain):
                pass
            username = user["name"]
            if username not in usernames_in_group:
                # add the user to the group!
                resp = jira.post(
                    "/rest/api/2/group/user?groupname={}".format(groupname),
                    json={"name": username},
                )
                if not resp.ok:
                    failures[groupname][username] = resp.text

    resp = jsonify(failures)
    resp.status_code = 502 if failures else 200
    return resp
Example #2
0
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
Example #3
0
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
Example #4
0
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
Example #5
0
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"
Example #6
0
def pr_opened(pr, bugsnag_context=None):
    bugsnag_context = bugsnag_context or {}
    user = pr["user"]["login"]
    repo = pr["base"]["repo"]["full_name"]
    people = get_people_file()

    if user in people and people[user].get("institution", "") == "edX":
        # not an open source pull request, don't create an issue for it
        print(
            "@{user} opened PR #{num} against {repo} (internal PR)".format(
                user=user, repo=pr["base"]["repo"]["full_name"],
                num=pr["number"]
            ),
            file=sys.stderr
        )
        return "internal pull request"

    field_resp = jira.get("/rest/api/2/field")
    if not field_resp.ok:
        raise requests.exceptions.RequestException(field_resp.text)
    field_map = dict(pop_dict_id(f) for f in field_resp.json())
    custom_fields = {
        value["name"]: id
        for id, value in field_map.items()
        if value["custom"]
    }

    user_resp = github.get(pr["user"]["url"])
    if user_resp.ok:
        user_name = user_resp.json().get("name", user)
    else:
        user_name = user

    # create an issue on JIRA!
    new_issue = {
        "fields": {
            "project": {
                "key": "OSPR",
            },
            "issuetype": {
                "name": "Pull Request Review",
            },
            "summary": pr["title"],
            "description": pr["body"],
            custom_fields["URL"]: pr["html_url"],
            custom_fields["PR Number"]: pr["number"],
            custom_fields["Repo"]: pr["base"]["repo"]["full_name"],
            custom_fields["Contributor Name"]: user_name,
        }
    }
    institution = people.get(user, {}).get("institution", None)
    if institution:
        new_issue["fields"][custom_fields["Customer"]] = [institution]
    bugsnag_context["new_issue"] = new_issue
    bugsnag.configure_request(meta_data=bugsnag_context)

    resp = jira.post("/rest/api/2/issue", data=json.dumps(new_issue))
    if not resp.ok:
        raise requests.exceptions.RequestException(resp.text)
    new_issue_body = resp.json()
    bugsnag_context["new_issue"]["key"] = new_issue_body["key"]
    bugsnag.configure_request(meta_data=bugsnag_context)
    # add a comment to the Github pull request with a link to the JIRA issue
    comment = {
        "body": github_pr_comment(pr, new_issue_body, people),
    }
    url = "/repos/{repo}/issues/{num}/comments".format(
        repo=repo, num=pr["number"],
    )
    comment_resp = github.post(url, data=json.dumps(comment))
    if not comment_resp.ok:
        raise requests.exceptions.RequestException(comment_resp.text)
    print(
        "@{user} opened PR #{num} against {repo}, created {issue} to track it".format(
            user=user, repo=repo,
            num=pr["number"], issue=new_issue_body["key"]
        ),
        file=sys.stderr
    )
    return "created!"
Example #7
0
def pr_closed(pr, bugsnag_context=None):
    bugsnag_context = bugsnag_context or {}
    repo = pr["base"]["repo"]["full_name"]

    merged = pr["merged"]
    issue_key = get_jira_issue_key(pr)
    if not issue_key:
        print(
            "Couldn't find JIRA issue for PR #{num} against {repo}".format(
                num=pr["number"], repo=repo,
            ),
            file=sys.stderr
        )
        return "no JIRA issue :("
    bugsnag_context["jira_key"] = issue_key
    bugsnag.configure_request(meta_data=bugsnag_context)

    # close the issue on JIRA
    transition_url = (
        "/rest/api/2/issue/{key}/transitions"
        "?expand=transitions.fields".format(key=issue_key)
    )
    transitions_resp = jira.get(transition_url)
    if not transitions_resp.ok:
        raise requests.exceptions.RequestException(transitions_resp.text)

    transitions = transitions_resp.json()["transitions"]

    bugsnag_context["transitions"] = transitions
    bugsnag.configure_request(meta_data=bugsnag_context)

    transition_name = "Merged" if merged else "Rejected"
    transition_id = None
    for t in transitions:
        if t["to"]["name"] == transition_name:
            transition_id = t["id"]
            break

    if not transition_id:
        # maybe the issue is *already* in the right status?
        issue_url = "/rest/api/2/issue/{key}".format(key=issue_key)
        issue_resp = jira.get(issue_url)
        if not issue_resp.ok:
            raise requests.exceptions.RequestException(issue_resp.text)
        issue = issue_resp.json()
        bugsnag_context["jira_issue"] = issue
        bugsnag.configure_request(meta_data=bugsnag_context)
        if issue["fields"]["status"]["name"] == transition_name:
            msg = "{key} is already in status {status}".format(
                key=issue_key, status=transition_name
            )
            print(msg, file=sys.stderr)
            return "nothing to do!"

    fail_msg = (
        "{key} cannot be transitioned directly to status {status}. "
        "Valid status transitions are: {valid}".format(
            key=issue_key, status=transition_name,
            valid=set(t["to"]["name"] for t in transitions),
        )
    )
    assert transition_id, fail_msg

    transition_resp = jira.post(transition_url, data=json.dumps({
        "transition": {
            "id": transition_id,
        }
    }))
    if not transition_resp.ok:
        raise requests.exceptions.RequestException(transition_resp.text)
    print(
        "PR #{num} against {repo} was {action}, moving {issue} to status {status}".format(
            num=pr["number"], repo=repo, action="merged" if merged else "closed",
            issue=issue_key, status="Merged" if merged else "Rejected",
        ),
        file=sys.stderr
    )
    return "closed!"
Example #8
0
def pr_closed(pr, bugsnag_context=None):
    bugsnag_context = bugsnag_context or {}
    repo = pr["base"]["repo"]["full_name"].decode('utf-8')

    merged = pr["merged"]
    issue_key = get_jira_issue_key(pr)
    if not issue_key:
        print(
            "Couldn't find JIRA issue for PR #{num} against {repo}".format(
                num=pr["number"], repo=repo,
            ),
            file=sys.stderr
        )
        return "no JIRA issue :("
    bugsnag_context["jira_key"] = issue_key
    bugsnag.configure_request(meta_data=bugsnag_context)

    # close the issue on JIRA
    transition_url = (
        "/rest/api/2/issue/{key}/transitions"
        "?expand=transitions.fields".format(key=issue_key)
    )
    transitions_resp = jira.get(transition_url)
    transitions_resp.raise_for_status()

    transitions = transitions_resp.json()["transitions"]

    bugsnag_context["transitions"] = transitions
    bugsnag.configure_request(meta_data=bugsnag_context)

    transition_name = "Merged" if merged else "Rejected"
    transition_id = None
    for t in transitions:
        if t["to"]["name"] == transition_name:
            transition_id = t["id"]
            break

    if not transition_id:
        # maybe the issue is *already* in the right status?
        issue_url = "/rest/api/2/issue/{key}".format(key=issue_key)
        issue_resp = jira.get(issue_url)
        issue_resp.raise_for_status()
        issue = issue_resp.json()
        bugsnag_context["jira_issue"] = issue
        bugsnag.configure_request(meta_data=bugsnag_context)
        current_status = issue["fields"]["status"]["name"].decode("utf-8")
        if current_status == transition_name:
            msg = "{key} is already in status {status}".format(
                key=issue_key, status=transition_name
            )
            print(msg, file=sys.stderr)
            return "nothing to do!"

        # nope, raise an error message
        fail_msg = (
            "{key} cannot be transitioned directly from status {curr_status} "
            "to status {new_status}. Valid status transitions are: {valid}".format(
                key=issue_key,
                new_status=transition_name,
                curr_status=current_status,
                valid=", ".join(t["to"]["name"].decode('utf-8') for t in transitions),
            )
        )
        raise Exception(fail_msg)

    transition_resp = jira.post(transition_url, json={
        "transition": {
            "id": transition_id,
        }
    })
    transition_resp.raise_for_status()
    print(
        "PR #{num} against {repo} was {action}, moving {issue} to status {status}".format(
            num=pr["number"],
            repo=repo,
            action="merged" if merged else "closed",
            issue=issue_key,
            status="Merged" if merged else "Rejected",
        ),
        file=sys.stderr
    )
    return "closed!"
Example #9
0
def pr_opened(pr, ignore_internal=True, check_contractor=True, bugsnag_context=None):
    bugsnag_context = bugsnag_context or {}
    user = pr["user"]["login"].decode('utf-8')
    repo = pr["base"]["repo"]["full_name"]
    num = pr["number"]
    if ignore_internal and is_internal_pull_request(pr):
        # not an open source pull request, don't create an issue for it
        print(
            "@{user} opened PR #{num} against {repo} (internal PR)".format(
                user=user, repo=repo, num=num,
            ),
            file=sys.stderr
        )
        return "internal pull request"

    if check_contractor and is_contractor_pull_request(pr):
        # don't create a JIRA issue, but leave a comment
        comment = {
            "body": github_contractor_pr_comment(pr),
        }
        url = "/repos/{repo}/issues/{num}/comments".format(
            repo=repo, num=num,
        )
        comment_resp = github.post(url, json=comment)
        comment_resp.raise_for_status()
        return "contractor pull request"

    issue_key = get_jira_issue_key(pr)
    if issue_key:
        msg = "Already created {key} for PR #{num} against {repo}".format(
            key=issue_key,
            num=pr["number"],
            repo=pr["base"]["repo"]["full_name"],
        )
        print(msg, file=sys.stderr)
        return msg

    repo = pr["base"]["repo"]["full_name"].decode('utf-8')
    people = get_people_file()
    custom_fields = get_jira_custom_fields()

    if user in people:
        user_name = people[user].get("name", "")
    else:
        user_resp = github.get(pr["user"]["url"])
        if user_resp.ok:
            user_name = user_resp.json().get("name", user)
        else:
            user_name = user

    # create an issue on JIRA!
    new_issue = {
        "fields": {
            "project": {
                "key": "OSPR",
            },
            "issuetype": {
                "name": "Pull Request Review",
            },
            "summary": pr["title"],
            "description": pr["body"],
            custom_fields["URL"]: pr["html_url"],
            custom_fields["PR Number"]: pr["number"],
            custom_fields["Repo"]: pr["base"]["repo"]["full_name"],
            custom_fields["Contributor Name"]: user_name,
        }
    }
    institution = people.get(user, {}).get("institution", None)
    if institution:
        new_issue["fields"][custom_fields["Customer"]] = [institution]
    bugsnag_context["new_issue"] = new_issue
    bugsnag.configure_request(meta_data=bugsnag_context)

    resp = jira.post("/rest/api/2/issue", json=new_issue)
    resp.raise_for_status()
    new_issue_body = resp.json()
    issue_key = new_issue_body["key"].decode('utf-8')
    bugsnag_context["new_issue"]["key"] = issue_key
    bugsnag.configure_request(meta_data=bugsnag_context)
    # add a comment to the Github pull request with a link to the JIRA issue
    comment = {
        "body": github_community_pr_comment(pr, new_issue_body, people),
    }
    url = "/repos/{repo}/issues/{num}/comments".format(
        repo=repo, num=pr["number"],
    )
    comment_resp = github.post(url, json=comment)
    comment_resp.raise_for_status()

    # Add the "Needs Triage" label to the PR
    issue_url = "/repos/{repo}/issues/{num}".format(repo=repo, num=pr["number"])
    label_resp = github.patch(issue_url, data=json.dumps({"labels": ["needs triage", "open-source-contribution"]}))
    label_resp.raise_for_status()

    print(
        "@{user} opened PR #{num} against {repo}, created {issue} to track it".format(
            user=user, repo=repo,
            num=pr["number"], issue=issue_key,
        ),
        file=sys.stderr
    )
    return "created {key}".format(key=issue_key)
Example #10
0
def pr_opened(pr, ignore_internal=True, check_contractor=True, bugsnag_context=None):
    bugsnag_context = bugsnag_context or {}
    user = pr["user"]["login"].decode('utf-8')
    repo = pr["base"]["repo"]["full_name"]
    num = pr["number"]
    if ignore_internal and is_internal_pull_request(pr):
        # not an open source pull request, don't create an issue for it
        print(
            "@{user} opened PR #{num} against {repo} (internal PR)".format(
                user=user, repo=repo, num=num,
            ),
            file=sys.stderr
        )
        return "internal pull request"

    if check_contractor and is_contractor_pull_request(pr):
        # don't create a JIRA issue, but leave a comment
        comment = {
            "body": github_contractor_pr_comment(pr),
        }
        url = "/repos/{repo}/issues/{num}/comments".format(
            repo=repo, num=num,
        )
        comment_resp = github.post(url, json=comment)
        if not comment_resp.ok:
            raise requests.exceptions.RequestException(comment_resp.text)
        return "contractor pull request"

    issue_key = get_jira_issue_key(pr)
    if issue_key:
        msg = "Already created {key} for PR #{num} against {repo}".format(
            key=issue_key,
            num=pr["number"],
            repo=pr["base"]["repo"]["full_name"],
        )
        print(msg, file=sys.stderr)
        return msg

    repo = pr["base"]["repo"]["full_name"].decode('utf-8')
    people = get_people_file()
    custom_fields = get_jira_custom_fields()

    if user in people:
        user_name = people[user].get("name", "")
    else:
        user_resp = github.get(pr["user"]["url"])
        if user_resp.ok:
            user_name = user_resp.json().get("name", user)
        else:
            user_name = user

    # create an issue on JIRA!
    new_issue = {
        "fields": {
            "project": {
                "key": "OSPR",
            },
            "issuetype": {
                "name": "Pull Request Review",
            },
            "summary": pr["title"],
            "description": pr["body"],
            custom_fields["URL"]: pr["html_url"],
            custom_fields["PR Number"]: pr["number"],
            custom_fields["Repo"]: pr["base"]["repo"]["full_name"],
            custom_fields["Contributor Name"]: user_name,
        }
    }
    institution = people.get(user, {}).get("institution", None)
    if institution:
        new_issue["fields"][custom_fields["Customer"]] = [institution]
    bugsnag_context["new_issue"] = new_issue
    bugsnag.configure_request(meta_data=bugsnag_context)

    resp = jira.post("/rest/api/2/issue", json=new_issue)
    if not resp.ok:
        raise requests.exceptions.RequestException(resp.text)
    new_issue_body = resp.json()
    issue_key = new_issue_body["key"].decode('utf-8')
    bugsnag_context["new_issue"]["key"] = issue_key
    bugsnag.configure_request(meta_data=bugsnag_context)
    # add a comment to the Github pull request with a link to the JIRA issue
    comment = {
        "body": github_community_pr_comment(pr, new_issue_body, people),
    }
    url = "/repos/{repo}/issues/{num}/comments".format(
        repo=repo, num=pr["number"],
    )
    comment_resp = github.post(url, json=comment)
    if not comment_resp.ok:
        raise requests.exceptions.RequestException(comment_resp.text)

    # Add the "Needs Triage" label to the PR
    issue_url = "/repos/{repo}/issues/{num}".format(repo=repo, num=pr["number"])
    label_resp = github.patch(issue_url, data=json.dumps({"labels": ["needs triage", "open-source-contribution"]}))
    if not label_resp.ok:
        raise requests.exceptions.RequestException(label_resp.text)

    print(
        "@{user} opened PR #{num} against {repo}, created {issue} to track it".format(
            user=user, repo=repo,
            num=pr["number"], issue=issue_key,
        ),
        file=sys.stderr
    )
    return "created {key}".format(key=issue_key)
Example #11
0
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
Example #12
0
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"