Example #1
0
def test_invalid_yaml():
    fake_repo = mock.Mock()
    fake_repo.get_contents.return_value = mock.Mock(
        decoded_content="  ,;  dkqjshdmlksj\nhkqlsjdh\n-\n  qsjkdlkq\n")
    with pytest.raises(rules.NoRules) as excinfo:
        rules.get_branch_rule(fake_repo, "master")
    assert '.mergify.yml is invalid at position: (1:3)' in str(excinfo.value)
Example #2
0
def check_configuration_changes(event_type, data, event_pull):
    if event_pull.base.repo.default_branch == event_pull.base.ref:
        ref = None
        for f in event_pull.get_files():
            if f.filename == ".mergify.yml":
                ref = f.contents_url.split("?ref=")[1]

        if ref is not None:
            try:
                mergify_config = rules.get_mergify_config(event_pull.base.repo,
                                                          ref=ref)
                if "rules" in mergify_config:
                    rules.get_branch_rule(mergify_config['rules'],
                                          event_pull.base.ref)
            except rules.InvalidRules as e:  # pragma: no cover
                # Not configured, post status check with the error message
                # TODO(sileht): we can annotate the .mergify.yml file in Github
                # UI with that API
                check_api.set_check_run(
                    event_pull,
                    "Mergify — future config checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e)
                    })
            else:
                check_api.set_check_run(
                    event_pull,
                    "Mergify — future config checker",
                    "completed",
                    "success",
                    output={
                        "title": "The new Mergify configuration is valid",
                        "summary": "No action required",
                    })

            check_api.set_check_run(
                event_pull,
                "Mergify — disabled due to configuration change",
                "completed",
                "success",
                output={
                    "title": "Mergify configuration has been modified",
                    "summary": "The pull request needs to be merged manually",
                })

            return True
    return False
Example #3
0
def test():
    import github

    from mergify_engine import gh_pr
    from mergify_engine import utils

    utils.setup_logging()
    config.log()
    gh_pr.monkeypatch_github()

    parts = sys.argv[1].split("/")

    LOG.info("Getting repo %s ..." % sys.argv[1])

    integration = github.GithubIntegration(config.INTEGRATION_ID,
                                           config.PRIVATE_KEY)

    installation_id = utils.get_installation_id(integration, parts[3])
    token = integration.get_access_token(installation_id).token
    g = github.Github(token)
    user = g.get_user(parts[3])
    repo = user.get_repo(parts[4])
    LOG.info("Protecting repo %s branch %s ..." % (sys.argv[1], sys.argv[2]))
    rule = rules.get_branch_rule(repo, sys.argv[2])
    configure_protection_if_needed(repo, sys.argv[2], rule)
Example #4
0
    def test_branch_disabled(self):
        old_rule = {
            "protection": {
                "required_status_checks": {
                    "strict": True,
                    "contexts": ["continuous-integration/no-ci"],
                },
                "required_pull_request_reviews": {
                    "dismiss_stale_reviews": True,
                    "require_code_owner_reviews": False,
                    "required_approving_review_count": 1,
                },
                "restrictions": None,
                "enforce_admins": False,
            }
        }
        branch_protection.protect(self.r_main, "disabled", old_rule)

        config = rules.get_mergify_config(self.r_main)
        rule = rules.get_branch_rule(config['rules'], "disabled")
        self.assertEqual(None, rule)
        data = branch_protection.get_protection(self.r_main, "disabled")
        self.assertFalse(
            branch_protection.is_configured(self.r_main, "disabled", rule,
                                            data))

        self.create_pr("disabled")
        self.assertEqual([], self.processor._get_cached_branches())
        self.assertEqual([], self._get_queue("disabled"))

        data = branch_protection.get_protection(self.r_main, "disabled")
        self.assertTrue(
            branch_protection.is_configured(self.r_main, "disabled", rule,
                                            data))
Example #5
0
    def test_branch_disabled(self):
        old_rule = {
            "protection": {
                "required_status_checks": {
                    "strict": True,
                    "contexts": ["continuous-integration/no-ci"],
                },
                "required_pull_request_reviews": {
                    "dismiss_stale_reviews": True,
                    "require_code_owner_reviews": False,
                    "required_approving_review_count": 1,
                },
                "restrictions": None,
                "enforce_admins": False,
            }
        }
        gh_branch.protect(self.r_main, "disabled", old_rule)

        rule = rules.get_branch_rule(self.r_main, "disabled")
        self.assertEqual(None, rule)
        self.assertFalse(gh_branch.is_configured(self.r_main, "disabled",
                                                 rule))

        self.create_pr("disabled")
        self.assertEqual([], self.engine.get_cached_branches())
        self.assertEqual([], self.engine.build_queue("disabled"))

        self.assertTrue(gh_branch.is_configured(self.r_main, "disabled", rule))
Example #6
0
def check_configuration_changes(event_type, data, event_pull):
    if (event_type == "pull_request"
            and data["action"] in ["opened", "synchronize"]
            and event_pull.base.repo.default_branch == event_pull.base.ref):
        ref = None
        for f in event_pull.get_files():
            if f.filename == ".mergify.yml":
                ref = f.contents_url.split("?ref=")[1]

        if ref is not None:
            try:
                mergify_config = rules.get_mergify_config(event_pull.base.repo,
                                                          ref=ref)
                if "rules" in mergify_config:
                    rules.get_branch_rule(mergify_config['rules'],
                                          event_pull.base.ref)
            except rules.InvalidRules as e:  # pragma: no cover
                # Not configured, post status check with the error message
                # TODO(sileht): we can annotate the .mergify.yml file in Github
                # UI with that API
                check_api.set_check_run(
                    event_pull,
                    "future-config-checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e)
                    })
            else:
                check_api.set_check_run(
                    event_pull,
                    "future-config-checker",
                    "completed",
                    "success",
                    output={
                        "title": "The new Mergify configuration is valid",
                        "summary": "No action required",
                    })
Example #7
0
def validate_with_get_branch_rule(config, branch="master"):
    return rules.get_branch_rule(config['rules'], branch)
Example #8
0
 def _get_queue(self, branch):
     config = rules.get_mergify_config(self.r_main)
     branch_rule = rules.get_branch_rule(config['rules'], branch)
     collaborators = [self.u_main.id]
     return self.processor._build_queue(branch, branch_rule, collaborators)
Example #9
0
def validate_with_get_branch_rule(config, branch="master"):
    fake_repo = mock.Mock()
    fake_repo.get_contents.return_value = mock.Mock(
        decoded_content=yaml.dump(config))
    return rules.get_branch_rule(fake_repo, branch)
Example #10
0
    def handle(self, event_type, data):
        # Everything start here

        incoming_pull = self.get_incoming_pull_from_event(event_type, data)
        self.log_formated_event(event_type, incoming_pull, data)

        if not incoming_pull:  # pragma: no cover
            LOG.info("No pull request found in the event %s, "
                     "ignoring" % event_type)
            return

        if (event_type == "status"
                and incoming_pull.head.sha != data["sha"]):  # pragma: no cover
            LOG.info("No need to proceed queue (got status of an old commit)")
            return

        elif event_type == "status" and incoming_pull.merged:
            LOG.info("No need to proceed queue (got status of a merged "
                     "pull request)")
            return

        # CHECK IF THE CONFIGURATION IS GOING TO CHANGE
        if (event_type == "pull_request"
                and data["action"] in ["opened", "synchronize"]
                and self.repository.default_branch == incoming_pull.base.ref):
            ref = None
            for f in incoming_pull.get_files():
                if f.filename == ".mergify.yml":
                    ref = f.contents_url.split("?ref=")[1]

            if ref is not None:
                try:
                    rules.get_branch_rule(self.repository,
                                          incoming_pull.base.ref, ref)
                except rules.InvalidRules as e:  # pragma: no cover
                    # Not configured, post status check with the error message
                    incoming_pull.mergify_engine_github_post_check_status(
                        self._redis, self.installation_id, "failure", str(e),
                        "future-config-checker")
                else:
                    incoming_pull.mergify_engine_github_post_check_status(
                        self._redis, self.installation_id, "success",
                        "The new configuration is valid",
                        "future-config-checker")

        # BRANCH CONFIGURATION CHECKING
        branch_rule = None
        try:
            branch_rule = rules.get_branch_rule(self.repository,
                                                incoming_pull.base.ref)
        except rules.NoRules as e:
            LOG.info("No need to proceed queue (.mergify.yml is missing)")
            return
        except rules.InvalidRules as e:  # pragma: no cover
            # Not configured, post status check with the error message
            if (event_type == "pull_request"
                    and data["action"] in ["opened", "synchronize"]):
                incoming_pull.mergify_engine_github_post_check_status(
                    self._redis, self.installation_id, "failure", str(e))
            return

        try:
            gh_branch.configure_protection_if_needed(self.repository,
                                                     incoming_pull.base.ref,
                                                     branch_rule)
        except github.GithubException as e:  # pragma: no cover
            if e.status == 404 and e.data["message"] == "Branch not found":
                LOG.info("%s: branch no longer exists: %s",
                         incoming_pull.pretty(), e.message)
                return
            raise

        if not branch_rule:
            LOG.info("Mergify disabled on branch %s", incoming_pull.base.ref)
            return

        # PULL REQUEST UPDATER

        fullify_extra = {
            # NOTE(sileht): Both are used by compute_approvals
            "branch_rule": branch_rule,
            "collaborators":
            [u.id for u in self.repository.get_collaborators()],
        }

        if incoming_pull.state == "closed":
            self._cache_remove_pull(incoming_pull)
            LOG.info("Just update cache (pull_request closed)")

            if (event_type == "pull_request"
                    and data["action"] in ["closed", "labeled"]
                    and incoming_pull.merged):
                backports.backports(self.repository, incoming_pull,
                                    branch_rule["automated_backport_labels"],
                                    self._installation_token)

            if event_type == "pull_request" and data["action"] == "closed":
                self.get_processor().proceed_queue(incoming_pull.base.ref,
                                                   **fullify_extra)

                if not incoming_pull.merged:
                    incoming_pull.mergify_engine_github_post_check_status(
                        self._redis, self.installation_id, "success",
                        "Pull request closed unmerged")

                if incoming_pull.head.ref.startswith("mergify/bp/%s" %
                                                     incoming_pull.base.ref):
                    try:
                        self.repository.get_git_ref(
                            "heads/%s" % incoming_pull.head.ref).delete()
                        LOG.info("%s: branch %s deleted",
                                 incoming_pull.pretty(),
                                 incoming_pull.head.ref)
                    except github.UnknownObjectException:  # pragma: no cover
                        pass

            return

        # First, remove informations we don't want to get from cache, so their
        # will be got/computed by PullRequest.fullify()
        if event_type == "refresh":
            cache = {}
            old_status = None
        else:
            cache = self.get_cache_for_pull_number(incoming_pull.base.ref,
                                                   incoming_pull.number)
            cache = dict((k, v) for k, v in cache.items()
                         if k.startswith("mergify_engine_"))
            old_status = cache.pop("mergify_engine_status", None)
            if event_type == "status":
                cache.pop("mergify_engine_required_statuses", None)
            elif event_type == "pull_request_review":
                cache.pop("mergify_engine_approvals", None)
            elif (event_type == "pull_request"
                  and data["action"] == "synchronize"):
                cache.pop("mergify_engine_required_statuses", None)

        incoming_pull.fullify(cache, **fullify_extra)
        self._cache_save_pull(incoming_pull)

        if (event_type == "pull_request_review"
                and data["review"]["user"]["id"]
                not in fullify_extra["collaborators"]):
            LOG.info("Just update cache (pull_request_review non-collab)")
            return

        # NOTE(sileht): PullRequest updated or comment posted, maybe we need to
        # update github
        # Get and refresh the queues
        if old_status != incoming_pull.mergify_engine["status"]:
            incoming_pull.mergify_engine_github_post_check_status(
                self._redis,
                self.installation_id,
                incoming_pull.mergify_engine["status"]["github_state"],
                incoming_pull.mergify_engine["status"]["github_description"],
            )

        self.get_processor().proceed_queue(incoming_pull.base.ref,
                                           **fullify_extra)
Example #11
0
 def _get_queue(self, branch):
     branch_rule = rules.get_branch_rule(self.r_main, branch)
     collaborators = [self.u_main.id]
     return self.processor._build_queue(branch, branch_rule, collaborators)
Example #12
0
    def handle(self, branch_rules, event_type, data, incoming_pull):
        # Everything start here
        incoming_branch = incoming_pull.g_pull.base.ref
        incoming_state = incoming_pull.g_pull.state

        try:
            branch_rule = rules.get_branch_rule(branch_rules, incoming_branch)
        except rules.InvalidRules as e:  # pragma: no cover
            # Not configured, post status check with the error message
            if (event_type == "pull_request"
                    and data["action"] in ["opened", "synchronize"]):
                check_api.set_check_run(
                    incoming_pull.g_pull,
                    "current-config-checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The Mergify configuration is invalid",
                        "summary": str(e)
                    })

            return

        try:
            branch_protection.configure_protection_if_needed(
                self.repository, incoming_branch, branch_rule)
        except github.GithubException as e:  # pragma: no cover
            if e.status == 404 and e.data["message"] == "Branch not found":
                LOG.info("head branch no longer exists",
                         pull_request=incoming_pull)
                return
            raise

        if not branch_rule:
            LOG.info("Mergify disabled on branch", branch=incoming_branch)
            return

        # PULL REQUEST UPDATER

        collaborators = [u.id for u in self.repository.get_collaborators()]

        if incoming_state == "closed":
            self._cache_remove_pull(incoming_pull)
            LOG.info("Just update cache (pull request closed)")

            if (event_type == "pull_request"
                    and data["action"] in ["closed", "labeled"]
                    and incoming_pull.g_pull.merged):
                backports.backport_from_labels(
                    incoming_pull, branch_rule["automated_backport_labels"],
                    self._installation_token)

            if event_type == "pull_request" and data["action"] == "closed":
                self.get_processor().proceed_queue(incoming_branch,
                                                   branch_rule, collaborators)

                if not incoming_pull.g_pull.merged:
                    incoming_pull.post_check_status(
                        "success", "Pull request closed unmerged")

                head_branch = incoming_pull.g_pull.head.ref
                if head_branch.startswith("mergify/bp/%s" % incoming_branch):
                    try:
                        self.repository.get_git_ref("heads/%s" %
                                                    head_branch).delete()
                        LOG.info("branch deleted",
                                 pull_request=incoming_pull,
                                 branch=head_branch)
                    except github.GithubException as e:  # pragma: no cover
                        if e.status != 404:
                            raise

            return

        # First, remove informations we don't want to get from cache, so their
        # will be recomputed by MergifyPullV1.complete()
        if event_type == "refresh":
            cache = {}
            old_status = None
        else:
            cache = self.get_cache_for_pull_number(incoming_branch,
                                                   incoming_pull.g_pull.number)
            cache = dict((k, v) for k, v in cache.items()
                         if k.startswith("mergify_engine_"))
            old_status = cache.pop("mergify_engine_status", None)
            if event_type in ["status", "check_run", "check_suite"]:
                cache.pop("mergify_engine_required_statuses", None)
            elif event_type == "pull_request_review":
                cache.pop("mergify_engine_reviews_ok", None)
                cache.pop("mergify_engine_reviews_ko", None)
            elif (event_type == "pull_request"
                  and data["action"] == "synchronize"):
                cache.pop("mergify_engine_required_statuses", None)

        changed = incoming_pull.complete(cache, branch_rule, collaborators)
        if changed:
            self._cache_save_pull(incoming_pull)

        if (event_type == "pull_request_review"
                and data["review"]["user"]["id"] not in collaborators):
            LOG.info("Just update cache (pull_request_review non-collab)")
            return

        # NOTE(sileht): PullRequest updated or comment posted, maybe we need to
        # update github
        # Get and refresh the queues
        if old_status != incoming_pull.status:
            incoming_pull.post_check_status(
                incoming_pull.github_state,
                incoming_pull.github_description,
            )

        self.get_processor().proceed_queue(incoming_branch, branch_rule,
                                           collaborators)
Example #13
0
    def handle(self, event_type, data):
        # Everything start here

        if event_type == "status":
            # Don't compute the queue for nothing
            if data["context"].startswith("%s/" % config.CONTEXT):
                return
            elif data["context"] == "continuous-integration/travis-ci/push":
                return

        # Get the current pull request
        incoming_pull = gh_pr.from_event(self._r, data)
        if not incoming_pull and event_type == "status":
            # It's safe to take the one from cache, since only status have
            # changed
            incoming_pull = self.get_incoming_pull_from_cache(data["sha"])
            if not incoming_pull:
                issues = list(self._g.search_issues("is:pr %s" % data["sha"]))
                if len(issues) >= 1:
                    incoming_pull = self._r.get_pull(issues[0].number)

        if not incoming_pull:
            LOG.info("No pull request found in the event %s, "
                     "ignoring" % event_type)
            return

        # Log the event
        self.log_formated_event(event_type, incoming_pull, data)

        # Don't handle private repo for now
        if self._r.private:
            LOG.info("No need to proceed queue (private repo)")
            return

        # Unhandled and already logged
        if event_type not in [
                "pull_request", "pull_request_review", "status", "refresh"
        ]:
            LOG.info("No need to proceed queue (unwanted event_type)")
            return

        elif event_type == "status" and incoming_pull.head.sha != data["sha"]:
            LOG.info("No need to proceed queue (got status of an old commit)")
            return

        # We don't care about *assigned/review_request*/edited
        elif (event_type == "pull_request" and data["action"] not in [
                "opened", "reopened", "closed", "synchronize", "edited",
                "labeled", "unlabeled"
        ]):
            LOG.info("No need to proceed queue (unwanted pull_request action)")
            return

        # CHECK IF THE CONFIGURATION IS GOING TO CHANGE
        if self._r.default_branch == incoming_pull.base.ref:
            ref = None
            for f in incoming_pull.get_files():
                if f.filename == ".mergify.yml":
                    ref = f.contents_url.split("?ref=")[1]

            if ref is not None:
                try:
                    rules.get_branch_rule(self._r, incoming_pull.base.ref, ref)
                except rules.NoRules as e:
                    # Not configured, post status check with the error message
                    incoming_pull.mergify_engine_github_post_check_status(
                        self._redis, self._installation_id, "failure", str(e),
                        "future-config-checker")
                else:
                    incoming_pull.mergify_engine_github_post_check_status(
                        self._redis, self._installation_id, "success",
                        "The new configuration is valid",
                        "future-config-checker")

        # BRANCH CONFIGURATION CHECKING
        branch_rule = None
        try:
            branch_rule = rules.get_branch_rule(self._r,
                                                incoming_pull.base.ref)
        except rules.NoRules as e:
            # Not configured, post status check with the error message
            incoming_pull.mergify_engine_github_post_check_status(
                self._redis, self._installation_id, "failure", str(e))
            return

        try:
            gh_branch.configure_protection_if_needed(self._r,
                                                     incoming_pull.base.ref,
                                                     branch_rule)
        except github.UnknownObjectException:
            LOG.exception("Fail to protect branch, disabled mergify")
            return

        if not branch_rule:
            LOG.info("Mergify disabled on branch %s", incoming_pull.base.ref)
            return

        # PULL REQUEST UPDATER

        fullify_extra = {
            # NOTE(sileht): Both are used by compute_approvals
            "branch_rule": branch_rule,
            "collaborators": [u.id for u in self._r.get_collaborators()]
        }

        if incoming_pull.state == "closed":
            self.cache_remove_pull(incoming_pull)
            self.proceed_queue(incoming_pull.base.ref, **fullify_extra)
            LOG.info("Just update cache (pull_request closed)")
            return

        if (event_type == "status"
                and data["context"] == "continuous-integration/travis-ci/pr"):
            fullify_extra["travis"] = data

        # First, remove informations we don't want to get from cache, so their
        # will be got/computed by PullRequest.fullify()
        if event_type == "refresh":
            cache = {}
        else:
            cache = self.get_cache_for_pull_number(incoming_pull.base.ref,
                                                   incoming_pull.number)
            cache = dict((k, v) for k, v in cache.items()
                         if k.startswith("mergify_engine_"))
            cache.pop("mergify_engine_weight", None)
            cache.pop("mergify_engine_status_desc", None)
            cache.pop("mergify_engine_weight_and_status", None)

            if (event_type == "status" and data["state"]
                    == cache.get("mergify_engine_travis_state")):
                LOG.info("No need to proceed queue (got status without "
                         "state change '%s')" % data["state"])
                return
            elif event_type == "status":
                cache.pop("mergify_engine_combined_status", None)
                cache["mergify_engine_ci_statuses"] = {}
                cache["mergify_engine_travis_state"] = data["state"]
                cache["mergify_engine_travis_url"] = data["target_url"]
                if data["state"] in ENDING_STATES:
                    cache.pop("mergify_engine_travis_detail", None)
                else:
                    cache["mergify_engine_travis_detail"] = {}

            elif event_type == "pull_request_review":
                cache.pop("mergify_engine_reviews", None)
                cache.pop("mergify_engine_approvals", None)
                cache.pop("mergify_engine_approved", None)
            elif event_type == "pull_request":
                if data["action"] not in ["closed", "edited"]:
                    cache.pop("mergify_engine_commits", None)
                if data["action"] == "synchronize":
                    # NOTE(sileht): hardcode ci status that will be refresh
                    # on next travis event
                    cache.pop("mergify_engine_combined_status", None)
                    cache["mergify_engine_ci_statuses"] = {}
                    cache.pop("mergify_engine_travis_state", None)
                    cache.pop("mergify_engine_travis_url", None)
                    cache.pop("mergify_engine_travis_detail", None)

        incoming_pull = incoming_pull.fullify(cache, **fullify_extra)
        self.cache_save_pull(incoming_pull)

        # NOTE(sileht): just refresh this pull request in cache
        if event_type == "status" and data["state"] == "pending":
            LOG.info("Just update cache (ci status pending)")
            return
        elif event_type == "pull_request" and data["action"] == "edited":
            LOG.info("Just update cache (pull_request edited)")
            return
        elif (event_type == "pull_request_review"
              and data["review"]["user"]["id"]
              not in fullify_extra["collaborators"]):
            LOG.info("Just update cache (pull_request_review non-collab)")
            return

        # TODO(sileht): Disable that until we can configure it in the yml file
        # NOTE(sileht): We check the state of incoming_pull and the event
        # because user can have restart a travis job between the event
        # received and when we looks at it with travis API
        #  if (event_type == "status"
        #          and data["state"] in ENDING_STATES
        #          and data["context"] in ["continuous-integration/travis-ci",
        #                                  "continuous-integration/travis-ci/pr"]
        #          and incoming_pull.mergify_engine["travis_state"]
        #          in ENDING_STATES
        #          and incoming_pull.mergify_engine["travis_detail"]):
        #      incoming_pull.mergify_engine_travis_post_build_results()

        # NOTE(sileht): PullRequest updated or comment posted, maybe we need to
        # update github
        # Get and refresh the queues
        if event_type in ["pull_request", "pull_request_review", "refresh"]:
            incoming_pull.mergify_engine_github_post_check_status(
                self._redis, self._installation_id)

        self.proceed_queue(incoming_pull.base.ref, **fullify_extra)