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)
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
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)
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))
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))
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", })
def validate_with_get_branch_rule(config, branch="master"): return rules.get_branch_rule(config['rules'], branch)
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)
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)
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)
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)
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)
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)