async def run_action(
    ctxt: context.Context,
    action: typing.Tuple[str, str, actions.Action],
    user: typing.Optional[github_types.GitHubAccount],
) -> typing.Tuple[check_api.Result, str]:
    command, command_args, method = action

    statsd.increment("engine.commands.count", tags=[f"name:{command}"])

    report = await method.run(
        ctxt,
        rules.EvaluatedRule("", rules.RuleConditions([]),
                            rules.RuleMissingConditions([]), {}, False, []),
    )

    if command_args:
        command_full = f"{command} {command_args}"
    else:
        command_full = command

    conclusion = report.conclusion.name.lower()
    summary = "> " + "\n> ".join(report.summary.split("\n")).strip()

    ctxt.log.info(
        "command %s",
        conclusion,
        command_full=command_full,
        report=report,
        user=user["login"] if user else None,
    )
    return (
        report,
        f"**Command `{command_full}`: {conclusion}**\n> **{report.title}**\n{summary}\n",
    )
Exemple #2
0
 async def get_rule(
     self,
     ctxt: context.Context,
 ) -> EvaluatedActionRule:  # pragma: no cover
     return EvaluatedActionRule(
         "", rules.RuleConditions([]), rules.RuleMissingConditions([])
     )
Exemple #3
0
    async def get_rule(
        self,
        ctxt: context.Context,
    ) -> actions.EvaluatedActionRule:
        missing_conditions = []
        conditions = await self._get_branch_protection_conditions(ctxt)
        for condition in conditions:
            if not await condition(ctxt.pull_request):
                missing_conditions.append(condition)

        ear = actions.EvaluatedActionRule(
            "due to branch protection",
            rules.RuleConditions(conditions),
            rules.RuleMissingConditions(missing_conditions),
        )
        return ear
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 == []