Beispiel #1
0
def test_get_mergify_config_invalid(invalid):
    with pytest.raises(InvalidRules):
        client = mock.Mock()
        client.item.return_value = {
            "content": encodebytes(invalid.encode()).decode()
        }
        filename, schema = get_mergify_config(client, "xyz")
Beispiel #2
0
 def get_config(self) -> rules.MergifyConfig:
     try:
         return rules.get_mergify_config(
             context.MergifyConfigFile(
                 {
                     "type": "file",
                     "content": "whatever",
                     "sha": github_types.SHAType("whatever"),
                     "path": ".mergify.yml",
                     "decoded_content": self.mergify_yml,
                 }
             )
         )
     except rules.InvalidRules as exc:
         detail = list(
             map(
                 lambda e: {
                     "loc": ("body", "mergify_yml"),
                     "msg": rules.InvalidRules.format_error(e),
                     "type": "mergify_config_error",
                 },
                 sorted(exc.errors, key=str),
             )
         )
         raise fastapi.HTTPException(status_code=422, detail=detail)
Beispiel #3
0
    def test_branch_disabled(self):
        old_rule = {
            "protection": {
                "required_status_checks": {
                    "strict": True,
                    "contexts": ["continuous-integration/no-ci"],
                },
                "required_pull_request_reviews": {
                    "dismiss_stale_reviews": True,
                    "require_code_owner_reviews": False,
                    "required_approving_review_count": 1,
                },
                "restrictions": None,
                "enforce_admins": False,
            }
        }
        branch_protection.protect(self.r_main, "disabled", old_rule)

        config = rules.get_mergify_config(self.r_main)
        rule = rules.get_branch_rule(config['rules'], "disabled")
        self.assertEqual(None, rule)
        data = branch_protection.get_protection(self.r_main, "disabled")
        self.assertFalse(
            branch_protection.is_configured(self.r_main, "disabled", rule,
                                            data))

        self.create_pr("disabled")
        self.assertEqual([], self.processor._get_cached_branches())
        self.assertEqual([], self._get_queue("disabled"))

        data = branch_protection.get_protection(self.r_main, "disabled")
        self.assertTrue(
            branch_protection.is_configured(self.r_main, "disabled", rule,
                                            data))
def test_actions_with_options_none():
    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha="azertyuiop",
        path="whatever",
        decoded_content="""
defaults:
  actions:
    post_check:
    rebase:
    comment:
      bot_account: "foobar"
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      comment:
      rebase:
        bot_account: "foobar"
      post_check:
            """,
    )

    config = rules.get_mergify_config(file)

    assert [
        list(rule.actions.keys()) for rule in config["pull_request_rules"]
    ][0] == [
        "comment",
        "rebase",
        "post_check",
    ]
async def test_get_mergify_config(valid: str,
                                  redis_cache: utils.RedisCache) -> None:
    async def item(*args, **kwargs):
        return github_types.GitHubContentFile({
            "content":
            encodebytes(valid.encode()).decode(),
            "path":
            ".mergify.yml",
            "type":
            "file",
            "sha":
            "azertyu",
        })

    client = mock.Mock()
    client.item.return_value = item()

    installation = context.Installation(
        github_types.GitHubAccountIdType(0),
        github_types.GitHubLogin("foobar"),
        subscription.Subscription(redis_cache, 0, False, "", frozenset()),
        client,
        redis_cache,
    )
    repository = context.Repository(
        installation,
        github_types.GitHubRepositoryName("xyz"),
        github_types.GitHubRepositoryIdType(0),
    )

    config_file = await repository.get_mergify_config_file()
    assert config_file is not None
    schema = get_mergify_config(config_file)
    assert isinstance(schema, dict)
    assert "pull_request_rules" in schema
Beispiel #6
0
def test_get_mergify_config(valid):
    client = mock.Mock()
    client.item.return_value = {
        "content": encodebytes(valid.encode()).decode()
    }
    filename, schema = get_mergify_config(client, "xyz")
    assert isinstance(schema, dict)
    assert "pull_request_rules" in schema
def test_default_with_no_pull_requests_rules():
    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha="azertyuiop",
        path="whatever",
        decoded_content="""
defaults:
  actions:
    merge:
       strict: "smart"
""",
    )

    assert rules.get_mergify_config(file)
    config = rules.get_mergify_config(file)

    assert config["pull_request_rules"].rules == []
Beispiel #8
0
async def create_queue_freeze(
    queue_freeze_payload: QueueFreezePayload,
    application: application_mod.Application = fastapi.Depends(  # noqa: B008
        security.get_application
    ),
    queue_name: rules.QueueName = fastapi.Path(  # noqa: B008
        ..., description="The name of the queue"
    ),
    repository_ctxt: context.Repository = fastapi.Depends(  # noqa: B008
        security.get_repository_context
    ),
) -> QueueFreezeResponse:

    if queue_freeze_payload.reason == "":
        queue_freeze_payload.reason = "No freeze reason was specified."

    config_file = await repository_ctxt.get_mergify_config_file()
    if config_file is None:
        raise fastapi.HTTPException(
            status_code=404, detail="Mergify configuration file is missing."
        )

    config = get_mergify_config(config_file)
    queue_rules = config["queue_rules"]
    if all(queue_name != rule.name for rule in queue_rules):
        raise fastapi.HTTPException(
            status_code=404, detail=f'The queue "{queue_name}" does not exist.'
        )

    qf = await freeze.QueueFreeze.get(repository_ctxt, queue_name)
    if qf is None:
        qf = freeze.QueueFreeze(
            repository=repository_ctxt,
            name=queue_name,
            reason=queue_freeze_payload.reason,
            application_name=application.name,
            application_id=application.id,
            freeze_date=date.utcnow(),
        )
        await qf.save()

    elif qf.reason != queue_freeze_payload.reason:
        qf.reason = queue_freeze_payload.reason
        await qf.save()

    return QueueFreezeResponse(
        queue_freezes=[
            QueueFreeze(
                name=qf.name,
                reason=qf.reason,
                application_name=qf.application_name,
                application_id=qf.application_id,
                freeze_date=qf.freeze_date,
            )
        ],
    )
Beispiel #9
0
def check_configuration_changes(ctxt):
    if ctxt.pull["base"]["repo"]["default_branch"] == ctxt.pull["base"]["ref"]:
        ref = None
        for f in ctxt.files:
            if f["filename"] in rules.MERGIFY_CONFIG_FILENAMES:
                ref = f["contents_url"].split("?ref=")[1]

        if ref is not None:
            try:
                rules.get_mergify_config(ctxt.client,
                                         ctxt.pull["base"]["repo"]["name"],
                                         ref=ref)
            except rules.InvalidRules as e:
                # Not configured, post status check with the error message
                check_api.set_check_run(
                    ctxt,
                    actions_runner.SUMMARY_NAME,
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e),
                        "annotations": e.get_annotations(e.filename),
                    },
                )
            else:
                check_api.set_check_run(
                    ctxt,
                    actions_runner.SUMMARY_NAME,
                    "completed",
                    "success",
                    output={
                        "title":
                        "The new Mergify configuration is valid",
                        "summary":
                        "This pull request must be merged "
                        "manually because it modifies Mergify configuration",
                    },
                )

            return True
    return False
Beispiel #10
0
def check_configuration_changes(event_pull):
    if event_pull.base.repo.default_branch == event_pull.base.ref:
        ref = None
        for f in event_pull.get_files():
            if f.filename in rules.MERGIFY_CONFIG_FILENAMES:
                ref = f.contents_url.split("?ref=")[1]

        if ref is not None:
            try:
                rules.get_mergify_config(event_pull.base.repo, ref=ref)
            except rules.InvalidRules as e:  # pragma: no cover
                # Not configured, post status check with the error message
                # TODO(sileht): we can annotate the .mergify.yml file in Github
                # UI with that API
                check_api.set_check_run(
                    event_pull,
                    "Summary",
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e),
                    },
                )
            else:
                check_api.set_check_run(
                    event_pull,
                    "Summary",
                    "completed",
                    "success",
                    output={
                        "title":
                        "The new Mergify configuration is valid",
                        "summary":
                        "This pull request must be merged "
                        "manually because it modifies Mergify configuration",
                    },
                )

            return True
    return False
Beispiel #11
0
def check_configuration_changes(event_type, data, event_pull):
    if event_pull.base.repo.default_branch == event_pull.base.ref:
        ref = None
        for f in event_pull.get_files():
            if f.filename == ".mergify.yml":
                ref = f.contents_url.split("?ref=")[1]

        if ref is not None:
            try:
                mergify_config = rules.get_mergify_config(event_pull.base.repo,
                                                          ref=ref)
                if "rules" in mergify_config:
                    rules.get_branch_rule(mergify_config['rules'],
                                          event_pull.base.ref)
            except rules.InvalidRules as e:  # pragma: no cover
                # Not configured, post status check with the error message
                # TODO(sileht): we can annotate the .mergify.yml file in Github
                # UI with that API
                check_api.set_check_run(
                    event_pull,
                    "Mergify — future config checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e)
                    })
            else:
                check_api.set_check_run(
                    event_pull,
                    "Mergify — future config checker",
                    "completed",
                    "success",
                    output={
                        "title": "The new Mergify configuration is valid",
                        "summary": "No action required",
                    })

            check_api.set_check_run(
                event_pull,
                "Mergify — disabled due to configuration change",
                "completed",
                "success",
                output={
                    "title": "Mergify configuration has been modified",
                    "summary": "The pull request needs to be merged manually",
                })

            return True
    return False
Beispiel #12
0
def job_refresh_all():
    integration = github.GithubIntegration(config.INTEGRATION_ID,
                                           config.PRIVATE_KEY)

    counts = [0, 0, 0]
    for install in utils.get_installations(integration):
        counts[0] += 1
        token = integration.get_access_token(install["id"]).token
        g = github.Github(token,
                          base_url="https://api.%s" % config.GITHUB_DOMAIN)
        i = g.get_installation(install["id"])

        for r in i.get_repos():
            if r.archived:  # pragma: no cover
                continue
            try:
                rules.get_mergify_config(r)
            except github.GithubException as e:  # pragma: no cover
                if e.status == 404:
                    continue
                else:
                    raise
            except rules.NoRules:
                pass

            counts[1] += 1
            for p in list(r.get_pulls()):
                # Mimic the github event format
                data = {
                    'repository': r.raw_data,
                    'installation': {'id': install["id"]},
                    'pull_request': p.raw_data,
                }
                engine.run.s('refresh', data).apply_async()

    LOG.info("Refreshing %s installations, %s repositories, "
             "%s branches", *counts)
Beispiel #13
0
async def repository_queues_configuration(
    repository_ctxt: context.Repository = fastapi.Depends(  # noqa: B008
        security.get_repository_context),
) -> QueuesConfig:

    config_file = await repository_ctxt.get_mergify_config_file()
    if config_file is None:
        return QueuesConfig()

    config = get_mergify_config(config_file)

    return QueuesConfig([
        QueueRule(
            config=rule.config,
            name=rule.name,
        ) for rule in config["queue_rules"]
    ])
Beispiel #14
0
    async def refresh(self) -> None:
        config_file = await self.repository.get_mergify_config_file()
        if config_file is None:
            self.log.warning(
                "train can't be refreshed, the mergify configuration is missing",
            )
            return
        try:
            mergify_config = rules.get_mergify_config(config_file)
        except rules.InvalidRules as e:  # pragma: no cover
            self.log.warning(
                "train can't be refreshed, the mergify configuration is invalid",
                summary=str(e),
                annotations=e.get_annotations(e.filename),
            )
            return

        await self._populate_cars(mergify_config["queue_rules"])
        await self._save()
        self.log.info("train cars refreshed")
Beispiel #15
0
def test_command_loader_with_defaults() -> None:
    raw_config = """
defaults:
  actions:
    backport:
      branches:
        - branch-3.1
        - branch-3.2
      ignore_conflicts: false
"""

    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha=github_types.SHAType("azertyuiop"),
        path="whatever",
        decoded_content=raw_config,
    )
    config = rules.get_mergify_config(file)
    command = commands_runner.load_command(config, "@mergifyio backport")
    assert command.name == "backport"
    assert command.args == ""
    assert isinstance(command.action, BackportAction)
    assert command.action.config == {
        "assignees": [],
        "branches": ["branch-3.1", "branch-3.2"],
        "bot_account":
        None,
        "regexes": [],
        "ignore_conflicts":
        False,
        "labels": [],
        "label_conflicts":
        "conflicts",
        "title":
        "{{ title }} (backport #{{ number }})",
        "body":
        "This is an automatic backport of pull request #{{number}} done by [Mergify](https://mergify.com).\n{{ cherry_pick_error }}",
    }
Beispiel #16
0
def check_configuration_changes(event_type, data, event_pull):
    if (event_type == "pull_request"
            and data["action"] in ["opened", "synchronize"]
            and event_pull.base.repo.default_branch == event_pull.base.ref):
        ref = None
        for f in event_pull.get_files():
            if f.filename == ".mergify.yml":
                ref = f.contents_url.split("?ref=")[1]

        if ref is not None:
            try:
                mergify_config = rules.get_mergify_config(event_pull.base.repo,
                                                          ref=ref)
                if "rules" in mergify_config:
                    rules.get_branch_rule(mergify_config['rules'],
                                          event_pull.base.ref)
            except rules.InvalidRules as e:  # pragma: no cover
                # Not configured, post status check with the error message
                # TODO(sileht): we can annotate the .mergify.yml file in Github
                # UI with that API
                check_api.set_check_run(
                    event_pull,
                    "future-config-checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The new Mergify configuration is invalid",
                        "summary": str(e)
                    })
            else:
                check_api.set_check_run(
                    event_pull,
                    "future-config-checker",
                    "completed",
                    "success",
                    output={
                        "title": "The new Mergify configuration is valid",
                        "summary": "No action required",
                    })
Beispiel #17
0
def run(event_type, data):
    """Everything starts here."""
    installation_id = data["installation"]["id"]
    owner = data["repository"]["owner"]["login"]
    repo = data["repository"]["name"]

    client = github.get_client(owner, repo, installation_id)

    raw_pull = get_github_pull_from_event(client, event_type, data)

    if not raw_pull:  # pragma: no cover
        LOG.info(
            "No pull request found in the event %s, ignoring",
            event_type,
            gh_owner=owner,
            gh_repo=repo,
        )
        return

    pull = mergify_pull.MergifyPull(client, raw_pull)
    # Override pull_request with the updated one
    data["pull_request"] = pull.data

    pull.log.info("Pull request found in the event %s", event_type)

    if ("base" not in pull.data or "repo" not in pull.data["base"]
            or len(list(pull.data["base"]["repo"].keys())) < 70):
        pull.log.warning(
            "the pull request payload looks suspicious",
            event_type=event_type,
            data=data,
        )

    if (event_type == "status"
            and pull.data["head"]["sha"] != data["sha"]):  # pragma: no cover
        pull.log.info(
            "No need to proceed queue (got status of an old commit)", )
        return

    elif (event_type in ["status", "check_suite", "check_run"]
          and pull.data["merged"]):  # pragma: no cover
        pull.log.info(
            "No need to proceed queue (got status of a merged pull request)", )
        return
    elif (event_type in ["check_suite", "check_run"]
          and pull.data["head"]["sha"] !=
          data[event_type]["head_sha"]):  # pragma: no cover
        pull.log.info(
            "No need to proceed queue (got %s of an old "
            "commit)",
            event_type,
        )
        return

    if check_configuration_changes(pull.g_pull):
        pull.log.info("Configuration changed, ignoring", )
        return

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = rules.get_mergify_config(pull.g_pull.base.repo)
    except rules.NoRules:  # pragma: no cover
        pull.log.info("No need to proceed queue (.mergify.yml is missing)", )
        return
    except rules.InvalidRules as e:  # pragma: no cover
        # Not configured, post status check with the error message
        if event_type == "pull_request" and data["action"] in [
                "opened", "synchronize"
        ]:
            check_api.set_check_run(
                pull.g_pull,
                "Summary",
                "completed",
                "failure",
                output={
                    "title": "The Mergify configuration is invalid",
                    "summary": str(e),
                },
            )
        return

    # Add global and mandatory rules
    mergify_config["pull_request_rules"].rules.extend(
        rules.load_pull_request_rules_schema(MERGIFY_RULE["rules"]))

    subscription = sub_utils.get_subscription(utils.get_redis_for_cache(),
                                              installation_id)

    if pull.data["base"]["repo"][
            "private"] and not subscription["subscription_active"]:
        check_api.set_check_run(
            pull.g_pull,
            "Summary",
            "completed",
            "failure",
            output={
                "title": "Mergify is disabled",
                "summary": subscription["subscription_reason"],
            },
        )
        return

    # CheckRun are attached to head sha, so when user add commits or force push
    # we can't directly get the previous Mergify Summary. So we copy it here, then
    # anything that looks at it in next celery tasks will find it.
    if event_type == "pull_request" and data["action"] == "synchronize":
        copy_summary_from_previous_head_sha(pull.g_pull, data["before"])

    sources = [{"event_type": event_type, "data": data}]

    commands_runner.spawn_pending_commands_tasks(pull, sources)

    if event_type == "issue_comment":
        commands_runner.run_command(pull, sources, data["comment"]["body"],
                                    data["comment"]["user"])
    else:
        actions_runner.handle(mergify_config["pull_request_rules"], pull,
                              sources)
Beispiel #18
0
def run(event_type, data, subscription):
    """Everything starts here."""
    integration = github.GithubIntegration(config.INTEGRATION_ID,
                                           config.PRIVATE_KEY)
    installation_id = data["installation"]["id"]
    try:
        installation_token = integration.get_access_token(
            installation_id).token
    except github.UnknownObjectException:  # pragma: no cover
        LOG.error("token for install %d does not exists anymore (%s)",
                  installation_id, data["repository"]["full_name"])
        return

    g = github.Github(installation_token)
    try:
        if config.LOG_RATELIMIT:  # pragma: no cover
            rate = g.get_rate_limit().rate
            LOG.info("ratelimit: %s/%s, reset at %s",
                     rate.remaining,
                     rate.limit,
                     rate.reset,
                     repository=data["repository"]["name"])

        repo = g.get_repo(data["repository"]["owner"]["login"] + "/" +
                          data["repository"]["name"])

        # NOTE(sileht): Workaround for when we receive check_suite completed
        # without conclusion
        if (event_type == "check_suite" and data["action"] == "completed"
                and not data["check_suite"]["conclusion"]):  # pragma: no cover
            data = check_api.workaround_for_unfinished_check_suite(repo, data)

        event_pull = get_github_pull_from_event(g, repo, installation_id,
                                                event_type, data)

        if not event_pull:  # pragma: no cover
            LOG.info("No pull request found in the event %s, "
                     "ignoring", event_type)
            return

        LOG.info("Pull request found in the event %s",
                 event_type,
                 pull_request=event_pull)

        if (event_type == "status"
                and event_pull.head.sha != data["sha"]):  # pragma: no cover
            LOG.info("No need to proceed queue (got status of an old commit)")
            return

        elif (event_type in ["status", "check_suite", "check_run"]
              and event_pull.merged):  # pragma: no cover
            LOG.info("No need to proceed queue (got status of a merged "
                     "pull request)")
            return
        elif (event_type in ["check_suite", "check_run"]
              and event_pull.head.sha !=
              data[event_type]["head_sha"]):  # pragma: no cover
            LOG.info("No need to proceed queue (got %s of an old "
                     "commit)", event_type)
            return

        check_configuration_changes(event_type, data, event_pull)

        # BRANCH CONFIGURATION CHECKING
        try:
            mergify_config = rules.get_mergify_config(repo)
        except rules.NoRules as e:  # pragma: no cover
            LOG.info("No need to proceed queue (.mergify.yml is missing)")
            return
        except rules.InvalidRules as e:  # pragma: no cover
            # Not configured, post status check with the error message
            if (event_type == "pull_request"
                    and data["action"] in ["opened", "synchronize"]):
                check_api.set_check_run(
                    event_pull,
                    "current-config-checker",
                    "completed",
                    "failure",
                    output={
                        "title": "The Mergify configuration is invalid",
                        "summary": str(e)
                    })
            return

        create_metrics(event_type, data)

        # NOTE(sileht): At some point we may need to reget the
        # installation_token within each next tasks, in case we reach the
        # expiration
        if "rules" in mergify_config:
            v1.handle(installation_id, installation_token, subscription,
                      mergify_config["rules"], event_type, data,
                      event_pull.raw_data)

        elif "pull_request_rules" in mergify_config:
            v2.handle.s(installation_id, installation_token, subscription,
                        mergify_config["pull_request_rules"].as_dict(),
                        event_type, data, event_pull.raw_data).apply_async()

        else:  # pragma: no cover
            raise RuntimeError("Unexpected configuration version")

    except github.BadCredentialsException:  # pragma: no cover
        LOG.error("token for install %d is no longuer valid (%s)",
                  data["installation"]["id"], data["repository"]["full_name"])
    except github.RateLimitExceededException:  # pragma: no cover
        LOG.error("rate limit reached for install %d (%s)",
                  data["installation"]["id"], data["repository"]["full_name"])
    def handle_installation(installation):
        try:
            _id = installation["id"]
            target_type = installation["target_type"]
            account = installation["account"]["login"]

            LOG.info("Get subscription", account=account)
            subs = sub_utils.get_subscription(redis, _id)
            subscribed = subs["subscription_active"]

            costs[(subscribed, target_type, account)] = (
                subs["subscription_cost"]
            )

            with installations_lock:
                installations[(subscribed, target_type)] += 1

            token = integration.get_access_token(_id).token
            g = github.Github(token, base_url="https://api.%s" %
                              config.GITHUB_DOMAIN)

            if installation["target_type"] == "Organization":
                LOG.info("Get members",
                         install=installation["account"]["login"])
                org = g.get_organization(installation["account"]["login"])
                value = len(list(org.get_members()))

                users_per_installation[
                    (subscribed, target_type, account)] = value
            else:
                users_per_installation[
                    (subscribed, target_type, account)] = 1

            LOG.info("Get repos", account=account)

            repositories = sorted(g.get_installation(_id).get_repos(),
                                  key=operator.attrgetter("private"))
            for private, repos in itertools.groupby(
                    repositories, key=operator.attrgetter("private")):

                configured_repos = 0
                unconfigured_repos = 0
                for repo in repos:
                    try:
                        rules.get_mergify_config(repo)
                        configured_repos += 1
                        redis.sadd("badges.tmp", repo.full_name)
                    except github.GithubException as e:
                        if e.status >= 500:  # pragma: no cover
                            raise
                        unconfigured_repos += 1
                    except (rules.InvalidRules, rules.NoRules):
                        unconfigured_repos += 1

                repositories_per_installation[
                    (subscribed, target_type, account, private, True)
                ] = configured_repos
                repositories_per_installation[
                    (subscribed, target_type, account, private, False)
                ] = unconfigured_repos
        except github.GithubException as e:  # pragma: no cover
            # Ignore rate limit/abuse, authorization issues
            # and GitHub malfunction
            if e.status not in (403, 401) and e.status < 500:
                raise
Beispiel #20
0
def run(client, pull, subscription, sources):
    LOG.debug("engine get context")
    ctxt = context.Context(client, pull, subscription)
    ctxt.log.debug("engine start processing context")

    issue_comment_sources = []

    for source in sources:
        if source["event_type"] == "issue_comment":
            issue_comment_sources.append(source)
        else:
            ctxt.sources.append(source)

    ctxt.log.debug("engine run pending commands")
    commands_runner.run_pending_commands_tasks(ctxt)

    if issue_comment_sources:
        ctxt.log.debug("engine handle commands")
        for source in issue_comment_sources:
            commands_runner.handle(
                ctxt,
                source["data"]["comment"]["body"],
                source["data"]["comment"]["user"],
            )

    if not ctxt.sources:
        return

    if ctxt.client.auth.permissions_need_to_be_updated:
        check_api.set_check_run(
            ctxt,
            "Summary",
            "completed",
            "failure",
            output={
                "title": "Required GitHub permissions are missing.",
                "summary":
                "You can accept them at https://dashboard.mergify.io/",
            },
        )
        return

    ctxt.log.debug("engine check configuration change")
    if check_configuration_changes(ctxt):
        ctxt.log.info("Configuration changed, ignoring")
        return

    ctxt.log.debug("engine get configuration")
    # BRANCH CONFIGURATION CHECKING
    try:
        filename, mergify_config = rules.get_mergify_config(ctxt)
    except rules.NoRules:  # pragma: no cover
        ctxt.log.info("No need to proceed queue (.mergify.yml is missing)")
        return
    except rules.InvalidRules as e:  # pragma: no cover
        # Not configured, post status check with the error message
        if any((s["event_type"] == "pull_request"
                and s["data"]["action"] in ["opened", "synchronize"]
                for s in ctxt.sources)):
            check_api.set_check_run(
                ctxt,
                actions_runner.SUMMARY_NAME,
                "completed",
                "failure",
                output={
                    "title": "The Mergify configuration is invalid",
                    "summary": str(e),
                    "annotations": e.get_annotations(e.filename),
                },
            )
        return

    # Add global and mandatory rules
    mergify_config["pull_request_rules"].rules.extend(
        rules.PullRequestRules.from_list(MERGIFY_RULE["rules"]).rules)

    if ctxt.pull["base"]["repo"][
            "private"] and not subscription["subscription_active"]:
        check_api.set_check_run(
            ctxt,
            actions_runner.SUMMARY_NAME,
            "completed",
            "failure",
            output={
                "title": "Mergify is disabled",
                "summary": subscription["subscription_reason"],
            },
        )
        return

    # CheckRun are attached to head sha, so when user add commits or force push
    # we can't directly get the previous Mergify Summary. So we copy it here, then
    # anything that looks at it in next engine runs will find it.

    synchronize_events = dict(((s["data"]["after"], s["data"])
                               for s in ctxt.sources
                               if s["event_type"] == "pull_request"
                               and s["data"]["action"] == "synchronize"))
    if synchronize_events:
        ctxt.log.debug("engine synchronize summary")

        # NOTE(sileht): We sometimes got many synchronize in a row, that not always the
        # last one that have the Summary, so we also looks in older one if necessary.
        after_sha = ctxt.pull["head"]["sha"]
        while synchronize_events:
            sync_event = synchronize_events.pop(after_sha, None)
            if sync_event:
                if copy_summary_from_previous_head_sha(ctxt,
                                                       sync_event["before"]):
                    break
                else:
                    after_sha = sync_event["before"]
            else:
                ctxt.log.warning(
                    "Got synchronize event but didn't find Summary on previous head sha",
                )
                break

    ctxt.log.debug("engine handle actions")
    actions_runner.handle(mergify_config["pull_request_rules"], ctxt)
Beispiel #21
0
async def test_get_mergify_config_invalid(
        invalid: str, redis_cache: utils.RedisCache) -> None:
    with pytest.raises(InvalidRules):

        async def item(*args, **kwargs):
            return github_types.GitHubContentFile({
                "content":
                encodebytes(invalid.encode()).decode(),
                "path":
                ".mergify.yml",
                "type":
                "file",
                "sha":
                "azertyu",
            })

        client = mock.Mock()
        client.item.return_value = item()

        gh_owner = github_types.GitHubAccount({
            "login":
            github_types.GitHubLogin("foobar"),
            "id":
            github_types.GitHubAccountIdType(0),
            "type":
            "User",
            "avatar_url":
            "",
        })
        gh_repo = github_types.GitHubRepository({
            "full_name":
            "foobar/xyz",
            "name":
            github_types.GitHubRepositoryName("xyz"),
            "private":
            False,
            "id":
            github_types.GitHubRepositoryIdType(0),
            "owner":
            gh_owner,
            "archived":
            False,
            "url":
            "",
            "html_url":
            "",
            "default_branch":
            github_types.GitHubRefType("ref"),
        })
        installation = context.Installation(
            github_types.GitHubAccountIdType(0),
            github_types.GitHubLogin("foobar"),
            subscription.Subscription(redis_cache, 0, False, "", frozenset()),
            client,
            redis_cache,
        )
        repository = context.Repository(
            installation,
            gh_repo,
        )

        config_file = await repository.get_mergify_config_file()
        assert config_file is not None
        get_mergify_config(config_file)
Beispiel #22
0
import pytest

from mergify_engine import config
from mergify_engine import context
from mergify_engine import github_types
from mergify_engine import rules
from mergify_engine.actions.backport import BackportAction
from mergify_engine.actions.rebase import RebaseAction
from mergify_engine.engine import commands_runner
from mergify_engine.tests.unit import conftest

EMPTY_CONFIG = rules.get_mergify_config(
    context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha=github_types.SHAType("azertyuiop"),
        path="whatever",
        decoded_content="",
    ))


def test_command_loader() -> None:
    with pytest.raises(commands_runner.CommandInvalid):
        commands_runner.load_command(EMPTY_CONFIG,
                                     "@mergifyio notexist foobar\n")

    with pytest.raises(commands_runner.CommandInvalid):
        commands_runner.load_command(EMPTY_CONFIG,
                                     "@mergifyio comment foobar\n")

    with pytest.raises(commands_runner.CommandInvalid):
Beispiel #23
0
 def _get_queue(self, branch):
     config = rules.get_mergify_config(self.r_main)
     branch_rule = rules.get_branch_rule(config['rules'], branch)
     collaborators = [self.u_main.id]
     return self.processor._build_queue(branch, branch_rule, collaborators)
Beispiel #24
0
def test_action_queue_with_no_default_queue():
    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha="azertyuiop",
        path="whatever",
        decoded_content="""
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      queue:
        name: missing
            """,
    )

    with pytest.raises(rules.InvalidRules) as e:
        rules.get_mergify_config(file)

    assert str(e.value.error) == "missing queue not found"

    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha="azertyuiop",
        path="whatever",
        decoded_content="""
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      queue:
            """,
    )

    with pytest.raises(rules.InvalidRules) as e:
        rules.get_mergify_config(file)

    assert (
        str(e.value.error) ==
        "required key not provided @ data['pull_request_rules'][0]['actions']['queue']['name']"
    )

    # TODO(sileht): This is currently required to set a queue name to set other
    # queue options If one days we have more options like queue name, we may
    # need to revisit the validators system to make all voluptuous.Required()
    # keys optional for defaults
    file = context.MergifyConfigFile(
        type="file",
        content="whatever",
        sha="azertyuiop",
        path="whatever",
        decoded_content="""
defaults:
  actions:
    queue:
      merge_bot_account: foobar
queue_rules:
  - name: default
    conditions: []
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      queue:
        name: default
            """,
    )

    with pytest.raises(rules.InvalidRules) as e:
        rules.get_mergify_config(file)

    assert (
        str(e.value.error) ==
        "required key not provided @ data['defaults']['actions']['queue']['name']"
    )
Beispiel #25
0
def run(event_type, data):
    """Everything starts here."""
    installation_id = data["installation"]["id"]
    installation_token = utils.get_installation_token(installation_id)
    if not installation_token:
        return

    g = github.Github(installation_token,
                      base_url="https://api.%s" % config.GITHUB_DOMAIN)

    if config.LOG_RATELIMIT:  # pragma: no cover
        rate = g.get_rate_limit().rate
        LOG.info("ratelimit: %s/%s, reset at %s",
                 rate.remaining, rate.limit, rate.reset,
                 repository=data["repository"]["name"])

    repo = g.get_repo(data["repository"]["owner"]["login"] + "/" +
                      data["repository"]["name"])

    event_pull = get_github_pull_from_event(repo, event_type, data)

    if not event_pull:  # pragma: no cover
        LOG.info("No pull request found in the event %s, "
                 "ignoring", event_type)
        return

    LOG.info("Pull request found in the event %s", event_type,
             repo=repo.full_name,
             pull_request=event_pull)

    subscription = sub_utils.get_subscription(utils.get_redis_for_cache(),
                                              installation_id)

    if repo.private and not subscription["subscription_active"]:
        check_api.set_check_run(
            event_pull, "Summary",
            "completed", "failure", output={
                "title": "Mergify is disabled",
                "summary": subscription["subscription_reason"],
            })
        return

    if ("base" not in event_pull.raw_data or
            "repo" not in event_pull.raw_data["base"] or
            len(list(event_pull.raw_data["base"]["repo"].keys())) < 70):
        LOG.warning("the pull request payload looks suspicious",
                    event_type=event_type,
                    data=data,
                    pull_request=event_pull.raw_data,
                    repo=repo.fullname)

    if (event_type == "status" and
            event_pull.head.sha != data["sha"]):  # pragma: no cover
        LOG.info("No need to proceed queue (got status of an old commit)",
                 repo=repo.full_name,
                 pull_request=event_pull)
        return

    elif (event_type in ["status", "check_suite", "check_run"] and
          event_pull.merged):  # pragma: no cover
        LOG.info("No need to proceed queue (got status of a merged "
                 "pull request)",
                 repo=repo.full_name,
                 pull_request=event_pull)
        return
    elif (event_type in ["check_suite", "check_run"] and
          event_pull.head.sha != data[event_type]["head_sha"]
          ):  # pragma: no cover
        LOG.info("No need to proceed queue (got %s of an old "
                 "commit)", event_type,
                 repo=repo.full_name,
                 pull_request=event_pull)
        return

    if check_configuration_changes(event_pull):
        LOG.info("Configuration changed, ignoring",
                 repo=repo.full_name,
                 pull_request=event_pull)
        return

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = rules.get_mergify_config(repo)
    except rules.NoRules:  # pragma: no cover
        LOG.info("No need to proceed queue (.mergify.yml is missing)",
                 repo=repo.full_name,
                 pull_request=event_pull)
        return
    except rules.InvalidRules as e:  # pragma: no cover
        # Not configured, post status check with the error message
        if (event_type == "pull_request" and
                data["action"] in ["opened", "synchronize"]):
            check_api.set_check_run(
                event_pull, "Summary", "completed",
                "failure", output={
                    "title": "The Mergify configuration is invalid",
                    "summary": str(e)
                })
        return

    create_metrics(event_type, data)

    v2.handle.s(
        installation_id,
        mergify_config["pull_request_rules"].as_dict(),
        event_type, data, event_pull.raw_data
    ).apply_async()
Beispiel #26
0
async def report(
    url: str,
) -> typing.Union[context.Context, github.AsyncGithubInstallationClient, None]:
    redis_cache = utils.create_aredis_for_cache(max_idle_time=0)

    try:
        owner, repo, pull_number = _url_parser(url)
    except ValueError:
        print(f"{url} is not valid")
        return None

    try:
        client = github.aget_client(owner)
    except exceptions.MergifyNotInstalled:
        print(f"* Mergify is not installed on account {owner}")
        return None

    # Do a dumb request just to authenticate
    await client.get("/")

    if client.auth.installation is None:
        print("No installation detected")
        return None

    print(f"* INSTALLATION ID: {client.auth.installation['id']}")

    if client.auth.owner_id is None:
        raise RuntimeError("Unable to get owner_id")

    if repo is None:
        slug = None
    else:
        slug = owner + "/" + repo

    cached_sub = await subscription.Subscription.get_subscription(
        redis_cache, client.auth.owner_id)
    db_sub = await subscription.Subscription._retrieve_subscription_from_db(
        redis_cache, client.auth.owner_id)

    cached_tokens = await user_tokens.UserTokens.get(redis_cache,
                                                     client.auth.owner_id)
    db_tokens = await user_tokens.UserTokens._retrieve_from_db(
        redis_cache, client.auth.owner_id)

    print(f"* SUBSCRIBED (cache/db): {cached_sub.active} / {db_sub.active}")
    print("* Features (cache):")
    for f in db_sub.features:
        print(f"  - {f.value}")
    print("* Features (db):")
    for f in cached_sub.features:
        print(f"  - {f.value}")

    await report_dashboard_synchro(client.auth.installation["id"], cached_sub,
                                   cached_tokens, "ENGINE-CACHE", slug)
    await report_dashboard_synchro(client.auth.installation["id"], db_sub,
                                   db_tokens, "DASHBOARD", slug)

    await report_worker_status(owner)

    installation = context.Installation(client.auth.owner_id, owner,
                                        cached_sub, client, redis_cache)

    if repo is not None:
        repo_info: github_types.GitHubRepository = await client.item(
            f"/repos/{owner}/{repo}")
        repository = context.Repository(installation, repo_info["name"],
                                        repo_info["id"])

        print(
            f"* REPOSITORY IS {'PRIVATE' if repo_info['private'] else 'PUBLIC'}"
        )

        print("* CONFIGURATION:")
        mergify_config = None
        config_file = await repository.get_mergify_config_file()
        if config_file is None:
            print(".mergify.yml is missing")
        else:
            print(f"Config filename: {config_file['path']}")
            print(config_file["decoded_content"].decode())
            try:
                mergify_config = rules.get_mergify_config(config_file)
            except rules.InvalidRules as e:  # pragma: no cover
                print(f"configuration is invalid {str(e)}")
            else:
                mergify_config["pull_request_rules"].rules.extend(
                    engine.MERGIFY_BUILTIN_CONFIG["pull_request_rules"].rules)

        if pull_number is None:
            async for branch in typing.cast(
                    typing.AsyncGenerator[github_types.GitHubBranch, None],
                    client.items(f"/repos/{owner}/{repo}/branches"),
            ):
                # TODO(sileht): Add some informations on the train
                q: queue.QueueBase = naive.Queue(repository, branch["name"])
                await report_queue("QUEUES", q)

                q = merge_train.Train(repository, branch["name"])
                await q.load()
                await report_queue("TRAIN", q)

        else:
            repository = context.Repository(
                installation, github_types.GitHubRepositoryName(repo))
            ctxt = await repository.get_pull_request_context(
                github_types.GitHubPullRequestNumber(int(pull_number)))

            # FIXME queues could also be printed if no pull number given
            # TODO(sileht): display train if any
            q = await naive.Queue.from_context(ctxt)
            print(
                f"* QUEUES: {', '.join([f'#{p}' for p in await q.get_pulls()])}"
            )
            q = await merge_train.Train.from_context(ctxt)
            print(
                f"* TRAIN: {', '.join([f'#{p}' for p in await q.get_pulls()])}"
            )
            print("* PULL REQUEST:")
            pr_data = await ctxt.pull_request.items()
            pprint.pprint(pr_data, width=160)

            is_behind = await ctxt.is_behind
            print(f"is_behind: {is_behind}")

            print(f"mergeable_state: {ctxt.pull['mergeable_state']}")

            print("* MERGIFY LAST CHECKS:")
            for c in await ctxt.pull_engine_check_runs:
                print(
                    f"[{c['name']}]: {c['conclusion']} | {c['output'].get('title')}"
                )
                print("> " + "\n> ".join(
                    ("No Summary", ) if c["output"]["summary"] is None else
                    c["output"]["summary"].split("\n")))

            if mergify_config is not None:
                print("* MERGIFY LIVE MATCHES:")
                match = await mergify_config["pull_request_rules"
                                             ].get_pull_request_rule(ctxt)
                summary_title, summary = await actions_runner.gen_summary(
                    ctxt, match)
                print(f"[Summary]: success | {summary_title}")
                print("> " + "\n> ".join(summary.strip().split("\n")))

            return ctxt

    return client
Beispiel #27
0
async def test_get_mergify_config_with_defaults(
        redis_cache: utils.RedisCache) -> None:

    config = """
defaults:
  actions:
    comment:
      bot_account: foo-bot
    rebase:
      bot_account: test-bot-account
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      comment:
        message: I love Mergify
      rebase: {}
"""

    async def item(*args, **kwargs):
        return github_types.GitHubContentFile({
            "content":
            encodebytes(config.encode()).decode(),
            "path":
            ".mergify.yml",
            "type":
            "file",
            "sha":
            "azertyu",
        })

    client = mock.Mock()
    client.item.return_value = item()

    installation = context.Installation(
        github_types.GitHubAccountIdType(0),
        github_types.GitHubLogin("foobar"),
        subscription.Subscription(redis_cache, 0, False, "", frozenset()),
        client,
        redis_cache,
    )
    repository = context.Repository(
        installation,
        github_types.GitHubRepositoryName("xyz"),
        github_types.GitHubRepositoryIdType(0),
    )
    config_file = await repository.get_mergify_config_file()
    assert config_file is not None

    schema = get_mergify_config(config_file)
    assert isinstance(schema, dict)

    assert len(schema["pull_request_rules"].rules) == 1

    comment = schema["pull_request_rules"].rules[0].actions["comment"].config
    assert comment == {"message": "I love Mergify", "bot_account": "foo-bot"}

    rebase = schema["pull_request_rules"].rules[0].actions["rebase"].config
    assert rebase == {"bot_account": "test-bot-account"}

    config = """
defaults:
  actions:
    comment:
      message: I love Mergify
      bot_account: AutoBot
pull_request_rules:
  - name: ahah
    conditions:
    - base=master
    actions:
      comment:
        message: I really love Mergify
"""

    client = mock.Mock()
    client.item.return_value = item()

    installation = context.Installation(
        github_types.GitHubAccountIdType(0),
        github_types.GitHubLogin("foobar"),
        subscription.Subscription(redis_cache, 0, False, "", frozenset()),
        client,
        redis_cache,
    )
    repository = context.Repository(
        installation,
        github_types.GitHubRepositoryName("xyz"),
        github_types.GitHubRepositoryIdType(0),
    )
    config_file = await repository.get_mergify_config_file()
    assert config_file is not None

    schema = get_mergify_config(config_file)
    assert isinstance(schema, dict)

    assert len(schema["pull_request_rules"].rules) == 1

    comment = schema["pull_request_rules"].rules[0].actions["comment"].config
    assert comment == {
        "message": "I really love Mergify",
        "bot_account": "AutoBot"
    }
Beispiel #28
0
def run(event_type, data):
    """Everything starts here."""
    installation_id = data["installation"]["id"]
    installation_token = utils.get_installation_token(installation_id)
    if not installation_token:
        return

    g = github.Github(installation_token,
                      base_url="https://api.%s" % config.GITHUB_DOMAIN)

    if config.LOG_RATELIMIT:  # pragma: no cover
        rate = g.get_rate_limit().rate
        LOG.info(
            "ratelimit: %s/%s, reset at %s",
            rate.remaining,
            rate.limit,
            rate.reset,
            repository=data["repository"]["name"],
        )

    try:
        repo = g.get_repo(data["repository"]["owner"]["login"] + "/" +
                          data["repository"]["name"])
    except github.UnknownObjectException:  # pragma: no cover
        LOG.info("Repository not found in the event %s, ignoring", event_type)
        return

    event_pull = get_github_pull_from_event(repo, event_type, data)

    if not event_pull:  # pragma: no cover
        LOG.info("No pull request found in the event %s, "
                 "ignoring", event_type)
        return

    # Override pull_request with the updated one
    data["pull_request"] = event_pull.raw_data

    LOG.info(
        "Pull request found in the event %s",
        event_type,
        repo=repo.full_name,
        pull_request=event_pull,
    )

    if ("base" not in event_pull.raw_data
            or "repo" not in event_pull.raw_data["base"]
            or len(list(event_pull.raw_data["base"]["repo"].keys())) < 70):
        LOG.warning(
            "the pull request payload looks suspicious",
            event_type=event_type,
            data=data,
            pull_request=event_pull.raw_data,
            repo=repo.fullname,
        )

    if (event_type == "status"
            and event_pull.head.sha != data["sha"]):  # pragma: no cover
        LOG.info(
            "No need to proceed queue (got status of an old commit)",
            repo=repo.full_name,
            pull_request=event_pull,
        )
        return

    elif (event_type in ["status", "check_suite", "check_run"]
          and event_pull.merged):  # pragma: no cover
        LOG.info(
            "No need to proceed queue (got status of a merged "
            "pull request)",
            repo=repo.full_name,
            pull_request=event_pull,
        )
        return
    elif (event_type in ["check_suite", "check_run"] and event_pull.head.sha !=
          data[event_type]["head_sha"]):  # pragma: no cover
        LOG.info(
            "No need to proceed queue (got %s of an old "
            "commit)",
            event_type,
            repo=repo.full_name,
            pull_request=event_pull,
        )
        return

    if check_configuration_changes(event_pull):
        LOG.info(
            "Configuration changed, ignoring",
            repo=repo.full_name,
            pull_request=event_pull,
        )
        return

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = rules.get_mergify_config(repo)
    except rules.NoRules:  # pragma: no cover
        LOG.info(
            "No need to proceed queue (.mergify.yml is missing)",
            repo=repo.full_name,
            pull_request=event_pull,
        )
        return
    except rules.InvalidRules as e:  # pragma: no cover
        # Not configured, post status check with the error message
        if event_type == "pull_request" and data["action"] in [
                "opened", "synchronize"
        ]:
            check_api.set_check_run(
                event_pull,
                "Summary",
                "completed",
                "failure",
                output={
                    "title": "The Mergify configuration is invalid",
                    "summary": str(e),
                },
            )
        return

    subscription = sub_utils.get_subscription(utils.get_redis_for_cache(),
                                              installation_id)

    if repo.private and not subscription["subscription_active"]:
        check_api.set_check_run(
            event_pull,
            "Summary",
            "completed",
            "failure",
            output={
                "title": "Mergify is disabled",
                "summary": subscription["subscription_reason"],
            },
        )
        return

    # CheckRun are attached to head sha, so when user add commits or force push
    # we can't directly get the previous Mergify Summary. So we copy it here, then
    # anything that looks at it in next celery tasks will find it.
    if event_type == "pull_request" and data["action"] == "synchronize":
        copy_summary_from_previous_head_sha(event_pull, data["before"])

    commands_runner.spawn_pending_commands_tasks(installation_id, event_type,
                                                 data, event_pull)

    if event_type == "issue_comment":
        commands_runner.run_command.s(installation_id, event_type, data,
                                      data["comment"]["body"]).apply_async()
    else:
        actions_runner.handle.s(
            installation_id,
            mergify_config["pull_request_rules"].as_dict(),
            event_type,
            data,
        ).apply_async()
Beispiel #29
0
async def run(
    ctxt: context.Context,
    sources: typing.List[context.T_PayloadEventSource],
) -> None:
    LOG.debug("engine get context")
    ctxt.log.debug("engine start processing context")

    issue_comment_sources: typing.List[T_PayloadEventIssueCommentSource] = []

    for source in sources:
        if source["event_type"] == "issue_comment":
            issue_comment_sources.append(
                typing.cast(T_PayloadEventIssueCommentSource, source))
        else:
            ctxt.sources.append(source)

    ctxt.log.debug("engine run pending commands")
    await commands_runner.run_pending_commands_tasks(ctxt)

    if issue_comment_sources:
        ctxt.log.debug("engine handle commands")
        for ic_source in issue_comment_sources:
            await commands_runner.handle(
                ctxt,
                ic_source["data"]["comment"]["body"],
                ic_source["data"]["comment"]["user"],
            )

    if not ctxt.sources:
        return

    if ctxt.client.auth.permissions_need_to_be_updated:
        await ctxt.set_summary_check(
            check_api.Result(
                check_api.Conclusion.FAILURE,
                title="Required GitHub permissions are missing.",
                summary="You can accept them at https://dashboard.mergify.io/",
            ))
        return

    config_file = await ctxt.repository.get_mergify_config_file()

    ctxt.log.debug("engine check configuration change")
    if await _check_configuration_changes(ctxt, config_file):
        ctxt.log.info("Configuration changed, ignoring")
        return

    ctxt.log.debug("engine get configuration")
    if config_file is None:
        ctxt.log.info("No need to proceed queue (.mergify.yml is missing)")
        return

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = rules.get_mergify_config(config_file)
    except rules.InvalidRules as e:  # pragma: no cover
        ctxt.log.info(
            "The Mergify configuration is invalid",
            summary=str(e),
            annotations=e.get_annotations(e.filename),
        )
        # Not configured, post status check with the error message
        for s in ctxt.sources:
            if s["event_type"] == "pull_request":
                event = typing.cast(github_types.GitHubEventPullRequest,
                                    s["data"])
                if event["action"] in ("opened", "synchronize"):
                    await ctxt.set_summary_check(
                        check_api.Result(
                            check_api.Conclusion.FAILURE,
                            title="The Mergify configuration is invalid",
                            summary=str(e),
                            annotations=e.get_annotations(e.filename),
                        ))
                    break
        return

    # Add global and mandatory rules
    mergify_config["pull_request_rules"].rules.extend(
        DEFAULT_PULL_REQUEST_RULES.rules)

    if ctxt.pull["base"]["repo"][
            "private"] and not ctxt.subscription.has_feature(
                subscription.Features.PRIVATE_REPOSITORY):
        ctxt.log.info("mergify disabled: private repository")
        await ctxt.set_summary_check(
            check_api.Result(
                check_api.Conclusion.FAILURE,
                title="Mergify is disabled",
                summary=ctxt.subscription.reason,
            ))
        return

    await _ensure_summary_on_head_sha(ctxt)

    # NOTE(jd): that's fine for now, but I wonder if we wouldn't need a higher abstraction
    # to have such things run properly. Like hooks based on events that you could
    # register. It feels hackish otherwise.
    for s in ctxt.sources:
        if s["event_type"] == "pull_request":
            event = typing.cast(github_types.GitHubEventPullRequest, s["data"])
            if event["action"] == "closed":
                await ctxt.clear_cached_last_summary_head_sha()
                break

    ctxt.log.debug("engine handle actions")
    if ctxt.is_merge_queue_pr():
        await queue_runner.handle(mergify_config["queue_rules"], ctxt)
    else:
        await actions_runner.handle(mergify_config["pull_request_rules"], ctxt)
Beispiel #30
0
async def _check_configuration_changes(
    ctxt: context.Context,
    current_mergify_config_file: typing.Optional[context.MergifyConfigFile],
) -> bool:
    if ctxt.pull["base"]["repo"]["default_branch"] != ctxt.pull["base"]["ref"]:
        return False

    if ctxt.closed:
        # merge_commit_sha is a merge between the PR and the base branch only when the pull request is open
        # after it's None or the resulting commit of the pull request merge (maybe a rebase, squash, merge).
        # As the PR is closed, we don't care about the config change detector.
        return False

    # NOTE(sileht): This heuristic works only if _ensure_summary_on_head_sha()
    # is called after _check_configuration_changes().
    # If we don't have the real summary yet it means the pull request has just
    # been open or synchronize or we never see it.
    summary = await ctxt.get_engine_check_run(constants.SUMMARY_NAME)
    if summary and summary["output"]["title"] not in (
            constants.INITIAL_SUMMARY_TITLE,
            constants.CONFIGURATION_MUTIPLE_FOUND_SUMMARY_TITLE,
    ):
        if await ctxt.get_engine_check_run(
                constants.CONFIGURATION_CHANGED_CHECK_NAME):
            return True
        elif await ctxt.get_engine_check_run(
                constants.CONFIGURATION_DELETED_CHECK_NAME):
            return True
        else:
            return False

    preferred_filename = (None if current_mergify_config_file is None else
                          current_mergify_config_file["path"])

    # NOTE(sileht): pull.base.sha is unreliable as its the sha when the PR is
    # open and not the merge-base/fork-point. So we compare the configuration from the base
    # branch with the one of the merge commit. If the configuration is changed by the PR, they will be
    # different.
    config_files: typing.Dict[str, context.MergifyConfigFile] = {}
    async for config_file in ctxt.repository.iter_mergify_config_files(
            ref=ctxt.pull["merge_commit_sha"],
            preferred_filename=preferred_filename):
        config_files[config_file["path"]] = config_file

    if len(config_files) >= 2:
        raise MultipleConfigurationFileFound(list(config_files.values()))

    future_mergify_config_file = (list(config_files.values())[0]
                                  if config_files else None)

    if current_mergify_config_file is None:
        if future_mergify_config_file is None:
            return False
    else:
        if future_mergify_config_file is None:
            # Configuration is deleted by the pull request
            await check_api.set_check_run(
                ctxt,
                constants.CONFIGURATION_DELETED_CHECK_NAME,
                check_api.Result(
                    check_api.Conclusion.SUCCESS,
                    title="The Mergify configuration has been deleted",
                    summary=
                    "Mergify will still continue to listen to commands.",
                ),
            )
            return True

        elif (current_mergify_config_file["path"]
              == future_mergify_config_file["path"]
              and current_mergify_config_file["sha"]
              == future_mergify_config_file["sha"]):
            # Nothing change between main branch and the pull request
            return False

    try:
        rules.get_mergify_config(future_mergify_config_file)
    except rules.InvalidRules as e:
        # Not configured, post status check with the error message
        await check_api.set_check_run(
            ctxt,
            constants.CONFIGURATION_CHANGED_CHECK_NAME,
            check_api.Result(
                check_api.Conclusion.FAILURE,
                title="The new Mergify configuration is invalid",
                summary=str(e),
                annotations=e.get_annotations(e.filename),
            ),
        )
    else:
        await check_api.set_check_run(
            ctxt,
            constants.CONFIGURATION_CHANGED_CHECK_NAME,
            check_api.Result(
                check_api.Conclusion.SUCCESS,
                title="The new Mergify configuration is valid",
                summary="This pull request has to be merged manually",
            ),
        )

    return True