예제 #1
0
async def run_pending_commands_tasks(
        ctxt: context.Context, mergify_config: rules.MergifyConfig) -> None:
    if ctxt.is_merge_queue_pr():
        # We don't allow any command yet
        return

    pendings = set()
    async for comment in ctxt.client.items(
            f"{ctxt.base_url}/issues/{ctxt.pull['number']}/comments"):
        if comment["user"]["id"] != config.BOT_USER_ID:
            continue
        match = COMMAND_RESULT_MATCHER.search(comment["body"])
        if match:
            command = match[1]
            state = match[2]
            if state == "pending":
                pendings.add(command)
            elif command in pendings:
                pendings.remove(command)

    for pending in pendings:
        await handle(ctxt,
                     mergify_config,
                     f"@Mergifyio {pending}",
                     None,
                     rerun=True)
예제 #2
0
async def handle(
    ctxt: context.Context,
    mergify_config: rules.MergifyConfig,
    comment: str,
    user: typing.Optional[github_types.GitHubAccount],
    rerun: bool = False,
) -> None:
    # Run command only if this is a pending task or if user have permission to do it.
    if not rerun and not user:
        raise RuntimeError("user must be set if rerun is false")

    def log(comment_out: str,
            result: typing.Optional[check_api.Result] = None) -> None:
        ctxt.log.info(
            "ran command",
            user_login=None if user is None else user["login"],
            rerun=rerun,
            comment_in=comment,
            comment_out=comment_out,
            result=result,
        )

    try:
        command = load_command(mergify_config, comment)
    except CommandInvalid as e:
        log(e.message)
        await post_comment(ctxt, e.message)
        return
    except NotACommand:
        return

    if user:
        if (user["id"] != ctxt.pull["user"]["id"]
                and user["id"] != config.BOT_USER_ID
                and not await ctxt.repository.has_write_permission(user)):
            message = f"@{user['login']} is not allowed to run commands"
            log(message)
            await post_comment(ctxt, message)
            return

    if (ctxt.configuration_changed
            and actions.ActionFlag.ALLOW_ON_CONFIGURATION_CHANGED
            not in command.action.flags):
        message = CONFIGURATION_CHANGE_MESSAGE
        log(message)
        await post_comment(ctxt, message)
        return

    if command.name != "refresh" and ctxt.is_merge_queue_pr():
        log(MERGE_QUEUE_COMMAND_MESSAGE)
        await post_comment(ctxt, MERGE_QUEUE_COMMAND_MESSAGE)
        return

    result, message = await run_command(ctxt, mergify_config, command, user)
    if result.conclusion is check_api.Conclusion.PENDING and rerun:
        log("action still pending", result)
        return

    log(message, result)
    await post_comment(ctxt, message)
예제 #3
0
async def handle(
    ctxt: context.Context,
    mergify_config: rules.MergifyConfig,
    comment: str,
    user: typing.Optional[github_types.GitHubAccount],
    rerun: bool = False,
) -> None:
    # Run command only if this is a pending task or if user have permission to do it.
    if not rerun and not user:
        raise RuntimeError("user must be set if rerun is false")

    def log(comment_out: str,
            result: typing.Optional[check_api.Result] = None) -> None:
        ctxt.log.info(
            "ran command",
            user_login=None if user is None else user["login"],
            rerun=rerun,
            comment_in=comment,
            comment_out=comment_out,
            result=result,
        )

    if "@mergifyio" in comment.lower():  # @mergify have been used instead
        footer = ""
    else:
        footer = "\n\n" + WRONG_ACCOUNT_MESSAGE

    if user:
        if (user["id"] != ctxt.pull["user"]["id"]
                and user["id"] != config.BOT_USER_ID
                and not await ctxt.repository.has_write_permission(user)):
            message = f"@{user['login']} is not allowed to run commands"
            log(message)
            await post_comment(ctxt, message + footer)
            return

    action = load_action(mergify_config, comment)
    if not action:
        message = UNKNOWN_COMMAND_MESSAGE
        log(message)
        await post_comment(ctxt, message + footer)
        return

    if action[0] != "refresh" and ctxt.is_merge_queue_pr():
        log(MERGE_QUEUE_COMMAND_MESSAGE)
        await post_comment(ctxt, MERGE_QUEUE_COMMAND_MESSAGE + footer)
        return

    result, message = await run_action(ctxt, action, user)
    if result.conclusion is check_api.Conclusion.PENDING and rerun:
        log("action still pending", result)
        return

    log(message, result)
    await post_comment(ctxt, message + footer)
예제 #4
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)
예제 #5
0
async def run(
    ctxt: context.Context,
    sources: typing.List[context.T_PayloadEventSource],
) -> typing.Optional[check_api.Result]:
    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)

    permissions_need_to_be_updated = github_app.permissions_need_to_be_updated(
        ctxt.repository.installation.installation)
    if permissions_need_to_be_updated:
        return check_api.Result(
            check_api.Conclusion.FAILURE,
            title="Required GitHub permissions are missing.",
            summary="You can accept them at https://dashboard.mergify.com/",
        )

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

    config_file = await ctxt.repository.get_mergify_config_file()

    try:
        ctxt.configuration_changed = await _check_configuration_changes(
            ctxt, config_file)
    except MultipleConfigurationFileFound as e:
        files = "\n * " + "\n * ".join(f["path"] for f in e.files)
        # NOTE(sileht): This replaces the summary, so we will may lost the
        # state of queue/comment action. But since we can't choice which config
        # file we need to use... we can't do much.
        return check_api.Result(
            check_api.Conclusion.FAILURE,
            title=constants.CONFIGURATION_MUTIPLE_FOUND_SUMMARY_TITLE,
            summary=
            f"You must keep only one of these configuration files in the repository: {files}",
        )

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = await ctxt.repository.get_mergify_config()
    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"):
                    return check_api.Result(
                        check_api.Conclusion.FAILURE,
                        title="The current Mergify configuration is invalid",
                        summary=str(e),
                        annotations=e.get_annotations(e.filename),
                    )
        return None

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

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

    await _ensure_summary_on_head_sha(ctxt)

    summary = await ctxt.get_engine_check_run(constants.SUMMARY_NAME)
    if (summary and summary["external_id"] is not None
            and summary["external_id"] != ""
            and summary["external_id"] != str(ctxt.pull["number"])):
        other_ctxt = await ctxt.repository.get_pull_request_context(
            github_types.GitHubPullRequestNumber(int(summary["external_id"])))
        # NOTE(sileht): allow to override the summary of another pull request
        # only if this one is closed, but this can still confuse users as the
        # check-runs created by merge/queue action will not be cleaned.
        # TODO(sileht): maybe cancel all other mergify engine check-runs in this case?
        if not other_ctxt.closed:
            # TODO(sileht): try to report that without check-runs/statuses to the user
            # and without spamming him with comment
            ctxt.log.info(
                "sha collision detected between pull requests",
                other_pull=summary["external_id"],
            )
            return None

    if not ctxt.has_been_opened() and summary is None:
        ctxt.log.warning(
            "the pull request doesn't have a summary",
            head_sha=ctxt.pull["head"]["sha"],
        )

    ctxt.log.debug("engine handle actions")
    if ctxt.is_merge_queue_pr():
        return await queue_runner.handle(mergify_config["queue_rules"], ctxt)
    else:
        return await actions_runner.handle(
            mergify_config["pull_request_rules"], ctxt)
예제 #6
0
async def run_pending_commands_tasks(
        ctxt: context.Context, mergify_config: rules.MergifyConfig) -> None:
    if ctxt.is_merge_queue_pr():
        # We don't allow any command yet
        return

    pendings = set()
    async for comment in ctxt.client.items(
            f"{ctxt.base_url}/issues/{ctxt.pull['number']}/comments",
            resource_name="comments",
            page_limit=20,
    ):
        if comment["user"]["id"] != config.BOT_USER_ID:
            continue

        # Old format
        match = COMMAND_RESULT_MATCHER_OLD.search(comment["body"])
        if match:
            command = match[1]
            state = match[2]
            if state == "pending":
                pendings.add(command)
            elif command in pendings:
                pendings.remove(command)

            continue

        # New format
        match = COMMAND_RESULT_MATCHER.search(comment["body"])

        if match is None:
            continue

        try:
            payload = json.loads(match[1])
        except Exception:
            LOG.warning("Unable to load command payload: %s", match[1])
            continue

        command = payload.get("command")
        if not command:
            continue

        conclusion_str = payload.get("conclusion")

        try:
            conclusion = check_api.Conclusion(conclusion_str)
        except ValueError:
            LOG.error("Unable to load conclusions %s", conclusion_str)
            continue

        if conclusion == check_api.Conclusion.PENDING:
            pendings.add(command)
        elif command in pendings:
            try:
                pendings.remove(command)
            except KeyError:
                LOG.error("Unable to remove command: %s", command)

    for pending in pendings:
        await handle(ctxt,
                     mergify_config,
                     f"@Mergifyio {pending}",
                     None,
                     rerun=True)