def process_queue(queue): queue_log = get_queue_logger(queue) _, installation_id, owner, repo, queue_base_branch = queue.split("~") pull_numbers = _get_pulls(queue) queue_log.debug("%d pulls queued", len(pull_numbers), queue=list(pull_numbers)) if not pull_numbers: queue_log.debug("no pull request for this queue") return pull_number = int(pull_numbers[0]) try: installation = github.get_installation(owner, repo, int(installation_id)) except exceptions.MergifyNotInstalled: _delete_queue(queue) return with github.get_client(owner, repo, installation) as client: data = client.item(f"pulls/{pull_number}") try: ctxt = mergify_context.MergifyContext(client, data) except exceptions.RateLimited as e: log = ctxt.log if ctxt else queue_log log.info("rate limited", remaining_seconds=e.countdown) return except exceptions.MergeableStateUnknown as e: # pragma: no cover e.ctxt.log.warning( "pull request with mergeable_state unknown retrying later", ) _move_pull_at_end(e.ctxt) return try: if ctxt.pull["base"]["ref"] != queue_base_branch: ctxt.log.debug( "pull request base branch have changed", old_branch=queue_base_branch, new_branch=ctxt.pull["base"]["ref"], ) _move_pull_to_new_base_branch(ctxt, queue_base_branch) elif ctxt.pull["state"] == "closed" or ctxt.is_behind: # NOTE(sileht): Pick up this pull request and rebase it again # or update its status and remove it from the queue ctxt.log.debug( "pull request needs to be updated again or has been closed", ) _handle_first_pull_in_queue(queue, ctxt) else: # NOTE(sileht): Pull request has not been merged or cancelled # yet wait next loop ctxt.log.debug("pull request checks are still in progress") except Exception: # pragma: no cover ctxt.log.error("Fail to process merge queue", exc_info=True) _move_pull_at_end(ctxt)
def test_get_commits_to_cherry_pick_rebase(commits): c1 = {"sha": "c1f", "parents": [], "commit": {"message": "foobar"}} c2 = {"sha": "c2", "parents": [c1], "commit": {"message": "foobar"}} commits.return_value = [c1, c2] client = mock.Mock() client.auth.get_access_token.return_value = "<token>" client.items.side_effect = fake_get_github_pulls_from_sha ctxt = mergify_context.MergifyContext( client, { "number": 6, "merged": True, "state": "closed", "html_url": "<html_url>", "base": { "ref": "ref", "repo": { "full_name": "user/ref", "name": "name", "private": False }, }, "head": { "ref": "fork", "repo": { "full_name": "fork/other", "name": "other", "private": False }, }, "user": { "login": "******" }, "merged_by": None, "merged_at": None, "mergeable_state": "clean", }, ) base_branch = {"sha": "base_branch", "parents": []} rebased_c1 = {"sha": "rebased_c1", "parents": [base_branch]} rebased_c2 = {"sha": "rebased_c2", "parents": [rebased_c1]} assert duplicate_pull._get_commits_to_cherrypick(ctxt, rebased_c2) == [ rebased_c1, rebased_c2, ]
def run_command_async(installation_id, pull_request_raw, sources, comment, user, rerun=False): owner = pull_request_raw["base"]["user"]["login"] repo = pull_request_raw["base"]["repo"]["name"] try: installation = github.get_installation(owner, repo, installation_id) except exceptions.MergifyNotInstalled: return with github.get_client(owner, repo, installation) as client: pull = mergify_context.MergifyContext(client, pull_request_raw) return run_command(pull, sources, comment, user, rerun)
def PullRequestUrl(v): _, owner, repo, _, pull_number = urlsplit(v).path.split("/") pull_number = int(pull_number) try: installation = github.get_installation(owner, repo) except exceptions.MergifyNotInstalled: raise PullRequestUrlInvalid( message="Mergify not installed on repository '%s'" % owner) with github.get_client(owner, repo, installation) as client: try: data = client.item(f"pulls/{pull_number}") except httpx.HTTPNotFound: raise PullRequestUrlInvalid( message=("Pull request '%s' not found" % v)) return mergify_context.MergifyContext(client, data)
def test_pull_behind(commits_tree_generator): expected, commits = commits_tree_generator client = mock.Mock() client.items.return_value = commits # /pulls/X/commits client.item.return_value = {"commit": {"sha": "base"}} # /branch/#foo ctxt = mergify_context.MergifyContext( client, pull={ "number": 1, "mergeable_state": "clean", "state": "open", "merged": False, "merged_at": None, "merged_by": None, "base": { "ref": "#foo" }, }, ) assert expected == ctxt.is_behind
def report(url): redis = utils.get_redis_for_cache() path = url.replace("https://github.com/", "") try: owner, repo, _, pull_number = path.split("/") except ValueError: print(f"Wrong URL: {url}") return slug = owner + "/" + repo try: installation = github.get_installation(owner, repo) except exceptions.MergifyNotInstalled: print("* Mergify is not installed there") return client = github.get_client(owner, repo, installation) print("* INSTALLATION ID: %s" % client.installation["id"]) cached_sub = sub_utils.get_subscription(redis, client.installation["id"]) db_sub = sub_utils._retrieve_subscription_from_db( client.installation["id"]) print("* SUBSCRIBED (cache/db): %s / %s" % (cached_sub["subscription_active"], db_sub["subscription_active"])) report_sub(client.installation["id"], slug, cached_sub, "ENGINE-CACHE") report_sub(client.installation["id"], slug, db_sub, "DASHBOARD") pull_raw = client.item(f"pulls/{pull_number}") ctxt = mergify_context.MergifyContext(client, pull_raw) print("* REPOSITORY IS %s" % "PRIVATE" if ctxt.pull["base"]["repo"]["private"] else "PUBLIC") print("* CONFIGURATION:") try: mergify_config_content = rules.get_mergify_config_content(ctxt) except rules.NoRules: # pragma: no cover print(".mergify.yml is missing") pull_request_rules = None else: print(mergify_config_content.decode()) try: mergify_config = rules.UserConfigurationSchema( mergify_config_content) except rules.InvalidRules as e: # pragma: no cover print("configuration is invalid %s" % str(e)) else: pull_request_rules_raw = mergify_config[ "pull_request_rules"].as_dict() pull_request_rules_raw["rules"].extend( engine.MERGIFY_RULE["rules"]) pull_request_rules = rules.PullRequestRules( **pull_request_rules_raw) print("* PULL REQUEST:") pprint.pprint(ctxt.to_dict(), width=160) print("is_behind: %s" % ctxt.is_behind) print("mergeable_state: %s" % ctxt.pull["mergeable_state"]) print("* MERGIFY LAST CHECKS:") checks = list(check_api.get_checks(ctxt, mergify_only=True)) for c in checks: print("[%s]: %s | %s" % (c["name"], c["conclusion"], c["output"].get("title"))) print("> " + "\n> ".join(c["output"].get("summary").split("\n"))) if pull_request_rules is not None: print("* MERGIFY LIVE MATCHES:") match = pull_request_rules.get_pull_request_rule(ctxt) summary_title, summary = actions_runner.gen_summary( ctxt, [{ "event_type": "refresh", "data": {} }], match) print("> %s" % summary_title) print(summary) return ctxt
def test_get_pull_request_rule(): client = mock.Mock() get_reviews = [{ "user": { "login": "******", "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }] get_files = [{"filename": "README.rst"}, {"filename": "setup.py"}] get_team_members = [{"login": "******"}, {"login": "******"}] get_checks = [] get_statuses = [{ "context": "continuous-integration/fake-ci", "state": "success" }] client.item.return_value = {"permission": "write"} # get review user perm client.items.side_effect = [ get_reviews, get_files, get_checks, get_statuses, get_team_members, ] ctxt = mergify_context.MergifyContext( client, { "number": 1, "html_url": "<html_url>", "state": "closed", "merged_by": None, "merged_at": None, "merged": False, "milestone": None, "mergeable_state": "unstable", "assignees": [], "labels": [], "author": "jd", "base": { "ref": "master", "repo": { "name": "name", "private": False }, }, "head": { "ref": "myfeature", "sha": "<sha>" }, "locked": False, "requested_reviewers": [], "requested_teams": [], "title": "My awesome job", "body": "I rock", "user": { "login": "******" }, }, ) # Don't catch data in these tests ctxt.to_dict = ctxt._get_consolidated_data # Empty conditions pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": [], "actions": {} }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert [(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule["actions"] == {} pull_request_rules = rules.PullRequestRules([{ "name": "hello", "conditions": ["base:master"], "actions": {} }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["hello"] assert [r["name"] for r, _ in match.matching_rules] == ["hello"] assert [(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule["actions"] == {} pull_request_rules = rules.PullRequestRules([ { "name": "hello", "conditions": ["base:master"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["hello", "backport"] assert [r["name"] for r, _ in match.matching_rules] == ["hello", "backport"] assert [(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule["actions"] == {} pull_request_rules = rules.PullRequestRules([ { "name": "hello", "conditions": ["#files=3"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["hello", "backport"] assert [r["name"] for r, _ in match.matching_rules] == ["backport"] for rule in match.rules: assert rule["actions"] == {} pull_request_rules = rules.PullRequestRules([ { "name": "hello", "conditions": ["#files=2"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["hello", "backport"] assert [r["name"] for r, _ in match.matching_rules] == ["hello", "backport"] assert [(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule["actions"] == {} # No match pull_request_rules = rules.PullRequestRules([{ "name": "merge", "conditions": [ "base=xyz", "status-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["merge"] assert [r["name"] for r, _ in match.matching_rules] == [] pull_request_rules = rules.PullRequestRules([{ "name": "merge", "conditions": [ "base=master", "status-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["merge"] assert [r["name"] for r, _ in match.matching_rules] == ["merge"] assert [(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule["actions"] == {} pull_request_rules = rules.PullRequestRules([ { "name": "merge", "conditions": [ "base=master", "status-success=continuous-integration/fake-ci", "#approved-reviews-by>=2", ], "actions": {}, }, { "name": "fast merge", "conditions": [ "base=master", "label=fast-track", "status-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge with alternate ci", "conditions": [ "base=master", "label=fast-track", "status-success=continuous-integration/fake-ci-bis", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge from a bot", "conditions": [ "base=master", "author=mybot", "status-success=continuous-integration/fake-ci", ], "actions": {}, }, ]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == [ "merge", "fast merge", "fast merge with alternate ci", "fast merge from a bot", ] assert [r["name"] for r, _ in match.matching_rules] == [ "merge", "fast merge", "fast merge with alternate ci", ] for rule in match.rules: assert rule["actions"] == {} assert match.matching_rules[0][0]["name"] == "merge" assert len(match.matching_rules[0][1]) == 1 assert str(match.matching_rules[0][1][0]) == "#approved-reviews-by>=2" assert match.matching_rules[1][0]["name"] == "fast merge" assert len(match.matching_rules[1][1]) == 1 assert str(match.matching_rules[1][1][0]) == "label=fast-track" assert match.matching_rules[2][0]["name"] == "fast merge with alternate ci" assert len(match.matching_rules[2][1]) == 2 assert str(match.matching_rules[2][1][0]) == "label=fast-track" assert (str(match.matching_rules[2][1][1]) == "status-success=continuous-integration/fake-ci-bis") # Team conditions with one review missing pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": [ "approved-reviews-by=@orgs/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 1 assert str(match.matching_rules[0][1][0]) == "#approved-reviews-by>=2" get_reviews.append({ "user": { "login": "******", "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }) client.items.side_effect = [ get_reviews, get_files, get_checks, get_statuses, get_team_members, ] # Drop caches del ctxt.__dict__["checks"] del ctxt.__dict__["reviews"] del ctxt.__dict__["files"] del ctxt.__dict__["consolidated_reviews"] # Team conditions with no review missing pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": [ "approved-reviews-by=@orgs/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 0 # Forbidden labels, when no label set pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": ["-label~=^(status/wip|status/blocked|review/need2)$"], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 0 # Forbidden labels, when forbiden label set ctxt.pull["labels"] = [{"name": "status/wip"}] match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 1 assert str(match.matching_rules[0][1][0]) == ( "-label~=^(status/wip|status/blocked|review/need2)$") # Forbidden labels, when other label set ctxt.pull["labels"] = [{"name": "allowed"}] match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 0 # Test team expander pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": ["author~=^(user1|user2|another-jd)$"], "actions": {}, }]) match = pull_request_rules.get_pull_request_rule(ctxt) assert [r["name"] for r in match.rules] == ["default"] assert [r["name"] for r, _ in match.matching_rules] == ["default"] assert match.matching_rules[0][0]["name"] == "default" assert len(match.matching_rules[0][1]) == 0
def _run(client, event_type, data): 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=client.owner, gh_repo=client.repo, ) return ctxt = mergify_context.MergifyContext(client, raw_pull) # Override pull_request with the updated one data["pull_request"] = ctxt.pull ctxt.log.info("Pull request found in the event %s", event_type) if ("base" not in ctxt.pull or "repo" not in ctxt.pull["base"] or len(list(ctxt.pull["base"]["repo"].keys())) < 70): ctxt.log.warning( "the pull request payload looks suspicious", event_type=event_type, data=data, ) if (event_type == "status" and ctxt.pull["head"]["sha"] != data["sha"]): # pragma: no cover ctxt.log.info( "No need to proceed queue (got status of an old commit)", ) return elif (event_type in ["status", "check_suite", "check_run"] and ctxt.pull["merged"]): # pragma: no cover ctxt.log.info( "No need to proceed queue (got status of a merged pull request)", ) return elif (event_type in ["check_suite", "check_run"] and ctxt.pull["head"]["sha"] != data[event_type]["head_sha"]): # pragma: no cover ctxt.log.info( "No need to proceed queue (got %s of an old " "commit)", event_type, ) return if check_configuration_changes(ctxt): ctxt.log.info("Configuration changed, ignoring", ) return # BRANCH CONFIGURATION CHECKING try: 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 event_type == "pull_request" and data["action"] in [ "opened", "synchronize" ]: check_api.set_check_run( ctxt, "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(), client.installation["id"]) if ctxt.pull["base"]["repo"][ "private"] and not subscription["subscription_active"]: check_api.set_check_run( ctxt, "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(ctxt, data["before"]) sources = [{"event_type": event_type, "data": data}] commands_runner.spawn_pending_commands_tasks(ctxt, sources) if event_type == "issue_comment": commands_runner.run_command(ctxt, sources, data["comment"]["body"], data["comment"]["user"]) else: actions_runner.handle(mergify_config["pull_request_rules"], ctxt, sources)