def test_get_mergify_config_invalid(invalid): with pytest.raises(InvalidRules): client = mock.Mock() client.item.return_value = { "content": encodebytes(invalid.encode()).decode() } filename, schema = get_mergify_config(client, "xyz")
def get_config(self) -> rules.MergifyConfig: try: return rules.get_mergify_config( context.MergifyConfigFile( { "type": "file", "content": "whatever", "sha": github_types.SHAType("whatever"), "path": ".mergify.yml", "decoded_content": self.mergify_yml, } ) ) except rules.InvalidRules as exc: detail = list( map( lambda e: { "loc": ("body", "mergify_yml"), "msg": rules.InvalidRules.format_error(e), "type": "mergify_config_error", }, sorted(exc.errors, key=str), ) ) raise fastapi.HTTPException(status_code=422, detail=detail)
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_actions_with_options_none(): file = context.MergifyConfigFile( type="file", content="whatever", sha="azertyuiop", path="whatever", decoded_content=""" defaults: actions: post_check: rebase: comment: bot_account: "foobar" pull_request_rules: - name: ahah conditions: - base=master actions: comment: rebase: bot_account: "foobar" post_check: """, ) config = rules.get_mergify_config(file) assert [ list(rule.actions.keys()) for rule in config["pull_request_rules"] ][0] == [ "comment", "rebase", "post_check", ]
async def test_get_mergify_config(valid: str, redis_cache: utils.RedisCache) -> None: async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(valid.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None schema = get_mergify_config(config_file) assert isinstance(schema, dict) assert "pull_request_rules" in schema
def test_get_mergify_config(valid): client = mock.Mock() client.item.return_value = { "content": encodebytes(valid.encode()).decode() } filename, schema = get_mergify_config(client, "xyz") assert isinstance(schema, dict) assert "pull_request_rules" in schema
def test_default_with_no_pull_requests_rules(): file = context.MergifyConfigFile( type="file", content="whatever", sha="azertyuiop", path="whatever", decoded_content=""" defaults: actions: merge: strict: "smart" """, ) assert rules.get_mergify_config(file) config = rules.get_mergify_config(file) assert config["pull_request_rules"].rules == []
async def create_queue_freeze( queue_freeze_payload: QueueFreezePayload, application: application_mod.Application = fastapi.Depends( # noqa: B008 security.get_application ), queue_name: rules.QueueName = fastapi.Path( # noqa: B008 ..., description="The name of the queue" ), repository_ctxt: context.Repository = fastapi.Depends( # noqa: B008 security.get_repository_context ), ) -> QueueFreezeResponse: if queue_freeze_payload.reason == "": queue_freeze_payload.reason = "No freeze reason was specified." config_file = await repository_ctxt.get_mergify_config_file() if config_file is None: raise fastapi.HTTPException( status_code=404, detail="Mergify configuration file is missing." ) config = get_mergify_config(config_file) queue_rules = config["queue_rules"] if all(queue_name != rule.name for rule in queue_rules): raise fastapi.HTTPException( status_code=404, detail=f'The queue "{queue_name}" does not exist.' ) qf = await freeze.QueueFreeze.get(repository_ctxt, queue_name) if qf is None: qf = freeze.QueueFreeze( repository=repository_ctxt, name=queue_name, reason=queue_freeze_payload.reason, application_name=application.name, application_id=application.id, freeze_date=date.utcnow(), ) await qf.save() elif qf.reason != queue_freeze_payload.reason: qf.reason = queue_freeze_payload.reason await qf.save() return QueueFreezeResponse( queue_freezes=[ QueueFreeze( name=qf.name, reason=qf.reason, application_name=qf.application_name, application_id=qf.application_id, freeze_date=qf.freeze_date, ) ], )
def check_configuration_changes(ctxt): if ctxt.pull["base"]["repo"]["default_branch"] == ctxt.pull["base"]["ref"]: ref = None for f in ctxt.files: if f["filename"] in rules.MERGIFY_CONFIG_FILENAMES: ref = f["contents_url"].split("?ref=")[1] if ref is not None: try: rules.get_mergify_config(ctxt.client, ctxt.pull["base"]["repo"]["name"], ref=ref) except rules.InvalidRules as e: # Not configured, post status check with the error message check_api.set_check_run( ctxt, actions_runner.SUMMARY_NAME, "completed", "failure", output={ "title": "The new Mergify configuration is invalid", "summary": str(e), "annotations": e.get_annotations(e.filename), }, ) else: check_api.set_check_run( ctxt, actions_runner.SUMMARY_NAME, "completed", "success", output={ "title": "The new Mergify configuration is valid", "summary": "This pull request must be merged " "manually because it modifies Mergify configuration", }, ) return True return False
def check_configuration_changes(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 in rules.MERGIFY_CONFIG_FILENAMES: ref = f.contents_url.split("?ref=")[1] if ref is not None: try: rules.get_mergify_config(event_pull.base.repo, ref=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, "Summary", "completed", "failure", output={ "title": "The new Mergify configuration is invalid", "summary": str(e), }, ) else: check_api.set_check_run( event_pull, "Summary", "completed", "success", output={ "title": "The new Mergify configuration is valid", "summary": "This pull request must be merged " "manually because it modifies Mergify configuration", }, ) return True return False
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 job_refresh_all(): integration = github.GithubIntegration(config.INTEGRATION_ID, config.PRIVATE_KEY) counts = [0, 0, 0] for install in utils.get_installations(integration): counts[0] += 1 token = integration.get_access_token(install["id"]).token g = github.Github(token, base_url="https://api.%s" % config.GITHUB_DOMAIN) i = g.get_installation(install["id"]) for r in i.get_repos(): if r.archived: # pragma: no cover continue try: rules.get_mergify_config(r) except github.GithubException as e: # pragma: no cover if e.status == 404: continue else: raise except rules.NoRules: pass counts[1] += 1 for p in list(r.get_pulls()): # Mimic the github event format data = { 'repository': r.raw_data, 'installation': {'id': install["id"]}, 'pull_request': p.raw_data, } engine.run.s('refresh', data).apply_async() LOG.info("Refreshing %s installations, %s repositories, " "%s branches", *counts)
async def repository_queues_configuration( repository_ctxt: context.Repository = fastapi.Depends( # noqa: B008 security.get_repository_context), ) -> QueuesConfig: config_file = await repository_ctxt.get_mergify_config_file() if config_file is None: return QueuesConfig() config = get_mergify_config(config_file) return QueuesConfig([ QueueRule( config=rule.config, name=rule.name, ) for rule in config["queue_rules"] ])
async def refresh(self) -> None: config_file = await self.repository.get_mergify_config_file() if config_file is None: self.log.warning( "train can't be refreshed, the mergify configuration is missing", ) return try: mergify_config = rules.get_mergify_config(config_file) except rules.InvalidRules as e: # pragma: no cover self.log.warning( "train can't be refreshed, the mergify configuration is invalid", summary=str(e), annotations=e.get_annotations(e.filename), ) return await self._populate_cars(mergify_config["queue_rules"]) await self._save() self.log.info("train cars refreshed")
def test_command_loader_with_defaults() -> None: raw_config = """ defaults: actions: backport: branches: - branch-3.1 - branch-3.2 ignore_conflicts: false """ file = context.MergifyConfigFile( type="file", content="whatever", sha=github_types.SHAType("azertyuiop"), path="whatever", decoded_content=raw_config, ) config = rules.get_mergify_config(file) command = commands_runner.load_command(config, "@mergifyio backport") assert command.name == "backport" assert command.args == "" assert isinstance(command.action, BackportAction) assert command.action.config == { "assignees": [], "branches": ["branch-3.1", "branch-3.2"], "bot_account": None, "regexes": [], "ignore_conflicts": False, "labels": [], "label_conflicts": "conflicts", "title": "{{ title }} (backport #{{ number }})", "body": "This is an automatic backport of pull request #{{number}} done by [Mergify](https://mergify.com).\n{{ cherry_pick_error }}", }
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 run(event_type, data): """Everything starts here.""" installation_id = data["installation"]["id"] owner = data["repository"]["owner"]["login"] repo = data["repository"]["name"] client = github.get_client(owner, repo, installation_id) raw_pull = get_github_pull_from_event(client, event_type, data) if not raw_pull: # pragma: no cover LOG.info( "No pull request found in the event %s, ignoring", event_type, gh_owner=owner, gh_repo=repo, ) return pull = mergify_pull.MergifyPull(client, raw_pull) # Override pull_request with the updated one data["pull_request"] = pull.data pull.log.info("Pull request found in the event %s", event_type) if ("base" not in pull.data or "repo" not in pull.data["base"] or len(list(pull.data["base"]["repo"].keys())) < 70): pull.log.warning( "the pull request payload looks suspicious", event_type=event_type, data=data, ) if (event_type == "status" and pull.data["head"]["sha"] != data["sha"]): # pragma: no cover pull.log.info( "No need to proceed queue (got status of an old commit)", ) return elif (event_type in ["status", "check_suite", "check_run"] and pull.data["merged"]): # pragma: no cover pull.log.info( "No need to proceed queue (got status of a merged pull request)", ) return elif (event_type in ["check_suite", "check_run"] and pull.data["head"]["sha"] != data[event_type]["head_sha"]): # pragma: no cover pull.log.info( "No need to proceed queue (got %s of an old " "commit)", event_type, ) return if check_configuration_changes(pull.g_pull): pull.log.info("Configuration changed, ignoring", ) return # BRANCH CONFIGURATION CHECKING try: mergify_config = rules.get_mergify_config(pull.g_pull.base.repo) except rules.NoRules: # pragma: no cover pull.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" ]: check_api.set_check_run( pull.g_pull, "Summary", "completed", "failure", output={ "title": "The Mergify configuration is invalid", "summary": str(e), }, ) return # Add global and mandatory rules mergify_config["pull_request_rules"].rules.extend( rules.load_pull_request_rules_schema(MERGIFY_RULE["rules"])) subscription = sub_utils.get_subscription(utils.get_redis_for_cache(), installation_id) if pull.data["base"]["repo"][ "private"] and not subscription["subscription_active"]: check_api.set_check_run( pull.g_pull, "Summary", "completed", "failure", output={ "title": "Mergify is disabled", "summary": subscription["subscription_reason"], }, ) return # CheckRun are attached to head sha, so when user add commits or force push # we can't directly get the previous Mergify Summary. So we copy it here, then # anything that looks at it in next celery tasks will find it. if event_type == "pull_request" and data["action"] == "synchronize": copy_summary_from_previous_head_sha(pull.g_pull, data["before"]) sources = [{"event_type": event_type, "data": data}] commands_runner.spawn_pending_commands_tasks(pull, sources) if event_type == "issue_comment": commands_runner.run_command(pull, sources, data["comment"]["body"], data["comment"]["user"]) else: actions_runner.handle(mergify_config["pull_request_rules"], pull, sources)
def run(event_type, data, subscription): """Everything starts here.""" integration = github.GithubIntegration(config.INTEGRATION_ID, config.PRIVATE_KEY) installation_id = data["installation"]["id"] try: installation_token = integration.get_access_token( installation_id).token except github.UnknownObjectException: # pragma: no cover LOG.error("token for install %d does not exists anymore (%s)", installation_id, data["repository"]["full_name"]) return g = github.Github(installation_token) try: if config.LOG_RATELIMIT: # pragma: no cover rate = g.get_rate_limit().rate LOG.info("ratelimit: %s/%s, reset at %s", rate.remaining, rate.limit, rate.reset, repository=data["repository"]["name"]) repo = g.get_repo(data["repository"]["owner"]["login"] + "/" + data["repository"]["name"]) # NOTE(sileht): Workaround for when we receive check_suite completed # without conclusion if (event_type == "check_suite" and data["action"] == "completed" and not data["check_suite"]["conclusion"]): # pragma: no cover data = check_api.workaround_for_unfinished_check_suite(repo, data) event_pull = get_github_pull_from_event(g, repo, installation_id, event_type, data) if not event_pull: # pragma: no cover LOG.info("No pull request found in the event %s, " "ignoring", event_type) return LOG.info("Pull request found in the event %s", event_type, pull_request=event_pull) if (event_type == "status" and event_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 in ["status", "check_suite", "check_run"] and event_pull.merged): # pragma: no cover LOG.info("No need to proceed queue (got status of a merged " "pull request)") return elif (event_type in ["check_suite", "check_run"] and event_pull.head.sha != data[event_type]["head_sha"]): # pragma: no cover LOG.info("No need to proceed queue (got %s of an old " "commit)", event_type) return check_configuration_changes(event_type, data, event_pull) # BRANCH CONFIGURATION CHECKING try: mergify_config = rules.get_mergify_config(repo) except rules.NoRules as e: # pragma: no cover 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"]): check_api.set_check_run( event_pull, "current-config-checker", "completed", "failure", output={ "title": "The Mergify configuration is invalid", "summary": str(e) }) return create_metrics(event_type, data) # NOTE(sileht): At some point we may need to reget the # installation_token within each next tasks, in case we reach the # expiration if "rules" in mergify_config: v1.handle(installation_id, installation_token, subscription, mergify_config["rules"], event_type, data, event_pull.raw_data) elif "pull_request_rules" in mergify_config: v2.handle.s(installation_id, installation_token, subscription, mergify_config["pull_request_rules"].as_dict(), event_type, data, event_pull.raw_data).apply_async() else: # pragma: no cover raise RuntimeError("Unexpected configuration version") except github.BadCredentialsException: # pragma: no cover LOG.error("token for install %d is no longuer valid (%s)", data["installation"]["id"], data["repository"]["full_name"]) except github.RateLimitExceededException: # pragma: no cover LOG.error("rate limit reached for install %d (%s)", data["installation"]["id"], data["repository"]["full_name"])
def handle_installation(installation): try: _id = installation["id"] target_type = installation["target_type"] account = installation["account"]["login"] LOG.info("Get subscription", account=account) subs = sub_utils.get_subscription(redis, _id) subscribed = subs["subscription_active"] costs[(subscribed, target_type, account)] = ( subs["subscription_cost"] ) with installations_lock: installations[(subscribed, target_type)] += 1 token = integration.get_access_token(_id).token g = github.Github(token, base_url="https://api.%s" % config.GITHUB_DOMAIN) if installation["target_type"] == "Organization": LOG.info("Get members", install=installation["account"]["login"]) org = g.get_organization(installation["account"]["login"]) value = len(list(org.get_members())) users_per_installation[ (subscribed, target_type, account)] = value else: users_per_installation[ (subscribed, target_type, account)] = 1 LOG.info("Get repos", account=account) repositories = sorted(g.get_installation(_id).get_repos(), key=operator.attrgetter("private")) for private, repos in itertools.groupby( repositories, key=operator.attrgetter("private")): configured_repos = 0 unconfigured_repos = 0 for repo in repos: try: rules.get_mergify_config(repo) configured_repos += 1 redis.sadd("badges.tmp", repo.full_name) except github.GithubException as e: if e.status >= 500: # pragma: no cover raise unconfigured_repos += 1 except (rules.InvalidRules, rules.NoRules): unconfigured_repos += 1 repositories_per_installation[ (subscribed, target_type, account, private, True) ] = configured_repos repositories_per_installation[ (subscribed, target_type, account, private, False) ] = unconfigured_repos except github.GithubException as e: # pragma: no cover # Ignore rate limit/abuse, authorization issues # and GitHub malfunction if e.status not in (403, 401) and e.status < 500: raise
def run(client, pull, subscription, sources): LOG.debug("engine get context") ctxt = context.Context(client, pull, subscription) ctxt.log.debug("engine start processing context") issue_comment_sources = [] for source in sources: if source["event_type"] == "issue_comment": issue_comment_sources.append(source) else: ctxt.sources.append(source) ctxt.log.debug("engine run pending commands") commands_runner.run_pending_commands_tasks(ctxt) if issue_comment_sources: ctxt.log.debug("engine handle commands") for source in issue_comment_sources: commands_runner.handle( ctxt, source["data"]["comment"]["body"], source["data"]["comment"]["user"], ) if not ctxt.sources: return if ctxt.client.auth.permissions_need_to_be_updated: check_api.set_check_run( ctxt, "Summary", "completed", "failure", output={ "title": "Required GitHub permissions are missing.", "summary": "You can accept them at https://dashboard.mergify.io/", }, ) return ctxt.log.debug("engine check configuration change") if check_configuration_changes(ctxt): ctxt.log.info("Configuration changed, ignoring") return ctxt.log.debug("engine get configuration") # BRANCH CONFIGURATION CHECKING try: filename, mergify_config = rules.get_mergify_config(ctxt) except rules.NoRules: # pragma: no cover ctxt.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 any((s["event_type"] == "pull_request" and s["data"]["action"] in ["opened", "synchronize"] for s in ctxt.sources)): check_api.set_check_run( ctxt, actions_runner.SUMMARY_NAME, "completed", "failure", output={ "title": "The Mergify configuration is invalid", "summary": str(e), "annotations": e.get_annotations(e.filename), }, ) return # Add global and mandatory rules mergify_config["pull_request_rules"].rules.extend( rules.PullRequestRules.from_list(MERGIFY_RULE["rules"]).rules) if ctxt.pull["base"]["repo"][ "private"] and not subscription["subscription_active"]: check_api.set_check_run( ctxt, actions_runner.SUMMARY_NAME, "completed", "failure", output={ "title": "Mergify is disabled", "summary": subscription["subscription_reason"], }, ) return # CheckRun are attached to head sha, so when user add commits or force push # we can't directly get the previous Mergify Summary. So we copy it here, then # anything that looks at it in next engine runs will find it. synchronize_events = dict(((s["data"]["after"], s["data"]) for s in ctxt.sources if s["event_type"] == "pull_request" and s["data"]["action"] == "synchronize")) if synchronize_events: ctxt.log.debug("engine synchronize summary") # NOTE(sileht): We sometimes got many synchronize in a row, that not always the # last one that have the Summary, so we also looks in older one if necessary. after_sha = ctxt.pull["head"]["sha"] while synchronize_events: sync_event = synchronize_events.pop(after_sha, None) if sync_event: if copy_summary_from_previous_head_sha(ctxt, sync_event["before"]): break else: after_sha = sync_event["before"] else: ctxt.log.warning( "Got synchronize event but didn't find Summary on previous head sha", ) break ctxt.log.debug("engine handle actions") actions_runner.handle(mergify_config["pull_request_rules"], ctxt)
async def test_get_mergify_config_invalid( invalid: str, redis_cache: utils.RedisCache) -> None: with pytest.raises(InvalidRules): async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(invalid.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() gh_owner = github_types.GitHubAccount({ "login": github_types.GitHubLogin("foobar"), "id": github_types.GitHubAccountIdType(0), "type": "User", "avatar_url": "", }) gh_repo = github_types.GitHubRepository({ "full_name": "foobar/xyz", "name": github_types.GitHubRepositoryName("xyz"), "private": False, "id": github_types.GitHubRepositoryIdType(0), "owner": gh_owner, "archived": False, "url": "", "html_url": "", "default_branch": github_types.GitHubRefType("ref"), }) installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, gh_repo, ) config_file = await repository.get_mergify_config_file() assert config_file is not None get_mergify_config(config_file)
import pytest from mergify_engine import config from mergify_engine import context from mergify_engine import github_types from mergify_engine import rules from mergify_engine.actions.backport import BackportAction from mergify_engine.actions.rebase import RebaseAction from mergify_engine.engine import commands_runner from mergify_engine.tests.unit import conftest EMPTY_CONFIG = rules.get_mergify_config( context.MergifyConfigFile( type="file", content="whatever", sha=github_types.SHAType("azertyuiop"), path="whatever", decoded_content="", )) def test_command_loader() -> None: with pytest.raises(commands_runner.CommandInvalid): commands_runner.load_command(EMPTY_CONFIG, "@mergifyio notexist foobar\n") with pytest.raises(commands_runner.CommandInvalid): commands_runner.load_command(EMPTY_CONFIG, "@mergifyio comment foobar\n") with pytest.raises(commands_runner.CommandInvalid):
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 test_action_queue_with_no_default_queue(): file = context.MergifyConfigFile( type="file", content="whatever", sha="azertyuiop", path="whatever", decoded_content=""" pull_request_rules: - name: ahah conditions: - base=master actions: queue: name: missing """, ) with pytest.raises(rules.InvalidRules) as e: rules.get_mergify_config(file) assert str(e.value.error) == "missing queue not found" file = context.MergifyConfigFile( type="file", content="whatever", sha="azertyuiop", path="whatever", decoded_content=""" pull_request_rules: - name: ahah conditions: - base=master actions: queue: """, ) with pytest.raises(rules.InvalidRules) as e: rules.get_mergify_config(file) assert ( str(e.value.error) == "required key not provided @ data['pull_request_rules'][0]['actions']['queue']['name']" ) # TODO(sileht): This is currently required to set a queue name to set other # queue options If one days we have more options like queue name, we may # need to revisit the validators system to make all voluptuous.Required() # keys optional for defaults file = context.MergifyConfigFile( type="file", content="whatever", sha="azertyuiop", path="whatever", decoded_content=""" defaults: actions: queue: merge_bot_account: foobar queue_rules: - name: default conditions: [] pull_request_rules: - name: ahah conditions: - base=master actions: queue: name: default """, ) with pytest.raises(rules.InvalidRules) as e: rules.get_mergify_config(file) assert ( str(e.value.error) == "required key not provided @ data['defaults']['actions']['queue']['name']" )
def run(event_type, data): """Everything starts here.""" installation_id = data["installation"]["id"] installation_token = utils.get_installation_token(installation_id) if not installation_token: return g = github.Github(installation_token, base_url="https://api.%s" % config.GITHUB_DOMAIN) if config.LOG_RATELIMIT: # pragma: no cover rate = g.get_rate_limit().rate LOG.info("ratelimit: %s/%s, reset at %s", rate.remaining, rate.limit, rate.reset, repository=data["repository"]["name"]) repo = g.get_repo(data["repository"]["owner"]["login"] + "/" + data["repository"]["name"]) event_pull = get_github_pull_from_event(repo, event_type, data) if not event_pull: # pragma: no cover LOG.info("No pull request found in the event %s, " "ignoring", event_type) return LOG.info("Pull request found in the event %s", event_type, repo=repo.full_name, pull_request=event_pull) subscription = sub_utils.get_subscription(utils.get_redis_for_cache(), installation_id) if repo.private and not subscription["subscription_active"]: check_api.set_check_run( event_pull, "Summary", "completed", "failure", output={ "title": "Mergify is disabled", "summary": subscription["subscription_reason"], }) return if ("base" not in event_pull.raw_data or "repo" not in event_pull.raw_data["base"] or len(list(event_pull.raw_data["base"]["repo"].keys())) < 70): LOG.warning("the pull request payload looks suspicious", event_type=event_type, data=data, pull_request=event_pull.raw_data, repo=repo.fullname) if (event_type == "status" and event_pull.head.sha != data["sha"]): # pragma: no cover LOG.info("No need to proceed queue (got status of an old commit)", repo=repo.full_name, pull_request=event_pull) return elif (event_type in ["status", "check_suite", "check_run"] and event_pull.merged): # pragma: no cover LOG.info("No need to proceed queue (got status of a merged " "pull request)", repo=repo.full_name, pull_request=event_pull) return elif (event_type in ["check_suite", "check_run"] and event_pull.head.sha != data[event_type]["head_sha"] ): # pragma: no cover LOG.info("No need to proceed queue (got %s of an old " "commit)", event_type, repo=repo.full_name, pull_request=event_pull) return if check_configuration_changes(event_pull): LOG.info("Configuration changed, ignoring", repo=repo.full_name, pull_request=event_pull) return # BRANCH CONFIGURATION CHECKING try: mergify_config = rules.get_mergify_config(repo) except rules.NoRules: # pragma: no cover LOG.info("No need to proceed queue (.mergify.yml is missing)", repo=repo.full_name, pull_request=event_pull) 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"]): check_api.set_check_run( event_pull, "Summary", "completed", "failure", output={ "title": "The Mergify configuration is invalid", "summary": str(e) }) return create_metrics(event_type, data) v2.handle.s( installation_id, mergify_config["pull_request_rules"].as_dict(), event_type, data, event_pull.raw_data ).apply_async()
async def report( url: str, ) -> typing.Union[context.Context, github.AsyncGithubInstallationClient, None]: redis_cache = utils.create_aredis_for_cache(max_idle_time=0) try: owner, repo, pull_number = _url_parser(url) except ValueError: print(f"{url} is not valid") return None try: client = github.aget_client(owner) except exceptions.MergifyNotInstalled: print(f"* Mergify is not installed on account {owner}") return None # Do a dumb request just to authenticate await client.get("/") if client.auth.installation is None: print("No installation detected") return None print(f"* INSTALLATION ID: {client.auth.installation['id']}") if client.auth.owner_id is None: raise RuntimeError("Unable to get owner_id") if repo is None: slug = None else: slug = owner + "/" + repo cached_sub = await subscription.Subscription.get_subscription( redis_cache, client.auth.owner_id) db_sub = await subscription.Subscription._retrieve_subscription_from_db( redis_cache, client.auth.owner_id) cached_tokens = await user_tokens.UserTokens.get(redis_cache, client.auth.owner_id) db_tokens = await user_tokens.UserTokens._retrieve_from_db( redis_cache, client.auth.owner_id) print(f"* SUBSCRIBED (cache/db): {cached_sub.active} / {db_sub.active}") print("* Features (cache):") for f in db_sub.features: print(f" - {f.value}") print("* Features (db):") for f in cached_sub.features: print(f" - {f.value}") await report_dashboard_synchro(client.auth.installation["id"], cached_sub, cached_tokens, "ENGINE-CACHE", slug) await report_dashboard_synchro(client.auth.installation["id"], db_sub, db_tokens, "DASHBOARD", slug) await report_worker_status(owner) installation = context.Installation(client.auth.owner_id, owner, cached_sub, client, redis_cache) if repo is not None: repo_info: github_types.GitHubRepository = await client.item( f"/repos/{owner}/{repo}") repository = context.Repository(installation, repo_info["name"], repo_info["id"]) print( f"* REPOSITORY IS {'PRIVATE' if repo_info['private'] else 'PUBLIC'}" ) print("* CONFIGURATION:") mergify_config = None config_file = await repository.get_mergify_config_file() if config_file is None: print(".mergify.yml is missing") else: print(f"Config filename: {config_file['path']}") print(config_file["decoded_content"].decode()) try: mergify_config = rules.get_mergify_config(config_file) except rules.InvalidRules as e: # pragma: no cover print(f"configuration is invalid {str(e)}") else: mergify_config["pull_request_rules"].rules.extend( engine.MERGIFY_BUILTIN_CONFIG["pull_request_rules"].rules) if pull_number is None: async for branch in typing.cast( typing.AsyncGenerator[github_types.GitHubBranch, None], client.items(f"/repos/{owner}/{repo}/branches"), ): # TODO(sileht): Add some informations on the train q: queue.QueueBase = naive.Queue(repository, branch["name"]) await report_queue("QUEUES", q) q = merge_train.Train(repository, branch["name"]) await q.load() await report_queue("TRAIN", q) else: repository = context.Repository( installation, github_types.GitHubRepositoryName(repo)) ctxt = await repository.get_pull_request_context( github_types.GitHubPullRequestNumber(int(pull_number))) # FIXME queues could also be printed if no pull number given # TODO(sileht): display train if any q = await naive.Queue.from_context(ctxt) print( f"* QUEUES: {', '.join([f'#{p}' for p in await q.get_pulls()])}" ) q = await merge_train.Train.from_context(ctxt) print( f"* TRAIN: {', '.join([f'#{p}' for p in await q.get_pulls()])}" ) print("* PULL REQUEST:") pr_data = await ctxt.pull_request.items() pprint.pprint(pr_data, width=160) is_behind = await ctxt.is_behind print(f"is_behind: {is_behind}") print(f"mergeable_state: {ctxt.pull['mergeable_state']}") print("* MERGIFY LAST CHECKS:") for c in await ctxt.pull_engine_check_runs: print( f"[{c['name']}]: {c['conclusion']} | {c['output'].get('title')}" ) print("> " + "\n> ".join( ("No Summary", ) if c["output"]["summary"] is None else c["output"]["summary"].split("\n"))) if mergify_config is not None: print("* MERGIFY LIVE MATCHES:") match = await mergify_config["pull_request_rules" ].get_pull_request_rule(ctxt) summary_title, summary = await actions_runner.gen_summary( ctxt, match) print(f"[Summary]: success | {summary_title}") print("> " + "\n> ".join(summary.strip().split("\n"))) return ctxt return client
async def test_get_mergify_config_with_defaults( redis_cache: utils.RedisCache) -> None: config = """ defaults: actions: comment: bot_account: foo-bot rebase: bot_account: test-bot-account pull_request_rules: - name: ahah conditions: - base=master actions: comment: message: I love Mergify rebase: {} """ async def item(*args, **kwargs): return github_types.GitHubContentFile({ "content": encodebytes(config.encode()).decode(), "path": ".mergify.yml", "type": "file", "sha": "azertyu", }) client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None schema = get_mergify_config(config_file) assert isinstance(schema, dict) assert len(schema["pull_request_rules"].rules) == 1 comment = schema["pull_request_rules"].rules[0].actions["comment"].config assert comment == {"message": "I love Mergify", "bot_account": "foo-bot"} rebase = schema["pull_request_rules"].rules[0].actions["rebase"].config assert rebase == {"bot_account": "test-bot-account"} config = """ defaults: actions: comment: message: I love Mergify bot_account: AutoBot pull_request_rules: - name: ahah conditions: - base=master actions: comment: message: I really love Mergify """ client = mock.Mock() client.item.return_value = item() installation = context.Installation( github_types.GitHubAccountIdType(0), github_types.GitHubLogin("foobar"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("xyz"), github_types.GitHubRepositoryIdType(0), ) config_file = await repository.get_mergify_config_file() assert config_file is not None schema = get_mergify_config(config_file) assert isinstance(schema, dict) assert len(schema["pull_request_rules"].rules) == 1 comment = schema["pull_request_rules"].rules[0].actions["comment"].config assert comment == { "message": "I really love Mergify", "bot_account": "AutoBot" }
def run(event_type, data): """Everything starts here.""" installation_id = data["installation"]["id"] installation_token = utils.get_installation_token(installation_id) if not installation_token: return g = github.Github(installation_token, base_url="https://api.%s" % config.GITHUB_DOMAIN) if config.LOG_RATELIMIT: # pragma: no cover rate = g.get_rate_limit().rate LOG.info( "ratelimit: %s/%s, reset at %s", rate.remaining, rate.limit, rate.reset, repository=data["repository"]["name"], ) try: repo = g.get_repo(data["repository"]["owner"]["login"] + "/" + data["repository"]["name"]) except github.UnknownObjectException: # pragma: no cover LOG.info("Repository not found in the event %s, ignoring", event_type) return event_pull = get_github_pull_from_event(repo, event_type, data) if not event_pull: # pragma: no cover LOG.info("No pull request found in the event %s, " "ignoring", event_type) return # Override pull_request with the updated one data["pull_request"] = event_pull.raw_data LOG.info( "Pull request found in the event %s", event_type, repo=repo.full_name, pull_request=event_pull, ) if ("base" not in event_pull.raw_data or "repo" not in event_pull.raw_data["base"] or len(list(event_pull.raw_data["base"]["repo"].keys())) < 70): LOG.warning( "the pull request payload looks suspicious", event_type=event_type, data=data, pull_request=event_pull.raw_data, repo=repo.fullname, ) if (event_type == "status" and event_pull.head.sha != data["sha"]): # pragma: no cover LOG.info( "No need to proceed queue (got status of an old commit)", repo=repo.full_name, pull_request=event_pull, ) return elif (event_type in ["status", "check_suite", "check_run"] and event_pull.merged): # pragma: no cover LOG.info( "No need to proceed queue (got status of a merged " "pull request)", repo=repo.full_name, pull_request=event_pull, ) return elif (event_type in ["check_suite", "check_run"] and event_pull.head.sha != data[event_type]["head_sha"]): # pragma: no cover LOG.info( "No need to proceed queue (got %s of an old " "commit)", event_type, repo=repo.full_name, pull_request=event_pull, ) return if check_configuration_changes(event_pull): LOG.info( "Configuration changed, ignoring", repo=repo.full_name, pull_request=event_pull, ) return # BRANCH CONFIGURATION CHECKING try: mergify_config = rules.get_mergify_config(repo) except rules.NoRules: # pragma: no cover LOG.info( "No need to proceed queue (.mergify.yml is missing)", repo=repo.full_name, pull_request=event_pull, ) 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" ]: check_api.set_check_run( event_pull, "Summary", "completed", "failure", output={ "title": "The Mergify configuration is invalid", "summary": str(e), }, ) return subscription = sub_utils.get_subscription(utils.get_redis_for_cache(), installation_id) if repo.private and not subscription["subscription_active"]: check_api.set_check_run( event_pull, "Summary", "completed", "failure", output={ "title": "Mergify is disabled", "summary": subscription["subscription_reason"], }, ) return # CheckRun are attached to head sha, so when user add commits or force push # we can't directly get the previous Mergify Summary. So we copy it here, then # anything that looks at it in next celery tasks will find it. if event_type == "pull_request" and data["action"] == "synchronize": copy_summary_from_previous_head_sha(event_pull, data["before"]) commands_runner.spawn_pending_commands_tasks(installation_id, event_type, data, event_pull) if event_type == "issue_comment": commands_runner.run_command.s(installation_id, event_type, data, data["comment"]["body"]).apply_async() else: actions_runner.handle.s( installation_id, mergify_config["pull_request_rules"].as_dict(), event_type, data, ).apply_async()
async def run( ctxt: context.Context, sources: typing.List[context.T_PayloadEventSource], ) -> None: LOG.debug("engine get context") ctxt.log.debug("engine start processing context") issue_comment_sources: typing.List[T_PayloadEventIssueCommentSource] = [] for source in sources: if source["event_type"] == "issue_comment": issue_comment_sources.append( typing.cast(T_PayloadEventIssueCommentSource, source)) else: ctxt.sources.append(source) ctxt.log.debug("engine run pending commands") await commands_runner.run_pending_commands_tasks(ctxt) if issue_comment_sources: ctxt.log.debug("engine handle commands") for ic_source in issue_comment_sources: await commands_runner.handle( ctxt, ic_source["data"]["comment"]["body"], ic_source["data"]["comment"]["user"], ) if not ctxt.sources: return if ctxt.client.auth.permissions_need_to_be_updated: await ctxt.set_summary_check( check_api.Result( check_api.Conclusion.FAILURE, title="Required GitHub permissions are missing.", summary="You can accept them at https://dashboard.mergify.io/", )) return config_file = await ctxt.repository.get_mergify_config_file() ctxt.log.debug("engine check configuration change") if await _check_configuration_changes(ctxt, config_file): ctxt.log.info("Configuration changed, ignoring") return ctxt.log.debug("engine get configuration") if config_file is None: ctxt.log.info("No need to proceed queue (.mergify.yml is missing)") return # BRANCH CONFIGURATION CHECKING try: mergify_config = rules.get_mergify_config(config_file) except rules.InvalidRules as e: # pragma: no cover ctxt.log.info( "The Mergify configuration is invalid", summary=str(e), annotations=e.get_annotations(e.filename), ) # Not configured, post status check with the error message for s in ctxt.sources: if s["event_type"] == "pull_request": event = typing.cast(github_types.GitHubEventPullRequest, s["data"]) if event["action"] in ("opened", "synchronize"): await ctxt.set_summary_check( check_api.Result( check_api.Conclusion.FAILURE, title="The Mergify configuration is invalid", summary=str(e), annotations=e.get_annotations(e.filename), )) break return # Add global and mandatory rules mergify_config["pull_request_rules"].rules.extend( DEFAULT_PULL_REQUEST_RULES.rules) if ctxt.pull["base"]["repo"][ "private"] and not ctxt.subscription.has_feature( subscription.Features.PRIVATE_REPOSITORY): ctxt.log.info("mergify disabled: private repository") await ctxt.set_summary_check( check_api.Result( check_api.Conclusion.FAILURE, title="Mergify is disabled", summary=ctxt.subscription.reason, )) return await _ensure_summary_on_head_sha(ctxt) # NOTE(jd): that's fine for now, but I wonder if we wouldn't need a higher abstraction # to have such things run properly. Like hooks based on events that you could # register. It feels hackish otherwise. for s in ctxt.sources: if s["event_type"] == "pull_request": event = typing.cast(github_types.GitHubEventPullRequest, s["data"]) if event["action"] == "closed": await ctxt.clear_cached_last_summary_head_sha() break ctxt.log.debug("engine handle actions") if ctxt.is_merge_queue_pr(): await queue_runner.handle(mergify_config["queue_rules"], ctxt) else: await actions_runner.handle(mergify_config["pull_request_rules"], ctxt)
async def _check_configuration_changes( ctxt: context.Context, current_mergify_config_file: typing.Optional[context.MergifyConfigFile], ) -> bool: if ctxt.pull["base"]["repo"]["default_branch"] != ctxt.pull["base"]["ref"]: return False if ctxt.closed: # merge_commit_sha is a merge between the PR and the base branch only when the pull request is open # after it's None or the resulting commit of the pull request merge (maybe a rebase, squash, merge). # As the PR is closed, we don't care about the config change detector. return False # NOTE(sileht): This heuristic works only if _ensure_summary_on_head_sha() # is called after _check_configuration_changes(). # If we don't have the real summary yet it means the pull request has just # been open or synchronize or we never see it. summary = await ctxt.get_engine_check_run(constants.SUMMARY_NAME) if summary and summary["output"]["title"] not in ( constants.INITIAL_SUMMARY_TITLE, constants.CONFIGURATION_MUTIPLE_FOUND_SUMMARY_TITLE, ): if await ctxt.get_engine_check_run( constants.CONFIGURATION_CHANGED_CHECK_NAME): return True elif await ctxt.get_engine_check_run( constants.CONFIGURATION_DELETED_CHECK_NAME): return True else: return False preferred_filename = (None if current_mergify_config_file is None else current_mergify_config_file["path"]) # NOTE(sileht): pull.base.sha is unreliable as its the sha when the PR is # open and not the merge-base/fork-point. So we compare the configuration from the base # branch with the one of the merge commit. If the configuration is changed by the PR, they will be # different. config_files: typing.Dict[str, context.MergifyConfigFile] = {} async for config_file in ctxt.repository.iter_mergify_config_files( ref=ctxt.pull["merge_commit_sha"], preferred_filename=preferred_filename): config_files[config_file["path"]] = config_file if len(config_files) >= 2: raise MultipleConfigurationFileFound(list(config_files.values())) future_mergify_config_file = (list(config_files.values())[0] if config_files else None) if current_mergify_config_file is None: if future_mergify_config_file is None: return False else: if future_mergify_config_file is None: # Configuration is deleted by the pull request await check_api.set_check_run( ctxt, constants.CONFIGURATION_DELETED_CHECK_NAME, check_api.Result( check_api.Conclusion.SUCCESS, title="The Mergify configuration has been deleted", summary= "Mergify will still continue to listen to commands.", ), ) return True elif (current_mergify_config_file["path"] == future_mergify_config_file["path"] and current_mergify_config_file["sha"] == future_mergify_config_file["sha"]): # Nothing change between main branch and the pull request return False try: rules.get_mergify_config(future_mergify_config_file) except rules.InvalidRules as e: # Not configured, post status check with the error message await check_api.set_check_run( ctxt, constants.CONFIGURATION_CHANGED_CHECK_NAME, check_api.Result( check_api.Conclusion.FAILURE, title="The new Mergify configuration is invalid", summary=str(e), annotations=e.get_annotations(e.filename), ), ) else: await check_api.set_check_run( ctxt, constants.CONFIGURATION_CHANGED_CHECK_NAME, check_api.Result( check_api.Conclusion.SUCCESS, title="The new Mergify configuration is valid", summary="This pull request has to be merged manually", ), ) return True