Skip to content

Commit ef8d7e6

Browse files
authored
feat: cherry-pick retry/rebase commands + pre-commit auto-fix + security hardening (#1108)
<h3>PR Summary by Qodo</h3> Add cherry-pick retry + PR rebase commands, pre-commit auto-fix, and command guards <code>✨ Enhancement</code> <code>🐞 Bug fix</code> <code>🧪 Tests</code> <code>📝 Documentation</code> <code>🕐 40+ Minutes</code> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <h3>Walkthroughs</h3> <details open> <summary>User Description</summary> <br/> ## Summary ### New commands (#1103) - `/cherry-pick-retry <branch>` — Retry a failed cherry-pick on merged PRs - `/rebase` — Rebase any open PR onto its base branch (with bot-PR ownership validation) ### Pre-commit auto-fix (#1089) - Run pre-commit after cherry-pick, before push - If files modified, commit fixes automatically ### Security hardening - Add `is_user_valid_to_run_commands` guard to all 7 previously unguarded commands: `/cherry-pick`, `/assign-reviewer`, `/assign-reviewers`, `/check-can-merge`, `/verified`, `/wip`, `/test-oracle` - Verify bot ownership before closing cherry-pick PR on retry Closes #1103 Closes #1089 </details> <details open> <summary>AI Description</summary> <br/> <pre> • Add <b><i>/cherry-pick-retry</i></b> and <b><i>/rebase</i></b> issue-comment commands to unblock bot-driven workflows. • Run pre-commit after cherry-pick and auto-commit any formatter fixes before pushing. • Harden command execution by gating previously unguarded commands with repo ownership checks. </pre> </details> <details> <summary>Diagram</summary> <br/> ```mermaid graph TD A["Issue comment"] --> B["IssueCommentHandler"] --> C["OwnersFileHandler"] B --> D["RunnerHandler"] --> E["Local git worktree"] --> F{{"GitHub API/remote"}} D --> G["Pre-commit"] --> E B --> H["PullRequestHandler"] ``` </details> <details> <summary>High-Level Assessment</summary> <br/> The following are alternative approaches to this PR: <details> <summary><b>1. Use GitHub&#x27;s built-in &#x27;Update branch&#x27; / merge-upstream flow</b></summary> - ➕ Avoids local git worktree + force-push complexity - ➕ Leverages GitHub-native conflict reporting and permissions - ➖ Not always available/enabled for all repos and branch protection rules - ➖ Doesn&#x27;t cover all rebase semantics; may merge instead of rebase </details> <details> <summary><b>2. Centralize command authorization via a dispatcher/decorator</b></summary> - ➕ Reduces repeated guard boilerplate per command - ➕ Makes it harder to accidentally introduce unguarded commands in future - ➖ Refactor touches many call sites; higher churn and regression risk - ➖ Less explicit per-command control (unless carefully designed) </details> <details> <summary><b>3. Run formatting fixes only in CI and comment results (no auto-commit)</b></summary> - ➕ Avoids bot-authored commits and force-push interactions - ➕ Keeps PR history purely authored by humans unless opted-in - ➖ Doesn&#x27;t unblock cherry-pick automation; still requires manual fix+push - ➖ Adds latency and back-and-forth for small formatting issues </details> **Recommendation:** The PR’s approach is appropriate for a bot-driven maintenance workflow: rebase and cherry-pick actions need local git to be deterministic, and the added authorization gates materially reduce abuse risk. Consider a follow-up to centralize authorization in the command dispatcher to prevent future unguarded commands and reduce repetition. </details> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <h3>File Changes</h3> <details> <summary><strong>Enhancement</strong> (3)</summary> <blockquote> <details> <summary><strong>issue_comment_handler.py</strong> <code>Add /cherry-pick-retry + /rebase dispatch and guard sensitive commands</code> <code>+153/-0</code></summary> <br/> >Add /cherry-pick-retry + /rebase dispatch and guard sensitive commands > ><pre> >• Registers new command constants and routes &#x27;/cherry-pick-retry&#x27; and &#x27;/rebase&#x27; to the appropriate handlers. Adds &#x27;is_user_valid_to_run_commands&#x27; checks to previously unguarded commands (e.g., cherry-pick, assign-reviewer(s), check-can-merge, verified, wip, test-oracle). Implements &#x27;process_cherry_pick_retry_command&#x27; to validate merged state, require an existing cherry-pick label, close bot-owned prior cherry-pick PRs referencing the original PR, then re-run cherry-pick. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-b868fb1872f7796f85f4d0e1dca51b2720e89a4af12baa480e21d8f22333de83'>webhook_server/libs/handlers/issue_comment_handler.py</a> <hr/> </details> </blockquote> <blockquote> <details> <summary><strong>runner_handler.py</strong> <code>Run pre-commit auto-fix during cherry-pick and add PR rebase implementation</code> <code>+200/-0</code></summary> <br/> >Run pre-commit auto-fix during cherry-pick and add PR rebase implementation > ><pre> >• Enhances &#x27;cherry_pick()&#x27; to optionally run pre-commit in the cherry-pick worktree and, if hooks modified files, auto-stage and commit the fixes before pushing. Adds &#x27;rebase_pr()&#x27; which rebases the PR head onto its base branch and force-pushes with lease; for bot-owned PRs, it enforces that only the PR assignee (initiator) or maintainers can run the rebase. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-0cb54c95cafda12d8d169c7b03ac484738f4cf925c22f6e6b8b8c5db0730ce42'>webhook_server/libs/handlers/runner_handler.py</a> <hr/> </details> </blockquote> <blockquote> <details> <summary><strong>constants.py</strong> <code>Add command constants for cherry-pick retry and rebase</code> <code>+2/-0</code></summary> <br/> >Add command constants for cherry-pick retry and rebase > ><pre> >• Defines &#x27;COMMAND_CHERRY_PICK_RETRY_STR&#x27; and &#x27;COMMAND_REBASE_STR&#x27; for consistent command parsing and documentation. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-9a2d73fb31266bc568369ee81f15b1ebb12d9703f412a0ab65cf7c5a5b98060f'>webhook_server/utils/constants.py</a> <hr/> </details> </blockquote> </details> <details> <summary><strong>Tests</strong> (2)</summary> <blockquote> <details> <summary><strong>test_issue_comment_handler.py</strong> <code>Add tests for new commands and authorization guards</code> <code>+510/-0</code></summary> <br/> >Add tests for new commands and authorization guards > ><pre> >• Introduces async tests ensuring unauthorized users are blocked from newly guarded commands. Adds coverage for &#x27;/cherry-pick-retry&#x27; dispatch, argument validation, merged/label preconditions, bot-ownership checks when closing existing cherry-pick PRs, and &#x27;/rebase&#x27; dispatch/guard behavior. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-cbfe12aa39c2b47d87a6f8d21da369eb9a1c64424c49d3584a1a220f68f05c18'>webhook_server/tests/test_issue_comment_handler.py</a> <hr/> </details> </blockquote> <blockquote> <details> <summary><strong>test_runner_handler.py</strong> <code>Add rebase_pr and cherry-pick pre-commit auto-fix test coverage</code> <code>+418/-0</code></summary> <br/> >Add rebase_pr and cherry-pick pre-commit auto-fix test coverage > ><pre> >• Adds a new &#x27;TestRebasePr&#x27; suite covering non-open PR rejection, bot-owned PR authorization (assignee/maintainer), worktree failures, conflict handling with abort, and success paths. Adds &#x27;TestCherryPickPreCommitAutoFix&#x27; to validate pre-commit execution, auto-commit behavior when hooks modify files, and skipping when disabled. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-beb446b71d75b7a427d17e48e75971c270de4820cb9786a12af9e4f988887269'>webhook_server/tests/test_runner_handler.py</a> <hr/> </details> </blockquote> </details> <details> <summary><strong>Documentation</strong> (1)</summary> <blockquote> <details> <summary><strong>pull_request_handler.py</strong> <code>Expose new commands in the welcome/help message</code> <code>+5/-1</code></summary> <br/> >Expose new commands in the welcome/help message > ><pre> >• Extends the generated welcome section to document &#x27;/cherry-pick-retry&#x27; and adds a new &#x27;Branch Management&#x27; section for &#x27;/rebase&#x27;. Ensures rebase help text appears even when cherry-pick operations are not enabled. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1108/files#diff-8644dc42c86db802123c2ba72847dca72589fe19f330ecc70621af895a72fc8a'>webhook_server/libs/handlers/pull_request_handler.py</a> <hr/> </details> </blockquote> </details> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <a href="https://www.qodo.ai"><img src="https://www.qodo.ai/wp-content/uploads/2025/03/qodo-logo.svg" width="80" alt="Qodo Logo"></a>
1 parent b8c21fc commit ef8d7e6

9 files changed

Lines changed: 1691 additions & 40 deletions

webhook_server/libs/github_api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ def __init__(self, hook_data: dict[Any, Any], headers: Headers, logger: logging.
195195
github_app_api=github_app_api, repository=self.repository_full_name
196196
)
197197

198+
self.app_bot_login: str = "" # Initialized async in process()
199+
198200
if not (self.repository or self.repository_by_github_app):
199201
self.logger.error(f"{self.log_prefix} Failed to get repository.")
200202
return
@@ -537,6 +539,31 @@ async def process(self) -> Any:
537539
# Initialize auto-verified users from API users (async operation)
538540
await self.add_api_users_to_auto_verified_and_merged_users()
539541

542+
# Initialize app bot login for bot-PR identification (async)
543+
if not self.app_bot_login:
544+
_github_app_api = await github_api_call(
545+
get_repository_github_app_api,
546+
config_=self.config,
547+
repository_name=self.repository_full_name,
548+
logger=self.logger,
549+
log_prefix=self.log_prefix,
550+
)
551+
if _github_app_api:
552+
try:
553+
self.app_bot_login = await github_api_call(
554+
lambda: _github_app_api.get_user().login,
555+
logger=self.logger,
556+
log_prefix=self.log_prefix,
557+
)
558+
except asyncio.CancelledError:
559+
raise
560+
except Exception:
561+
self.logger.exception(
562+
f"{self.log_prefix} Failed to get app bot login — bot-PR detection may not work"
563+
)
564+
else:
565+
self.logger.debug(f"{self.log_prefix} No GitHub App API available — app_bot_login not set")
566+
540567
event_log: str = f"Event type: {self.github_event}. event ID: {self.x_github_delivery}"
541568

542569
# Start webhook routing context step

webhook_server/libs/handlers/issue_comment_handler.py

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
COMMAND_ASSIGN_REVIEWER_STR,
2525
COMMAND_ASSIGN_REVIEWERS_STR,
2626
COMMAND_CHECK_CAN_MERGE_STR,
27+
COMMAND_CHERRY_PICK_RETRY_STR,
2728
COMMAND_CHERRY_PICK_STR,
29+
COMMAND_REBASE_STR,
2830
COMMAND_REGENERATE_WELCOME_STR,
2931
COMMAND_REPROCESS_STR,
3032
COMMAND_RETEST_STR,
@@ -160,6 +162,8 @@ async def user_commands(
160162
COMMAND_RETEST_STR,
161163
COMMAND_REPROCESS_STR,
162164
COMMAND_CHERRY_PICK_STR,
165+
COMMAND_CHERRY_PICK_RETRY_STR,
166+
COMMAND_REBASE_STR,
163167
COMMAND_ASSIGN_REVIEWERS_STR,
164168
COMMAND_CHECK_CAN_MERGE_STR,
165169
BUILD_AND_PUSH_CONTAINER_STR,
@@ -219,6 +223,7 @@ async def user_commands(
219223
COMMAND_RETEST_STR,
220224
COMMAND_ASSIGN_REVIEWER_STR,
221225
COMMAND_ADD_ALLOWED_USER_STR,
226+
COMMAND_CHERRY_PICK_RETRY_STR,
222227
)
223228
and not _args
224229
):
@@ -253,6 +258,10 @@ async def user_commands(
253258
self.logger.debug(f"{self.log_prefix} Added reaction to comment.")
254259

255260
if _command == COMMAND_ASSIGN_REVIEWER_STR:
261+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
262+
pull_request=pull_request, reviewed_user=reviewed_user
263+
):
264+
return
256265
await self._add_reviewer_by_user_comment(pull_request=pull_request, reviewer=_args)
257266

258267
elif _command == COMMAND_ADD_ALLOWED_USER_STR:
@@ -264,16 +273,44 @@ async def user_commands(
264273
)
265274

266275
elif _command == COMMAND_ASSIGN_REVIEWERS_STR:
276+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
277+
pull_request=pull_request, reviewed_user=reviewed_user
278+
):
279+
return
267280
await self.owners_file_handler.assign_reviewers(pull_request=pull_request)
268281

269282
elif _command == COMMAND_CHECK_CAN_MERGE_STR:
283+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
284+
pull_request=pull_request, reviewed_user=reviewed_user
285+
):
286+
return
270287
await self.pull_request_handler.check_if_can_be_merged(pull_request=pull_request)
271288

272289
elif _command == COMMAND_CHERRY_PICK_STR:
290+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
291+
pull_request=pull_request, reviewed_user=reviewed_user
292+
):
293+
return
273294
await self.process_cherry_pick_command(
274295
pull_request=pull_request, command_args=_args, reviewed_user=reviewed_user
275296
)
276297

298+
elif _command == COMMAND_CHERRY_PICK_RETRY_STR:
299+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
300+
pull_request=pull_request, reviewed_user=reviewed_user
301+
):
302+
return
303+
await self.process_cherry_pick_retry_command(
304+
pull_request=pull_request, command_args=_args, reviewed_user=reviewed_user
305+
)
306+
307+
elif _command == COMMAND_REBASE_STR:
308+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
309+
pull_request=pull_request, reviewed_user=reviewed_user
310+
):
311+
return
312+
await self.runner_handler.rebase_pr(pull_request=pull_request, reviewed_user=reviewed_user)
313+
277314
elif _command == COMMAND_RETEST_STR:
278315
await self.process_retest_command(
279316
pull_request=pull_request, command_args=_args, reviewed_user=reviewed_user
@@ -295,6 +332,10 @@ async def user_commands(
295332
await self.pull_request_handler.regenerate_welcome_message(pull_request=pull_request)
296333

297334
elif _command == COMMAND_TEST_ORACLE_STR:
335+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
336+
pull_request=pull_request, reviewed_user=reviewed_user
337+
):
338+
return
298339
task = asyncio.create_task(
299340
call_test_oracle(
300341
github_webhook=self.github_webhook,
@@ -322,6 +363,10 @@ async def user_commands(
322363
)
323364

324365
elif _command == WIP_STR:
366+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
367+
pull_request=pull_request, reviewed_user=reviewed_user
368+
):
369+
return
325370
wip_for_title: str = f"{WIP_STR.upper()}:"
326371
if remove:
327372
label_changed = await self.labels_handler._remove_label(pull_request=pull_request, label=WIP_STR)
@@ -374,6 +419,10 @@ async def user_commands(
374419
await self.labels_handler._add_label(pull_request=pull_request, label=HOLD_LABEL_STR)
375420

376421
elif _command == VERIFIED_LABEL_STR:
422+
if not await self.owners_file_handler.is_user_valid_to_run_commands(
423+
pull_request=pull_request, reviewed_user=reviewed_user
424+
):
425+
return
377426
if remove:
378427
await self.labels_handler._remove_label(pull_request=pull_request, label=VERIFIED_LABEL_STR)
379428
await self.check_run_handler.set_check_queued(name=VERIFIED_LABEL_STR)
@@ -526,7 +575,8 @@ async def process_cherry_pick_command(
526575

527576
cp_labels: list[str] = [f"{CHERRY_PICK_LABEL_PREFIX}{_branch}" for _branch in _branches_to_process]
528577

529-
if not self.hook_data["issue"].get("pull_request", {}).get("merged_at"):
578+
is_merged = await github_api_call(lambda: pull_request.merged, logger=self.logger, log_prefix=self.log_prefix)
579+
if not is_merged:
530580
info_msg: str = f"""
531581
Cherry-pick requested for PR: `{pull_request.title}` by user `{reviewed_user}`
532582
Adding label/s `{" ".join(cp_labels)}` for automatic cherry-pick once the PR is merged
@@ -558,6 +608,165 @@ async def process_cherry_pick_command(
558608
for _cp_label in cp_labels:
559609
await self.labels_handler._add_label(pull_request=pull_request, label=_cp_label)
560610

611+
async def process_cherry_pick_retry_command(
612+
self, pull_request: PullRequest, command_args: str, reviewed_user: str
613+
) -> None:
614+
"""Process cherry-pick-retry command on a merged PR.
615+
616+
Retries a cherry-pick for a specific branch when the original cherry-pick
617+
failed or the cherry-pick PR has issues. Only works on merged PRs where
618+
the cherry-pick label already exists.
619+
620+
Steps:
621+
1. Validate the PR is merged
622+
2. Validate the cherry-pick label exists (this is a retry, not a new cherry-pick)
623+
3. Close any existing failed cherry-pick PR for that branch (created by bot)
624+
4. Re-run cherry_pick() for the target branch
625+
626+
Args:
627+
pull_request: The original merged pull request
628+
command_args: Target branch name to retry cherry-pick for
629+
reviewed_user: User who requested the retry
630+
"""
631+
target_branch = command_args.strip()
632+
633+
if not target_branch:
634+
msg = "cherry-pick-retry requires a branch name"
635+
self.logger.debug(f"{self.log_prefix} {msg}")
636+
await github_api_call(
637+
pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
638+
)
639+
return
640+
641+
# Validate exactly one branch name
642+
if " " in target_branch:
643+
msg = "cherry-pick-retry accepts exactly one branch name"
644+
self.logger.debug(f"{self.log_prefix} {msg}")
645+
await github_api_call(
646+
pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
647+
)
648+
return
649+
650+
self.logger.info(f"{self.log_prefix} Processing cherry-pick retry for branch {target_branch}")
651+
652+
# Validate PR is merged
653+
is_merged = await github_api_call(lambda: pull_request.merged, logger=self.logger, log_prefix=self.log_prefix)
654+
if not is_merged:
655+
msg = "Cherry-pick retry can only be used on merged PRs"
656+
self.logger.debug(f"{self.log_prefix} {msg}")
657+
await github_api_call(
658+
pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
659+
)
660+
return
661+
662+
# Validate the cherry-pick label exists (this is a retry, not a new cherry-pick)
663+
cherry_pick_label = f"{CHERRY_PICK_LABEL_PREFIX}{target_branch}"
664+
existing_labels = {
665+
label.name
666+
for label in await github_api_call(
667+
lambda: list(pull_request.labels), logger=self.logger, log_prefix=self.log_prefix
668+
)
669+
}
670+
if cherry_pick_label not in existing_labels:
671+
msg = (
672+
f"Cherry-pick label `{cherry_pick_label}` not found on this PR.\n"
673+
f"Use `/{COMMAND_CHERRY_PICK_STR} {target_branch}` to create a new cherry-pick."
674+
)
675+
self.logger.debug(f"{self.log_prefix} {msg}")
676+
await github_api_call(
677+
pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
678+
)
679+
return
680+
681+
# Find and close existing failed cherry-pick PR for this branch (created by bot)
682+
pr_title_prefix = f"CherryPicked: [{target_branch}]"
683+
original_pr_url = await github_api_call(
684+
lambda: pull_request.html_url, logger=self.logger, log_prefix=self.log_prefix
685+
)
686+
self.logger.debug(
687+
f"{self.log_prefix} Cherry-pick retry: looking for open PRs with title prefix "
688+
f"'{pr_title_prefix}' referencing {original_pr_url}"
689+
)
690+
# Convert PaginatedList to list inside github_api_call to avoid blocking iteration.
691+
# After materialization, basic properties (.title, .number, .user.login, .body) are cached.
692+
open_pulls = await github_api_call(
693+
lambda: list(self.repository.get_pulls(state="open")),
694+
logger=self.logger,
695+
log_prefix=self.log_prefix,
696+
)
697+
self.logger.debug(f"{self.log_prefix} Cherry-pick retry: found {len(open_pulls)} open PRs to scan")
698+
closed_old_pr = False
699+
for open_pr in open_pulls:
700+
if not open_pr.title.startswith(pr_title_prefix):
701+
self.logger.debug(
702+
f"{self.log_prefix} Cherry-pick retry: PR #{open_pr.number} title "
703+
f"'{open_pr.title}' does not match prefix '{pr_title_prefix}', skipping"
704+
)
705+
continue
706+
707+
self.logger.debug(f"{self.log_prefix} Cherry-pick retry: PR #{open_pr.number} title matches prefix")
708+
709+
# Verify the PR was created by our app's bot
710+
# If app_bot_login is not set, skip the close step entirely — can't verify ownership
711+
if not self.github_webhook.app_bot_login:
712+
self.logger.error(
713+
f"{self.log_prefix} Cherry-pick retry: app_bot_login not set — "
714+
"cannot verify PR ownership, skipping close step"
715+
)
716+
break
717+
718+
if open_pr.user.login != self.github_webhook.app_bot_login:
719+
self.logger.debug(
720+
f"{self.log_prefix} Cherry-pick retry: PR #{open_pr.number} author "
721+
f"'{open_pr.user.login}' is not our app bot "
722+
f"'{self.github_webhook.app_bot_login}', skipping"
723+
)
724+
continue
725+
726+
self.logger.debug(f"{self.log_prefix} Cherry-pick retry: PR #{open_pr.number} author matches our app bot")
727+
728+
# Check if the PR body references the original PR
729+
pr_body = open_pr.body or ""
730+
if original_pr_url not in pr_body:
731+
self.logger.debug(
732+
f"{self.log_prefix} Cherry-pick retry: PR #{open_pr.number} body does not "
733+
f"contain original PR URL '{original_pr_url}', skipping"
734+
)
735+
continue
736+
737+
self.logger.info(f"{self.log_prefix} Closing existing cherry-pick PR #{open_pr.number} for retry")
738+
await github_api_call(
739+
open_pr.edit,
740+
state="closed",
741+
logger=self.logger,
742+
log_prefix=self.log_prefix,
743+
)
744+
await github_api_call(
745+
open_pr.create_issue_comment,
746+
f"Closed by cherry-pick retry requested by @{reviewed_user} on {original_pr_url}",
747+
logger=self.logger,
748+
log_prefix=self.log_prefix,
749+
)
750+
closed_old_pr = True
751+
break
752+
753+
if not closed_old_pr:
754+
self.logger.debug(f"{self.log_prefix} Cherry-pick retry: no existing cherry-pick PR found to close")
755+
756+
# Re-run cherry-pick for the target branch
757+
self.logger.info(f"{self.log_prefix} Retrying cherry-pick to {target_branch}")
758+
await github_api_call(
759+
pull_request.create_issue_comment,
760+
f"Retrying cherry-pick to `{target_branch}` requested by @{reviewed_user}",
761+
logger=self.logger,
762+
log_prefix=self.log_prefix,
763+
)
764+
await self.runner_handler.cherry_pick(
765+
pull_request=pull_request,
766+
target_branch=target_branch,
767+
assign_to_pr_owner=self.github_webhook.cherry_pick_assign_to_pr_author,
768+
)
769+
561770
async def process_retest_command(
562771
self, pull_request: PullRequest, command_args: str, reviewed_user: str, automerge: bool = False
563772
) -> None:

webhook_server/libs/handlers/owners_files_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ async def is_user_valid_to_run_commands(self, pull_request: PullRequest, reviewe
481481
allow_user_comment = f"/{COMMAND_ADD_ALLOWED_USER_STR} @{reviewed_user}"
482482

483483
comment_msg = f"""
484-
{reviewed_user} is not allowed to run retest commands.
484+
{reviewed_user} is not allowed to run commands.
485485
maintainers can allow it by comment `{allow_user_comment}`
486486
Maintainers:
487487
- {"\n - ".join(allowed_user_to_approve)}

webhook_server/libs/handlers/pull_request_handler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,8 +795,12 @@ def _prepare_cherry_pick_section(self) -> str:
795795
return """#### Cherry-pick Operations
796796
* `/cherry-pick <branch>` - Schedule cherry-pick to target branch when PR is merged
797797
* Multiple branches: `/cherry-pick branch1 branch2 branch3`
798+
* `/cherry-pick-retry <branch>` - Retry a failed cherry-pick (merged PRs only)
799+
800+
#### Branch Management
801+
* `/rebase` - Rebase this PR branch onto its base branch
798802
"""
799-
return ""
803+
return "\n#### Branch Management\n* `/rebase` - Rebase this PR branch onto its base branch\n"
800804

801805
async def label_all_opened_pull_requests_merge_state_after_merged(self) -> None:
802806
"""

0 commit comments

Comments
 (0)