Ejemplo n.º 1
0
def rescan_repository(self, repo):
    """
    rescans a single repo for new prs
    """
    github = github_bp.session
    sentry_extra_context({"repo": repo})
    url = "/repos/{repo}/pulls".format(repo=repo)
    created = {}
    if not self.request.called_directly:
        self.update_state(state='STARTED', meta={'repo': repo})

    def page_callback(response):
        if not response.ok or self.request.called_directly:
            return
        current_url = URLObject(response.url)
        current_page = int(current_url.query_dict.get("page", 1))
        link_last = response.links.get("last")
        if link_last:
            last_url = URLObject(link_last['url'])
            last_page = int(last_url.query_dict["page"])
        else:
            last_page = current_page
        state_meta = {
            "repo": repo,
            "current_page": current_page,
            "last_page": last_page
        }
        self.update_state(state='STARTED', meta=state_meta)

    for pull_request in paginated_get(url,
                                      session=github,
                                      callback=page_callback):
        sentry_extra_context({"pull_request": pull_request})
        issue_key = get_jira_issue_key(pull_request)
        is_internal = is_internal_pull_request(pull_request)
        if not issue_key and not is_internal:
            # `pull_request_opened()` is a celery task, but by calling it as
            # a function instead of calling `.delay()` on it, we're eagerly
            # executing the task now, instead of adding it to the task queue
            # so it is executed later. As a result, this will return the values
            # that the `pull_request_opened()` function returns, rather than
            # return an AsyncResult object.
            issue_key, issue_created = pull_request_opened(pull_request)  # pylint: disable=no-value-for-parameter
            if issue_created:
                created[pull_request["number"]] = issue_key

    logger.info(
        "Created {num} JIRA issues on repo {repo}. PRs are {prs}".format(
            num=len(created),
            repo=repo,
            prs=created.keys(),
        ), )
    info = {"repo": repo}
    if created:
        info["created"] = created
    return info
Ejemplo n.º 2
0
def rescan_repository(self, repo):
    """
    rescans a single repo for new prs
    """
    github = github_bp.session
    sentry.client.extra_context({"repo": repo})
    url = "/repos/{repo}/pulls".format(repo=repo)
    created = {}
    if not self.request.called_directly:
        self.update_state(state='STARTED', meta={'repo': repo})

    def page_callback(response):
        if not response.ok or self.request.called_directly:
            return
        current_url = URLObject(response.url)
        current_page = int(current_url.query_dict.get("page", 1))
        link_last = response.links.get("last")
        if link_last:
            last_url = URLObject(link_last['url'])
            last_page = int(last_url.query_dict["page"])
        else:
            last_page = current_page
        state_meta = {
            "repo": repo,
            "current_page": current_page,
            "last_page": last_page
        }
        self.update_state(state='STARTED', meta=state_meta)

    for pull_request in paginated_get(url, session=github, callback=page_callback):
        sentry.client.extra_context({"pull_request": pull_request})
        issue_key = get_jira_issue_key(pull_request)
        is_internal = is_internal_pull_request(pull_request)
        if not issue_key and not is_internal:
            # `pull_request_opened()` is a celery task, but by calling it as
            # a function instead of calling `.delay()` on it, we're eagerly
            # executing the task now, instead of adding it to the task queue
            # so it is executed later. As a result, this will return the values
            # that the `pull_request_opened()` function returns, rather than
            # return an AsyncResult object.
            issue_key, issue_created = pull_request_opened(pull_request)
            if issue_created:
                created[pull_request["number"]] = issue_key

    logger.info(
        "Created {num} JIRA issues on repo {repo}. PRs are {prs}".format(
            num=len(created), repo=repo, prs=created.keys(),
        ),
    )
    info = {"repo": repo}
    if created:
        info["created"] = created
    return info
Ejemplo n.º 3
0
def pull_request_comment(comment_event):
    """
    Process a comment on a pull request.
    """
    # Creates object that works with get_jira_issue_key, may want to refactor that
    # function later.
    repo = comment_event["repository"]["name"]
    issue_number = comment_event["issue"]["number"]

    pr = {"base": {"repo": {"full_name": repo}}, "number": issue_number }
    issue_key = get_jira_issue_key(pr)

    # Currently we stop quietly if there's no pre-existing ticket in OSPR
    if not issue_key:
        msg = "No OSPR ticket for PR #{num}".format(num=issue_number,)
        logger.info(msg)
        return None, False

    triaged_issue = issue_to_triage(issue_key) # Move JIRA ticket to triage

    if not triaged_issue:
        msg = "Unable to move PR #{num} to 'Needs Triage on Jira'".format(num=issue_number,)
        logger.info(msg)
        return None, False

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

    # Comment on Github
    comment = {
        "body": "Thanks for the nudge! Your pull request has been added to our list for review.  You can see " + \
            "your ticket on JIRA here: " + str(triaged_issue),
    }
    url = "/repos/{repo}/issues/{num}/comments".format(
        repo=repo, num=issue_number,
    )
    comment_resp = github.post(url, json=comment)
    comment_resp.raise_for_status()

    return issue_key, True
Ejemplo n.º 4
0
def pull_request_opened(pull_request, ignore_internal=True, check_contractor=True):
    """
    Process a pull request. This is called when a pull request is opened, or
    when the pull requests of a repo are re-scanned. By default, this function
    will ignore internal pull requests, and will add a comment to pull requests
    made by contractors (if if has not yet added a comment). However,
    this function can be called in such a way that it processes those pull
    requests anyway.

    This function must be idempotent. Every time the repositories are re-scanned,
    this function will be called for pull requests that have already been opened.
    As a result, it should not comment on the pull request without checking to
    see if it has *already* commented on the pull request.

    Returns a 2-tuple. The first element in the tuple is the key of the JIRA
    issue associated with the pull request, if any, as a string. The second
    element in the tuple is a boolean indicating if this function did any
    work, such as making a JIRA issue or commenting on the pull request.
    """
    github = github_bp.session
    pr = pull_request
    user = pr["user"]["login"].decode('utf-8')
    repo = pr["base"]["repo"]["full_name"]
    num = pr["number"]
    is_internal_pr = is_internal_pull_request(pr)
    has_cl = has_internal_cover_letter(pr)
    is_beta = is_beta_tester_pull_request(pr)

    if is_internal_pr and not has_cl and is_beta:
        logger.info(
            "Adding cover letter template to PR #{num} against {repo}".format(
                repo=repo, num=num,
            ),
        )
        comment = {
            "body": github_internal_cover_letter(pr),
        }
        url = "/repos/{repo}/issues/{num}/comments".format(
            repo=repo, num=num,
        )

        comment_resp = github.post(url, json=comment)
        comment_resp.raise_for_status()

    if ignore_internal and is_internal_pr:
        # not an open source pull request, don't create an issue for it
        logger.info(
            "@{user} opened PR #{num} against {repo} (internal PR)".format(
                user=user, repo=repo, num=num,
            ),
        )
        return None, False

    if check_contractor and is_contractor_pull_request(pr):
        # have we already left a contractor comment?
        if has_contractor_comment(pr):
            return None, False

        # 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 None, True

    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"],
        )
        logger.info(msg)
        return issue_key, False

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

    user_name = None
    if user in people:
        user_name = people[user].get("name", "")
    if not user_name:
        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]
    sentry.client.extra_context({"new_issue": new_issue})

    resp = jira_bp.session.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')
    new_issue["key"] = issue_key
    sentry.client.extra_context({"new_issue": new_issue})
    # 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()

    logger.info(
        "@{user} opened PR #{num} against {repo}, created {issue} to track it".format(
            user=user, repo=repo,
            num=pr["number"], issue=issue_key,
        ),
    )
    return issue_key, True
Ejemplo n.º 5
0
def pull_request_closed(pull_request):
    """
    A GitHub pull request has been merged or closed. Synchronize the JIRA issue
    to also be in the "merged" or "closed" state. Returns a boolean: True
    if the JIRA issue was correctly synchronized, False otherwise. (However,
    these booleans are ignored.)
    """
    jira = jira_bp.session
    pr = pull_request
    repo = pr["base"]["repo"]["full_name"].decode('utf-8')

    merged = pr["merged"]
    issue_key = get_jira_issue_key(pr)
    if not issue_key:
        logger.info(
            "Couldn't find JIRA issue for PR #{num} against {repo}".format(
                num=pr["number"], repo=repo,
            ),
        )
        return "no JIRA issue :("
    sentry.client.extra_context({"jira_key": issue_key})

    # 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 transitions_resp.status_code == 404:
        # JIRA issue has been deleted
        return False
    transitions_resp.raise_for_status()

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

    sentry.client.extra_context({"transitions": transitions})

    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()
        sentry.client.extra_context({"jira_issue": issue})
        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
            )
            logger.info(msg)
            return False

        # 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()
    logger.info(
        "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",
        ),
    )
    return True
Ejemplo n.º 6
0
def pull_request_closed(pull_request):
    pr = pull_request
    repo = pr["base"]["repo"]["full_name"].decode("utf-8")

    merged = pr["merged"]
    issue_key = get_jira_issue_key(pr)
    if not issue_key:
        logger.info("Couldn't find JIRA issue for PR #{num} against {repo}".format(num=pr["number"], repo=repo))
        return "no JIRA issue :("
    sentry.client.extra_context({"jira_key": issue_key})

    # 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"]

    sentry.client.extra_context({"transitions": transitions})

    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()
        sentry.client.extra_context({"jira_issue": issue})
        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)
            logger.info(msg)
            return False

        # 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()
    logger.info(
        "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",
        )
    )
    return True