def handle(installation_id, pull_request_rules_raw, event_type, data): installation_token = utils.get_installation_token(installation_id) if not installation_token: return # Some mandatory rules pull_request_rules_raw["rules"].extend(MERGIFY_RULE["rules"]) pull_request_rules = rules.PullRequestRules(**pull_request_rules_raw) pull = mergify_pull.MergifyPull.from_raw( installation_id, installation_token, data["pull_request"] ) match = pull_request_rules.get_pull_request_rule(pull) checks = dict( (c.name, c) for c in check_api.get_checks(pull.g_pull, mergify_only=True) ) summary_check = checks.get(SUMMARY_NAME) previous_conclusions = load_conclusions(pull, summary_check) conclusions = run_actions( installation_id, installation_token, event_type, data, pull, match, checks, previous_conclusions, ) post_summary(event_type, data, pull, match, summary_check, conclusions)
def test_pull_request_rule_schema_invalid(): for invalid, match in ( ({ "name": "hello", "conditions": ["this is wrong"], "actions": {}, }, "Invalid condition "), ({ "name": "hello", "conditions": ["head|4"], "actions": {}, }, "Invalid condition "), ({ "name": "hello", "conditions": [{ "foo": "bar" }], "actions": {}, }, r"expected str @ data\[0\]\['conditions'\]\[0\]"), ({ "name": "hello", "conditions": [], "actions": {}, "foobar": True, }, "extra keys not allowed"), ({ "name": "hello", "conditions": [], "actions": { "merge": True, }, }, r"expected a dictionary for dictionary value " r"@ data\[0\]\['actions'\]\['merge'\]"), ({ "name": "hello", "conditions": [], "actions": { "backport": True, }, }, r"expected a dictionary for dictionary value " r"@ data\[0\]\['actions'\]\['backport'\]"), ({ "name": "hello", "conditions": [], "actions": { "merge": { "strict": "yes", }, }, }, r"expected bool for dictionary value @ " r"data\[0\]\['actions'\]\['merge'\]\['strict'\]"), ): with pytest.raises(voluptuous.MultipleInvalid, match=match): rules.PullRequestRules([invalid])
def handle(installation_id, installation_token, subscription, pull_request_rules_raw, event_type, data, pull_raw): pull_request_rules = rules.PullRequestRules(**pull_request_rules_raw) pull = mergify_pull.MergifyPull.from_raw(installation_id, installation_token, pull_raw) match = pull_request_rules.get_pull_request_rule(pull) checks = dict((c.name, c) for c in check_api.get_checks(pull.g_pull) if c._rawData['app']['id'] == config.INTEGRATION_ID) post_summary(pull, match, checks) run_actions(installation_id, installation_token, subscription, event_type, data, pull, match, checks)
def test_pull_request_rule(): for valid in ({ "name": "hello", "conditions": [ "head:master", ], "actions": {}, }, { "name": "hello", "conditions": [ "base:foo", "base:baz", ], "actions": {}, }): rules.PullRequestRules([valid])
def test_convert_with_some_none(): old_rules = { 'rules': { 'default': None, 'branches': { '^stable/.*': { 'protection': None, 'merge_strategy': { 'method': 'rebase', 'rebase_fallback': "merge", }, }, '^unstable/.*': { 'required_pull_request_reviews': None, 'required_status_checks': None, }, }, }, } converted = convert.convert_config(old_rules["rules"]) assert converted == [{ "name": "^stable/.* branch", "conditions": ["base~=^stable/.*", "label!=no-mergify"], "actions": { "merge": { "method": "rebase", "rebase_fallback": "merge", }, }, }, { "name": "^unstable/.* branch", "conditions": ["base~=^unstable/.*", "label!=no-mergify", '#approved-reviews-by>=1'], "actions": { "merge": { "method": "merge", "rebase_fallback": "merge", }, } }] # Validate generated conf with the schema rules.PullRequestRules(converted)
def test_null_branch(): old_rules = { 'rules': { 'default': { 'protection': { 'required_pull_request_reviews': { 'required_approving_review_count': 1 }, 'required_status_checks': { 'contexts': ['continuous-integration/travis-ci'], } }, 'merge_strategy': { 'method': 'rebase', } }, 'branches': { 'gh-pages': None, }, }, } converted = convert.convert_config(old_rules["rules"]) assert converted == [ { "name": "default", "conditions": [ "-base=gh-pages", "label!=no-mergify", "#approved-reviews-by>=1", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": "merge", }, }, }, ] # Validate generated conf with the schema rules.PullRequestRules(converted)
def handle(installation_id, pull_request_rules_raw, event_type, data, pull_raw): installation_token = utils.get_installation_token(installation_id) if not installation_token: return # Some mandatory rules pull_request_rules_raw["rules"].extend(MERGIFY_RULE["rules"]) pull_request_rules = rules.PullRequestRules(**pull_request_rules_raw) pull = mergify_pull.MergifyPull.from_raw(installation_id, installation_token, pull_raw) match = pull_request_rules.get_pull_request_rule(pull) checks = dict((c.name, c) for c in check_api.get_checks(pull.g_pull) if c._rawData['app']['id'] == config.INTEGRATION_ID) post_summary(event_type, data, pull, match, checks) run_actions(installation_id, installation_token, event_type, data, pull, match, checks)
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 integration = github.GithubIntegration(config.INTEGRATION_ID, config.PRIVATE_KEY) install_id = utils.get_installation_id(integration, owner, repo=repo) print("* INSTALLATION ID: %s" % install_id) cached_sub = sub_utils.get_subscription(redis, install_id) db_sub = sub_utils._retrieve_subscription_from_db(install_id) print("* SUBSCRIBED (cache/db): %s / %s" % (cached_sub["subscription_active"], db_sub["subscription_active"])) report_sub(install_id, slug, cached_sub, "ENGINE-CACHE") report_sub(install_id, slug, db_sub, "DASHBOARD") installation_token = integration.get_access_token(install_id).token g = github.Github(installation_token, base_url="https://api.%s" % config.GITHUB_DOMAIN) r = g.get_repo(owner + "/" + repo) print("* REPOSITORY IS %s" % "PRIVATE" if r.private else "PUBLIC") print("* CONFIGURATION:") try: mergify_config_content = rules.get_mergify_config_content(r) except rules.NoRules: # pragma: no cover print(".mergify.yml is missing") 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( actions_runner.MERGIFY_RULE["rules"]) pull_request_rules = rules.PullRequestRules(**pull_request_rules_raw) try: p = r.get_pull(int(pull_number)) except github.UnknownObjectException: print("Wrong pull request number") return g, None mp = mergify_pull.MergifyPull(g, p, install_id) print("* PULL REQUEST:") pprint.pprint(mp.to_dict(), width=160) try: print("is_behind: %s" % mp.is_behind()) except github.GithubException as e: print("Unable to know if pull request branch is behind: %s" % e) print("mergeable_state: %s" % mp.g_pull.mergeable_state) print("* MERGIFY LAST CHECKS:") checks = list(check_api.get_checks(p)) for c in checks: if c._rawData["app"]["id"] == config.INTEGRATION_ID: print("[%s]: %s | %s" % (c.name, c.conclusion, c.output.get("title"))) print("> " + "\n> ".join(c.output.get("summary").split("\n"))) print("* MERGIFY LIVE MATCHES:") match = pull_request_rules.get_pull_request_rule(mp) summary_title, summary = actions_runner.gen_summary( "refresh", {}, mp, match) print("> %s" % summary_title) print(summary) return g, p
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(): g = mock.Mock() team = mock.Mock() team.slug = "my-reviewers" team.get_members.return_value = [ mock.Mock(login="******"), mock.Mock(login="******") ] org = mock.Mock() org.get_teams.return_value = [team] g.get_organization.return_value = org g_pull = mock.Mock() g_pull.assignees = [] g_pull.labels = [] g_pull.get_review_requests.return_value = ([], []) g_pull.author = "jd" g_pull.base.ref = "master" g_pull.head.ref = "myfeature" g_pull.base.repo.get_collaborator_permission.return_value = "write" g_pull._rawData = {'locked': False} g_pull.title = "My awesome job" g_pull.body = "I rock" g_pull.user.login = "******" file1 = mock.Mock() file1.filename = "README.rst" file2 = mock.Mock() file2.filename = "setup.py" g_pull.get_files.return_value = [file1, file2] review = mock.Mock() review.user.login = "******" review.state = "APPROVED" review._rawData = {"author_association": "MEMBER"} g_pull.get_reviews.return_value = [review] pull_request = mergify_pull.MergifyPull(g=g, g_pull=g_pull, installation_id=123) # Don't catch data in these tests pull_request.to_dict = pull_request._get_consolidated_data fake_ci = mock.Mock() fake_ci.context = "continuous-integration/fake-ci" fake_ci.state = "success" pull_request._get_checks = mock.Mock() pull_request._get_checks.return_value = [fake_ci] # Empty conditions pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": [], "actions": {} }]) match = pull_request_rules.get_pull_request_rule(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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" review2 = mock.Mock() review2.user.login = "******" review2.state = "APPROVED" review2._rawData = {"author_association": "MEMBER"} g_pull.get_reviews.return_value = [review, review2] # 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(pull_request) 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(pull_request) 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 label = mock.Mock() label.name = "status/wip" pull_request.g_pull.labels = [label] match = pull_request_rules.get_pull_request_rule(pull_request) 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 label = mock.Mock() label.name = "allowed" pull_request.g_pull.labels = [label] match = pull_request_rules.get_pull_request_rule(pull_request) 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(pull_request) 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 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 def client_items(url, *args, **kwargs): if url == "/repos/another-jd/name/pulls/1/reviews": return get_reviews elif url == "/repos/another-jd/name/pulls/1/files": return get_files elif url == "/repos/another-jd/name/commits/<sha>/check-runs": return get_checks elif url == "/repos/another-jd/name/commits/<sha>/status": return get_statuses elif url == "/orgs/orgs/teams/my-reviewers/members": return get_team_members else: raise RuntimeError(f"not handled url {url}") client.items.side_effect = client_items ctxt = context.Context( client, { "number": 1, "html_url": "<html_url>", "state": "closed", "merged_by": None, "merged_at": None, "merged": False, "draft": False, "milestone": None, "mergeable_state": "unstable", "assignees": [], "labels": [], "base": { "ref": "master", "repo": { "name": "name", "private": False }, "user": { "login": "******" }, "sha": "mew", }, "head": { "ref": "myfeature", "sha": "<sha>" }, "locked": False, "requested_reviewers": [], "requested_teams": [], "title": "My awesome job", "body": "I rock", "user": { "login": "******" }, }, {}, ) # Empty conditions pull_request_rules = rules.PullRequestRules( [rules.Rule(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 [rules.EvaluatedRule.from_rule(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([{ "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 [rules.EvaluatedRule.from_rule(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "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 [rules.EvaluatedRule.from_rule(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:foobar"], "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 = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:another-jd"], "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 [rules.EvaluatedRule.from_rule(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule.actions == {} # No match pull_request_rules = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=xyz", "check-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 = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=master", "check-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 [rules.EvaluatedRule.from_rule(r, []) for r in match.rules] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "merge", "conditions": [ "base=master", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=2", ], "actions": {}, }, { "name": "fast merge", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge with alternate ci", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci-bis", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge from a bot", "conditions": [ "base=master", "author=mybot", "check-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].name == "merge" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") assert match.matching_rules[1].name == "fast merge" assert len(match.matching_rules[1].missing_conditions) == 1 assert str( match.matching_rules[1].missing_conditions[0]) == "label=fast-track" assert match.matching_rules[2].name == "fast merge with alternate ci" assert len(match.matching_rules[2].missing_conditions) == 2 assert str( match.matching_rules[2].missing_conditions[0]) == "label=fast-track" assert (str(match.matching_rules[2].missing_conditions[1]) == "check-success=continuous-integration/fake-ci-bis") # Team conditions with one review missing pull_request_rules = pull_request_rule_from_list([{ "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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") get_reviews.append({ "user": { "login": "******", "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }) del ctxt.__dict__["reviews"] del ctxt.__dict__["consolidated_reviews"] # Team conditions with no review missing pull_request_rules = pull_request_rule_from_list([{ "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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Forbidden labels, when no label set pull_request_rules = pull_request_rule_from_list([{ "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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert str(match.matching_rules[0].missing_conditions[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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Test team expander pull_request_rules = pull_request_rule_from_list([{ "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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0
async def test_get_pull_request_rule(redis_cache: utils.RedisCache) -> None: client = mock.Mock() get_reviews = [{ "user": { "login": "******", "id": 12321, "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }] get_files = [{"filename": "README.rst"}, {"filename": "setup.py"}] get_team_members = [{ "login": "******", "id": 12321 }, { "login": "******", "id": 2644 }] get_checks: typing.List[github_types.GitHubCheckRun] = [] get_statuses: typing.List[github_types.GitHubStatus] = [{ "context": "continuous-integration/fake-ci", "state": "success", "description": "foobar", "target_url": "http://example.com", "avatar_url": "", }] async def client_item(url, *args, **kwargs): if url == "/repos/another-jd/name/collaborators/sileht/permission": return {"permission": "write"} elif url == "/repos/another-jd/name/collaborators/jd/permission": return {"permission": "write"} raise RuntimeError(f"not handled url {url}") client.item.side_effect = client_item async def client_items(url, *args, **kwargs): if url == "/repos/another-jd/name/pulls/1/reviews": for r in get_reviews: yield r elif url == "/repos/another-jd/name/pulls/1/files": for f in get_files: yield f elif url == "/repos/another-jd/name/commits/<sha>/check-runs": for c in get_checks: yield c elif url == "/repos/another-jd/name/commits/<sha>/status": for s in get_statuses: yield s elif url == "/orgs/another-jd/teams/my-reviewers/members": for tm in get_team_members: yield tm else: raise RuntimeError(f"not handled url {url}") client.items.side_effect = client_items installation = context.Installation( github_types.GitHubAccountIdType(2644), github_types.GitHubLogin("another-jd"), subscription.Subscription(redis_cache, 0, False, "", frozenset()), client, redis_cache, ) repository = context.Repository( installation, github_types.GitHubRepositoryName("name"), github_types.GitHubRepositoryIdType(123321), ) ctxt = await context.Context.create( repository, github_types.GitHubPullRequest({ "id": github_types.GitHubPullRequestId(0), "number": github_types.GitHubPullRequestNumber(1), "commits": 1, "html_url": "<html_url>", "merge_commit_sha": None, "maintainer_can_modify": True, "rebaseable": True, "state": "closed", "merged_by": None, "merged_at": None, "merged": False, "draft": False, "mergeable_state": "unstable", "labels": [], "changed_files": 1, "base": { "label": "repo", "ref": github_types.GitHubRefType("master"), "repo": { "id": github_types.GitHubRepositoryIdType(123321), "name": github_types.GitHubRepositoryName("name"), "full_name": "another-jd/name", "private": False, "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "owner": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, "sha": github_types.SHAType("mew"), }, "head": { "label": "foo", "ref": github_types.GitHubRefType("myfeature"), "sha": github_types.SHAType("<sha>"), "repo": { "id": github_types.GitHubRepositoryIdType(123321), "name": github_types.GitHubRepositoryName("head"), "full_name": "another-jd/head", "private": False, "archived": False, "url": "", "default_branch": github_types.GitHubRefType(""), "owner": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }, "title": "My awesome job", "user": { "login": github_types.GitHubLogin("another-jd"), "id": github_types.GitHubAccountIdType(2644), "type": "User", "avatar_url": "", }, }), ) # Empty conditions pull_request_rules = rules.PullRequestRules([ rules.Rule(name="default", conditions=rules.RuleConditions([]), actions={}) ]) match = await 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 [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([{ "name": "hello", "conditions": ["base:master"], "actions": {} }]) match = await 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 [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["base:master"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await 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 [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:foobar"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await 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 = pull_request_rule_from_list([ { "name": "hello", "conditions": ["author:another-jd"], "actions": {} }, { "name": "backport", "conditions": ["base:master"], "actions": {} }, ]) match = await 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 [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} # No match pull_request_rules = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=xyz", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = await 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 = pull_request_rule_from_list([{ "name": "merge", "conditions": [ "base=master", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }]) match = await 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 [ rules.EvaluatedRule.from_rule(r, rules.RuleMissingConditions([]), []) for r in match.rules ] == match.matching_rules for rule in match.rules: assert rule.actions == {} pull_request_rules = pull_request_rule_from_list([ { "name": "merge", "conditions": [ "base=master", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=2", ], "actions": {}, }, { "name": "fast merge", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge with alternate ci", "conditions": [ "base=master", "label=fast-track", "check-success=continuous-integration/fake-ci-bis", "#approved-reviews-by>=1", ], "actions": {}, }, { "name": "fast merge from a bot", "conditions": [ "base=master", "author=mybot", "check-success=continuous-integration/fake-ci", ], "actions": {}, }, ]) match = await 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].name == "merge" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") assert match.matching_rules[1].name == "fast merge" assert len(match.matching_rules[1].missing_conditions) == 1 assert str( match.matching_rules[1].missing_conditions[0]) == "label=fast-track" assert match.matching_rules[2].name == "fast merge with alternate ci" assert len(match.matching_rules[2].missing_conditions) == 2 assert str( match.matching_rules[2].missing_conditions[0]) == "label=fast-track" assert (str(match.matching_rules[2].missing_conditions[1]) == "check-success=continuous-integration/fake-ci-bis") # Team conditions with one review missing pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [ "approved-reviews-by=@another-jd/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert (str(match.matching_rules[0].missing_conditions[0]) == "#approved-reviews-by>=2") get_reviews.append({ "user": { "login": "******", "id": 2644, "type": "User" }, "state": "APPROVED", "author_association": "MEMBER", }) del ctxt._cache["reviews"] del ctxt._cache["consolidated_reviews"] # Team conditions with no review missing pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [ "approved-reviews-by=@another-jd/my-reviewers", "#approved-reviews-by>=2", ], "actions": {}, }]) match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Forbidden labels, when no label set pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": ["-label~=^(status/wip|status/blocked|review/need2)$"], "actions": {}, }]) match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Forbidden labels, when forbiden label set ctxt.pull["labels"] = [{ "id": 0, "color": "#1234", "default": False, "name": "status/wip" }] match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 1 assert str(match.matching_rules[0].missing_conditions[0]) == ( "-label~=^(status/wip|status/blocked|review/need2)$") # Forbidden labels, when other label set ctxt.pull["labels"] = [{ "id": 0, "color": "#1234", "default": False, "name": "allowed" }] match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # Test team expander pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": ["author~=^(user1|user2|another-jd)$"], "actions": {}, }]) match = await 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].name == "default" assert len(match.matching_rules[0].missing_conditions) == 0 # branch protection async def client_item_with_branch_protection_enabled(url, *args, **kwargs): if url == "/repos/another-jd/name/branches/master": return { "protection": { "enabled": True, "required_status_checks": { "contexts": ["awesome-ci"] }, }, } raise RuntimeError(f"not handled url {url}") client.item.side_effect = client_item_with_branch_protection_enabled pull_request_rules = pull_request_rule_from_list([{ "name": "default", "conditions": [], "actions": { "merge": {}, "comment": { "message": "yo" } }, }]) match = await pull_request_rules.get_pull_request_rule(ctxt) assert [r.name for r in match.rules] == ["default", "default"] assert list(match.matching_rules[0].actions.keys()) == ["merge"] assert [str(c) for c in match.matching_rules[0].conditions ] == ["check-success=awesome-ci"] assert [str(c) for c in match.matching_rules[0].missing_conditions ] == ["check-success=awesome-ci"] assert list(match.matching_rules[1].actions.keys()) == ["comment"] assert match.matching_rules[1].conditions == []
def test_convert_simple(): old_rules = { 'rules': { 'default': { 'automated_backport_labels': { 'backport-to-3.1': 'stable/3.1', 'backport-to-3.0': 'stable/3.0', }, 'protection': { 'required_pull_request_reviews': { 'required_approving_review_count': 2 }, 'required_status_checks': { 'contexts': ['continuous-integration/travis-ci'], 'strict': True } }, 'merge_strategy': { 'method': 'rebase' } }, 'branches': { '^stable/.*': { 'protection': { 'required_pull_request_reviews': { 'required_approving_review_count': 1 } } } } } } converted = convert.convert_config(old_rules["rules"]) assert converted == [ { "name": "default", "conditions": [ "-base~=^stable/.*", "label!=no-mergify", "#approved-reviews-by>=2", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": "merge", "strict": True, }, }, }, { "name": "backport stable/3.0", "conditions": [ "-base~=^stable/.*", "label!=no-mergify", "label=backport-to-3.0" ], "actions": { "backport": { "branches": ["stable/3.0"], }, }, }, { "name": "backport stable/3.1", "conditions": [ "-base~=^stable/.*", "label!=no-mergify", "label=backport-to-3.1" ], "actions": { "backport": { "branches": ["stable/3.1"], }, }, }, { "name": "^stable/.* branch", "conditions": [ "base~=^stable/.*", "label!=no-mergify", "#approved-reviews-by>=1", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": "merge", "strict": True, }, }, }, { "name": "backport stable/3.0 from ^stable/.*", "conditions": ["base~=^stable/.*", "label!=no-mergify", "label=backport-to-3.0"], "actions": { "backport": { "branches": ["stable/3.0"], }, } }, { "name": "backport stable/3.1 from ^stable/.*", "conditions": ["base~=^stable/.*", "label!=no-mergify", "label=backport-to-3.1"], "actions": { "backport": { "branches": ["stable/3.1"], }, }, }, ] # Validate generated conf with the schema rules.PullRequestRules(converted)
def test_pull_request_rule_schema_invalid(): for invalid, match in ( ( { "name": "hello", "conditions": ["this is wrong"], "actions": {} }, "Invalid condition ", ), ( { "name": "invalid regexp", "conditions": ["head~=(lol"], "actions": {} }, r"Invalid condition 'head~=\(lol'. Invalid arguments: " r"missing \), " r"unterminated subpattern at position 0 @ ", ), ( { "name": "hello", "conditions": ["head|4"], "actions": {} }, "Invalid condition ", ), ( { "name": "hello", "conditions": [{ "foo": "bar" }], "actions": {} }, r"expected str @ data\[0\]\['conditions'\]\[0\]", ), ( { "name": "hello", "conditions": [], "actions": {}, "foobar": True }, "extra keys not allowed", ), ( { "name": "hello", "conditions": [], "actions": { "merge": True } }, r"expected a dictionary for dictionary value " r"@ data\[0\]\['actions'\]\['merge'\]", ), ( { "name": "hello", "conditions": [], "actions": { "backport": { "regexes": ["(azerty"] } }, }, r"missing \), unterminated subpattern at position 0 " r"@ data\[0\]\['actions'\]\['backport'\]\['regexes'\]\[0\]", ), ( { "name": "hello", "conditions": [], "actions": { "backport": True } }, r"expected a dictionary for dictionary value " r"@ data\[0\]\['actions'\]\['backport'\]", ), ( { "name": "hello", "conditions": [], "actions": { "merge": { "strict": "yes" } }, }, r"expected bool for dictionary value @ " r"data\[0\]\['actions'\]\['merge'\]\['strict'\]", ), ): with pytest.raises(voluptuous.MultipleInvalid, match=match): print(invalid) rules.PullRequestRules([invalid])
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 test_get_pull_request_rule(): g_pull = mock.Mock() g_pull.assignees = [] g_pull.labels = [] g_pull.get_review_requests.return_value = ([], []) g_pull.author = "jd" g_pull.base.ref = "master" g_pull.head.ref = "myfeature" g_pull.base.repo.get_collaborator_permission.return_value = "write" g_pull._rawData = {'locked': False} g_pull.title = "My awesome job" g_pull.body = "I rock" file1 = mock.Mock() file1.filename = "README.rst" file2 = mock.Mock() file2.filename = "setup.py" g_pull.get_files.return_value = [file1, file2] review = mock.Mock() review.user.login = "******" review.state = "APPROVED" review._rawData = {"author_association": "MEMBER"} g_pull.get_reviews.return_value = [review] pull_request = mergify_pull.MergifyPull(g_pull=g_pull, installation_id=123) fake_ci = mock.Mock() fake_ci.context = "continuous-integration/fake-ci" fake_ci.state = "success" pull_request._get_checks = mock.Mock() pull_request._get_checks.return_value = [fake_ci] # Empty conditions pull_request_rules = rules.PullRequestRules([{ "name": "default", "conditions": [], "actions": {} }]) match = pull_request_rules.get_pull_request_rule(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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(pull_request) 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" )
def report(url): redis = utils.get_redis_for_cache() path = url.replace("https://github.com/", "") owner, repo, _, pull_number = path.split("/") integration = github.GithubIntegration(config.INTEGRATION_ID, config.PRIVATE_KEY) install_id = utils.get_installation_id(integration, owner, repo=repo) print("* INSTALLATION ID: %s" % install_id) cached_sub = sub_utils.get_subscription(redis, install_id) db_sub = sub_utils._retrieve_subscription_from_db(install_id) print("* SUBSCRIBED (cache/db): %s / %s" % (cached_sub["subscription_active"], db_sub["subscription_active"])) print("* SUB DETAIL: %s" % db_sub["subscription_reason"]) print("* NUMBER OF CACHED TOKENS: %d" % len(cached_sub["tokens"])) try: for login, token in cached_sub["tokens"].items(): try: repos = get_repositories_setuped(token, install_id) except github.BadCredentialsException: print("** token for %s invalid" % login) except github.GithubException as e: if e.status != 401: raise print("** token for %s invalid" % login) else: if any((r["full_name"] == owner + "/" + repo) for r in repos): print("* MERGIFY INSTALLED AND ENABLED ON THIS REPOSITORY") else: print("* MERGIFY INSTALLED BUT DISABLED " "ON THIS REPOSITORY") break else: print("* MERGIFY DOESN'T HAVE ANY VALID OAUTH TOKENS") except github.UnknownObjectException: print("* MERGIFY SEEMS NOT INSTALLED") installation_token = integration.get_access_token(install_id).token g = github.Github(installation_token, base_url="https://api.%s" % config.GITHUB_DOMAIN) r = g.get_repo(owner + "/" + repo) print("* REPOSITORY IS %s" % "PRIVATE" if r.private else "PUBLIC") print("* CONFIGURATION:") try: mergify_config_content = rules.get_mergify_config_content(r) except rules.NoRules: # pragma: no cover print(".mergify.yml is missing") 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( actions_runner.MERGIFY_RULE["rules"]) pull_request_rules = rules.PullRequestRules(**pull_request_rules_raw) try: p = r.get_pull(int(pull_number)) except github.UnknownObjectException: print("Wrong pull request number") return g, None mp = mergify_pull.MergifyPull(g, p, install_id) print("* PULL REQUEST:") pprint.pprint(mp.to_dict(), width=160) try: print("is_behind: %s" % mp.is_behind()) except github.GithubException as e: print("Unable to know if pull request branch is behind: %s" % e) print("mergeable_state: %s" % mp.g_pull.mergeable_state) print("* MERGIFY LAST CHECKS:") checks = list(check_api.get_checks(p)) for c in checks: if c._rawData["app"]["id"] == config.INTEGRATION_ID: print("[%s]: %s | %s" % (c.name, c.conclusion, c.output.get("title"))) print("> " + "\n> ".join(c.output.get("summary").split("\n"))) print("* MERGIFY LIVE MATCHES:") match = pull_request_rules.get_pull_request_rule(mp) summary_title, summary = actions_runner.gen_summary( "refresh", {}, mp, match) print("> %s" % summary_title) print(summary) return g, p
def test_convert_rebase_fallback(): old_rules = { 'rules': { 'default': { 'protection': { 'required_pull_request_reviews': { 'required_approving_review_count': 2 }, 'required_status_checks': { 'contexts': ['continuous-integration/travis-ci'], 'strict': True } }, 'merge_strategy': { 'method': 'rebase', 'rebase_fallback': "none", } }, 'branches': { '^stable/.*': { 'merge_strategy': { 'method': 'rebase', 'rebase_fallback': "merge", }, }, '^unstable/.*': { 'merge_strategy': { 'method': 'rebase', 'rebase_fallback': "none", }, }, }, }, } converted = convert.convert_config(old_rules["rules"]) assert converted == [ { "name": "default", "conditions": [ "-base~=^stable/.*", "-base~=^unstable/.*", "label!=no-mergify", "#approved-reviews-by>=2", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": None, "strict": True, }, }, }, { "name": "^stable/.* branch", "conditions": [ "base~=^stable/.*", "label!=no-mergify", "#approved-reviews-by>=2", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": "merge", "strict": True, }, }, }, { "name": "^unstable/.* branch", "conditions": [ "base~=^unstable/.*", "label!=no-mergify", "#approved-reviews-by>=2", "status-success=continuous-integration/travis-ci" ], "actions": { "merge": { "method": "rebase", "rebase_fallback": None, "strict": True, }, }, }, ] # Validate generated conf with the schema rules.PullRequestRules(converted)