def _add_assignees(self, ctxt: context.Context, users_to_add: typing.List[str]) -> check_api.Result: assignees = self._wanted_users(ctxt, users_to_add) if assignees: try: ctxt.client.post( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/assignees", json={"assignees": assignees}, ) except http.HTTPClientSideError as e: # pragma: no cover return check_api.Result( check_api.Conclusion.PENDING, "Unable to add assignees", f"GitHub error: [{e.status_code}] `{e.message}`", ) return check_api.Result( check_api.Conclusion.SUCCESS, "Assignees added", ", ".join(assignees), ) return check_api.Result( check_api.Conclusion.SUCCESS, "Empty users list", "No user added to assignees", )
async def _verify_template( self, ctxt: context.Context ) -> typing.Optional[check_api.Result]: try: await ctxt.pull_request.render_template( self.config["title"], extra_variables={"destination_branch": "whatever"}, ) except context.RenderTemplateFailure as rmf: # can't occur, template have been checked earlier return check_api.Result( check_api.Conclusion.FAILURE, self.FAILURE_MESSAGE, f"Invalid title message: {rmf}", ) try: await ctxt.pull_request.render_template( self.config["body"], extra_variables={ "destination_branch": "whatever", "cherry_pick_error": "whatever", }, ) except context.RenderTemplateFailure as rmf: # can't occur, template have been checked earlier return check_api.Result( check_api.Conclusion.FAILURE, self.FAILURE_MESSAGE, f"Invalid body message: {rmf}", ) return None
def pre_rebase_check( ctxt: context.Context) -> typing.Optional[check_api.Result]: # If PR from a public fork but cannot be edited if (ctxt.pull_from_fork and not ctxt.pull["base"]["repo"]["private"] and not ctxt.pull["maintainer_can_modify"]): return check_api.Result( check_api.Conclusion.FAILURE, "Pull request can't be updated with latest base branch changes", "Mergify needs the permission to update the base branch of the pull request.\n" f"{ctxt.pull['base']['repo']['owner']['login']} needs to " "[authorize modification on its base branch]" "(https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork/).", ) # If PR from a private fork but cannot be edited: # NOTE(jd): GitHub removed the ability to configure `maintainer_can_modify` on private # fork we which make rebase impossible elif (ctxt.pull_from_fork and ctxt.pull["base"]["repo"]["private"] and not ctxt.pull["maintainer_can_modify"]): return check_api.Result( check_api.Conclusion.FAILURE, "Pull request can't be updated with latest base branch changes", "Mergify needs the permission to update the base branch of the pull request.\n" "GitHub does not allow a GitHub App to modify base branch for a private fork.\n" "You cannot `rebase` a pull request from a private fork.", ) else: return None
def run(self, ctxt, rule, missing_conditions) -> check_api.Result: if not config.GITHUB_APP: return check_api.Result( check_api.Conclusion.FAILURE, "Unavailable with GitHub Action", "Due to GitHub Action limitation, the `rebase` command is only available " "with the Mergify GitHub App.", ) if ctxt.is_behind: if ctxt.github_workflow_changed(): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Pull request must be rebased manually.", "GitHub App like Mergify are not allowed to rebase pull request where `.github/workflows` is changed.", ) try: branch_updater.update_with_git(ctxt, "rebase", self.config["bot_account"]) return check_api.Result( check_api.Conclusion.SUCCESS, "Branch has been successfully rebased", "", ) except branch_updater.BranchUpdateFailure as e: return check_api.Result(check_api.Conclusion.FAILURE, "Branch rebase failed", str(e)) else: return check_api.Result(check_api.Conclusion.SUCCESS, "Branch already up to date", "")
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 ctxt.set_summary_check( check_api.Result( check_api.Conclusion.FAILURE, title="The new Mergify configuration is invalid", summary=str(e), annotations=e.get_annotations(e.filename), ) ) else: ctxt.set_summary_check( check_api.Result( check_api.Conclusion.SUCCESS, title="The new Mergify configuration is valid", summary="This pull request must be merged manually because it modifies Mergify configuration", ) ) return True return False
def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: wanted = set() for user in set(self.config["users"]): try: user = ctxt.pull_request.render_template(user) except context.RenderTemplateFailure as rmf: # NOTE: this should never happen since the template is validated when parsing the config Ъци return check_api.Result(check_api.Conclusion.FAILURE, "Invalid assignee", str(rmf)) else: wanted.add(user) already = set((user["login"] for user in ctxt.pull["assignees"])) assignees = list(wanted - already) if assignees: try: ctxt.client.post( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/assignees", json={"assignees": assignees}, ) except http.HTTPClientSideError as e: # pragma: no cover return check_api.Result( check_api.Conclusion.PENDING, "Unable to add assignees", f"GitHub error: [{e.status_code}] `{e.message}`", ) return check_api.Result( check_api.Conclusion.SUCCESS, "Assignees added", ", ".join(self.config["users"]), )
def _subscription_status( self, ctxt: context.Context) -> typing.Optional[check_api.Result]: if self.config[ "update_bot_account"] and not ctxt.subscription.has_feature( subscription.Features.MERGE_BOT_ACCOUNT): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Merge with `update_bot_account` set is unavailable", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) elif self.config[ "merge_bot_account"] and not ctxt.subscription.has_feature( subscription.Features.MERGE_BOT_ACCOUNT): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Merge with `merge_bot_account` set is unavailable", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) elif self.config[ "priority"] != merge_base.PriorityAliases.medium.value and not ctxt.subscription.has_feature( subscription.Features.PRIORITY_QUEUES): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Merge with `priority` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) return None
async def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: try: bot_account = await action_utils.render_bot_account( ctxt, self.config["bot_account"], option_name="bot_account", required_feature=subscription.Features.BOT_ACCOUNT, missing_feature_message= "Rebase with `update_bot_account` set is unavailable", ) except action_utils.RenderBotAccountFailure as e: return check_api.Result(e.status, e.title, e.reason) try: await branch_updater.rebase_with_git( ctxt, subscription.Features.BOT_ACCOUNT, bot_account) except branch_updater.BranchUpdateFailure as e: return check_api.Result(check_api.Conclusion.FAILURE, e.title, e.message) await signals.send(ctxt, "action.rebase", {"bot_account": bool(self.config["bot_account"])}) return check_api.Result( check_api.Conclusion.SUCCESS, "Branch has been successfully rebased", "", )
async def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: if self.config["message"] is None: return check_api.Result(check_api.Conclusion.SUCCESS, "Message is not set", "") if self.config["bot_account"] and not ctxt.subscription.has_feature( subscription.Features.BOT_ACCOUNT): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Comments with `bot_account` set are disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) try: message = await ctxt.pull_request.render_template( self.config["message"]) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid comment message", str(rmf), ) if bot_account := self.config["bot_account"]: user_tokens = await ctxt.repository.installation.get_user_tokens() oauth_token = user_tokens.get_token_for(bot_account) if not oauth_token: return check_api.Result( check_api.Conclusion.FAILURE, f"Unable to comment: user `{bot_account}` is unknown. ", f"Please make sure `{bot_account}` has logged in Mergify dashboard.", )
async def _add_assignees( self, ctxt: context.Context, users_to_add: typing.List[str]) -> check_api.Result: users_to_add_parsed = await self.wanted_users(ctxt, users_to_add) assignees = list(users_to_add_parsed - {a["login"] for a in ctxt.pull["assignees"]}) if assignees: try: await ctxt.client.post( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/assignees", json={"assignees": assignees}, ) except http.HTTPClientSideError as e: # pragma: no cover return check_api.Result( check_api.Conclusion.PENDING, "Unable to add assignees", f"GitHub error: [{e.status_code}] `{e.message}`", ) await signals.send(ctxt, "action.assign.added") return check_api.Result( check_api.Conclusion.SUCCESS, "Assignees added", ", ".join(assignees), ) return check_api.Result( check_api.Conclusion.SUCCESS, "Empty users list", "No user added to assignees", )
async def run( self, ctxt: context.Context, rule: rules.EvaluatedRule ) -> check_api.Result: train = await merge_train.Train.from_context(ctxt) if await train.get_position(ctxt) is None: return check_api.Result( check_api.Conclusion.NEUTRAL, title="The pull request is not queued", summary="", ) # manually set a status, to not automatically re-embark it await check_api.set_check_run( ctxt, constants.MERGE_QUEUE_SUMMARY_NAME, check_api.Result( check_api.Conclusion.CANCELLED, title="The pull request has been removed from the queue by an `unqueue` command", summary="", ), ) await train.remove_pull(ctxt) await signals.send( ctxt.repository, ctxt.pull["number"], "action.unqueue", signals.EventNoMetadata(), ) return check_api.Result( check_api.Conclusion.SUCCESS, title="The pull request has been removed from the queue", summary="", )
async def run(self, ctxt: context.Context, rule: "rules.EvaluatedRule") -> check_api.Result: try: merge_bot_account = await action_utils.render_bot_account( ctxt, self.config["merge_bot_account"], option_name="merge_bot_account", required_feature=subscription.Features.MERGE_BOT_ACCOUNT, missing_feature_message= "Merge with `merge_bot_account` set is unavailable", # NOTE(sileht): we don't allow admin, because if branch protection are # enabled, but not enforced on admins, we may bypass them required_permissions=["write", "maintain"], ) except action_utils.RenderBotAccountFailure as e: return check_api.Result(e.status, e.title, e.reason) if self.config["method"] == "fast-forward": if self.config["commit_message_template"] is not None: return check_api.Result( check_api.Conclusion.FAILURE, "Commit message can't be changed with fast-forward merge method", "`commit_message_template` must not be set if `method: fast-forward` is set.", ) report = await self.merge_report(ctxt) if report is not None: return report return await self._merge(ctxt, rule, None, merge_bot_account)
async def _remove_assignees( self, ctxt: context.Context, users_to_remove: typing.List[str]) -> check_api.Result: assignees = await self.wanted_users(ctxt, users_to_remove) if assignees: try: await ctxt.client.request( "DELETE", f"{ctxt.base_url}/issues/{ctxt.pull['number']}/assignees", json={"assignees": assignees}, ) except http.HTTPClientSideError as e: # pragma: no cover return check_api.Result( check_api.Conclusion.PENDING, "Unable to remove assignees", f"GitHub error: [{e.status_code}] `{e.message}`", ) return check_api.Result( check_api.Conclusion.SUCCESS, "Assignees removed", ", ".join(assignees), ) return check_api.Result( check_api.Conclusion.SUCCESS, "Empty users list", "No user removed from assignees", )
async def _subscription_status( self, ctxt: context.Context) -> typing.Optional[check_api.Result]: if self.queue_rule.config[ "speculative_checks"] > 1 and not ctxt.subscription.has_feature( subscription.Features.QUEUE_ACTION): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with `speculative_checks` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) elif self.config[ "priority"] != merge_base.PriorityAliases.medium.value and not ctxt.subscription.has_feature( subscription.Features.PRIORITY_QUEUES): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with `priority` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) bot_account_result = await action_utils.validate_bot_account( ctxt, self.config["merge_bot_account"], option_name="merge_bot_account", required_feature=subscription.Features.MERGE_BOT_ACCOUNT, missing_feature_message= "Queue with `merge_bot_account` set is unavailable", ) if bot_account_result is not None: return bot_account_result return None
async def run( self, ctxt: context.Context, rule: rules.EvaluatedRule ) -> check_api.Result: labels_changed = False pull_labels = {label["name"] for label in ctxt.pull["labels"]} if self.config["add"]: all_label = [ label["name"] async for label in ctxt.client.items(f"{ctxt.base_url}/labels") ] for label in self.config["add"]: if label not in all_label: color = f"{random.randrange(16 ** 6):06x}" # nosec try: await ctxt.client.post( f"{ctxt.base_url}/labels", json={"name": label, "color": color}, ) except http.HTTPClientSideError: continue missing_labels = set(self.config["add"]) - pull_labels if missing_labels: await ctxt.client.post( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/labels", json={"labels": list(missing_labels)}, ) labels_changed = True if self.config["remove_all"]: if ctxt.pull["labels"]: await ctxt.client.delete( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/labels" ) labels_changed = True elif self.config["remove"]: for label in self.config["remove"]: if label in pull_labels: label_escaped = parse.quote(label, safe="") try: await ctxt.client.delete( f"{ctxt.base_url}/issues/{ctxt.pull['number']}/labels/{label_escaped}" ) except http.HTTPClientSideError: continue labels_changed = True if labels_changed: await signals.send(ctxt, "action.label") return check_api.Result( check_api.Conclusion.SUCCESS, "Labels added/removed", "" ) else: return check_api.Result( check_api.Conclusion.SUCCESS, "No label to add or remove", "" )
async def run( self, ctxt: context.Context, rule: "rules.EvaluatedRule" ) -> check_api.Result: if not config.GITHUB_APP: if self.config["strict_method"] == "rebase": return check_api.Result( check_api.Conclusion.FAILURE, "Misconfigured for GitHub Action", "Due to GitHub Action limitation, `strict_method: rebase` " "is only available with the Mergify GitHub App", ) if self.config["bot_account"] is not None: if ctxt.subscription.has_feature(subscription.Features.MERGE_BOT_ACCOUNT): ctxt.log.info("legacy bot_account used by paid plan") else: ctxt.log.info("legacy bot_account used by free plan") self._set_effective_priority(ctxt) ctxt.log.info("process merge", config=self.config) q = await self._get_queue(ctxt) report = await self.merge_report(ctxt) if report is not None: await q.remove_pull(ctxt) return report if self.config["strict"] in ( StrictMergeParameter.fasttrack, StrictMergeParameter.ordered, ): if await self._should_be_queued(ctxt, q): await q.add_pull(ctxt, typing.cast(queue.PullQueueConfig, self.config)) else: await q.remove_pull(ctxt) return check_api.Result( check_api.Conclusion.CANCELLED, "The pull request has been removed from the queue", "The queue conditions cannot be satisfied due to failing checks.", ) try: if await self._should_be_merged(ctxt, q): result = await self._merge(ctxt, rule, q) elif await self._should_be_synced(ctxt, q): result = await self._sync_with_base_branch(ctxt, rule, q) else: result = await self.get_queue_status( ctxt, rule, q, is_behind=await ctxt.is_behind ) except Exception: await q.remove_pull(ctxt) raise if result.conclusion is not check_api.Conclusion.PENDING: await q.remove_pull(ctxt) return result
async def _post(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: # TODO(sileht): Don't run it if conditions contains the rule itself, as it can # created an endless loop of events. if not ctxt.subscription.has_feature( subscription.Features.CUSTOM_CHECKS): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Custom checks are disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) extra_variables: typing.Dict[str, typing.Union[str, bool]] = { "check_rule_name": rule.name, "check_succeed": rule.conditions.match, "check_conditions": rule.conditions.get_summary(), } try: title = await ctxt.pull_request.render_template( self.config["title"], extra_variables, ) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid title template", str(rmf), ) try: summary = await ctxt.pull_request.render_template( self.config["summary"], extra_variables) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid summary template", str(rmf), ) if rule.conditions.match: conclusion = check_api.Conclusion.SUCCESS else: conclusion = check_api.Conclusion.FAILURE check = await ctxt.get_engine_check_run( rule.get_check_name("post_check")) if (not check or check["conclusion"] != conclusion.value or check["output"]["title"] != title or check["output"]["summary"] != summary): await signals.send( ctxt.repository, ctxt.pull["number"], "action.post_check", signals.EventNoMetadata(), ) return check_api.Result(conclusion, title, summary)
async def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: if ctxt.pull_from_fork: return check_api.Result(check_api.Conclusion.SUCCESS, "Pull request come from fork", "") if not self.config["force"]: pulls_using_this_branch = [ pull async for pull in ctxt.client.items( f"{ctxt.base_url}/pulls", resource_name="pulls", page_limit=20, params={"base": ctxt.pull["head"]["ref"]}, ) ] + [ pull async for pull in ctxt.client.items( f"{ctxt.base_url}/pulls", resource_name="pulls", page_limit=5, params={"head": ctxt.pull["head"]["label"]}, ) if pull["number"] is not ctxt.pull["number"] ] if pulls_using_this_branch: pulls_using_this_branch_formatted = "\n".join( f"* Pull request #{p['number']}" for p in pulls_using_this_branch) return check_api.Result( check_api.Conclusion.NEUTRAL, "Not deleting the head branch", f"Branch `{ctxt.pull['head']['ref']}` was not deleted " f"because it is used by:\n{pulls_using_this_branch_formatted}", ) ref_to_delete = parse.quote(ctxt.pull["head"]["ref"], safe="") try: await ctxt.client.delete( f"{ctxt.base_url}/git/refs/heads/{ref_to_delete}") except http.HTTPClientSideError as e: if e.status_code == 404 or (e.status_code == 422 and "Reference does not exist" in e.message): return check_api.Result( check_api.Conclusion.SUCCESS, f"Branch `{ctxt.pull['head']['ref']}` does not exist", "", ) else: return check_api.Result( check_api.Conclusion.FAILURE, "Unable to delete the head branch", f"GitHub error: [{e.status_code}] `{e.message}`", ) await signals.send(ctxt, "action.delete_head_branch") return check_api.Result( check_api.Conclusion.SUCCESS, f"Branch `{ctxt.pull['head']['ref']}` has been deleted", "", )
def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: payload = {"event": self.config["type"]} if self.config["message"]: try: body = ctxt.pull_request.render_template(self.config["message"]) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid review message", str(rmf) ) else: body = None if not body and self.config["type"] != "APPROVE": body = ( "Pull request automatically reviewed by Mergify: %s" % self.config["type"] ) if body: payload["body"] = body # TODO(sileht): We should catch it some how, when we drop pygithub for sure reviews = reversed( list( filter( lambda r: r["user"]["id"] is not config.BOT_USER_ID, ctxt.reviews ) ) ) for review in reviews: if ( review["body"] == (body or "") and review["state"] == EVENT_STATE_MAP[self.config["type"]] ): # Already posted return check_api.Result( check_api.Conclusion.SUCCESS, "Review already posted", "" ) elif ( self.config["type"] == "REQUEST_CHANGES" and review["state"] == "APPROVED" ): break elif ( self.config["type"] == "APPROVE" and review["state"] == "CHANGES_REQUESTED" ): break ctxt.client.post( f"{ctxt.base_url}/pulls/{ctxt.pull['number']}/reviews", json=payload ) return check_api.Result(check_api.Conclusion.SUCCESS, "Review posted", "")
async def _post(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: # TODO(sileht): Don't run it if conditions contains the rule itself, as it can # created an endless loop of events. if not ctxt.subscription.has_feature( subscription.Features.CUSTOM_CHECKS): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Custom checks are disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) check_succeed = not bool(rule.missing_conditions) check_conditions = "" for cond in rule.conditions: checked = " " if cond in rule.missing_conditions else "X" check_conditions += f"\n- [{checked}] `{cond}`" if cond.description: check_conditions += f" [{cond.description}]" extra_variables = { "check_rule_name": rule.name, "check_succeed": check_succeed, "check_conditions": check_conditions, } try: title = await ctxt.pull_request.render_template( self.config["title"], extra_variables, ) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid title template", str(rmf), ) try: summary = await ctxt.pull_request.render_template( self.config["summary"], extra_variables) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid summary template", str(rmf), ) await signals.send(ctxt, "action.post_check") if rule.missing_conditions: return check_api.Result(check_api.Conclusion.FAILURE, title, summary) else: return check_api.Result(check_api.Conclusion.SUCCESS, title, summary)
def run(self, ctxt, rule, missing_conditions) -> check_api.Result: if not config.GITHUB_APP: if self.config["strict_method"] == "rebase": return check_api.Result( check_api.Conclusion.FAILURE, "Misconfigured for GitHub Action", "Due to GitHub Action limitation, `strict_method: rebase` " "is only available with the Mergify GitHub App", ) if self.config[ "update_bot_account"] and not ctxt.subscription.has_feature( subscription.Features.MERGE_BOT_ACCOUNT): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Merge with `update_bot_account` set are disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) if self.config[ "merge_bot_account"] and not ctxt.subscription.has_feature( subscription.Features.MERGE_BOT_ACCOUNT): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Merge with `merge_bot_account` set are disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) self._set_effective_priority(ctxt) ctxt.log.info("process merge", config=self.config) q = queue.Queue.from_context(ctxt) result = helpers.merge_report(ctxt, self.config["strict"]) if result: q.remove_pull(ctxt.pull["number"]) return result if self.config["strict"] in ("smart+fasttrack", "smart+ordered"): q.add_pull(ctxt, self.config) if self._should_be_merged(ctxt, q): try: result = self._merge(ctxt, rule, missing_conditions, q) if result.conclusion is not check_api.Conclusion.PENDING: q.remove_pull(ctxt.pull["number"]) return result except Exception: q.remove_pull(ctxt.pull["number"]) raise else: return self._sync_with_base_branch(ctxt, rule, missing_conditions, q)
async def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: check = await ctxt.get_engine_check_run( constants.MERGE_QUEUE_SUMMARY_NAME) if not check: return check_api.Result( check_api.Conclusion.FAILURE, title= "This pull request head commit has not been previously disembarked from queue.", summary="", ) if check_api.Conclusion(check["conclusion"]) in [ check_api.Conclusion.SUCCESS, check_api.Conclusion.NEUTRAL, check_api.Conclusion.PENDING, ]: return check_api.Result( check_api.Conclusion.NEUTRAL, title="This pull request is already queued", summary="", ) await check_api.set_check_run( ctxt, constants.MERGE_QUEUE_SUMMARY_NAME, check_api.Result( check_api.Conclusion.NEUTRAL, "This pull request can be re-embarked automatically", "", ), ) # NOTE(sileht): refresh it to maybe, retrigger the queue action. await utils.send_pull_refresh( ctxt.redis.stream, ctxt.pull["base"]["repo"], pull_request_number=ctxt.pull["number"], action="user", source="action/command/requeue", ) await signals.send( ctxt.repository, ctxt.pull["number"], "action.requeue", signals.EventNoMetadata(), ) return check_api.Result( check_api.Conclusion.SUCCESS, title= "The queue state of this pull request has been cleaned. It can be re-embarked automatically", summary="", )
def _sync_with_base_branch( self, ctxt: context.Context, rule: "rules.EvaluatedRule", q: queue.Queue ) -> check_api.Result: # If PR from a public fork but cannot be edited if ( ctxt.pull_from_fork and not ctxt.pull["base"]["repo"]["private"] and not ctxt.pull["maintainer_can_modify"] ): return check_api.Result( check_api.Conclusion.FAILURE, "Pull request can't be updated with latest base branch changes", "Mergify needs the permission to update the base branch of the pull request.\n" f"{ctxt.pull['base']['repo']['owner']['login']} needs to " "[authorize modification on its base branch]" "(https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork/).", ) # If PR from a private fork but cannot be edited: # NOTE(jd): GitHub removed the ability to configure `maintainer_can_modify` on private fork we which make strict mode broken elif ( ctxt.pull_from_fork and ctxt.pull["base"]["repo"]["private"] and not ctxt.pull["maintainer_can_modify"] ): return check_api.Result( check_api.Conclusion.FAILURE, "Pull request can't be updated with latest base branch changes", "Mergify needs the permission to update the base branch of the pull request.\n" "GitHub does not allow a GitHub App to modify base branch for a private fork.\n" "You cannot use strict mode with a pull request from a private fork.", ) method = self.config["strict_method"] user = self.config["update_bot_account"] or self.config["bot_account"] try: if method == "merge": branch_updater.update_with_api(ctxt) else: branch_updater.update_with_git(ctxt, method, user) except branch_updater.BranchUpdateFailure as e: # NOTE(sileht): Maybe the PR has been rebased and/or merged manually # in the meantime. So double check that to not report a wrong status. ctxt.update() output = self.merge_report(ctxt) if output: return output else: q.move_pull_at_end(ctxt.pull["number"], self.config) return check_api.Result( check_api.Conclusion.FAILURE, "Base branch update has failed", e.message, ) else: return self.get_strict_status(ctxt, rule, q, is_behind=False)
async def run(self, ctxt: context.Context, rule: rules.EvaluatedRule) -> check_api.Result: if ctxt.have_been_synchronized(): # FIXME(sileht): Currently sender id is not the bot by the admin # user that enroll the repo in Mergify, because branch_updater uses # his access_token instead of the Mergify installation token. # As workaround we track in redis merge commit id # This is only true for method="rebase" if await ctxt.redis.get(f"branch-update-{ctxt.pull['head']['sha']}" ): return check_api.Result( check_api.Conclusion.SUCCESS, "Updated by Mergify, ignoring", "", ) try: message = await ctxt.pull_request.render_template( self.config["message"]) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid dismiss reviews message", str(rmf), ) errors = set() for review in (await ctxt.consolidated_reviews())[1]: conf = self.config.get(review["state"].lower(), False) if conf and (conf is True or review["user"]["login"] in conf): try: await ctxt.client.put( f"{ctxt.base_url}/pulls/{ctxt.pull['number']}/reviews/{review['id']}/dismissals", json={"message": message}, ) except http.HTTPClientSideError as e: # pragma: no cover errors.add( f"GitHub error: [{e.status_code}] `{e.message}`") if errors: return check_api.Result( check_api.Conclusion.PENDING, "Unable to dismiss review", "\n".join(errors), ) else: await signals.send(ctxt, "action.dismiss_reviews") return check_api.Result(check_api.Conclusion.SUCCESS, "Review dismissed", "") else: return check_api.Result( check_api.Conclusion.SUCCESS, "Nothing to do, pull request have not been synchronized", "", )
def run(self, ctxt, rule, missing_conditions) -> check_api.Result: if self._have_been_synchronized(ctxt): # FIXME(sileht): Currently sender id is not the bot by the admin # user that enroll the repo in Mergify, because branch_updater uses # his access_token instead of the Mergify installation token. # As workaround we track in redis merge commit id # This is only true for method="rebase" # FIXME ignore type: https://github.com/python/typeshed/pull/4655 with utils.get_redis_for_cache() as redis: # type: ignore if redis.get("branch-update-%s" % ctxt.pull["head"]["sha"]): return check_api.Result( check_api.Conclusion.SUCCESS, "Rebased/Updated by us, nothing to do", "", ) try: message = ctxt.pull_request.render_template( self.config["message"]) except context.RenderTemplateFailure as rmf: return check_api.Result( check_api.Conclusion.FAILURE, "Invalid dismiss reviews message", str(rmf), ) errors = set() for review in ctxt.consolidated_reviews[1]: conf = self.config.get(review["state"].lower(), False) if conf and (conf is True or review["user"]["login"] in conf): try: ctxt.client.put( f"{ctxt.base_url}/pulls/{ctxt.pull['number']}/reviews/{review['id']}/dismissals", json={"message": message}, ) except http.HTTPClientSideError as e: # pragma: no cover errors.add( f"GitHub error: [{e.status_code}] `{e.message}`") if errors: return check_api.Result( check_api.Conclusion.PENDING, "Unable to dismiss review", "\n".join(errors), ) else: return check_api.Result(check_api.Conclusion.SUCCESS, "Review dismissed", "") else: return check_api.Result( check_api.Conclusion.SUCCESS, "Nothing to do, pull request have not been synchronized", "", )
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 config_file_to_validate: typing.Optional[context.MergifyConfigFile] = None preferred_filename = (None if current_mergify_config_file is None else current_mergify_config_file["path"]) # NOTE(sileht): Just a shorcut to do two requests instead of three. if ctxt.pull["changed_files"] <= 100: for f in await ctxt.files: if f["filename"] in context.Repository.MERGIFY_CONFIG_FILENAMES: preferred_filename = f["filename"] break else: return False async for config_file in ctxt.repository.iter_mergify_config_files( ref=ctxt.pull["head"]["sha"], preferred_filename=preferred_filename): if (current_mergify_config_file is None or config_file["path"] != current_mergify_config_file["path"]): config_file_to_validate = config_file break elif config_file["sha"] != current_mergify_config_file["sha"]: config_file_to_validate = config_file break if config_file_to_validate is None: return False try: rules.get_mergify_config(config_file_to_validate) except rules.InvalidRules as e: # Not configured, post status check with the error message await ctxt.set_summary_check( 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 ctxt.set_summary_check( check_api.Result( check_api.Conclusion.SUCCESS, title="The new Mergify configuration is valid", summary= "This pull request must be merged manually because it modifies Mergify configuration", )) return True
async def test_check_run_api(self) -> None: await self.setup_repo() p = await self.create_pr() ctxt = await context.Context.create(self.repository_ctxt, p, []) await check_api.set_check_run( ctxt, "Test", check_api.Result(check_api.Conclusion.PENDING, title="PENDING", summary="PENDING"), ) checks = await ctxt.pull_engine_check_runs assert len(checks) == 1 assert checks[0]["status"] == "in_progress" assert checks[0]["conclusion"] is None assert checks[0]["completed_at"] is None assert checks[0]["output"]["title"] == "PENDING" assert checks[0]["output"]["summary"] == "PENDING" await check_api.set_check_run( ctxt, "Test", check_api.Result(check_api.Conclusion.CANCELLED, title="CANCELLED", summary="CANCELLED"), ) ctxt._caches = context.ContextCaches() checks = await ctxt.pull_engine_check_runs assert len(checks) == 1 assert checks[0]["status"] == "completed" assert checks[0]["conclusion"] == "cancelled" assert checks[0]["completed_at"] is not None assert checks[0]["output"]["title"] == "CANCELLED" assert checks[0]["output"]["summary"] == "CANCELLED" await check_api.set_check_run( ctxt, "Test", check_api.Result(check_api.Conclusion.PENDING, title="PENDING", summary="PENDING"), ) ctxt._caches = context.ContextCaches() checks = await ctxt.pull_engine_check_runs assert len(checks) == 1 assert checks[0]["status"] == "in_progress" assert checks[0]["conclusion"] is None assert checks[0]["completed_at"] is None assert checks[0]["output"]["title"] == "PENDING" assert checks[0]["output"]["summary"] == "PENDING"
async def _subscription_status( self, ctxt: context.Context ) -> typing.Optional[check_api.Result]: if self.queue_count > 1 and not ctxt.subscription.has_feature( subscription.Features.QUEUE_ACTION ): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with more than 1 rule set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"] ), ) elif self.queue_rule.config[ "speculative_checks" ] > 1 and not ctxt.subscription.has_feature(subscription.Features.QUEUE_ACTION): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with `speculative_checks` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"] ), ) elif self.queue_rule.config[ "batch_size" ] > 1 and not ctxt.subscription.has_feature(subscription.Features.QUEUE_ACTION): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with `batch_size` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"] ), ) elif self.config[ "priority" ] != queue.PriorityAliases.medium.value and not ctxt.subscription.has_feature( subscription.Features.PRIORITY_QUEUES ): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue with `priority` set is unavailable.", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"] ), ) return None
async def run(self, ctxt: context.Context, rule: "rules.EvaluatedRule") -> check_api.Result: if not ctxt.subscription.has_feature( subscription.Features.QUEUE_ACTION): return check_api.Result( check_api.Conclusion.ACTION_REQUIRED, "Queue action is disabled", ctxt.subscription.missing_feature_reason( ctxt.pull["base"]["repo"]["owner"]["login"]), ) q = await merge_train.Train.from_context(ctxt) car = q.get_car(ctxt) if car and car.state == "updated": # NOTE(sileht): This car doesn't have tmp pull, so we have the # MERGE_QUEUE_SUMMARY and train reset here need_reset = ctxt.have_been_synchronized() or await ctxt.is_behind if need_reset: status = check_api.Conclusion.PENDING ctxt.log.info("train will be reset") await q.reset() else: queue_rule_evaluated = await self.queue_rule.get_pull_request_rule( ctxt) status = await merge_train.get_queue_rule_checks_status( ctxt, queue_rule_evaluated) await car.update_summaries(status, will_be_reset=need_reset) if ctxt.user_refresh_requested() or ctxt.admin_refresh_requested(): # NOTE(sileht): user ask a refresh, we just remove the previous state of this # check and the method _should_be_queue will become true again :) check = await ctxt.get_engine_check_run( constants.MERGE_QUEUE_SUMMARY_NAME) if check and check_api.Conclusion(check["conclusion"]) not in [ check_api.Conclusion.SUCCESS, check_api.Conclusion.PENDING, ]: await check_api.set_check_run( ctxt, constants.MERGE_QUEUE_SUMMARY_NAME, check_api.Result( check_api.Conclusion.PENDING, "The pull request has been refreshed and is going to be re-embarked soon", "", ), ) return await super().run(ctxt, rule)
async def _ensure_summary_on_head_sha(ctxt: context.Context) -> None: for check in await ctxt.pull_engine_check_runs: if check[ "name"] == ctxt.SUMMARY_NAME and actions_runner.load_conclusions_line( ctxt, check): return opened = ctxt.has_been_opened() sha = await ctxt.get_cached_last_summary_head_sha() if sha is None: if not opened: ctxt.log.warning( "the pull request doesn't have the last summary head sha stored in redis" ) return previous_summary = await _get_summary_from_sha(ctxt, sha) if previous_summary: await ctxt.set_summary_check( check_api.Result( check_api.Conclusion(previous_summary["conclusion"]), title=previous_summary["output"]["title"], summary=previous_summary["output"]["summary"], )) elif not opened: ctxt.log.warning("the pull request doesn't have a summary")