Example #1
0
def process_pr(gh, repo, issue, dryRun, cmsbuild_user="******"):
    import yaml
    if ignore_issue(repo, issue): return
    prId = issue.number
    #if prId in [ 15876 ] : return
    repository = repo.full_name
    print "Working on ", repo.full_name, " for PR/Issue ", prId
    cmssw_repo = False
    create_test_property = False
    if repository.endswith("/" + GH_CMSSW_REPO): cmssw_repo = True
    packages = set([])
    create_external_issue = False
    add_external_category = False
    signing_categories = set([])
    new_package_message = ""
    mustClose = False
    releaseManagers = []
    signatures = {}
    watchers = []
    #Process Pull Request
    pkg_categories = set([])
    if issue.pull_request:
        try:
            pr = repo.get_pull(prId)
            if cmssw_repo and (pr.base.ref == CMSSW_DEVEL_BRANCH):
                if pr.state != "closed":
                    print "This pull request must go in to master branch"
                    if not dryRun:
                        edit_pr(get_token(gh),
                                repo.full_name,
                                prId,
                                base="master")
                        msg = format(
                            "@%(user)s, %(dev_branch)s branch is closed for direct updates. cms-bot is going to move this PR to master branch.\n"
                            "In future, please use cmssw master branch to submit your changes.\n",
                            user=issue.user.login.encode("ascii", "ignore"),
                            dev_branch=CMSSW_DEVEL_BRANCH)
                        issue.create_comment(msg)
                return
        except:
            print "Could not find the pull request ", prId, ", may be it is an issue"
            return
        # A pull request is by default closed if the branch is a closed one.
        if pr.base.ref in RELEASE_BRANCH_CLOSED: mustClose = True
        # Process the changes for the given pull request so that we can determine the
        # signatures it requires.
        if cmssw_repo:
            packages = sorted([
                x for x in set([
                    "/".join(x.filename.split("/", 2)[0:2])
                    for x in pr.get_files()
                ])
            ])
            updateMilestone(repo, issue, pr, dryRun)
            create_test_property = True
        else:
            add_external_category = True
            packages = set(["externals/" + repository])
            if repository != CMSDIST_REPO_NAME:
                if not repository.endswith("/cms-bot"):
                    create_external_issue = True
            else:
                create_test_property = True
                if not re.match(VALID_CMSDIST_BRANCHES, pr.base.ref):
                    print "Skipping PR as it does not belong to valid CMSDIST branch"
                    return

        print "Following packages affected:"
        print "\n".join(packages)
        pkg_categories = set([
            category for package in packages
            for category, category_packages in CMSSW_CATEGORIES.items()
            if package in category_packages
        ])
        signing_categories.update(pkg_categories)

        # For PR, we always require tests.
        signing_categories.add("tests")
        if add_external_category: signing_categories.add("externals")
        # We require ORP approval for releases which are in production.
        # or all externals package
        if (not cmssw_repo) or (pr.base.ref in RELEASE_BRANCH_PRODUCTION):
            print "This pull request requires ORP approval"
            signing_categories.add("orp")
            for l1 in CMSSW_L1:
                if not l1 in CMSSW_L2: CMSSW_L2[l1] = []
                if not "orp" in CMSSW_L2[l1]: CMSSW_L2[l1].append("orp")

        print "Following categories affected:"
        print "\n".join(signing_categories)

        if cmssw_repo:
            # If there is a new package, add also a dummy "new" category.
            all_packages = [
                package for category_packages in CMSSW_CATEGORIES.values()
                for package in category_packages
            ]
            has_category = all(
                [package in all_packages for package in packages])
            if not has_category:
                new_package_message = "\nThe following packages do not have a category, yet:\n\n"
                new_package_message += "\n".join([
                    package
                    for package in packages if not package in all_packages
                ]) + "\n"
                new_package_message += "Please create a PR for https://github.com/cms-sw/cms-bot/blob/master/categories.py to assign category\n"
                print new_package_message
                signing_categories.add("new-package")

        # Add watchers.yaml information to the WATCHERS dict.
        WATCHERS = (yaml.load(file(join(SCRIPT_DIR, "watchers.yaml"))))
        # Given the packages check if there are additional developers watching one or more.
        author = pr.user.login
        watchers = set([
            user for package in packages
            for user, watched_regexp in WATCHERS.items()
            for regexp in watched_regexp
            if re.match("^" + regexp + ".*", package) and user != author
        ])
        #Handle category watchers
        for user, cats in (yaml.load(
                file(join(SCRIPT_DIR, "category-watchers.yaml")))).items():
            for cat in cats:
                if cat in signing_categories:
                    print "Added ", user, " to watch due to cat", cat
                    watchers.add(user)

        # Handle watchers
        watchingGroups = yaml.load(file(join(SCRIPT_DIR, "groups.yaml")))
        for watcher in [x for x in watchers]:
            if not watcher in watchingGroups: continue
            watchers.remove(watcher)
            watchers.update(set(watchingGroups[watcher]))
        watchers = set(["@" + u for u in watchers])
        print "Watchers " + ", ".join(watchers)

        last_commit = None
        try:
            # This requires at least PyGithub 1.23.0. Making it optional for the moment.
            last_commit = pr.get_commits().reversed[0].commit
        except:
            # This seems to fail for more than 250 commits. Not sure if the
            # problem is github itself or the bindings.
            try:
                last_commit = pr.get_commits()[pr.commits - 1].commit
            except IndexError:
                print "Index error: May be PR with no commits"
                return
        last_commit_date = last_commit.committer.date
        print "Latest commit by ", last_commit.committer.name.encode(
            "ascii", "ignore"), " at ", last_commit_date
        print "Latest commit message: ", last_commit.message.encode(
            "ascii", "ignore")
        print "Latest commit sha: ", last_commit.sha
        releaseManagers = list(
            set(
                RELEASE_MANAGERS.get(pr.base.ref, []) +
                SPECIAL_RELEASE_MANAGERS))

    # Process the issue comments
    signatures = dict([(x, "pending") for x in signing_categories])
    already_seen = False
    pull_request_updated = False
    comparison_done = False
    comparison_notrun = False
    tests_already_queued = False
    tests_requested = False
    mustMerge = False
    external_issue_number = ""
    trigger_test_on_signature = True
    has_categories_approval = False
    cmsdist_pr = ''
    cmssw_prs = ''
    extra_wfs = ''
    assign_cats = {}
    hold = {}
    extra_labels = {}
    last_test_start_time = None
    body_firstline = issue.body.encode("ascii", "ignore").split("\n",
                                                                1)[0].strip()
    abort_test = False
    need_external = False
    if (issue.user.login == cmsbuild_user) and \
       re.match(ISSUE_SEEN_MSG,body_firstline):
        already_seen = True
    elif re.match(
            "^type\s+(bug(-fix|fix|)|(new-|)feature)|urgent|backport\s+(of\s+|)#\d+$",
            body_firstline, re.I):
        check_extra_labels(body_firstline.lower(), extra_labels)
    for comment in issue.get_comments():
        commenter = comment.user.login
        comment_msg = comment.body.encode("ascii", "ignore")

        # The first line is an invariant.
        comment_lines = [
            l.strip() for l in comment_msg.split("\n") if l.strip()
        ]
        first_line = comment_lines[0:1]
        if not first_line: continue
        first_line = first_line[0]
        if (commenter == cmsbuild_user) and re.match(ISSUE_SEEN_MSG,
                                                     first_line):
            already_seen = True
            pull_request_updated = False
            if create_external_issue:
                external_issue_number = comment_msg.split(
                    "external issue " + CMSDIST_REPO_NAME + "#",
                    2)[-1].split("\n")[0]
                if not re.match("^[1-9][0-9]*$", external_issue_number):
                    print "ERROR: Unknow external issue PR format:", external_issue_number
                    external_issue_number = ""
            continue

        assign_type, new_cats = get_assign_categories(first_line)
        if new_cats:
            if (assign_type
                    == "new categories assigned:") and (commenter
                                                        == cmsbuild_user):
                for ex_cat in new_cats:
                    if ex_cat in assign_cats: assign_cats[ex_cat] = 1
            if ((commenter in CMSSW_L2)
                    or (commenter in CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
                if assign_type == "assign":
                    for ex_cat in new_cats:
                        if not ex_cat in signing_categories:
                            assign_cats[ex_cat] = 0
                            signing_categories.add(ex_cat)
                            signatures[ex_cat] = "pending"
                elif assign_type == "unassign":
                    for ex_cat in new_cats:
                        if ex_cat in assign_cats:
                            assign_cats.pop(ex_cat)
                            signing_categories.remove(ex_cat)
                            signatures.pop(ex_cat)
            continue

        # Some of the special users can say "hold" prevent automatic merging of
        # fully signed PRs.
        if re.match("^hold$", first_line, re.I):
            if commenter in CMSSW_L1 + CMSSW_L2.keys(
            ) + releaseManagers + PR_HOLD_MANAGERS:
                hold[commenter] = 1
            continue
        if re.match(
                "^type\s+(bug(-fix|fix|)|(new-|)feature)|urgent|backport\s+(of\s+|)#\d+$",
                first_line, re.I):
            if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers + [
                    issue.user.login
            ]:
                check_extra_labels(first_line.lower(), extra_labels)
            continue
        if re.match("^unhold$", first_line, re.I):
            if commenter in CMSSW_L1:
                hold = {}
            elif commenter in CMSSW_L2.keys(
            ) + releaseManagers + PR_HOLD_MANAGERS:
                if hold.has_key(commenter): del hold[commenter]
            continue
        if (commenter == cmsbuild_user) and (re.match("^" + HOLD_MSG + ".+",
                                                      first_line)):
            for u in first_line.split(HOLD_MSG, 2)[1].split(","):
                u = u.strip().lstrip("@")
                if hold.has_key(u): hold[u] = 0
        if re.match("^close$", first_line, re.I):
            if (not issue.pull_request
                    and (commenter in CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
                mustClose = True
            continue

        # Ignore all other messages which are before last commit.
        if issue.pull_request and (comment.created_at < last_commit_date):
            print "Ignoring comment done before the last commit."
            pull_request_updated = True
            continue

        # Check for cmsbuild_user comments and tests requests only for pull requests
        if commenter == cmsbuild_user:
            if not issue.pull_request: continue
            sec_line = comment_lines[1:2]
            if not sec_line: sec_line = ""
            else: sec_line = sec_line[0]
            if re.match("Comparison is ready", first_line):
                if ('tests'
                        in signatures) and signatures["tests"] != 'pending':
                    comparison_done = True
            elif re.match("^Comparison not run.+", first_line):
                if ('tests'
                        in signatures) and signatures["tests"] != 'pending':
                    comparison_notrun = True
            elif re.match(FAILED_TESTS_MSG, first_line) or re.match(
                    IGNORING_TESTS_MSG, first_line):
                tests_already_queued = False
                tests_requested = False
                signatures["tests"] = "pending"
                trigger_test_on_signature = False
            elif re.match("Pull request ([^ #]+|)[#][0-9]+ was updated[.].*",
                          first_line):
                pull_request_updated = False
            elif re.match(TRIGERING_TESTS_MSG, first_line):
                tests_already_queued = True
                tests_requested = False
                signatures["tests"] = "started"
                trigger_test_on_signature = False
                last_test_start_time = comment.created_at
                abort_test = False
                need_external = False
                if sec_line.startswith("Using externals from cms-sw/cmsdist#"):
                    need_external = True
            elif re.match(TESTS_RESULTS_MSG, first_line):
                test_sha = sec_line.replace("Tested at: ", "").strip()
                if (test_sha != last_commit.sha) and (
                        test_sha != 'UNKNOWN') and (not "I had the issue "
                                                    in first_line):
                    print "Ignoring test results for sha:", test_sha
                    continue
                trigger_test_on_signature = False
                tests_already_queued = False
                tests_requested = False
                comparison_done = False
                comparison_notrun = False
                if "+1" in first_line:
                    signatures["tests"] = "approved"
                elif "-1" in first_line:
                    signatures["tests"] = "rejected"
                else:
                    signatures["tests"] = "pending"
                print 'Previous tests already finished, resetting test request state to ', signatures[
                    "tests"]
            elif re.match(TRIGERING_TESTS_ABORT_MSG, first_line):
                abort_test = False
            continue

        if issue.pull_request:
            # Check if the release manager asked for merging this.
            if (commenter in releaseManagers + CMSSW_L1) and re.match(
                    "^\s*(merge)\s*$", first_line, re.I):
                mustMerge = True
                mustClose = False
                continue

            # Check if the someone asked to trigger the tests
            if commenter in TRIGGER_PR_TESTS + CMSSW_L2.keys(
            ) + CMSSW_L1 + releaseManagers:
                ok, cmsdist_pr, cmssw_prs, extra_wfs = check_test_cmd(
                    first_line)
                if ok:
                    print 'Tests requested:', commenter, 'asked to test this PR with cmsdist_pr=%s, cmssw_prs=%s and workflows=%s' % (
                        cmsdist_pr, cmssw_prs, extra_wfs)
                    trigger_test_on_signature = False
                    if tests_already_queued:
                        print "Test results not obtained in ", comment.created_at - last_test_start_time
                        diff = time.mktime(
                            comment.created_at.timetuple()) - time.mktime(
                                last_test_start_time.timetuple())
                        if diff >= TEST_WAIT_GAP:
                            print "Looks like tests are stuck, will try to re-queue"
                            tests_already_queued = False
                    if not tests_already_queued:
                        print 'cms-bot will request test for this PR'
                        tests_requested = True
                        comparison_done = False
                        comparison_notrun = False
                        if not cmssw_repo:
                            cmsdist_pr = ''
                            cmssw_prs = ''
                        signatures["tests"] = "pending"
                    else:
                        print 'Tests already request for this PR'
                    continue
                elif (REGEX_TEST_ABORT.match(first_line)
                      and ((signatures["tests"] == "started") or
                           ((signatures["tests"] != "pending") and
                            (not comparison_done)))):
                    tests_already_queued = False
                    abort_test = True
                    signatures["tests"] = "pending"

        # Check L2 signoff for users in this PR signing categories
        if commenter in CMSSW_L2 and [
                x for x in CMSSW_L2[commenter] if x in signing_categories
        ]:
            ctype = ""
            selected_cats = []
            if re.match("^([+]1|approve[d]?|sign|signed)$", first_line, re.I):
                ctype = "+1"
                selected_cats = CMSSW_L2[commenter]
            elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
                ctype = "-1"
                selected_cats = CMSSW_L2[commenter]
            elif re.match("^[+-][a-z][a-z0-9]+$", first_line, re.I):
                category_name = first_line[1:].lower()
                if category_name in CMSSW_L2[commenter]:
                    ctype = first_line[0] + "1"
                    selected_cats = [category_name]
            elif re.match("^(reopen)$", first_line, re.I):
                ctype = "reopen"
            if ctype == "+1":
                for sign in selected_cats:
                    signatures[sign] = "approved"
                    has_categories_approval = True
                    if sign == "orp": mustClose = False
            elif ctype == "-1":
                for sign in selected_cats:
                    signatures[sign] = "rejected"
                    has_categories_approval = False
                    if sign == "orp": mustClose = False
            elif ctype == "reopen":
                if "orp" in CMSSW_L2[commenter]:
                    signatures["orp"] = "pending"
                    mustClose = False
            continue

    is_hold = len(hold) > 0
    new_blocker = False
    blockers = ""
    for u in hold:
        blockers += " @" + u + ","
        if hold[u]: new_blocker = True
    blockers = blockers.rstrip(",")

    new_assign_cats = []
    for ex_cat in assign_cats:
        if assign_cats[ex_cat] == 1: continue
        new_assign_cats.append(ex_cat)

    print "All assigned cats:", ",".join(assign_cats.keys())
    print "Newly assigned cats:", ",".join(new_assign_cats)

    # Labels coming from signature.
    labels = []
    for cat in signing_categories:
        l = cat + "-pending"
        if cat in signatures: l = cat + "-" + signatures[cat]
        labels.append(l)

    if not issue.pull_request and len(signing_categories) == 0:
        labels.append("pending-assignment")
    # Additional labels.
    if is_hold: labels.append("hold")

    # Add additional labels
    for lab in extra_labels:
        labels.append(extra_labels[lab])

    if cmssw_repo and issue.pull_request:
        if comparison_done:
            labels.append("comparison-available")
        elif comparison_notrun:
            labels.append("comparison-notrun")
        else:
            labels.append("comparison-pending")

    # Now updated the labels.
    missingApprovals = [
        x for x in labels
        if not x.endswith("-approved") and not x.startswith("orp") and
        not x.startswith("tests") and not x.startswith("pending-assignment")
        and not x.startswith("comparison")
        and not x in ["backport", "urgent", "bug-fix", "new-feature"]
    ]

    if not missingApprovals:
        print "The pull request is complete."
    if missingApprovals:
        labels.append("pending-signatures")
    elif not "pending-assignment" in labels:
        labels.append("fully-signed")
    if need_external: labels.append("requires-external")
    labels = set(labels)

    print "The labels of the pull request should be:\n  " + "\n  ".join(labels)

    # We update labels only if they are different.
    old_labels = set([x.name for x in issue.labels])
    new_categories = set([])
    for nc_lab in pkg_categories:
        ncat = [
            nc_lab for oc_lab in old_labels if oc_lab.startswith(nc_lab + '-')
        ]
        if ncat: continue
        new_categories.add(nc_lab)

    if new_assign_cats:
        new_l2s = [
            "@" + name for name, l2_categories in CMSSW_L2.items()
            for signature in new_assign_cats if signature in l2_categories
        ]
        if not dryRun:
            issue.create_comment(
                "New categories assigned: " + ",".join(new_assign_cats) +
                "\n\n" + ",".join(new_l2s) +
                " you have been requested to review this Pull request/Issue and eventually sign? Thanks"
            )

    #update blocker massge
    if new_blocker:
        if not dryRun:
            issue.create_comment(
                HOLD_MSG + blockers +
                '\nThey need to issue an `unhold` command to remove the `hold` state or L1 can `unhold` it for all'
            )
        print "Blockers:", blockers

    labels_changed = True
    if old_labels == labels:
        labels_changed = False
        print "Labels unchanged."
    elif not dryRun:
        issue.edit(labels=list(labels))

    # Check if it needs to be automatically closed.
    if mustClose == True and issue.state == "open":
        print "This pull request must be closed."
        if not dryRun: issue.edit(state="closed")

    if not issue.pull_request:
        issueMessage = None
        if not already_seen:
            uname = ""
            if issue.user.name:
                uname = issue.user.name.encode("ascii", "ignore")
            l2s = ", ".join(["@" + name for name in CMSSW_ISSUES_TRACKERS])
            issueMessage = format(
                "%(msgPrefix)s @%(user)s"
                " %(name)s.\n\n"
                "%(l2s)s can you please review it and eventually sign/assign?"
                " Thanks.\n\n"
                "cms-bot commands are listed <a href=\"http://cms-sw.github.io/cms-bot-cmssw-issues.html\">here</a>\n",
                msgPrefix=NEW_ISSUE_PREFIX,
                user=issue.user.login.encode("ascii", "ignore"),
                name=uname,
                l2s=l2s)
        elif ("fully-signed" in labels) and (not "fully-signed" in old_labels):
            issueMessage = "This issue is fully signed and ready to be closed."
        print "Issue Message:", issueMessage
        if issueMessage and not dryRun: issue.create_comment(issueMessage)
        return

    # get release managers
    SUPER_USERS = (yaml.load(file(join(SCRIPT_DIR, "super-users.yaml"))))
    releaseManagersList = ", ".join(
        ["@" + x for x in set(releaseManagers + SUPER_USERS)])

    #For now, only trigger tests for cms-sw/cmssw and cms-sw/cmsdist
    if create_test_property:
        # trigger the tests and inform it in the thread.
        if trigger_test_on_signature and has_categories_approval:
            tests_requested = True
        cmsdist_issue = None
        test_msg = TRIGERING_TESTS_MSG
        if (tests_requested or abort_test) and cmsdist_pr:
            try:
                cmsdist_repo = gh.get_repo(CMSDIST_REPO_NAME)
                cmsdist_pull = cmsdist_repo.get_pull(int(cmsdist_pr))
                cmsdist_issue = cmsdist_repo.get_issue(int(cmsdist_pr))
                test_msg = test_msg + "\nUsing externals from " + CMSDIST_REPO_NAME + "#" + cmsdist_pr
            except UnknownObjectException as e:
                print "Error getting cmsdist PR:", e.data['message']
                test_msg = IGNORING_TESTS_MSG + "\n**ERROR**: Unable to find cmsdist Pull request " + CMSDIST_REPO_NAME + "#" + cmsdist_pr
        if not dryRun:
            if tests_requested:
                issue.create_comment(test_msg)
                if cmsdist_issue:
                    cmsdist_issue.create_comment(TRIGERING_TESTS_MSG +
                                                 "\nUsing cmssw from " +
                                                 CMSSW_REPO_NAME + "#" +
                                                 str(prId))
                if (not cmsdist_pr) or cmsdist_issue:
                    create_properties_file_tests(repository,
                                                 prId,
                                                 cmsdist_pr,
                                                 cmssw_prs,
                                                 extra_wfs,
                                                 dryRun,
                                                 abort=False)
            elif abort_test:
                issue.create_comment(TRIGERING_TESTS_ABORT_MSG)
                if cmsdist_issue:
                    cmsdist_issue.create_comment(TRIGERING_TESTS_ABORT_MSG)
                if (not cmsdist_pr) or cmsdist_issue:
                    create_properties_file_tests(repository,
                                                 prId,
                                                 cmsdist_pr,
                                                 "",
                                                 "",
                                                 dryRun,
                                                 abort=True)

    # Do not complain about tests
    requiresTestMessage = " after it passes the integration tests"
    if "tests-approved" in labels:
        requiresTestMessage = " (tests are also fine)"
    elif "tests-rejected" in labels:
        requiresTestMessage = " (but tests are reportedly failing)"

    autoMergeMsg = ""
    if (("fully-signed" in labels) and ("tests-approved" in labels) and
        ((not "orp" in signatures) or (signatures["orp"] == "approved"))):
        autoMergeMsg = "This pull request will be automatically merged."
    else:
        if is_hold:
            autoMergeMsg = format(
                "This PR is put on hold by %(blockers)s. They have"
                " to `unhold` to remove the `hold` state or"
                " %(managers)s will have to `merge` it by"
                " hand.",
                blockers=blockers,
                managers=releaseManagersList)
        elif "new-package-pending" in labels:
            autoMergeMsg = format(
                "This pull request requires a new package and "
                " will not be merged. %(managers)s",
                managers=releaseManagersList)
        elif ("orp" in signatures) and (signatures["orp"] != "approved"):
            autoMergeMsg = format(
                "This pull request requires discussion in the"
                " ORP meeting before it's merged. %(managers)s",
                managers=releaseManagersList)

    devReleaseRelVal = ""
    if (pr.base.ref
            in RELEASE_BRANCH_PRODUCTION) and (pr.base.ref != "master"):
        devReleaseRelVal = " and once validation in the development release cycle " + CMSSW_DEVEL_BRANCH + " is complete"

    if ("fully-signed" in labels) and (not "fully-signed" in old_labels):
        messageFullySigned = format(
            "This pull request is fully signed and it will be"
            " integrated in one of the next %(branch)s IBs"
            "%(requiresTest)s"
            "%(devReleaseRelVal)s."
            " %(autoMerge)s",
            requiresTest=requiresTestMessage,
            autoMerge=autoMergeMsg,
            devReleaseRelVal=devReleaseRelVal,
            branch=pr.base.ref)
        print "Fully signed message updated"
        if not dryRun: issue.create_comment(messageFullySigned)

    unsigned = [k for (k, v) in signatures.items() if v == "pending"]
    missing_notifications = [
        "@" + name for name, l2_categories in CMSSW_L2.items()
        for signature in signing_categories
        if signature in l2_categories and signature in unsigned
    ]

    missing_notifications = set(missing_notifications)
    # Construct message for the watchers
    watchersMsg = ""
    if watchers:
        watchersMsg = format(
            "%(watchers)s this is something you requested to"
            " watch as well.\n",
            watchers=", ".join(watchers))
    # Construct message for the release managers.
    managers = ", ".join(["@" + x for x in releaseManagers])

    releaseManagersMsg = ""
    if releaseManagers:
        releaseManagersMsg = format(
            "%(managers)s you are the release manager for this.\n",
            managers=managers)

    # Add a Warning if the pull request was done against a patch branch
    if cmssw_repo:
        warning_msg = ''
        if 'patchX' in pr.base.ref:
            print 'Must warn that this is a patch branch'
            base_release = pr.base.ref.replace('_patchX', '')
            base_release_branch = re.sub('[0-9]+$', 'X', base_release)
            warning_msg = format(
                "Note that this branch is designed for requested bug "
                "fixes specific to the %(base_rel)s release.\nIf you "
                "wish to make a pull request for the %(base_branch)s "
                "release cycle, please use the %(base_branch)s branch instead\n",
                base_rel=base_release,
                base_branch=base_release_branch)

        # We do not want to spam people for the old pull requests.
        messageNewPR = format(
            "%(msgPrefix)s @%(user)s"
            " %(name)s for %(branch)s.\n\n"
            "It involves the following packages:\n\n"
            "%(packages)s\n\n"
            "%(new_package_message)s\n"
            "%(l2s)s can you please review it and eventually sign?"
            " Thanks.\n"
            "%(watchers)s"
            "%(releaseManagers)s"
            "%(patch_branch_warning)s\n"
            "cms-bot commands are listed <a href=\"http://cms-sw.github.io/cms-bot-cmssw-cmds.html\">here</a>\n",
            msgPrefix=NEW_PR_PREFIX,
            user=pr.user.login,
            name=pr.user.name and "(%s)" % pr.user.name or "",
            branch=pr.base.ref,
            l2s=", ".join(missing_notifications),
            packages="\n".join(packages),
            new_package_message=new_package_message,
            watchers=watchersMsg,
            releaseManagers=releaseManagersMsg,
            patch_branch_warning=warning_msg)

        messageUpdatedPR = format(
            "Pull request #%(pr)s was updated."
            " %(signers)s can you please check and sign again.",
            pr=pr.number,
            signers=", ".join(missing_notifications))
    else:
        if create_external_issue:
            if not already_seen:
                if dryRun:
                    print "Should create a new issue in ", CMSDIST_REPO_NAME, " for this PR"
                else:
                    external_managers = [
                        "@" + name for name, l2_categories in CMSSW_L2.items()
                        if "externals" in l2_categories
                    ]
                    cmsdist_repo = gh.get_repo(CMSDIST_REPO_NAME)
                    cmsdist_title = format("%(repo)s#%(pr)s: %(title)s",
                                           title=pr.title.encode(
                                               "ascii", "ignore"),
                                           repo=repository,
                                           pr=pr.number)
                    cmsdist_body = format(
                        "%(msgPrefix)s @%(user)s"
                        " %(name)s for branch %(branch)s.\n\n"
                        "Pull Request Reference: %(repo)s#%(pr)s\n\n"
                        "%(externals_l2s)s can you please review it and eventually sign? Thanks.\n",
                        msgPrefix=NEW_PR_PREFIX,
                        repo=repository,
                        user=pr.user.login,
                        name=pr.user.name and "(%s)" % pr.user.name or "",
                        branch=pr.base.ref,
                        externals_l2s=", ".join(external_managers),
                        pr=pr.number)
                    cissue = cmsdist_repo.create_issue(cmsdist_title,
                                                       cmsdist_body)
                    external_issue_number = str(cissue.number)
                    print "Created a new issue ", CMSDIST_REPO_NAME, "#", external_issue_number
            if pull_request_updated and external_issue_number:
                if dryRun:
                    print "Should add an update message for issue ", CMSDIST_REPO_NAME, "#", external_issue_number
                else:
                    cmsdist_repo = gh.get_repo(CMSDIST_REPO_NAME)
                    cissue = cmsdist_repo.get_issue(int(external_issue_number))
                    cmsdist_body = format(
                        "Pull request %(repo)s#%(pr)s was updated.\n"
                        "Latest update by %(name)s with commit message\n%(message)s",
                        repo=repository,
                        pr=pr.number,
                        name=last_commit.committer.name.encode(
                            "ascii", "ignore"),
                        message=last_commit.message.encode("ascii", "ignore"))
                    cissue.create_comment(cmsdist_body)
        cmsdist_issue = ""
        if external_issue_number:
            cmsdist_issue = "\n\nexternal issue " + CMSDIST_REPO_NAME + "#" + external_issue_number

        messageNewPR = format(
            "%(msgPrefix)s @%(user)s"
            " %(name)s for branch %(branch)s.\n\n"
            "%(l2s)s can you please review it and eventually sign?"
            " Thanks.\n"
            "%(watchers)s"
            "You can sign-off by replying to this message having"
            " '+1' in the first line of your reply.\n"
            "You can reject by replying  to this message having"
            " '-1' in the first line of your reply."
            "%(cmsdist_issue)s",
            msgPrefix=NEW_PR_PREFIX,
            user=pr.user.login,
            name=pr.user.name and "(%s)" % pr.user.name or "",
            branch=pr.base.ref,
            title=pr.title.encode("ascii", "ignore"),
            l2s=", ".join(missing_notifications),
            watchers=watchersMsg,
            cmsdist_issue=cmsdist_issue)

        messageUpdatedPR = format(
            "Pull request #%(pr)s was updated."
            "%(cmsdist_issue)s",
            pr=pr.number,
            cmsdist_issue=cmsdist_issue)

    # Finally decide whether or not we should close the pull request:
    messageBranchClosed = format("This branch is closed for updates."
                                 " Closing this pull request.\n"
                                 " Please bring this up in the ORP"
                                 " meeting if really needed.\n")

    commentMsg = ""
    if (pr.base.ref in RELEASE_BRANCH_CLOSED) and (pr.state != "closed"):
        commentMsg = messageBranchClosed
    elif not already_seen:
        commentMsg = messageNewPR
    elif pull_request_updated or new_categories:
        commentMsg = messageUpdatedPR
    elif not missingApprovals:
        print "Pull request is already fully signed. Not sending message."
    else:
        print "Already notified L2 about " + str(pr.number)
    if commentMsg:
        print "The following comment will be made:"
        try:
            print commentMsg.decode("ascii", "replace")
        except:
            pass

    if commentMsg and not dryRun:
        issue.create_comment(commentMsg)

    # Check if it needs to be automatically merged.
    if all([
            "fully-signed" in labels, "tests-approved" in labels,
            "orp-approved" in labels, not "hold" in labels,
            not "new-package-pending" in labels
    ]):
        print "This pull request can be automatically merged"
        mustMerge = True
    else:
        print "This pull request will not be automatically merged."

    if mustMerge == True:
        print "This pull request must be merged."
        if not dryRun:
            try:
                pr.merge()
            except:
                pass
Example #2
0
def process_pr(repo_config,
               gh,
               repo,
               issue,
               dryRun,
               cmsbuild_user=None,
               force=False):
    if (not force) and ignore_issue(repo_config, repo, issue): return
    api_rate_limits(gh)
    prId = issue.number
    repository = repo.full_name
    repo_org, repo_name = repository.split("/", 1)
    if not cmsbuild_user: cmsbuild_user = repo_config.CMSBUILD_USER
    print "Working on ", repo.full_name, " for PR/Issue ", prId, "with admin user", cmsbuild_user
    cmssw_repo = (repo_name == GH_CMSSW_REPO)
    external_repo = len([
        e for e in EXTERNAL_REPOS + CMSDIST_REPOS
        if (repository == e) or (repo_org == e)
    ]) > 0
    official_repo = (repo_org == GH_CMSSW_ORGANIZATION)
    create_test_property = False
    packages = set([])
    create_external_issue = False
    add_external_category = False
    signing_categories = set([])
    new_package_message = ""
    mustClose = False
    releaseManagers = []
    signatures = {}
    watchers = []
    #Process Pull Request
    pkg_categories = set([])
    REGEX_EX_CMDS = "^type\s+(bug(-fix|fix|)|(new-|)feature)|urgent|backport\s+(of\s+|)(#|http(s|):/+github\.com/+%s/+pull/+)\d+$" % (
        repo.full_name)
    last_commit_date = None
    if issue.pull_request:
        pr = repo.get_pull(prId)
        if pr.changed_files == 0:
            print "Ignoring: PR with no files changed"
            return
        if cmssw_repo and official_repo and (pr.base.ref
                                             == CMSSW_DEVEL_BRANCH):
            if pr.state != "closed":
                print "This pull request must go in to master branch"
                if not dryRun:
                    edit_pr(get_token(gh), repo.full_name, prId, base="master")
                    msg = format(
                        "@%(user)s, %(dev_branch)s branch is closed for direct updates. cms-bot is going to move this PR to master branch.\n"
                        "In future, please use cmssw master branch to submit your changes.\n",
                        user=issue.user.login.encode("ascii", "ignore"),
                        dev_branch=CMSSW_DEVEL_BRANCH)
                    issue.create_comment(msg)
            return
        # A pull request is by default closed if the branch is a closed one.
        if pr.base.ref in RELEASE_BRANCH_CLOSED: mustClose = True
        # Process the changes for the given pull request so that we can determine the
        # signatures it requires.
        if cmssw_repo or not external_repo:
            if cmssw_repo and (pr.base.ref == "master"):
                signing_categories.add("code-checks")
            packages = sorted([
                x for x in set([
                    cmssw_file2Package(repo_config, f)
                    for f in get_changed_files(repo, pr)
                ])
            ])
            print "First Package: ", packages[0]
            if cmssw_repo: updateMilestone(repo, issue, pr, dryRun)
            create_test_property = True
        else:
            add_external_category = True
            packages = set(["externals/" + repository])
            if repo_name != GH_CMSDIST_REPO:
                if repo_name != "cms-bot":
                    create_external_issue = repo_config.CREATE_EXTERNAL_ISSUE
            else:
                create_test_property = True
                if not re.match(VALID_CMSDIST_BRANCHES, pr.base.ref):
                    print "Skipping PR as it does not belong to valid CMSDIST branch"
                    return

        print "Following packages affected:"
        print "\n".join(packages)
        pkg_categories = set([
            category for package in packages
            for category, category_packages in CMSSW_CATEGORIES.items()
            if package in category_packages
        ])
        signing_categories.update(pkg_categories)

        # For PR, we always require tests.
        signing_categories.add("tests")
        if add_external_category: signing_categories.add("externals")
        # We require ORP approval for releases which are in production.
        # or all externals package
        if official_repo and ((not cmssw_repo) or
                              (pr.base.ref in RELEASE_BRANCH_PRODUCTION)):
            print "This pull request requires ORP approval"
            signing_categories.add("orp")
            for l1 in CMSSW_L1:
                if not l1 in CMSSW_L2: CMSSW_L2[l1] = []
                if not "orp" in CMSSW_L2[l1]: CMSSW_L2[l1].append("orp")

        print "Following categories affected:"
        print "\n".join(signing_categories)

        if cmssw_repo:
            # If there is a new package, add also a dummy "new" category.
            all_packages = [
                package for category_packages in CMSSW_CATEGORIES.values()
                for package in category_packages
            ]
            has_category = all(
                [package in all_packages for package in packages])
            if not has_category:
                new_package_message = "\nThe following packages do not have a category, yet:\n\n"
                new_package_message += "\n".join([
                    package
                    for package in packages if not package in all_packages
                ]) + "\n"
                new_package_message += "Please create a PR for https://github.com/cms-sw/cms-bot/blob/master/categories_map.py to assign category\n"
                print new_package_message
                signing_categories.add("new-package")

        # Add watchers.yaml information to the WATCHERS dict.
        WATCHERS = read_repo_file(repo_config, "watchers.yaml", {})
        # Given the packages check if there are additional developers watching one or more.
        author = pr.user.login
        watchers = set([
            user for package in packages
            for user, watched_regexp in WATCHERS.items()
            for regexp in watched_regexp
            if re.match("^" + regexp + ".*", package) and user != author
        ])
        #Handle category watchers
        catWatchers = read_repo_file(repo_config, "category-watchers.yaml", {})
        for user, cats in catWatchers.items():
            for cat in cats:
                if cat in signing_categories:
                    print "Added ", user, " to watch due to cat", cat
                    watchers.add(user)

        # Handle watchers
        watchingGroups = read_repo_file(repo_config, "groups.yaml", {})
        for watcher in [x for x in watchers]:
            if not watcher in watchingGroups: continue
            watchers.remove(watcher)
            watchers.update(set(watchingGroups[watcher]))
        watchers = set(["@" + u for u in watchers])
        print "Watchers " + ", ".join(watchers)

        last_commit = None
        try:
            # This requires at least PyGithub 1.23.0. Making it optional for the moment.
            last_commit = pr.get_commits().reversed[0].commit
        except:
            # This seems to fail for more than 250 commits. Not sure if the
            # problem is github itself or the bindings.
            try:
                last_commit = pr.get_commits()[pr.commits - 1].commit
            except IndexError:
                print "Index error: May be PR with no commits"
                return
        last_commit_date = last_commit.committer.date
        print "Latest commit by ", last_commit.committer.name.encode(
            "ascii", "ignore"), " at ", last_commit_date
        print "Latest commit message: ", last_commit.message.encode(
            "ascii", "ignore")
        print "Latest commit sha: ", last_commit.sha
        releaseManagers = list(
            set(
                RELEASE_MANAGERS.get(pr.base.ref, []) +
                SPECIAL_RELEASE_MANAGERS))

    # Process the issue comments
    signatures = dict([(x, "pending") for x in signing_categories])
    pre_checks = ("code-checks" in signing_categories)
    already_seen = None
    pull_request_updated = False
    comparison_done = False
    comparison_notrun = False
    tests_already_queued = False
    tests_requested = False
    mustMerge = False
    external_issue_number = ""
    trigger_test_on_signature = True
    has_categories_approval = False
    cmsdist_pr = ''
    cmssw_prs = ''
    extra_wfs = ''
    assign_cats = {}
    hold = {}
    extra_labels = {}
    last_test_start_time = None
    body_firstline = issue.body.encode("ascii", "ignore").split("\n",
                                                                1)[0].strip()
    abort_test = False
    need_external = False
    trigger_code_checks = False
    triggerred_code_ckecks = False
    backport_pr_num = ""
    if (issue.user.login == cmsbuild_user) and \
       re.match(ISSUE_SEEN_MSG,body_firstline):
        already_seen = issue
        backport_pr_num = get_backported_pr(
            issue.body.encode("ascii", "ignore"))
    elif re.match(REGEX_EX_CMDS, body_firstline, re.I):
        check_extra_labels(body_firstline.lower(), extra_labels)
    all_comments = [issue]
    for c in issue.get_comments():
        all_comments.append(c)
    for comment in all_comments:
        commenter = comment.user.login
        comment_msg = comment.body.encode("ascii", "ignore")

        # The first line is an invariant.
        comment_lines = [
            l.strip() for l in comment_msg.split("\n") if l.strip()
        ]
        first_line = comment_lines[0:1]
        if not first_line: continue
        first_line = first_line[0]
        if (commenter == cmsbuild_user) and re.match(ISSUE_SEEN_MSG,
                                                     first_line):
            already_seen = comment
            backport_pr_num = get_backported_pr(comment_msg)
            if issue.pull_request and last_commit_date and (
                    comment.created_at >= last_commit_date):
                pull_request_updated = False
            if create_external_issue:
                external_issue_number = comment_msg.split(
                    "external issue " + CMSDIST_REPO_NAME + "#",
                    2)[-1].split("\n")[0]
                if not re.match("^[1-9][0-9]*$", external_issue_number):
                    print "ERROR: Unknow external issue PR format:", external_issue_number
                    external_issue_number = ""
            continue

        assign_type, new_cats = get_assign_categories(first_line)
        if new_cats:
            if (assign_type
                    == "new categories assigned:") and (commenter
                                                        == cmsbuild_user):
                for ex_cat in new_cats:
                    if ex_cat in assign_cats: assign_cats[ex_cat] = 1
            if ((commenter in CMSSW_L2)
                    or (commenter in CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
                if assign_type == "assign":
                    for ex_cat in new_cats:
                        if not ex_cat in signing_categories:
                            assign_cats[ex_cat] = 0
                            signing_categories.add(ex_cat)
                            signatures[ex_cat] = "pending"
                elif assign_type == "unassign":
                    for ex_cat in new_cats:
                        if ex_cat in assign_cats:
                            assign_cats.pop(ex_cat)
                            signing_categories.remove(ex_cat)
                            signatures.pop(ex_cat)
            continue

        # Some of the special users can say "hold" prevent automatic merging of
        # fully signed PRs.
        if re.match("^hold$", first_line, re.I):
            if commenter in CMSSW_L1 + CMSSW_L2.keys(
            ) + releaseManagers + PR_HOLD_MANAGERS:
                hold[commenter] = 1
            continue
        if re.match(REGEX_EX_CMDS, first_line, re.I):
            if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers + [
                    issue.user.login
            ]:
                check_extra_labels(first_line.lower(), extra_labels)
            continue
        if re.match("^unhold$", first_line, re.I):
            if commenter in CMSSW_L1:
                hold = {}
            elif commenter in CMSSW_L2.keys(
            ) + releaseManagers + PR_HOLD_MANAGERS:
                if hold.has_key(commenter): del hold[commenter]
            continue
        if (commenter == cmsbuild_user) and (re.match("^" + HOLD_MSG + ".+",
                                                      first_line)):
            for u in first_line.split(HOLD_MSG, 2)[1].split(","):
                u = u.strip().lstrip("@")
                if hold.has_key(u): hold[u] = 0
        if re.match("^close$", first_line, re.I):
            if (not issue.pull_request
                    and (commenter in CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
                mustClose = True
            continue

        # Ignore all other messages which are before last commit.
        if issue.pull_request and (comment.created_at < last_commit_date):
            pull_request_updated = True
            continue

        if ("code-checks" == first_line) and ("code-checks" in signatures):
            signatures["code-checks"] = "pending"
            trigger_code_checks = True
            continue

        # Check for cmsbuild_user comments and tests requests only for pull requests
        if commenter == cmsbuild_user:
            if not issue.pull_request: continue
            sec_line = comment_lines[1:2]
            if not sec_line: sec_line = ""
            else: sec_line = sec_line[0]
            if re.match("Comparison is ready", first_line):
                if ('tests'
                        in signatures) and signatures["tests"] != 'pending':
                    comparison_done = True
            elif "-code-checks" == first_line:
                signatures["code-checks"] = "rejected"
                trigger_code_checks = False
                triggerred_code_ckecks = False
            elif "+code-checks" == first_line:
                signatures["code-checks"] = "approved"
                trigger_code_checks = False
                triggerred_code_ckecks = False
            elif TRIGERING_CODE_CHECK_MSG == first_line:
                trigger_code_checks = False
                triggerred_code_ckecks = True
                signatures["code-checks"] = "pending"
            elif re.match("^Comparison not run.+", first_line):
                if ('tests'
                        in signatures) and signatures["tests"] != 'pending':
                    comparison_notrun = True
            elif re.match(FAILED_TESTS_MSG, first_line) or re.match(
                    IGNORING_TESTS_MSG, first_line):
                tests_already_queued = False
                tests_requested = False
                signatures["tests"] = "pending"
                trigger_test_on_signature = False
            elif re.match("Pull request ([^ #]+|)[#][0-9]+ was updated[.].*",
                          first_line):
                pull_request_updated = False
            elif re.match(TRIGERING_TESTS_MSG, first_line):
                tests_already_queued = True
                tests_requested = False
                signatures["tests"] = "started"
                trigger_test_on_signature = False
                last_test_start_time = comment.created_at
                abort_test = False
                need_external = False
                if sec_line.startswith("Using externals from cms-sw/cmsdist#"):
                    need_external = True
            elif re.match(TESTS_RESULTS_MSG, first_line):
                test_sha = sec_line.replace("Tested at: ", "").strip()
                if (test_sha != last_commit.sha) and (
                        test_sha != 'UNKNOWN') and (not "I had the issue "
                                                    in first_line):
                    print "Ignoring test results for sha:", test_sha
                    continue
                trigger_test_on_signature = False
                tests_already_queued = False
                tests_requested = False
                comparison_done = False
                comparison_notrun = False
                if "+1" in first_line:
                    signatures["tests"] = "approved"
                elif "-1" in first_line:
                    signatures["tests"] = "rejected"
                else:
                    signatures["tests"] = "pending"
                print 'Previous tests already finished, resetting test request state to ', signatures[
                    "tests"]
            elif re.match(TRIGERING_TESTS_ABORT_MSG, first_line):
                abort_test = False
            continue

        if issue.pull_request:
            # Check if the release manager asked for merging this.
            if (commenter in releaseManagers + CMSSW_L1) and re.match(
                    "^\s*(merge)\s*$", first_line, re.I):
                mustMerge = True
                mustClose = False
                if (commenter in CMSSW_L1) and ("orp" in signatures):
                    signatures["orp"] = "approved"
                continue

            # Check if the someone asked to trigger the tests
            if commenter in TRIGGER_PR_TESTS + CMSSW_L2.keys(
            ) + CMSSW_L1 + releaseManagers + [repo_org]:
                ok, cmsdist_pr, cmssw_prs, extra_wfs = check_test_cmd(
                    first_line)
                if ok:
                    print 'Tests requested:', commenter, 'asked to test this PR with cmsdist_pr=%s, cmssw_prs=%s and workflows=%s' % (
                        cmsdist_pr, cmssw_prs, extra_wfs)
                    trigger_test_on_signature = False
                    if tests_already_queued:
                        print "Test results not obtained in ", comment.created_at - last_test_start_time
                        diff = time.mktime(
                            comment.created_at.timetuple()) - time.mktime(
                                last_test_start_time.timetuple())
                        if diff >= TEST_WAIT_GAP:
                            print "Looks like tests are stuck, will try to re-queue"
                            tests_already_queued = False
                    if not tests_already_queued:
                        print 'cms-bot will request test for this PR'
                        tests_requested = True
                        comparison_done = False
                        comparison_notrun = False
                        if not cmssw_repo:
                            cmsdist_pr = ''
                            cmssw_prs = ''
                        signatures["tests"] = "pending"
                    else:
                        print 'Tests already request for this PR'
                    continue
                elif (REGEX_TEST_ABORT.match(first_line)
                      and ((signatures["tests"] == "started") or
                           ((signatures["tests"] != "pending") and
                            (not comparison_done)))):
                    tests_already_queued = False
                    abort_test = True
                    signatures["tests"] = "pending"

        # Check L2 signoff for users in this PR signing categories
        if commenter in CMSSW_L2 and [
                x for x in CMSSW_L2[commenter] if x in signing_categories
        ]:
            ctype = ""
            selected_cats = []
            if re.match("^([+]1|approve[d]?|sign|signed)$", first_line, re.I):
                ctype = "+1"
                selected_cats = CMSSW_L2[commenter]
            elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
                ctype = "-1"
                selected_cats = CMSSW_L2[commenter]
            elif re.match("^[+-][a-z][a-z0-9]+$", first_line, re.I):
                category_name = first_line[1:].lower()
                if category_name in CMSSW_L2[commenter]:
                    ctype = first_line[0] + "1"
                    selected_cats = [category_name]
            elif re.match("^(reopen)$", first_line, re.I):
                ctype = "reopen"
            if ctype == "+1":
                for sign in selected_cats:
                    signatures[sign] = "approved"
                    has_categories_approval = True
                    if sign == "orp": mustClose = False
            elif ctype == "-1":
                for sign in selected_cats:
                    signatures[sign] = "rejected"
                    has_categories_approval = False
                    if sign == "orp": mustClose = False
            elif ctype == "reopen":
                if "orp" in CMSSW_L2[commenter]:
                    signatures["orp"] = "pending"
                    mustClose = False
            continue

    is_hold = len(hold) > 0
    new_blocker = False
    blockers = ""
    for u in hold:
        blockers += " @" + u + ","
        if hold[u]: new_blocker = True
    blockers = blockers.rstrip(",")

    new_assign_cats = []
    for ex_cat in assign_cats:
        if assign_cats[ex_cat] == 1: continue
        new_assign_cats.append(ex_cat)

    print "All assigned cats:", ",".join(assign_cats.keys())
    print "Newly assigned cats:", ",".join(new_assign_cats)

    # Labels coming from signature.
    labels = []
    for cat in signing_categories:
        l = cat + "-pending"
        if cat in signatures: l = cat + "-" + signatures[cat]
        labels.append(l)

    if not issue.pull_request and len(signing_categories) == 0:
        labels.append("pending-assignment")
    # Additional labels.
    if is_hold: labels.append("hold")

    dryRunOrig = dryRun
    if pre_checks and ((not already_seen) or pull_request_updated):
        for cat in ["code-checks"]:
            if (cat in signatures) and (signatures[cat] != "approved"):
                dryRun = True
                break

    old_labels = set([x.name.encode("ascii", "ignore") for x in issue.labels])
    print "Stats:", backport_pr_num, extra_labels
    print "Old Labels:", sorted(old_labels)
    if "backport" in extra_labels:
        if backport_pr_num != extra_labels["backport"][1]:
            try:
                bp_pr = repo.get_pull(int(extra_labels["backport"][1]))
                backport_pr_num = extra_labels["backport"][1]
                if bp_pr.merged: extra_labels["backport"][0] = "backport-ok"
            except Exception, e:
                print "Error: Unknown PR", backport_pr_num, "\n", e
                backport_pr_num = ""
                extra_labels.pop("backport")

            if already_seen:
                if dryRun:
                    print "Update PR seen message to include backport PR number", backport_pr_num
                else:
                    new_msg = ""
                    for l in already_seen.body.encode("ascii",
                                                      "ignore").split("\n"):
                        if BACKPORT_STR in l: continue
                        new_msg += l + "\n"
                    if backport_pr_num:
                        new_msg = "%s%s%s\n" % (new_msg, BACKPORT_STR,
                                                backport_pr_num)
                    already_seen.edit(body=new_msg)
        elif ("backport-ok" in old_labels):
            extra_labels["backport"][0] = "backport-ok"
Example #3
0
def process_pr(gh, repo, issue, dryRun, cmsbuild_user="******"):
  if ignore_issue(repo, issue): return
  prId = issue.number
  repository = repo.full_name
  print "Working on ",repo.full_name," for PR/Issue ",prId
  cmssw_repo = False
  create_test_property = False
  if repository.endswith("/"+GH_CMSSW_REPO): cmssw_repo = True
  packages = set([])
  create_external_issue = False
  add_external_category = False
  signing_categories = set([])
  new_package_message = ""
  mustClose = False
  releaseManagers = []
  signatures = {}
  watchers = []
  #Process Pull Request
  if issue.pull_request:
    try:
      pr   = repo.get_pull(prId)
    except:
      print "Could not find the pull request ",prId,", may be it is an issue"
      return
    # A pull request is by default closed if the branch is a closed one.
    if pr.base.ref in RELEASE_BRANCH_CLOSED: mustClose = True
    # Process the changes for the given pull request so that we can determine the
    # signatures it requires.
    if cmssw_repo:
      packages = sorted([x for x in set(["/".join(x.filename.split("/", 2)[0:2])
                           for x in pr.get_files()])])
      updateMilestone(repo, issue, pr, dryRun)
      create_test_property = True
    else:
      add_external_category = True
      packages = set (["externals/"+repository])
      if repository != CMSDIST_REPO_NAME:
        if not repository.endswith("/cms-bot"): create_external_issue = True
      else:
        create_test_property = True
        if not re.match(VALID_CMSDIST_BRANCHES,pr.base.ref):
          print "Skipping PR as it does not belong to valid CMSDIST branch"
          return

    print "Following packages affected:"
    print "\n".join(packages)
    signing_categories = set([category for package in packages
                              for category, category_packages in CMSSW_CATEGORIES.items()
                              if package in category_packages])

    # For PR, we always require tests.
    signing_categories.add("tests")
    if add_external_category: signing_categories.add("externals")
    # We require ORP approval for releases which are in production.
    # or all externals package
    if (not cmssw_repo) or (pr.base.ref in RELEASE_BRANCH_PRODUCTION):
      print "This pull request requires ORP approval"
      signing_categories.add("orp")
      for l1 in CMSSW_L1:
        if not l1 in CMSSW_L2: CMSSW_L2[l1]=[]
        if not "orp" in CMSSW_L2[l1]: CMSSW_L2[l1].append("orp")

    print "Following categories affected:"
    print "\n".join(signing_categories)

    if cmssw_repo:
      # If there is a new package, add also a dummy "new" category.
      all_packages = [package for category_packages in CMSSW_CATEGORIES.values()
                              for package in category_packages]
      has_category = all([package in all_packages for package in packages])
      if not has_category:
        new_package_message = "\nThe following packages do not have a category, yet:\n\n"
        new_package_message += "\n".join([package for package in packages if not package in all_packages]) + "\n"
        print new_package_message
        signing_categories.add("new-package")

    # Add watchers.yaml information to the WATCHERS dict.
    WATCHERS = (yaml.load(file(join(SCRIPT_DIR, "watchers.yaml"))))
    # Given the packages check if there are additional developers watching one or more.
    author = pr.user.login
    watchers = set([user for package in packages
                         for user, watched_regexp in WATCHERS.items()
                         for regexp in watched_regexp
                         if re.match("^" + regexp + ".*", package) and user != author])
    #Handle category watchers
    for user, cats in (yaml.load(file(join(SCRIPT_DIR, "category-watchers.yaml")))).items():
      for cat in cats:
        if cat in signing_categories:
          print "Added ",user, " to watch due to cat",cat
          watchers.add(user)

    # Handle watchers
    watchingGroups = yaml.load(file(join(SCRIPT_DIR, "groups.yaml")))
    for watcher in [x for x in watchers]:
      if not watcher in watchingGroups: continue
      watchers.remove(watcher)
      watchers.update(set(watchingGroups[watcher]))      
    watchers = set(["@" + u for u in watchers])
    print "Watchers " + ", ".join(watchers)

    last_commit = None
    try:
      # This requires at least PyGithub 1.23.0. Making it optional for the moment.
      last_commit = pr.get_commits().reversed[0].commit
    except:
      # This seems to fail for more than 250 commits. Not sure if the
      # problem is github itself or the bindings.
      last_commit = pr.get_commits()[pr.commits - 1].commit
    last_commit_date = last_commit.committer.date
    print "Latest commit by ",last_commit.committer.name.encode("ascii", "ignore")," at ",last_commit_date
    print "Latest commit message: ",last_commit.message.encode("ascii", "ignore")
    print "Latest commit sha: ",last_commit.sha
    releaseManagers=list(set(RELEASE_MANAGERS.get(pr.base.ref, [])+SPECIAL_RELEASE_MANAGERS))

  # Process the issue comments
  signatures = dict([(x, "pending") for x in signing_categories])
  already_seen = False
  pull_request_updated = False
  comparison_done = False
  comparison_notrun = False
  tests_already_queued = False
  tests_requested = False
  mustMerge = False
  external_issue_number=""
  trigger_test_on_signature = True
  has_categories_approval = False
  cmsdist_pr = ''
  assign_cats = {}
  hold = {}
  extra_labels = {}
  last_test_start_time = None
  if (issue.user.login == cmsbuild_user) and \
     re.match(ISSUE_SEEN_MSG,issue.body.encode("ascii", "ignore").split("\n",1)[0].strip()):
    already_seen = True
  for comment in issue.get_comments():
    commenter = comment.user.login
    comment_msg = comment.body.encode("ascii", "ignore")

    # The first line is an invariant.
    comment_lines = [ l.strip() for l in comment_msg.split("\n") if l.strip() ]
    first_line = comment_lines[0:1]
    if not first_line: continue
    first_line = first_line[0]
    if (commenter == cmsbuild_user) and re.match(ISSUE_SEEN_MSG, first_line):
      already_seen = True
      pull_request_updated = False
      if create_external_issue:
        external_issue_number=comment_msg.split("external issue "+CMSDIST_REPO_NAME+"#",2)[-1].split("\n")[0]
        if not re.match("^[1-9][0-9]*$",external_issue_number):
          print "ERROR: Unknow external issue PR format:",external_issue_number
          external_issue_number=""
      continue

    assign_type, new_cats = get_assign_categories(first_line)
    if new_cats:
      if (assign_type == "new categories assigned:") and (commenter == cmsbuild_user):
        for ex_cat in new_cats:
          if ex_cat in assign_cats: assign_cats[ex_cat] = 1
      if ((commenter in CMSSW_L2) and [x for x in CMSSW_L2[commenter] if x in signing_categories]) or \
          (not issue.pull_request and (commenter in  CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
        if assign_type == "assign":
          for ex_cat in new_cats:
            if not ex_cat in signing_categories:
              assign_cats[ex_cat] = 0
              signing_categories.add(ex_cat)
        elif assign_type == "unassign":
          for ex_cat in new_cats:
            if ex_cat in assign_cats:
              assign_cats.pop(ex_cat)
              signing_categories.remove(ex_cat)
      continue

    # Some of the special users can say "hold" prevent automatic merging of
    # fully signed PRs.
    if re.match("^hold$", first_line, re.I):
      if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers: hold[commenter]=1
      continue
    if re.match("^type\s+(bug(-fix|)|(new-|)feature)$", first_line, re.I):
      if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers + [issue.user.login]:
        if "bug" in first_line.lower():
          extra_labels["type"]="bug-fix"
        elif "feature" in first_line.lower():
          extra_labels["type"]="new-feature"
      continue
    if re.match("^unhold$", first_line, re.I):
      if commenter in CMSSW_L1:
        hold = {}
      elif commenter in CMSSW_L2.keys() + releaseManagers:
        if hold.has_key(commenter): del hold[commenter]
        for u in hold: hold[u]=1
      continue
    if (commenter == cmsbuild_user) and (re.match("^"+HOLD_MSG+".+", first_line)):
      for u in first_line.split(HOLD_MSG,2)[1].split(","):
        u = u.strip().lstrip("@")
        if hold.has_key(u): hold[u]=0
    if re.match("^close$", first_line, re.I):
      if (not issue.pull_request and (commenter in  CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
        mustClose = True
      continue

    # Ignore all other messages which are before last commit.
    if issue.pull_request and (comment.created_at < last_commit_date):
      print "Ignoring comment done before the last commit."
      pull_request_updated = True
      continue

    # Check for cmsbuild_user comments and tests requests only for pull requests
    if commenter == cmsbuild_user:
      if not issue.pull_request: continue
      if re.match("Comparison is ready", first_line):
        comparison_done = True
        comparison_notrun = False
        trigger_test_on_signature = False
      elif re.match("^Comparison not run.+",first_line):
        comparison_notrun = True
        comparison_done = False
        trigger_test_on_signature = False
      elif re.match( FAILED_TESTS_MSG, first_line) or re.match(IGNORING_TESTS_MSG, first_line):
        tests_already_queued = False
        tests_requested = False
        signatures["tests"] = "pending"
        trigger_test_on_signature = False
      elif re.match("Pull request ([^ #]+|)[#][0-9]+ was updated[.].*", first_line):
        pull_request_updated = False
      elif re.match( TRIGERING_TESTS_MSG, first_line):
        tests_already_queued = True
        tests_requested = False
        signatures["tests"] = "started"
        trigger_test_on_signature = False
        last_test_start_time = comment.created_at
      elif re.match( TESTS_RESULTS_MSG, first_line):
        test_sha = comment_lines[1:2]
        if test_sha: test_sha = test_sha[0].replace("Tested at: ","").strip()
        if (test_sha != last_commit.sha) and (test_sha != 'UNKNOWN'):
          print "Ignoring test results for sha:",test_sha
          continue
        trigger_test_on_signature = False
        tests_already_queued = False
        tests_requested = False
        if re.match('^\s*[+]1\s*$', first_line):
          signatures["tests"] = "approved"
        else:
          signatures["tests"] = "rejected"
        print 'Previous tests already finished, resetting test request state to ',signatures["tests"]
      continue

    if issue.pull_request:
      # Check if the release manager asked for merging this.
      if (commenter in releaseManagers + CMSSW_L1) and re.match("^\s*(merge)\s*$", first_line, re.I):
        mustMerge = True
        mustClose = False
        continue

      # Check if the someone asked to trigger the tests
      if commenter in TRIGGER_PR_TESTS + CMSSW_L2.keys() + CMSSW_L1 + releaseManagers:
        m = REGEX_TEST_REQ.match(first_line)
        if m:
          print 'Tests requested:', commenter, 'asked to test this PR'
          trigger_test_on_signature = False
          cmsdist_pr = ''
          if tests_already_queued:
            print "Test results not obtained in ",comment.created_at-last_test_start_time
            diff = time.mktime(comment.created_at.timetuple()) - time.mktime(last_test_start_time.timetuple())
            if diff>=TEST_WAIT_GAP:
              print "Looks like tests are stuck, will try to re-queue"
              tests_already_queued = False
          if not tests_already_queued:
            print 'cms-bot will request test for this PR'
            tests_requested = True
            comparison_done = False
            comparison_notrun = False
            if cmssw_repo:
              cmsdist_pr = m.group(CMSDIST_PR_INDEX)
              if not cmsdist_pr: cmsdist_pr = ''
            signatures["tests"] = "pending"
          else:
            print 'Tests already request for this PR'
          continue

    # Check L2 signoff for users in this PR signing categories
    if commenter in CMSSW_L2 and [x for x in CMSSW_L2[commenter] if x in signing_categories]:
      if re.match("^([+]1|approve[d]?|sign|signed)$", first_line, re.I):
        for sign in CMSSW_L2[commenter]:
          signatures[sign] = "approved"
          has_categories_approval = True
          if sign == "orp": mustClose = False
      elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
        for sign in CMSSW_L2[commenter]:
          signatures[sign] = "rejected"
          has_categories_approval = False
          if sign == "orp": mustClose = True
      elif re.match("^(reopen)$", first_line, re.I):
        if "orp" in CMSSW_L2[commenter]:
          signatures["orp"] = "pending"
          mustClose = False
      continue

  is_hold = len(hold)>0
  new_blocker = False
  blockers = ""
  for u in hold:
    blockers += " @"+u+","
    if hold[u]: new_blocker = True
  blockers = blockers.rstrip(",")

  new_assign_cats = []
  for ex_cat in assign_cats:
    if assign_cats[ex_cat]==1: continue
    new_assign_cats.append(ex_cat)

  print "All assigned cats:",",".join(assign_cats.keys())
  print "Newly assigned cats:",",".join(new_assign_cats)

  # Labels coming from signature.
  labels = []
  for cat in signing_categories:
    l = cat+"-pending"
    if cat in signatures: l = cat+"-"+signatures[cat]
    labels.append(l)

  if not issue.pull_request and len(signing_categories)==0:
    labels.append("pending-assignment")
  # Additional labels.
  if is_hold: labels.append("hold")

  # Add additional labels
  for lab in extra_labels: labels.append(extra_labels[lab])

  if cmssw_repo and issue.pull_request:
    if comparison_done:
      labels.append("comparison-available")
    elif comparison_notrun:
      labels.append("comparison-notrun")
    else:
      labels.append("comparison-pending")

  print "The labels of the pull request should be:\n  "+"\n  ".join(labels)

  # Now updated the labels.
  missingApprovals = [x
                      for x in labels
                      if     not x.endswith("-approved")
                         and not x.startswith("orp")
                         and not x.startswith("tests")
                         and not x.startswith("pending-assignment")
                         and not x.startswith("comparison")]

  if not missingApprovals:
    print "The pull request is complete."
  if missingApprovals:
    labels.append("pending-signatures")
  elif not "pending-assignment" in labels:
    labels.append("fully-signed")
  labels = set(labels)

  # We update labels only if they are different.
  old_labels = set([x.name for x in issue.labels])

  if new_assign_cats:
    new_l2s = ["@" + name
               for name, l2_categories in CMSSW_L2.items()
               for signature in new_assign_cats
               if signature in l2_categories]
    if not dryRun: issue.create_comment("New categories assigned: "+",".join(new_assign_cats)+"\n\n"+",".join(new_l2s)+" you have been requested to review this Pull request/Issue and eventually sign? Thanks")

  #update blocker massge
  if new_blocker:
    if not dryRun: issue.create_comment(HOLD_MSG+blockers+'\nThey need to issue an `unhold` command to remove the `hold` state or L1 can `unhold` it for all')
    print "Blockers:",blockers

  labels_changed = True
  if old_labels == labels:
    labels_changed = False
    print "Labels unchanged."
  elif not dryRun:
    issue.edit(labels=list(labels))

  # Check if it needs to be automatically closed.
  if mustClose == True and issue.state == "open":
    print "This pull request must be closed."
    if not dryRun: issue.edit(state="closed")
 
  if not issue.pull_request:
    issueMessage = None
    if not already_seen:
      l2s = ", ".join([ "@" + name for name in CMSSW_ISSUES_TRACKERS ])
      issueMessage = format("%(msgPrefix)s @%(user)s"
                        " %(name)s.\n\n"
                        "%(l2s)s can you please review it and eventually sign/assign?"
                        " Thanks.\n\n"
                        "cms-bot commands are list here %(issue_url)s\n",
                        msgPrefix=NEW_ISSUE_PREFIX,
                        user=issue.user.login,
                        name=issue.user.name and "(%s)" % issue.user.name or "",
                        l2s=l2s,
                        issue_url=CMSSW_ISSUE_COMMANDS)
    elif ("fully-signed" in labels) and (not "fully-signed" in old_labels):
      issueMessage = "This issue is fully signed and ready to be closed."
    print "Issue Maeeage:",issueMessage
    if issueMessage and not dryRun: issue.create_comment(issueMessage)
    return

  # get release managers
  SUPER_USERS = (yaml.load(file(join(SCRIPT_DIR, "super-users.yaml"))))
  releaseManagersList = ", ".join(["@" + x for x in set(releaseManagers + SUPER_USERS)])

  #For now, only trigger tests for cms-sw/cmssw and cms-sw/cmsdist
  if create_test_property:
    # trigger the tests and inform it in the thread.
    if trigger_test_on_signature and has_categories_approval:
      tests_requested = True
    if tests_requested:
      test_msg = TRIGERING_TESTS_MSG
      cmsdist_issue = None
      if cmsdist_pr:
        try:
          cmsdist_repo = gh.get_repo(CMSDIST_REPO_NAME)
          cmsdist_pull = cmsdist_repo.get_pull(int(cmsdist_pr))
          cmsdist_issue = cmsdist_repo.get_issue(int(cmsdist_pr))
          test_msg = test_msg+"\nUsing externals from "+CMSDIST_REPO_NAME+"#"+cmsdist_pr
        except UnknownObjectException as e:
          print "Error getting cmsdist PR:",e.data['message']
          test_msg = IGNORING_TESTS_MSG+"\n**ERROR**: Unable to find cmsdist Pull request "+CMSDIST_REPO_NAME+"#"+cmsdist_pr
      if not dryRun:
        issue.create_comment( test_msg )
        if cmsdist_issue: cmsdist_issue.create_comment(TRIGERING_TESTS_MSG+"\nUsing cmssw from "+CMSSW_REPO_NAME+"#"+str(prId))
        if (not cmsdist_pr) or cmsdist_issue:
          create_properties_file_tests( repository, prId, cmsdist_pr, dryRun)

  # Do not complain about tests
  requiresTestMessage = " after it passes the integration tests"
  if "tests-approved" in labels:
    requiresTestMessage = " (tests are also fine)"
  elif "tests-rejected" in labels:
    requiresTestMessage = " (but tests are reportedly failing)"

  autoMergeMsg = ""
  if (("fully-signed" in labels) and ("tests-approved" in labels) and
      ((not "orp" in signatures) or (signatures["orp"] == "approved"))):
    autoMergeMsg = "This pull request will be automatically merged."
  else:
    if is_hold:
      autoMergeMsg = format("This PR is put on hold by %(blockers)s. They have"
                          " to `unhold` to remove the `hold` state or"
                          " %(managers)s will have to `merge` it by"
                          " hand.",
                          blockers=blockers,
                          managers=releaseManagersList)
    elif "new-package-pending" in labels:
      autoMergeMsg = format("This pull request requires a new package and "
                            " will not be merged. %(managers)s",
                            managers=releaseManagersList)
    elif ("orp" in signatures) and (signatures["orp"] != "approved"):
      autoMergeMsg = format("This pull request requires discussion in the"
                            " ORP meeting before it's merged. %(managers)s",
                            managers=releaseManagersList)

  devReleaseRelVal = ""
  if pr.base.ref in DEVEL_RELEASE_CYCLE:
    devReleaseRelVal = " and once validation in the development release cycle "+DEVEL_RELEASE_CYCLE[pr.base.ref]+" is complete"

  if ("fully-signed" in labels) and (not "fully-signed" in old_labels):
    messageFullySigned = format("This pull request is fully signed and it will be"
                              " integrated in one of the next %(branch)s IBs"
                              "%(requiresTest)s"
                              "%(devReleaseRelVal)s."
                              " %(autoMerge)s",
                              requiresTest=requiresTestMessage,
                              autoMerge = autoMergeMsg,
                              devReleaseRelVal=devReleaseRelVal,
                              branch=pr.base.ref)
    print "Fully signed message updated"
    if not dryRun: issue.create_comment(messageFullySigned)

  unsigned = [k for (k, v) in signatures.items() if v == "pending"]
  missing_notifications = ["@" + name
                            for name, l2_categories in CMSSW_L2.items()
                            for signature in signing_categories
                            if signature in l2_categories
                               and signature in unsigned]

  missing_notifications = set(missing_notifications)
  # Construct message for the watchers
  watchersMsg = ""
  if watchers:
    watchersMsg = format("%(watchers)s this is something you requested to"
                         " watch as well.\n",
                         watchers=", ".join(watchers))
  # Construct message for the release managers.
  managers = ", ".join(["@" + x for x in releaseManagers])

  releaseManagersMsg = ""
  if releaseManagers:
    releaseManagersMsg = format("%(managers)s you are the release manager for this.\n",
                                managers = managers)

  # Add a Warning if the pull request was done against a patch branch
  if cmssw_repo:
    warning_msg = ''
    if 'patchX' in pr.base.ref:
      print 'Must warn that this is a patch branch'
      base_release = pr.base.ref.replace( '_patchX', '' )
      base_release_branch = re.sub( '[0-9]+$', 'X', base_release )
      warning_msg = format("Note that this branch is designed for requested bug "
                         "fixes specific to the %(base_rel)s release.\nIf you "
                         "wish to make a pull request for the %(base_branch)s "
                         "release cycle, please use the %(base_branch)s branch instead\n",
                         base_rel=base_release,
                         base_branch=base_release_branch)

    # We do not want to spam people for the old pull requests.
    messageNewPR = format("%(msgPrefix)s @%(user)s"
                        " %(name)s for %(branch)s.\n\n"
                        "It involves the following packages:\n\n"
                        "%(packages)s\n\n"
                        "%(new_package_message)s\n"
                        "%(l2s)s can you please review it and eventually sign?"
                        " Thanks.\n"
                        "%(watchers)s"
                        "%(releaseManagers)s"
                        "%(patch_branch_warning)s\n"
                        "cms-bot commands are list here %(issue_url)s\n",
                        msgPrefix=NEW_PR_PREFIX,
                        user=pr.user.login,
                        name=pr.user.name and "(%s)" % pr.user.name or "",
                        branch=pr.base.ref,
                        l2s=", ".join(missing_notifications),
                        packages="\n".join(packages),
                        new_package_message=new_package_message,
                        watchers=watchersMsg,
                        releaseManagers=releaseManagersMsg,
                        patch_branch_warning=warning_msg,
                        issue_url=CMSSW_PULL_REQUEST_COMMANDS)

    messageUpdatedPR = format("Pull request #%(pr)s was updated."
                            " %(signers)s can you please check and sign again.",
                            pr=pr.number,
                            signers=", ".join(missing_notifications))
  else:
    if create_external_issue:
      if not already_seen:
        if dryRun:
          print "Should create a new issue in ",CMSDIST_REPO_NAME," for this PR"
        else:
          external_managers = ["@" + name for name, l2_categories in CMSSW_L2.items() if "externals" in l2_categories]
          cmsdist_repo  = gh.get_repo(CMSDIST_REPO_NAME)
          cmsdist_title = format("%(repo)s#%(pr)s: %(title)s",
                                 title=pr.title.encode("ascii", "ignore"),
                                 repo=repository,
                                 pr=pr.number)
          cmsdist_body = format("%(msgPrefix)s @%(user)s"
                                " %(name)s for branch %(branch)s.\n\n"
                                "Pull Request Reference: %(repo)s#%(pr)s\n\n"
                                "%(externals_l2s)s can you please review it and eventually sign? Thanks.\n",
                                msgPrefix=NEW_PR_PREFIX,
                                repo=repository,
                                user=pr.user.login,
                                name=pr.user.name and "(%s)" % pr.user.name or "",
                                branch=pr.base.ref,
                                externals_l2s=", ".join(external_managers),
                                pr=pr.number)
          cissue = cmsdist_repo.create_issue(cmsdist_title, cmsdist_body)
          external_issue_number = str(cissue.number)
          print "Created a new issue ",CMSDIST_REPO_NAME,"#",external_issue_number
      if pull_request_updated and external_issue_number:
        if dryRun:
          print "Should add an update message for issue ",CMSDIST_REPO_NAME,"#",external_issue_number
        else:
          cmsdist_repo  = gh.get_repo(CMSDIST_REPO_NAME)
          cissue = cmsdist_repo.get_issue(int(external_issue_number))
          cmsdist_body = format("Pull request %(repo)s#%(pr)s was updated.\n"
                                "Latest update by %(name)s with commit message\n%(message)s",
                                repo=repository,
                                pr=pr.number,
                                name=last_commit.committer.name.encode("ascii", "ignore"),
                                message=last_commit.message.encode("ascii", "ignore"))
          cissue.create_comment(cmsdist_body)
    cmsdist_issue = ""
    if external_issue_number:
      cmsdist_issue="\n\nexternal issue "+CMSDIST_REPO_NAME+"#"+external_issue_number

    messageNewPR = format("%(msgPrefix)s @%(user)s"
                          " %(name)s for branch %(branch)s.\n\n"
                          "%(l2s)s can you please review it and eventually sign?"
                          " Thanks.\n"
                          "%(watchers)s"
                          "You can sign-off by replying to this message having"
                          " '+1' in the first line of your reply.\n"
                          "You can reject by replying  to this message having"
                          " '-1' in the first line of your reply."
                          "%(cmsdist_issue)s",
                          msgPrefix=NEW_PR_PREFIX,
                          user=pr.user.login,
                          name=pr.user.name and "(%s)" % pr.user.name or "",
                          branch=pr.base.ref,
                          title=pr.title.encode("ascii", "ignore"),
                          l2s=", ".join(missing_notifications),
                          watchers=watchersMsg,
                          cmsdist_issue=cmsdist_issue)

    messageUpdatedPR = format("Pull request #%(pr)s was updated."
                              "%(cmsdist_issue)s",
                              pr=pr.number,
                              cmsdist_issue=cmsdist_issue)

  # Finally decide whether or not we should close the pull request:
  messageBranchClosed = format("This branch is closed for updates."
                               " Closing this pull request.\n"
                               " Please bring this up in the ORP"
                               " meeting if really needed.\n")

  commentMsg = ""
  if pr.base.ref in RELEASE_BRANCH_CLOSED:
    commentMsg = messageBranchClosed
  elif not already_seen:
    commentMsg = messageNewPR
  elif pull_request_updated:
    commentMsg = messageUpdatedPR
  elif not missingApprovals:
    print "Pull request is already fully signed. Not sending message."
  else:
    print "Already notified L2 about " + str(pr.number)
  if commentMsg:
    print "The following comment will be made:"
    try:
      print commentMsg.decode("ascii", "replace")
    except:
      pass

  if commentMsg and not dryRun:
    issue.create_comment(commentMsg)

  # Check if it needs to be automatically merged.
  if all(["fully-signed" in labels,
          "tests-approved" in labels,
          "orp-approved" in labels,
          not "hold" in labels,
          not "new-package-pending" in labels]):
    print "This pull request can be automatically merged"
    mustMerge = True
  else:
    print "This pull request will not be automatically merged."

  if mustMerge == True:
    print "This pull request must be merged."
    if not dryRun:
        try:
          pr.merge()
        except:
          pass
Example #4
0
def process_pr(repo_config, gh, repo, issue, dryRun, cmsbuild_user=None, force=False):
  if (not force) and ignore_issue(repo_config, repo, issue): return
  api_rate_limits(gh)
  prId = issue.number
  repository = repo.full_name
  repo_org, repo_name = repository.split("/",1)
  if not cmsbuild_user: cmsbuild_user=repo_config.CMSBUILD_USER
  print "Working on ",repo.full_name," for PR/Issue ",prId,"with admin user",cmsbuild_user
  cmssw_repo = (repo_name==GH_CMSSW_REPO)
  official_repo = (repo_org==GH_CMSSW_ORGANIZATION)
  external_repo = (repository!=CMSSW_REPO_NAME) and (len([e for e in EXTERNAL_REPOS if repo_org==e])>0)
  create_test_property = False
  packages = set([])
  create_external_issue = False
  add_external_category = False
  signing_categories = set([])
  new_package_message = ""
  mustClose = False
  releaseManagers = []
  signatures = {}
  watchers = []
  #Process Pull Request
  pkg_categories = set([])
  REGEX_EX_CMDS="^type\s+(bug(-fix|fix|)|(new-|)feature)|urgent|backport\s+(of\s+|)(#|http(s|):/+github\.com/+%s/+pull/+)\d+$" % (repo.full_name)
  known_ignore_tests='build-warnings|clang-warnings|none'
  REGEX_EX_IGNORE_CHKS='^ignore\s+(%s)(\s*,\s*(%s))*$' % (known_ignore_tests, known_ignore_tests)
  last_commit_date = None
  push_test_issue = False
  requestor = issue.user.login.encode("ascii", "ignore")
  ignore_tests = []
  if issue.pull_request:
    pr   = repo.get_pull(prId)
    if pr.changed_files==0:
      print "Ignoring: PR with no files changed"
      return
    if cmssw_repo and official_repo and (pr.base.ref == CMSSW_DEVEL_BRANCH):
      if pr.state != "closed":
        print "This pull request must go in to master branch"
        if not dryRun:
          edit_pr(get_token(gh), repo.full_name, prId, base="master")
          msg = format("@%(user)s, %(dev_branch)s branch is closed for direct updates. cms-bot is going to move this PR to master branch.\n"
                       "In future, please use cmssw master branch to submit your changes.\n",
                       user=requestor,
                       dev_branch=CMSSW_DEVEL_BRANCH)
          issue.create_comment(msg)
      return
    # A pull request is by default closed if the branch is a closed one.
    if pr.base.ref in RELEASE_BRANCH_CLOSED: mustClose = True
    # Process the changes for the given pull request so that we can determine the
    # signatures it requires.
    if cmssw_repo or not external_repo:
      if cmssw_repo and (pr.base.ref=="master"): signing_categories.add("code-checks")
      packages = sorted([x for x in set([cmssw_file2Package(repo_config, f)
                           for f in get_changed_files(repo, pr)])])
      print "First Package: ",packages[0]
      if cmssw_repo: updateMilestone(repo, issue, pr, dryRun)
      create_test_property = True
    else:
      add_external_category = True
      packages = set (["externals/"+repository])
      if repo_name != GH_CMSDIST_REPO:
        if not repo_name in ["cms-bot", "cmssdt-web"]: create_external_issue = repo_config.CREATE_EXTERNAL_ISSUE
      else:
        create_test_property = True
        if not re.match(VALID_CMSDIST_BRANCHES,pr.base.ref):
          print "Skipping PR as it does not belong to valid CMSDIST branch"
          return

    print "Following packages affected:"
    print "\n".join(packages)
    pkg_categories = set([category for package in packages
                              for category, category_packages in CMSSW_CATEGORIES.items()
                              if package in category_packages])
    signing_categories.update(pkg_categories)

    # For PR, we always require tests.
    signing_categories.add("tests")
    if add_external_category: signing_categories.add("externals")
    # We require ORP approval for releases which are in production.
    # or all externals package
    if official_repo and ((not cmssw_repo) or (pr.base.ref in RELEASE_BRANCH_PRODUCTION)):
      print "This pull request requires ORP approval"
      signing_categories.add("orp")
      for l1 in CMSSW_L1:
        if not l1 in CMSSW_L2: CMSSW_L2[l1]=[]
        if not "orp" in CMSSW_L2[l1]: CMSSW_L2[l1].append("orp")

    print "Following categories affected:"
    print "\n".join(signing_categories)

    if cmssw_repo:
      # If there is a new package, add also a dummy "new" category.
      all_packages = [package for category_packages in CMSSW_CATEGORIES.values()
                              for package in category_packages]
      has_category = all([package in all_packages for package in packages])
      if not has_category:
        new_package_message = "\nThe following packages do not have a category, yet:\n\n"
        new_package_message += "\n".join([package for package in packages if not package in all_packages]) + "\n"
        new_package_message += "Please create a PR for https://github.com/cms-sw/cms-bot/blob/master/categories_map.py to assign category\n"
        print new_package_message
        signing_categories.add("new-package")

    # Add watchers.yaml information to the WATCHERS dict.
    WATCHERS = read_repo_file(repo_config, "watchers.yaml", {})
    # Given the packages check if there are additional developers watching one or more.
    author = pr.user.login
    watchers = set([user for package in packages
                         for user, watched_regexp in WATCHERS.items()
                         for regexp in watched_regexp
                         if re.match("^" + regexp + ".*", package) and user != author])
    #Handle category watchers
    catWatchers = read_repo_file(repo_config, "category-watchers.yaml", {})
    for user, cats in catWatchers.items():
      for cat in cats:
        if cat in signing_categories:
          print "Added ",user, " to watch due to cat",cat
          watchers.add(user)

    # Handle watchers
    watchingGroups = read_repo_file(repo_config, "groups.yaml", {})
    for watcher in [x for x in watchers]:
      if not watcher in watchingGroups: continue
      watchers.remove(watcher)
      watchers.update(set(watchingGroups[watcher]))      
    watchers = set(["@" + u for u in watchers])
    print "Watchers " + ", ".join(watchers)

    last_commit = None
    try:
      # This requires at least PyGithub 1.23.0. Making it optional for the moment.
      last_commit = pr.get_commits().reversed[0].commit
    except:
      # This seems to fail for more than 250 commits. Not sure if the
      # problem is github itself or the bindings.
      try:
        last_commit = pr.get_commits()[pr.commits - 1].commit
      except IndexError:
        print "Index error: May be PR with no commits"
        return
    last_commit_date = last_commit.committer.date
    print "Latest commit by ",last_commit.committer.name.encode("ascii", "ignore")," at ",last_commit_date
    print "Latest commit message: ",last_commit.message.encode("ascii", "ignore")
    print "Latest commit sha: ",last_commit.sha
    extra_rm = RELEASE_MANAGERS.get(pr.base.ref, [])
    if repository==CMSDIST_REPO_NAME:
      br = "_".join(pr.base.ref.split("/")[:2][-1].split("_")[:3])+"_X"
      if br: extra_rm=extra_rm+RELEASE_MANAGERS.get(br, [])
    releaseManagers=list(set(extra_rm+SPECIAL_RELEASE_MANAGERS))
  else:
    try:
      if (repo_config.OPEN_ISSUE_FOR_PUSH_TESTS) and (requestor == cmsbuild_user) and re.match(PUSH_TEST_ISSUE_MSG,issue.title):
        signing_categories.add("tests")
        push_test_issue = True
    except: pass

  # Process the issue comments
  signatures = dict([(x, "pending") for x in signing_categories])
  pre_checks = ("code-checks" in signing_categories)
  already_seen = None
  pull_request_updated = False
  comparison_done = False
  comparison_notrun = False
  tests_already_queued = False
  tests_requested = False
  mustMerge = False
  external_issue_number=""
  trigger_test_on_signature = True
  has_categories_approval = False
  cmsdist_pr = ''
  cmssw_prs = ''
  extra_wfs = ''
  assign_cats = {}
  hold = {}
  extra_labels = {}
  last_test_start_time = None
  body_firstline = issue.body.encode("ascii", "ignore").split("\n",1)[0].strip() if issue.body else ""
  abort_test = False
  need_external = False
  trigger_code_checks=False
  triggerred_code_checks=False
  backport_pr_num = ""
  comp_warnings = False
  if (requestor == cmsbuild_user) and \
     re.match(ISSUE_SEEN_MSG,body_firstline):
    already_seen = issue
    backport_pr_num = get_backported_pr(issue.body.encode("ascii", "ignore")) if issue.body else ""
  elif re.match(REGEX_EX_CMDS, body_firstline, re.I):
    check_extra_labels(body_firstline.lower(), extra_labels)
  all_comments = [issue]
  for c in issue.get_comments(): all_comments.append(c)
  for comment in all_comments:
    commenter = comment.user.login.encode("ascii", "ignore")
    valid_commenter = commenter in TRIGGER_PR_TESTS + CMSSW_L2.keys() + CMSSW_L1 + releaseManagers + [repo_org]
    if (not valid_commenter) and (requestor!=commenter): continue
    comment_msg = comment.body.encode("ascii", "ignore") if comment.body else ""
    if (commenter in COMMENT_CONVERSION) and (comment.created_at<=COMMENT_CONVERSION[commenter]['comments_before']):
      orig_msg = comment_msg
      for cmt in COMMENT_CONVERSION[commenter]['comments']:
        comment_msg = comment_msg.replace(cmt[0],cmt[1])
      if (orig_msg != comment_msg): print "==>Updated Comment:",commenter,comment.created_at,"\n",comment_msg
    # The first line is an invariant.
    comment_lines = [ l.strip() for l in comment_msg.split("\n") if l.strip() ]
    first_line = comment_lines[0:1]
    if not first_line: continue
    first_line = first_line[0]
    if (commenter == cmsbuild_user) and re.match(ISSUE_SEEN_MSG, first_line):
      already_seen = comment
      backport_pr_num = get_backported_pr(comment_msg)
      if issue.pull_request and last_commit_date and (comment.created_at >= last_commit_date): pull_request_updated = False
      if create_external_issue:
        external_issue_number=comment_msg.split("external issue "+CMSDIST_REPO_NAME+"#",2)[-1].split("\n")[0]
        if not re.match("^[1-9][0-9]*$",external_issue_number):
          print "ERROR: Unknow external issue PR format:",external_issue_number
          external_issue_number=""
      continue

    assign_type, new_cats = get_assign_categories(first_line)
    if new_cats:
      if (assign_type == "new categories assigned:") and (commenter == cmsbuild_user):
        for ex_cat in new_cats:
          if ex_cat in assign_cats: assign_cats[ex_cat] = 1
      if ((commenter in CMSSW_L2) or (commenter in  CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
        if assign_type == "assign":
          for ex_cat in new_cats:
            if not ex_cat in signing_categories:
              assign_cats[ex_cat] = 0
              signing_categories.add(ex_cat)
              signatures[ex_cat]="pending"
        elif assign_type == "unassign":
          for ex_cat in new_cats:
            if ex_cat in assign_cats:
              assign_cats.pop(ex_cat)
              signing_categories.remove(ex_cat)
              signatures.pop(ex_cat)
      continue

    # Some of the special users can say "hold" prevent automatic merging of
    # fully signed PRs.
    if re.match("^hold$", first_line, re.I):
      if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers + PR_HOLD_MANAGERS: hold[commenter]=1
      continue
    if re.match(REGEX_EX_CMDS, first_line, re.I):
      if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers + [requestor]:
        check_extra_labels(first_line.lower(), extra_labels)
      continue
    if re.match(REGEX_EX_IGNORE_CHKS, first_line, re.I):
      if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers:
        ignore_tests = check_ignore_test (first_line.upper())
        if 'NONE' in ignore_tests: ignore_tests=[]
      continue
    if re.match("^unhold$", first_line, re.I):
      if commenter in CMSSW_L1:
        hold = {}
      elif commenter in CMSSW_L2.keys() + releaseManagers + PR_HOLD_MANAGERS:
        if hold.has_key(commenter): del hold[commenter]
      continue
    if (commenter == cmsbuild_user) and (re.match("^"+HOLD_MSG+".+", first_line)):
      for u in first_line.split(HOLD_MSG,2)[1].split(","):
        u = u.strip().lstrip("@")
        if hold.has_key(u): hold[u]=0
    if re.match("^close$", first_line, re.I):
      if (not issue.pull_request and (commenter in  CMSSW_ISSUES_TRACKERS + CMSSW_L1)):
        mustClose = True
      continue

    # Ignore all other messages which are before last commit.
    if issue.pull_request and (comment.created_at < last_commit_date):
      pull_request_updated = True
      continue

    if ("code-checks"==first_line) and ("code-checks" in signatures):
      signatures["code-checks"] = "pending"
      trigger_code_checks=True
      triggerred_code_checks=False
      continue

    # Check for cmsbuild_user comments and tests requests only for pull requests
    if commenter == cmsbuild_user:
      if not issue.pull_request and not push_test_issue: continue
      sec_line = comment_lines[1:2]
      if not sec_line: sec_line = ""
      else: sec_line = sec_line[0]
      if re.match("Comparison is ready", first_line):
        if ('tests' in signatures) and signatures["tests"]!='pending': comparison_done = True
      elif "-code-checks" == first_line:
        signatures["code-checks"] = "rejected"
        trigger_code_checks=False
        triggerred_code_checks=False
      elif "+code-checks" == first_line:
        signatures["code-checks"] = "approved"
        trigger_code_checks=False
        triggerred_code_checks=False
      elif TRIGERING_CODE_CHECK_MSG == first_line:
        trigger_code_checks=False
        triggerred_code_checks=True
        signatures["code-checks"] = "pending"
      elif re.match("^Comparison not run.+",first_line):
        if ('tests' in signatures) and signatures["tests"]!='pending': comparison_notrun = True
      elif re.match( FAILED_TESTS_MSG, first_line) or re.match(IGNORING_TESTS_MSG, first_line):
        tests_already_queued = False
        tests_requested = False
        signatures["tests"] = "pending"
        trigger_test_on_signature = False
      elif re.match("Pull request ([^ #]+|)[#][0-9]+ was updated[.].*", first_line):
        pull_request_updated = False
      elif re.match( TRIGERING_TESTS_MSG, first_line) or re.match( TRIGERING_TESTS_MSG1, first_line):
        tests_already_queued = True
        tests_requested = False
        signatures["tests"] = "started"
        trigger_test_on_signature = False
        last_test_start_time = comment.created_at
        abort_test = False
        need_external = False
        if sec_line.startswith("Using externals from cms-sw/cmsdist#"): need_external = True
      elif re.match( TESTS_RESULTS_MSG, first_line):
        test_sha = sec_line.replace("Tested at: ","").strip()
        if (not push_test_issue) and (test_sha != last_commit.sha) and (test_sha != 'UNKNOWN') and (not "I had the issue " in first_line):
          print "Ignoring test results for sha:",test_sha
          continue
        trigger_test_on_signature = False
        tests_already_queued = False
        tests_requested = False
        comparison_done = False
        comparison_notrun = False
        comp_warnings = False
        if "+1" in first_line:
          signatures["tests"] = "approved"
          comp_warnings = len([1 for l in comment_lines if 'Compilation Warnings: Yes' in l ])>0
        elif "-1" in first_line:
          signatures["tests"] = "rejected"
        else:
          signatures["tests"] = "pending"
        print 'Previous tests already finished, resetting test request state to ',signatures["tests"]
      elif re.match( TRIGERING_TESTS_ABORT_MSG, first_line):
        abort_test = False
      continue

    if issue.pull_request or push_test_issue:
      # Check if the release manager asked for merging this.
      if (commenter in releaseManagers + CMSSW_L1) and re.match("^\s*(merge)\s*$", first_line, re.I):
        mustMerge = True
        mustClose = False
        if (commenter in CMSSW_L1) and ("orp" in signatures): signatures["orp"] = "approved"
        continue

      # Check if the someone asked to trigger the tests
      if valid_commenter:
        ok, cmsdist_pr, cmssw_prs, extra_wfs = check_test_cmd(first_line)
        if ok:
          print 'Tests requested:', commenter, 'asked to test this PR with cmsdist_pr=%s, cmssw_prs=%s and workflows=%s' % (cmsdist_pr, cmssw_prs, extra_wfs)
          print "Comment message:",first_line
          trigger_test_on_signature = False
          if tests_already_queued:
            print "Test results not obtained in ",comment.created_at-last_test_start_time
            diff = time.mktime(comment.created_at.timetuple()) - time.mktime(last_test_start_time.timetuple())
            if diff>=TEST_WAIT_GAP:
              print "Looks like tests are stuck, will try to re-queue"
              tests_already_queued = False
          if not tests_already_queued:
            print 'cms-bot will request test for this PR'
            tests_requested = True
            comparison_done = False
            comparison_notrun = False
            if not cmssw_repo:
              cmsdist_pr = ''
              cmssw_prs = ''
            signatures["tests"] = "pending"
          else:
            print 'Tests already request for this PR'
          continue
        elif (REGEX_TEST_ABORT.match(first_line) and 
              ((signatures["tests"] == "started") or 
               ((signatures["tests"] != "pending") and (not comparison_done) and (not push_test_issue)))):
          tests_already_queued = False
          abort_test = True
          signatures["tests"] = "pending"

    # Check L2 signoff for users in this PR signing categories
    if commenter in CMSSW_L2 and [x for x in CMSSW_L2[commenter] if x in signing_categories]:
      ctype = ""
      selected_cats = []
      if re.match("^([+]1|approve[d]?|sign|signed)$", first_line, re.I):
        ctype = "+1"
        selected_cats = CMSSW_L2[commenter]
      elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
        ctype = "-1"
        selected_cats = CMSSW_L2[commenter]
      elif re.match("^[+-][a-z][a-z0-9]+$", first_line, re.I):
        category_name = first_line[1:].lower()
        if category_name in CMSSW_L2[commenter]:
          ctype = first_line[0]+"1"
          selected_cats = [ category_name ]
      elif re.match("^(reopen)$", first_line, re.I):
        ctype = "reopen"
      if ctype == "+1":
        for sign in selected_cats:
          signatures[sign] = "approved"
          has_categories_approval = True
          if sign == "orp": mustClose = False
      elif ctype == "-1":
        for sign in selected_cats:
          signatures[sign] = "rejected"
          has_categories_approval = False
          if sign == "orp": mustClose = False
      elif ctype == "reopen":
        if "orp" in CMSSW_L2[commenter]:
          signatures["orp"] = "pending"
          mustClose = False
      continue

  if push_test_issue:
    auto_close_push_test_issue = True
    try: auto_close_push_test_issue=repo_config.AUTO_CLOSE_PUSH_TESTS_ISSUE
    except: pass
    if auto_close_push_test_issue and (issue.state == "open") and ('tests' in signatures) and ((signatures["tests"] in ["approved","rejected"]) or abort_test):
      print "Closing the issue as it has been tested/aborted"
      if not dryRun: issue.edit(state="closed")
    if abort_test:
      job, bnum = get_jenkins_job(issue)
      if job and bnum:
        params = {}
        params["JENKINS_PROJECT_TO_KILL"]=job
        params["JENKINS_BUILD_NUMBER"]=bnum
        create_property_file("trigger-abort-%s" % job, params, dryRun)
    return

  is_hold = len(hold)>0
  new_blocker = False
  blockers = ""
  for u in hold:
    blockers += " @"+u+","
    if hold[u]: new_blocker = True
  blockers = blockers.rstrip(",")

  new_assign_cats = []
  for ex_cat in assign_cats:
    if assign_cats[ex_cat]==1: continue
    new_assign_cats.append(ex_cat)

  print "All assigned cats:",",".join(assign_cats.keys())
  print "Newly assigned cats:",",".join(new_assign_cats)
  print "Ignore tests:",ignore_tests

  # Labels coming from signature.
  labels = []
  for cat in signing_categories:
    l = cat+"-pending"
    if cat in signatures: l = cat+"-"+signatures[cat]
    labels.append(l)

  if not issue.pull_request and len(signing_categories)==0:
    labels.append("pending-assignment")
  # Additional labels.
  if is_hold: labels.append("hold")

  dryRunOrig = dryRun
  if pre_checks and ((not already_seen) or pull_request_updated):
    for cat in ["code-checks"]:
      if (cat in signatures) and (signatures[cat]!="approved"):
        dryRun=True
        break

  old_labels = set([x.name.encode("ascii", "ignore") for x in issue.labels])
  print "Stats:",backport_pr_num,extra_labels
  print "Old Labels:",sorted(old_labels)
  print "Compilation Warnings: ",comp_warnings
  if "backport" in extra_labels:
    if backport_pr_num!=extra_labels["backport"][1]:
      try:
        bp_pr = repo.get_pull(int(extra_labels["backport"][1]))
        backport_pr_num=extra_labels["backport"][1]
        if bp_pr.merged: extra_labels["backport"][0]="backport-ok"
      except Exception, e :
        print "Error: Unknown PR", backport_pr_num,"\n",e
        backport_pr_num=""
        extra_labels.pop("backport")

      if already_seen:
        if dryRun: print "Update PR seen message to include backport PR number",backport_pr_num
        else:
          new_msg = ""
          for l in already_seen.body.encode("ascii", "ignore").split("\n"):
            if BACKPORT_STR in l: continue
            new_msg += l+"\n"
          if backport_pr_num: new_msg="%s%s%s\n" % (new_msg, BACKPORT_STR, backport_pr_num)
          already_seen.edit(body=new_msg)
Example #5
0
def process_pr(gh, repo, prId, repository, dryRun):
  print "Working on ",repo.full_name," for PR ",prId
  external_issue_repo_name = "cms-sw/cmsdist"
  cmssw_repo = False
  if repository == "cms-sw/cmssw": cmssw_repo = True
  try:
    pr   = repo.get_pull(prId)
  except:
    print "Could not find the pull request ",prId,", may be it is an issue"
    return
  # Process the changes for the given pull request so that we can determine the
  # signatures it requires.
  create_external_issue = False
  add_external_category = False
  if cmssw_repo:
    packages = sorted([x for x in set(["/".join(x.filename.split("/", 2)[0:2])
                         for x in pr.get_files()])])

  else:
    add_external_category = True
    packages = set (["externals/"+repository])
    if repository != external_issue_repo_name: create_external_issue = True

  print "Following packages affected:"
  print "\n".join(packages)
  signing_categories = set([category for package in packages
                            for category, category_packages in CMSSW_CATEGORIES.items()
                            if package in category_packages])

  # We always require tests.
  signing_categories.add("tests")
  if add_external_category:
    signing_categories.add("externals")
  # We require ORP approval for releases which are in production.
  # or all externals package
  if (not cmssw_repo) or (pr.base.ref in RELEASE_BRANCH_PRODUCTION):
    print "This pull request requires ORP approval"
    signing_categories.add("orp")

  print "Following categories affected:"
  print "\n".join(signing_categories)

  if cmssw_repo:
    # If there is a new package, add also a dummy "new" category.
    all_packages = [package for category_packages in CMSSW_CATEGORIES.values()
                            for package in category_packages]
    has_category = all([package in all_packages for package in packages])
    new_package_message = ""
    if not has_category:
      new_package_message = "\nThe following packages do not have a category, yet:\n\n"
      new_package_message += "\n".join([package for package in packages if not package in all_packages]) + "\n"
      signing_categories.add("new-package")

  # Add watchers.yaml information to the WATCHERS dict.
  WATCHERS = (yaml.load(file("watchers.yaml")))
  # Given the packages check if there are additional developers watching one or more.
  author = pr.user.login
  watchers = set([user for package in packages
                       for user, watched_regexp in WATCHERS.items()
                       for regexp in watched_regexp
                       if re.match("^" + regexp + ".*", package) and user != author])
  # Handle watchers
  watchingGroups = yaml.load(file("groups.yaml"))
  for watcher in [x for x in watchers]:
    if not watcher in watchingGroups:
      continue
    watchers.remove(watcher)
    watchers.update(set(watchingGroups[watcher]))
  watchers = set(["@" + u for u in watchers])
  print "Watchers " + ", ".join(watchers)

  issue = repo.get_issue(prId)
  updateMilestone(repo, issue, pr, dryRun)

  # Process the issue comments
  signatures = dict([(x, "pending") for x in signing_categories])
  last_commit = None
  try:
    # This requires at least PyGithub 1.23.0. Making it optional for the moment.
    last_commit = pr.get_commits().reversed[0].commit
  except:
    # This seems to fail for more than 250 commits. Not sure if the
    # problem is github itself or the bindings.
    last_commit = pr.get_commits()[pr.commits - 1].commit
  last_commit_date = last_commit.committer.date
  print "Latest commit by ",last_commit.committer.name," at ",last_commit_date
  print "Latest commit message: ",last_commit.message
  is_hold = False
  already_seen = False
  pull_request_updated = False
  comparison_done = False
  tests_already_queued = False
  tests_requested = False
  # A pull request is by default closed if the branch is a closed one.
  mustClose = False
  mustMerge = False
  if pr.base.ref in RELEASE_BRANCH_CLOSED:
    mustClose = True
  requiresL1 = False
  releaseManagers=RELEASE_MANAGERS.get(pr.base.ref, [])
  external_issue_number=""
  for comment in issue.get_comments():
    comment_date = comment.created_at
    commenter = comment.user.login
    # Check special cmsbuild messages:
    # - Check we did not announce the pull request already
    # - Check we did not announce changes already
    comment_msg = comment.body.encode("ascii", "ignore")
    if commenter == "cmsbuild":
      if re.match("A new Pull Request was created by", comment_msg):
        already_seen = True
        pull_request_updated = False
        if create_external_issue:
          external_issue_number=comment_msg.split("external issue "+external_issue_repo_name+"#",2)[-1].split("\n")[0]
          if not re.match("^[1-9][0-9]*$",external_issue_number):
            print "ERROR: Unknow external issue PR format:",external_issue_number
            external_issue_number=""

    # Ignore all other messages which are before last commit.
    if comment_date < last_commit_date:
      print "Ignoring comment done before the last commit."
      pull_request_updated = True
      continue

    # The first line is an invariant.
    first_line = ""
    for l in comment_msg.split("\n"):
      if re.match("^[\n\t\r ]*$",l): continue
      first_line = l.strip("\n\t\r ")
      break

    # Check for cmsbuild comments
    if commenter == "cmsbuild":
      if re.match("Comparison is ready", first_line):
        comparison_done = True
      elif re.match( FAILED_TESTS_MSG, first_line):
        tests_already_queued = False
        tests_requested = False
        signatures["tests"] = "pending"
      elif re.match("Pull request ([^ #]+|)[#][0-9]+ was updated[.].*", first_line):
        pull_request_updated = False
      elif re.match( TRIGERING_TESTS_MSG, first_line):
        tests_already_queued = True
        tests_requested = False
        signatures["tests"] = "started"
      elif re.match( TESTS_RESULTS_MSG, first_line):
        tests_already_queued = False
        tests_requested = False
        if re.match('^\s*[+]1\s*$', first_line):
          signatures["tests"] = "approved"
        else:
          signatures["tests"] = "rejected"
        print 'Previous tests already finished, resetting test request state to ',signatures["tests"]
      continue

    # Check if the someone asked to trigger the tests
    if (commenter in TRIGGER_PR_TESTS
        or commenter in releaseManagers
        or commenter in CMSSW_L2.keys()):
      if re.match("^\s*(@cmsbuild\s*[,]*\s+|)(please\s*[,]*\s+|)test\s*$", first_line, re.I):
        print 'Tests requested:', commenter, 'asked to test this PR'
        if not tests_already_queued:
          print 'cms-bot will request test for this PR'
          tests_requested = True
          comparison_done = False
          signatures["tests"] = "pending"
        else:
          print 'Tests already request for this PR'
        continue

    # Check actions made by L1.
    # L1 signatures are only relevant for closed releases where
    # we have a orp signature requested.
    # Approving a pull request, sign it.
    # Rejecting a pull request, will also close it.
    # Use "reopen" to open a closed pull request.
    if commenter in CMSSW_L1:
      requiresL1 = True
      if not "orp" in signing_categories:
        requiresL1 = False
      elif re.match("^([+]1|approve[d]?)$", first_line, re.I):
        signatures["orp"] = "approved"
        mustClose = False
      elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
        signatures["orp"] = "rejected"
        mustClose = True
      elif re.match("reopen", first_line, re.I):
        signatures["orp"] = "pending"
        mustClose = False

    # Check if the release manager asked for merging this.
    if commenter in releaseManagers:
      if re.match("merge", first_line, re.I):
        mustMerge = True

    # Check L2 signoff for users in this PR signing categories
    if commenter in CMSSW_L2 and [x for x in CMSSW_L2[commenter] if x in signing_categories]:
      if re.match("^([+]1|approve[d]?|sign|signed)$", first_line, re.I):
        for sign in CMSSW_L2[commenter]:
          signatures[sign] = "approved"
      elif re.match("^([-]1|reject|rejected)$", first_line, re.I):
        for sign in CMSSW_L2[commenter]:
          signatures[sign] = "rejected"

    # Some of the special users can say "hold" prevent automatic merging of
    # fully signed PRs.
    if commenter in CMSSW_L1 + CMSSW_L2.keys() + releaseManagers:
      if re.match("^hold$", first_line, re.I):
        is_hold = True
        blocker = commenter

    # Check for release managers and and sign the tests category based on
    # their comment
    #+tested for approved
    #-tested for rejected
    if commenter in releaseManagers:
      if re.match("^[+](test|tested)$", first_line, re.I):
        signatures["tests"] = "approved"
      elif re.match("^[-](test|tested)$", first_line, re.I):
        signatures["tests"] = "rejected"

  print "The labels of the pull request should be:"
  # Labels coming from signature.
  labels = [x + "-pending" for x in signing_categories]

  for category, value in signatures.items():
    if not category in signing_categories:
      continue
    labels = [l for l in labels if not l.startswith(category+"-")]
    if value == "approved":
      labels.append(category + "-approved")
    elif value == "rejected":
      labels.append(category + "-rejected")
    elif value == "started":
       labels.append(category + "-started")
    else:
      labels.append(category + "-pending")

  # Additional labels.
  if is_hold:
    labels.append("hold")

  if cmssw_repo:
    if comparison_done:
      labels.append("comparison-available")
    else:
      labels.append("comparison-pending")

  print "\n".join(labels)

  # Now updated the labels.
  missingApprovals = [x
                      for x in labels
                      if not x.endswith("-approved")
                         and not x.startswith("orp")
                         and not x.startswith("tests")
                         and not x.startswith("comparison")
                         and not x == "hold"]

  if not missingApprovals:
    print "The pull request is complete."
  if missingApprovals:
    labels.append("pending-signatures")
  else:
    labels.append("fully-signed")
  labels = set(labels)

  # We update labels only if they are different.
  SUPER_USERS = (yaml.load(file("super-users.yaml")))
  old_labels = set([x.name for x in issue.labels])
  releaseManagersList = ", ".join(["@" + x for x in set(releaseManagers + SUPER_USERS)])
  releaseManagersMsg = ""
  if releaseManagers:
    releaseManagersMsg = format("%(rm)s can you please take care of it?",
                                 rm=releaseManagersList)

  #For now, only trigger tests for cms-sw/cmssw
  if cmssw_repo:
    # trigger the tests and inform it in the thread.
    if tests_requested:
      create_properties_file_tests( prId, dryRun )
      if not dryRun:
        pr.create_issue_comment( TRIGERING_TESTS_MSG )

  # Do not complain about tests
  requiresTestMessage = "or unless it breaks tests."
  if "tests-approved" in labels:
    requiresTestMessage = "(tests are also fine)."
  elif "tests-rejected" in labels:
    requiresTestMessage = "(but tests are reportedly failing)."

  autoMergeMsg = ""
  if all(["fully-signed"     in labels,
          not "hold"         in labels,
          not "orp-rejected" in labels,
          not "orp-pending"  in labels,
          "tests-approved"   in labels]):
    autoMergeMsg = "This pull request will be automatically merged."
  else:
    if "orp-pending" in labels or "orp-rejected" in labels:
      autoMergeMsg = format("This pull request requires discussion in the"
                            " ORP meeting before it's merged. %(managers)s",
                            managers=releaseManagersList)
    elif "new-package-pending" in labels:
      autoMergeMsg = format("This pull request requires a new package and "
                            " will not be merged. %(managers)s",
                            managers=releaseManagersList)
    elif "hold" in labels:
      autoMergeMsg = format("This PR is put on hold by @%(blocker)s. He / she"
                            " will have to remove the `hold` comment or"
                            " %(managers)s will have to merge it by"
                            " hand.",
                            blocker=blocker,
                            managers=releaseManagersList)

  devReleaseRelVal = ""
  if not pr.base.ref in DEVEL_RELEASE_CYCLE:
    devReleaseRelVal = "once checked with relvals in the development release cycle of CMSSW"
  messageFullySigned = format("This pull request is fully signed and it will be"
                              " integrated in one of the next %(branch)s IBs"
                              " %(devReleaseRelVal)s"
                              " %(requiresTest)s"
                              " %(autoMerge)s",
                              requiresTest=requiresTestMessage,
                              autoMerge = autoMergeMsg,
                              devReleaseRelVal=devReleaseRelVal,
                              branch=pr.base.ref)

  if old_labels == labels:
    print "Labels unchanged."
  elif not dryRun:
    issue.edit(labels=list(labels))
    if all(["fully-signed" in labels,
            not "orp-approved" in labels,
            not "orp-pending" in labels]):
      pr.create_issue_comment(messageFullySigned)
    elif "fully-signed" in labels and "orp-approved" in labels:
      pass
    elif "fully-signed" in labels and "orp-pending" in labels:
      pr.create_issue_comment(messageFullySigned)

  unsigned = [k for (k, v) in signatures.items() if v == "pending"]
  missing_notifications = ["@" + name
                            for name, l2_categories in CMSSW_L2.items()
                            for signature in signing_categories
                            if signature in l2_categories
                               and signature in unsigned]

  missing_notifications = set(missing_notifications)
  # Construct message for the watchers
  watchersMsg = ""
  if watchers:
    watchersMsg = format("%(watchers)s this is something you requested to"
                         " watch as well.\n",
                         watchers=", ".join(watchers))
  # Construct message for the release managers.
  managers = ", ".join(["@" + x for x in releaseManagers])

  releaseManagersMsg = ""
  if releaseManagers:
    releaseManagersMsg = format("%(managers)s you are the release manager for"
                                " this.\nYou can merge this pull request by"
                                " typing 'merge' in the first line of your"
                                " comment.",
                                managers = managers)

  # Construct message for ORP approval
  orpRequiredMsg = ""
  if requiresL1:
    orpRequiredMsg = format("\nThis pull requests was done for a production"
                            " branch and will require explicit ORP approval"
                            " on friday or L1 override.")

  # Add a Warning if the pull request was done against a patch branch
  if cmssw_repo:
    warning_msg = ''
    if 'patchX' in pr.base.ref:
      print 'Must warn that this is a patch branch'
      base_release = pr.base.ref.replace( '_patchX', '' )
      base_release_branch = re.sub( '[0-9]+$', 'X', base_release )
      warning_msg = format("Note that this branch is designed for requested bug "
                         "fixes specific to the %(base_rel)s release.\nIf you "
                         "wish to make a pull request for the %(base_branch)s "
                         "release cycle, please use the %(base_branch)s branch instead",
                         base_rel=base_release,
                         base_branch=base_release_branch)

    # We do not want to spam people for the old pull requests.
    messageNewPR = format("A new Pull Request was created by @%(user)s"
                        " %(name)s for %(branch)s.\n\n"
                        "%(title)s\n\n"
                        "It involves the following packages:\n\n"
                        "%(packages)s\n\n"
                        "%(new_package_message)s\n"
                        "%(l2s)s can you please review it and eventually sign?"
                        " Thanks.\n"
                        "%(watchers)s"
                        "You can sign-off by replying to this message having"
                        " '+1' in the first line of your reply.\n"
                        "You can reject by replying  to this message having"
                        " '-1' in the first line of your reply.\n"
                        "If you are a L2 or a release manager you can ask for"
                        " tests by saying 'please test' in the first line of a"
                        " comment.\n"
                        "%(releaseManagers)s"
                        "%(orpRequired)s"
                        "\n%(patch_branch_warning)s",
                        user=pr.user.login,
                        name=pr.user.name and "(%s)" % pr.user.name or "",
                        branch=pr.base.ref,
                        title=pr.title.encode("ascii", "ignore"),
                        l2s=", ".join(missing_notifications),
                        packages="\n".join(packages),
                        new_package_message=new_package_message,
                        watchers=watchersMsg,
                        releaseManagers=releaseManagersMsg,
                        orpRequired=orpRequiredMsg,
                        patch_branch_warning=warning_msg)

    messageUpdatedPR = format("Pull request #%(pr)s was updated."
                            " %(signers)s can you please check and sign again.",
                            pr=pr.number,
                            signers=", ".join(missing_notifications))
  else:
    if create_external_issue:
      if not already_seen:
        if dryRun:
          print "Should create a new issue in ",external_issue_repo_name," for this PR"
        else:
          external_managers = ["@" + name for name, l2_categories in CMSSW_L2.items() if "externals" in l2_categories]
          cmsdist_repo  = gh.get_repo(external_issue_repo_name)
          cmsdist_title = format("%(repo)s#%(pr)s: %(title)s",
                                 title=pr.title.encode("ascii", "ignore"),
                                 repo=repository,
                                 pr=pr.number)
          cmsdist_body = format("A new Pull Request was created by @%(user)s"
                                " %(name)s for branch %(branch)s.\n\n"
                                "Pull Request Reference: %(repo)s#%(pr)s\n\n"
                                "%(externals_l2s)s can you please review it and eventually sign? Thanks.\n",
                                repo=repository,
                                user=pr.user.login,
                                name=pr.user.name and "(%s)" % pr.user.name or "",
                                branch=pr.base.ref,
                                externals_l2s=", ".join(external_managers),
                                pr=pr.number)
          cissue = cmsdist_repo.create_issue(cmsdist_title, cmsdist_body)
          external_issue_number = str(cissue.number)
          print "Created a new issue ",external_issue_repo_name,"#",external_issue_number
      if pull_request_updated and external_issue_number:
        if dryRun:
          print "Should add an update message for issue ",external_issue_repo_name,"#",external_issue_number
        else:
          cmsdist_repo  = gh.get_repo(external_issue_repo_name)
          cissue = cmsdist_repo.get_issue(int(external_issue_number))
          cmsdist_body = format("Pull request %(repo)s#%(pr)s was updated.\n"
                                "Latest update by %(name)s with commit message\n%(message)s",
                                repo=repository,
                                pr=pr.number,
                                name=last_commit.committer.name.encode("ascii", "ignore"),
                                message=last_commit.message.encode("ascii", "ignore"))
          cissue.create_comment(cmsdist_body)
    cmsdist_issue = ""
    if external_issue_number:
      cmsdist_issue="\n\nexternal issue "+external_issue_repo_name+"#"+external_issue_number

    messageNewPR = format("A new Pull Request was created by @%(user)s"
                          " %(name)s for branch %(branch)s.\n\n"
                          "%(title)s\n\n"
                          "%(l2s)s can you please review it and eventually sign?"
                          " Thanks.\n"
                          "%(watchers)s"
                          "You can sign-off by replying to this message having"
                          " '+1' in the first line of your reply.\n"
                          "You can reject by replying  to this message having"
                          " '-1' in the first line of your reply."
                          "%(cmsdist_issue)s",
                          user=pr.user.login,
                          name=pr.user.name and "(%s)" % pr.user.name or "",
                          branch=pr.base.ref,
                          title=pr.title.encode("ascii", "ignore"),
                          l2s=", ".join(missing_notifications),
                          watchers=watchersMsg,
                          cmsdist_issue=cmsdist_issue)

    messageUpdatedPR = format("Pull request #%(pr)s was updated."
                              "%(cmsdist_issue)s",
                              pr=pr.number,
                              cmsdist_issue=cmsdist_issue)

  # Finally decide whether or not we should close the pull request:
  messageBranchClosed = format("This branch is closed for updates."
                               " Closing this pull request.\n"
                               " Please bring this up in the ORP"
                               " meeting if really needed.\n")

  commentMsg = ""
  if pr.base.ref in RELEASE_BRANCH_CLOSED:
    commentMsg = messageBranchClosed
  elif not already_seen:
    commentMsg = messageNewPR
  elif pull_request_updated:
    commentMsg = messageUpdatedPR
  elif not missingApprovals:
    print "Pull request is already fully signed. Not sending message."
  else:
    print "Already notified L2 about " + str(pr.number)
  if commentMsg:
    print "The following comment will be made:"
    try:
      print commentMsg.decode("ascii", "replace")
    except:
      pass

  if commentMsg and not dryRun:
    pr.create_issue_comment(commentMsg)

  # Check if it needs to be automatically closed.
  if mustClose == True and issue.state == "open":
    print "This pull request must be closed."
    if not dryRun:
      print issue.edit(state="closed")

  # Check if it needs to be automatically merged.
  if all(["fully-signed" in labels,
          "tests-approved" in labels,
          not "hold" in labels,
          not "orp-rejected" in labels,
          not "orp-pending" in labels,
          not "new-package-pending" in labels]):
    print "This pull request can be automatically merged"
    mustMerge = True
  else:
    print "This pull request will not be automatically merged."
    print not "orp-rejected" in labels, not "orp-pending" in labels

  if mustMerge == True:
    print "This pull request must be merged."
    if not dryRun:
        try:
          pr.merge()
        except:
          pass