Skip to content

feat: cherry-pick retry/rebase commands + pre-commit auto-fix + security hardening#1108

Merged
myakove merged 17 commits into
mainfrom
fix/issue-1103-1089-cherry-pick
Jun 10, 2026
Merged

feat: cherry-pick retry/rebase commands + pre-commit auto-fix + security hardening#1108
myakove merged 17 commits into
mainfrom
fix/issue-1103-1089-cherry-pick

Conversation

@myakove

@myakove myakove commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

PR Summary by Qodo

Add cherry-pick retry + PR rebase commands, pre-commit auto-fix, and command guards
✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

Walkthroughs

User Description

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

AI Description
• Add /cherry-pick-retry and /rebase 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.
Diagram
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"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use GitHub's built-in 'Update branch' / merge-upstream flow
  • ➕ 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't cover all rebase semantics; may merge instead of rebase
2. Centralize command authorization via a dispatcher/decorator
  • ➕ 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)
3. Run formatting fixes only in CI and comment results (no auto-commit)
  • ➕ Avoids bot-authored commits and force-push interactions
  • ➕ Keeps PR history purely authored by humans unless opted-in
  • ➖ Doesn't unblock cherry-pick automation; still requires manual fix+push
  • ➖ Adds latency and back-and-forth for small formatting issues

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.

Grey Divider

File Changes

Enhancement (3)
issue_comment_handler.py Add /cherry-pick-retry + /rebase dispatch and guard sensitive commands +153/-0

Add /cherry-pick-retry + /rebase dispatch and guard sensitive commands

• Registers new command constants and routes '/cherry-pick-retry' and '/rebase' to the appropriate handlers. Adds 'is_user_valid_to_run_commands' checks to previously unguarded commands (e.g., cherry-pick, assign-reviewer(s), check-can-merge, verified, wip, test-oracle). Implements 'process_cherry_pick_retry_command' 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.

webhook_server/libs/handlers/issue_comment_handler.py


runner_handler.py Run pre-commit auto-fix during cherry-pick and add PR rebase implementation +200/-0

Run pre-commit auto-fix during cherry-pick and add PR rebase implementation

• Enhances 'cherry_pick()' to optionally run pre-commit in the cherry-pick worktree and, if hooks modified files, auto-stage and commit the fixes before pushing. Adds 'rebase_pr()' 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.

webhook_server/libs/handlers/runner_handler.py


constants.py Add command constants for cherry-pick retry and rebase +2/-0

Add command constants for cherry-pick retry and rebase

• Defines 'COMMAND_CHERRY_PICK_RETRY_STR' and 'COMMAND_REBASE_STR' for consistent command parsing and documentation.

webhook_server/utils/constants.py


Tests (2)
test_issue_comment_handler.py Add tests for new commands and authorization guards +510/-0

Add tests for new commands and authorization guards

• Introduces async tests ensuring unauthorized users are blocked from newly guarded commands. Adds coverage for '/cherry-pick-retry' dispatch, argument validation, merged/label preconditions, bot-ownership checks when closing existing cherry-pick PRs, and '/rebase' dispatch/guard behavior.

webhook_server/tests/test_issue_comment_handler.py


test_runner_handler.py Add rebase_pr and cherry-pick pre-commit auto-fix test coverage +418/-0

Add rebase_pr and cherry-pick pre-commit auto-fix test coverage

• Adds a new 'TestRebasePr' suite covering non-open PR rejection, bot-owned PR authorization (assignee/maintainer), worktree failures, conflict handling with abort, and success paths. Adds 'TestCherryPickPreCommitAutoFix' to validate pre-commit execution, auto-commit behavior when hooks modify files, and skipping when disabled.

webhook_server/tests/test_runner_handler.py


Documentation (1)
pull_request_handler.py Expose new commands in the welcome/help message +5/-1

Expose new commands in the welcome/help message

• Extends the generated welcome section to document '/cherry-pick-retry' and adds a new 'Branch Management' section for '/rebase'. Ensures rebase help text appears even when cherry-pick operations are not enabled.

webhook_server/libs/handlers/pull_request_handler.py


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (1)

Context used

Grey Divider


Action required

1. open_pulls iterated without wrapper ✓ Resolved 📘 Rule violation ☼ Reliability
Description
process_cherry_pick_retry_command() iterates open_pulls, a PyGithub PaginatedList, directly,
which can trigger synchronous, blocking pagination API fetches outside github_api_call(). This
bypasses the standard asyncio.to_thread() wrapping and retry/backoff behavior, potentially
blocking the event loop and causing intermittent /cherry-pick-retry failures under transient
GitHub API errors.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R688-689]

+        # Iterate lazily — PyGithub's PaginatedList fetches pages on demand
+        for open_pr in open_pulls:
Evidence
PR Compliance ID 8 requires that any PaginatedList iteration that may lazily trigger GitHub API
calls be wrapped in github_api_call(). In the current code, only the get_pulls() call is wrapped
to obtain the PaginatedList, but the subsequent for open_pr in open_pulls: iteration happens
outside the wrapper; because PyGithub is synchronous and pagination fetches occur during iteration,
those on-demand page requests won’t run in asyncio.to_thread() and won’t benefit from the retry
wrapper, creating the risk of blocking and mid-scan failures.

CLAUDE.md: All Potentially Blocking PyGithub Operations Must Use github_api_call() (No Direct Calls or Raw asyncio.to_thread)
webhook_server/libs/handlers/issue_comment_handler.py[682-690]
webhook_server/utils/github_retry.py[1-7]
webhook_server/utils/github_retry.py[101-104]
webhook_server/libs/handlers/issue_comment_handler.py[682-689]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`process_cherry_pick_retry_command()` wraps `self.repository.get_pulls(state="open")` with `github_api_call()`, but then iterates the returned PyGithub `PaginatedList` directly. Because `PaginatedList` pagination fetches are lazy and synchronous, iteration can trigger additional blocking API requests outside `github_api_call()`, bypassing `asyncio.to_thread()` and the standard retry/backoff behavior.

## Issue Context
- `open_pulls` is obtained via `github_api_call(lambda: self.repository.get_pulls(state="open"), ...)`, which only wraps creation of the `PaginatedList` object, not the page-fetching that occurs during iteration.
- PyGithub is synchronous; the project’s `github_api_call()` exists to move these operations into a thread and apply retries.
- The code path notes/relies on lazy fetching behavior, which is exactly why the iteration must also be protected.

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[682-690]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. merged_at checked via hook_data ✓ Resolved 📘 Rule violation ≡ Correctness
Description
process_cherry_pick_retry_command() checks merge status via
self.hook_data["issue"].get("pull_request", {}).get("merged_at"), which defensively masks
missing/invalid webhook payload data instead of following the webhook spec and failing fast or using
the authoritative PR API state. This can incorrectly block /cherry-pick-retry (treating merged PRs
as unmerged) and hides malformed payload bugs.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R644-650]

+        if not self.hook_data["issue"].get("pull_request", {}).get("merged_at"):
+            msg = "Cherry-pick retry can only be used on merged PRs"
+            self.logger.debug(f"{self.log_prefix} {msg}")
+            await github_api_call(
+                pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+            )
+            return
Evidence
PR Compliance ID 5 requires webhook handlers to follow GitHub’s payload specification and fail fast
on malformed payloads rather than adding defensive checks/fallbacks for required/stable fields. The
new code uses chained .get(...) lookups for merged_at, which masks missing payload data and can
incorrectly treat merged PRs as unmerged, blocking the retry flow.

CLAUDE.md: Follow GitHub Webhook Specification for Payload Handling (No Breaking Assumptions About Stable Fields)
webhook_server/libs/handlers/issue_comment_handler.py[644-650]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`process_cherry_pick_retry_command()` determines whether the PR is merged by reading `merged_at` from `hook_data["issue"]["pull_request"]` using defensive `.get(...)` access. This violates the fail-fast webhook payload handling rule and can misclassify PR merge state.

## Issue Context
Per the compliance requirement, stable/required webhook fields should be accessed directly and malformed payloads should not be silently masked with fallback defaults; for merge state checks, prefer authoritative PR API fields (wrapped with `github_api_call()` as needed).

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[644-650]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Rebase requires bot login 🐞 Bug ☼ Reliability
Description
RunnerHandler.rebase_pr() aborts when app_bot_login is unset, which blocks /rebase for all PRs
(including normal user PRs) even though bot login is only needed to classify bot-owned PRs. This
turns a transient bot-login init failure into a complete command outage.
Code

webhook_server/libs/handlers/runner_handler.py[R1605-1619]

+        # If app_bot_login is not set, we cannot verify bot ownership — abort for safety
+        if not self.github_webhook.app_bot_login:
+            self.logger.error(
+                f"{self.log_prefix} Cannot identify app bot — app_bot_login not set. "
+                "Rebase cannot verify bot-PR ownership. Aborting."
+            )
+            await github_api_call(
+                pull_request.create_issue_comment,
+                "Rebase failed: cannot identify app bot for PR ownership verification. "
+                "Please check GitHub App configuration.",
+                logger=self.logger,
+                log_prefix=self.log_prefix,
+            )
+            return
+
Evidence
The new guard exits before bot/user classification and authorization checks, so /rebase is
impossible when app_bot_login is empty. app_bot_login can remain empty when initialization fails,
making this a real reliability regression.

webhook_server/libs/handlers/runner_handler.py[1600-1619]
webhook_server/libs/handlers/runner_handler.py[1620-1653]
webhook_server/libs/github_api.py[501-524]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`rebase_pr()` hard-returns if `github_webhook.app_bot_login` is empty. This prevents rebasing user-owned PRs even though the method already contains sufficient authorization logic for that case.

### Issue Context
`app_bot_login` is initialized in `GithubWebhook.process()` and may remain empty if the lookup fails; this should not prevent rebasing user-owned PRs.

### Fix Focus Areas
- webhook_server/libs/handlers/runner_handler.py[1604-1619]

### Implementation notes
- Remove the unconditional early-`return` on missing `app_bot_login`.
- Use a conservative fallback:
 - If `app_bot_login` is missing, treat the PR as **not verified bot-owned** and apply the stricter path (e.g., allow only PR owner or maintainers; for actual bot PRs this effectively becomes maintainers-only).
 - Alternatively, if you want to preserve bot-PR behavior, lazily fetch/initialize `app_bot_login` here (via the same mechanism used in `GithubWebhook.process()`) and only fail if that fetch also fails.
- Keep the existing bot-owned PR authorization behavior when `app_bot_login` is available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
4. app_bot_login exception logged wrongly ✓ Resolved 📘 Rule violation ◔ Observability
Description
In GithubWebhook.process(), the new except Exception path logs with logger.warning(...),
dropping the exception/traceback and violating the required exception-logging standard, which makes
bot-login initialization failures during webhook processing hard to diagnose. Additionally, if
get_repository_github_app_api() returns None/is falsy, app_bot_login can remain unset with no
log at all, potentially silently disabling bot-PR identification and causing incorrect behavior in
bot-PR-dependent flows.
Code

webhook_server/libs/github_api.py[R513-516]

+                except Exception:
+                    self.logger.warning(
+                        f"{self.log_prefix} Failed to get app bot login — bot-PR detection may not work"
+                    )
Evidence
PR Compliance ID 13 requires using logger.exception(...) for exception logging, but the cited code
catches Exception and only emits a warning, which omits the exception details/traceback and
reduces debuggability when initialization fails. The same code path also leaves app_bot_login at
its default empty value when _github_app_api is falsy, without any logging, so a failure to obtain
the GitHub App API (or an unexpected None return) can silently prevent bot-login initialization
and thus impair bot-PR detection without an observable signal.

CLAUDE.md: Use logger.exception() for exception logging; re-raise asyncio.CancelledError
webhook_server/libs/github_api.py[501-516]
webhook_server/libs/github_api.py[194-198]
webhook_server/libs/github_api.py[501-517]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GithubWebhook.process()` has insufficient diagnostics when initializing `self.app_bot_login`: exceptions are caught and logged with `logger.warning(...)`, which loses the exception/traceback and violates the exception-logging compliance requirement (use `logger.exception(...)` in `except` blocks, and re-raise `asyncio.CancelledError` if it could be caught). Also, when the GitHub App API object is missing/falsy, there is no log and `app_bot_login` stays empty, which can silently disable bot-PR identification.

## Issue Context
`self.app_bot_login` is used for bot-PR identification logic; if initialization fails (either via an exception or because `_github_app_api` is `None`/falsy), downstream bot-PR-dependent behavior can be incorrect and the root cause is hard to diagnose without a traceback or at least an explicit log indicating initialization could not occur.

## Fix Focus Areas
- webhook_server/libs/github_api.py[501-516]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. app_bot_login empty fallback ✓ Resolved 📘 Rule violation ☼ Reliability
Description
GithubWebhook.__init__() performs a blocking PyGithub call (github_app_api.get_user().login)
without wrapping it in github_api_call() and swallows any exception by falling back to an empty
string for app_bot_login. This can block the event loop and can silently break/disable the
bot-ownership checks used to gate behaviors in flows like cherry-pick retry and /rebase, leading
to misclassification of bot PRs and incorrect routing/cleanup actions.
Code

webhook_server/libs/github_api.py[R194-198]

+        # Store the app's bot login for identifying PRs created by our app
+        try:
+            self.app_bot_login: str = github_app_api.get_user().login
+        except Exception:
+            self.app_bot_login = ""
Evidence
The cited code path initializes app_bot_login by calling github_app_api.get_user().login
directly inside GithubWebhook.__init__, which violates the requirement (Rule 8) that PyGithub
operations be wrapped with github_api_call() to avoid blocking behavior in async contexts. It also
catches exceptions and sets app_bot_login to "", which Rule 6 discourages for required data
because it masks failures; downstream logic in the cherry-pick retry handler and rebase flow
compares open_pr.user.login / pr_user_login to self.github_webhook.app_bot_login to determine
whether a PR was created by the app bot, so an empty-string fallback causes those checks to fail
silently (e.g., skipping closing old bot-created cherry-pick PRs or applying the wrong authorization
rules).

CLAUDE.md: Wrap All Potentially Blocking PyGithub Operations with github_api_call() (No Direct Calls or Raw asyncio.to_thread)
CLAUDE.md: Zero Tolerance for Unnecessary Defensive Programming (Fail Fast; Only Guard Truly Optional/Uncertain Cases)
webhook_server/libs/github_api.py[194-199]
webhook_server/libs/handlers/issue_comment_handler.py[700-712]
webhook_server/libs/handlers/runner_handler.py[1604-1605]
webhook_server/libs/handlers/runner_handler.py[1600-1606]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GithubWebhook.__init__()` calls PyGithub (`github_app_api.get_user().login`) directly and catches all exceptions, defaulting `self.app_bot_login` to an empty string. This both violates the requirement to wrap blocking PyGithub operations with `github_api_call()` and introduces a defensive fallback that can mask real failures and silently break bot-ownership logic relied upon by `/cherry-pick-retry` and `/rebase` flows.

## Issue Context
`GithubWebhook` is instantiated inside an async background task, so synchronous PyGithub calls in `__init__` can block the event loop. The resolved `app_bot_login` is used to classify whether a PR was created by the app bot (e.g., `process_cherry_pick_retry_command()` only closes an existing bot-created cherry-pick PR when `open_pr.user.login == self.github_webhook.app_bot_login`, and `rebase_pr()` uses `pr_user_login == self.github_webhook.app_bot_login` to decide whether to apply bot-PR authorization rules); when initialization falls back to `""` on error, these comparisons fail without any diagnostic signal.

## Fix Focus Areas
- webhook_server/libs/github_api.py[194-199]
- webhook_server/libs/handlers/issue_comment_handler.py[700-712]
- webhook_server/libs/handlers/runner_handler.py[1604-1605]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Cherry-pick ignores precommit failures ✓ Resolved 🐞 Bug ☼ Reliability
Description
In RunnerHandler.cherry_pick(), a failing pre-commit run falls through to the push/PR-create path,
and even git add/git commit failures after auto-fix only emit warnings; this can create
cherry-pick PRs that still fail pre-commit or omit intended auto-fixes while the flow reports
success. As written, pre-commit failures are not surfaced as a failed cherry-pick check/run or a
blocking comment.
Code

webhook_server/libs/handlers/runner_handler.py[R1244-1291]

+                # Run pre-commit auto-fix if enabled for the repo
+                if self.github_webhook.pre_commit:
+                    self.logger.info(f"{self.log_prefix} Running pre-commit on cherry-pick worktree")
+                    pre_commit_cmd = f"uvx --directory {worktree_path} {PREK_STR} run --all-files"
+                    rc_pc, _out_pc, _err_pc = await run_command(
+                        command=pre_commit_cmd,
+                        log_prefix=self.log_prefix,
+                        redact_secrets=[github_token],
+                        mask_sensitive=self.github_webhook.mask_sensitive,
+                    )
+                    if not rc_pc:
+                        # Pre-commit returned non-zero — check if any files were
+                        # actually modified before committing.
+                        rc_diff, out_diff, _ = await run_command(
+                            command=f"{git_cmd} diff --name-only",
+                            log_prefix=self.log_prefix,
+                            redact_secrets=[github_token],
+                            mask_sensitive=self.github_webhook.mask_sensitive,
+                        )
+                        if rc_diff and out_diff.strip():
+                            self.logger.info(f"{self.log_prefix} Pre-commit modified files, committing fixes")
+                            rc_add, _, err_add = await run_command(
+                                command=f"{git_cmd} add -A",
+                                log_prefix=self.log_prefix,
+                                redact_secrets=[github_token],
+                                mask_sensitive=self.github_webhook.mask_sensitive,
+                            )
+                            if not rc_add:
+                                self.logger.warning(f"{self.log_prefix} git add failed after pre-commit fix: {err_add}")
+                            rc_commit, _, err_commit = await run_command(
+                                command=(
+                                    f"{git_cmd} commit -m"
+                                    f" {shlex.quote('pre-commit auto-fix for cherry-pick')}"
+                                    " --no-verify"
+                                ),
+                                log_prefix=self.log_prefix,
+                                redact_secrets=[github_token],
+                                mask_sensitive=self.github_webhook.mask_sensitive,
+                            )
+                            if not rc_commit:
+                                self.logger.warning(
+                                    f"{self.log_prefix} git commit failed after pre-commit fix: {err_commit}"
+                                )
+                        else:
+                            self.logger.debug(f"{self.log_prefix} Pre-commit failed but no files modified")
+                    else:
+                        self.logger.debug(f"{self.log_prefix} Pre-commit passed without modifications")
+
Evidence
The pre-commit failure path only logs debug/warnings and then continues into the push step;
run_command() clearly reports non-zero as False, so failures are being ignored by control flow.

webhook_server/libs/handlers/runner_handler.py[1244-1304]
webhook_server/utils/helpers.py[301-404]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Pre-commit failures (and auto-fix commit failures) do not stop the cherry-pick flow. The bot can proceed to push and create a PR, yielding confusing or incorrect results.

## Issue Context
`run_command()` returns `False` for non-zero return codes; the current logic only attempts to commit when pre-commit failed *and* produced diffs, and it doesn’t abort when `git add`/`git commit` fail.

## Fix Focus Areas
- webhook_server/libs/handlers/runner_handler.py[1244-1304]
- webhook_server/utils/helpers.py[301-404]

## Suggested implementation notes
- If pre-commit returns non-zero and there are no modifications to commit, treat as a hard failure: set the cherry-pick check-run to failure and comment with the redacted output.
- If auto-fix is attempted and `git add` or `git commit` fails, abort and report failure (don’t continue to push).
- After committing fixes, rerun pre-commit once to confirm it now passes; fail if it still doesn’t.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Rebase may clobber branches ✓ Resolved 🐞 Bug ⛨ Security
Description
RunnerHandler.rebase_pr() force-pushes origin <head_ref> without verifying the PR head
repository matches the cloned base-repo remote, so fork PRs (or same-name refs) can fail or
overwrite a base-repo branch. It also checks out head_ref even though the clone/worktree is
prepared around the fetched refs/remotes/origin/pr/<number> ref, making the rebase target
ambiguous for non-base-repo heads.
Code

webhook_server/libs/handlers/runner_handler.py[R1535-1650]

+        pr_user_login = await github_api_call(
+            lambda: pull_request.user.login, logger=self.logger, log_prefix=self.log_prefix
+        )
+
+        # Check if PR is bot-owned (e.g., cherry-pick PRs created by the app)
+        is_bot_pr = pr_user_login in self.github_webhook.auto_verified_and_merged_users
+
+        if is_bot_pr:
+            # For bot-owned PRs, validate the user is the PR assignee or a maintainer
+            assignees = await github_api_call(
+                lambda: [a.login for a in pull_request.assignees],
+                logger=self.logger,
+                log_prefix=self.log_prefix,
+            )
+            maintainers = await self.owners_file_handler.get_all_repository_maintainers()
+            if reviewed_user not in assignees and reviewed_user not in maintainers:
+                msg = (
+                    f"@{reviewed_user} is not authorized to rebase this bot-owned PR.\n"
+                    "Only the PR assignee (cherry-pick initiator) or maintainers can rebase."
+                )
+                self.logger.debug(f"{self.log_prefix} {msg}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+        base_ref = await github_api_call(lambda: pull_request.base.ref, logger=self.logger, log_prefix=self.log_prefix)
+        head_ref = await github_api_call(lambda: pull_request.head.ref, logger=self.logger, log_prefix=self.log_prefix)
+        github_token = self.github_webhook.token
+
+        self.logger.info(f"{self.log_prefix} Rebasing {head_ref} onto {base_ref}")
+
+        async with self._checkout_worktree(pull_request=pull_request, skip_merge=True) as (
+            success,
+            worktree_path,
+            out,
+            err,
+        ):
+            if not success:
+                msg = "Failed to prepare worktree for rebase"
+                self.logger.error(f"{self.log_prefix} {msg}: {out} --- {err}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            git_cmd = f"git --work-tree={worktree_path} --git-dir={worktree_path}/.git"
+
+            # Checkout the PR head branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} checkout {head_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
+                redacted_err = _redact_secrets(err, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                msg = f"Failed to checkout branch `{head_ref}`: {redacted_err}"
+                self.logger.error(f"{self.log_prefix} {msg}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            # Fetch the base branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} fetch origin {base_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
+                redacted_err = _redact_secrets(err, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                msg = f"Failed to fetch base branch `{base_ref}`: {redacted_err}"
+                self.logger.error(f"{self.log_prefix} {msg}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            # Rebase onto base branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} rebase origin/{base_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
+                # Abort the rebase to clean up
+                await run_command(
+                    command=f"{git_cmd} rebase --abort",
+                    log_prefix=self.log_prefix,
+                    redact_secrets=[github_token],
+                    mask_sensitive=self.github_webhook.mask_sensitive,
+                )
+                redacted_err = _redact_secrets(err, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                redacted_out = _redact_secrets(out, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                msg = (
+                    f"**Rebase failed** for `{head_ref}` onto `{base_ref}`:\n"
+                    f"```\n{redacted_out}\n{redacted_err}\n```\n"
+                    "Please resolve conflicts manually."
+                )
+                self.logger.error(f"{self.log_prefix} Rebase failed: {redacted_out} --- {redacted_err}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            # Force push the rebased branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} push --force-with-lease origin {head_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
Evidence
The repo clone is created from the base repository clone URL and fetches the PR ref into
refs/remotes/origin/pr/<number>, while rebase_pr() checks out and force-pushes head_ref to
origin (base remote) without verifying the head repo matches the base repo.

webhook_server/libs/handlers/runner_handler.py[1515-1657]
webhook_server/libs/github_api.py[318-388]
webhook_server/utils/helpers.py[686-740]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`rebase_pr()` force-pushes to `origin <head_ref>` using the base repo clone, but does not validate that the PR head repo is the same as the base repo. For fork PRs (or ref-name collisions), this can either fail or force-push an unintended branch in the base repository.

## Issue Context
- The base repo clone is created from `repository.clone_url` and fetches PR refs into `refs/remotes/origin/pr/<pr_number>`.
- `rebase_pr()` should either (a) refuse to operate on fork PRs, or (b) add/fetch the head remote and push back to the head repo safely.

## Fix Focus Areas
- webhook_server/libs/handlers/runner_handler.py[1515-1666]
- webhook_server/libs/github_api.py[318-388]
- webhook_server/utils/helpers.py[686-740]

## Suggested implementation notes
- Validate `pull_request.head.repo.full_name == self.github_webhook.repository_full_name` before any checkout/rebase/push; if not, comment and return.
- Create/reset a local branch from the fetched PR ref before rebasing, e.g. `git checkout -B <safe_head_branch> origin/pr/<pr_number>` (or use `pull_request.head.sha`).
- When running `git checkout/fetch/push`, pass `--` before ref names or otherwise ensure refs starting with `-` can’t be interpreted as options.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Rebase authorization too broad ✓ Resolved 🐞 Bug ⛨ Security
Description
/rebase is only gated by OwnersFileHandler.is_user_valid_to_run_commands(), but that set
includes repository contributors/reviewers; rebase_pr() then performs a push --force-with-lease,
allowing non-maintainers to rewrite other users’ PR branches. The additional authorization check
only applies to bot-owned PRs, not normal PRs.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R307-313]

+        elif _command == COMMAND_REBASE_STR:
+            if not await self.owners_file_handler.is_user_valid_to_run_commands(
+                pull_request=pull_request, reviewed_user=reviewed_user
+            ):
+                return
+            await self.runner_handler.rebase_pr(pull_request=pull_request, reviewed_user=reviewed_user)
+
Evidence
The /rebase dispatch checks only is_user_valid_to_run_commands(), and that method’s valid set
includes repository_contributors and all_pull_request_reviewers, while rebase_pr()
force-pushes.

webhook_server/libs/handlers/issue_comment_handler.py[307-313]
webhook_server/libs/handlers/owners_files_handler.py[475-525]
webhook_server/libs/handlers/runner_handler.py[1643-1646]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/rebase` is destructive (force-push) but is currently authorized using `is_user_valid_to_run_commands()`, which includes broad categories (contributors/reviewers). This allows users who are not PR owners/maintainers to trigger history rewrites.

## Issue Context
`is_user_valid_to_run_commands()` is designed for non-destructive commands (retest, etc.) and is too permissive for a force-push operation.

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[307-313]
- webhook_server/libs/handlers/owners_files_handler.py[475-525]
- webhook_server/libs/handlers/runner_handler.py[1515-1657]

## Suggested implementation notes
- Add an explicit authorization check for `/rebase`, e.g. `reviewed_user == pull_request.user.login` OR `reviewed_user in maintainers`.
- Consider enforcing the same rule inside `RunnerHandler.rebase_pr()` as defense-in-depth (so future callers can’t bypass it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Whitespace args bypass check ✓ Resolved 🐞 Bug ≡ Correctness
Description
IssueCommentHandler.user_commands() checks and not _args without stripping, so `/cherry-pick-retry
` bypasses the “requires an argument” path and reaches process_cherry_pick_retry_command() with an
empty branch name. This produces incorrect behavior/messages (e.g., looking for label
cherry-pick-) instead of a clear missing-argument error.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R223-229]

                COMMAND_RETEST_STR,
                COMMAND_ASSIGN_REVIEWER_STR,
                COMMAND_ADD_ALLOWED_USER_STR,
+                COMMAND_CHERRY_PICK_RETRY_STR,
            )
            and not _args
        ):
Evidence
The command parser keeps _args as-is (including whitespace) and the missing-argument guard only
checks for empty string, not whitespace; the retry handler then strips the args into target_branch
but doesn’t reject empty after stripping.

webhook_server/libs/handlers/issue_comment_handler.py[176-239]
webhook_server/libs/handlers/issue_comment_handler.py[631-666]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`user_commands()` treats whitespace-only arguments as present because it uses `not _args` rather than `not _args.strip()`. For `/cherry-pick-retry`, this allows an empty `target_branch` to flow into retry logic and yields misleading results.

### Issue Context
- `command.split(" ", 1)` preserves whitespace in `_args`.
- `process_cherry_pick_retry_command()` later does `target_branch = command_args.strip()` but does not explicitly reject the empty string.

### Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[176-239]
- webhook_server/libs/handlers/issue_comment_handler.py[611-656]

### Suggested fix
1. In the missing-arg validation block, change the condition to use `_args.strip()` (or pre-strip into `args_stripped`).
2. Add a defensive check in `process_cherry_pick_retry_command()` after `target_branch = command_args.strip()`:
  - if `not target_branch`: comment “cherry-pick-retry requires an argument” and return.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Merged check can crash 🐞 Bug ☼ Reliability
Description
process_cherry_pick_command() / process_cherry_pick_retry_command() now rely on
github_api_call(lambda: pull_request.merged) without handling exceptions, so a non-retryable
GitHub API error (or retries exhausted) will raise and fail the command instead of returning a
user-facing comment. Because command exceptions are propagated and re-raised by the issue-comment
handler, this can drop the command execution on intermittent API failures.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R578-579]

+        is_merged = await github_api_call(lambda: pull_request.merged, logger=self.logger, log_prefix=self.log_prefix)
+        if not is_merged:
Evidence
The new code calls github_api_call(lambda: pull_request.merged) in both command paths, and
github_api_call explicitly raises the last exception after retries rather than returning a
sentinel. The issue-comment handler aggregates command exceptions and re-raises, so an exception
here terminates command processing.

webhook_server/libs/handlers/issue_comment_handler.py[576-606]
webhook_server/libs/handlers/issue_comment_handler.py[644-652]
webhook_server/utils/github_retry.py[65-128]
webhook_server/libs/handlers/issue_comment_handler.py[120-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`github_api_call()` raises on non-retryable errors and after retry exhaustion. The new merged-status checks use it without a try/except, so `/cherry-pick` and `/cherry-pick-retry` can fail hard when GitHub is flaky or returns a permanent error, instead of failing gracefully with a comment.

## Issue Context
This change replaced a payload-based merge check with an API call. That improves correctness when it works, but it also introduces a new exception path that is currently unhandled.

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[578-579]
- webhook_server/libs/handlers/issue_comment_handler.py[645-646]

## Suggested fix
- Wrap the merged-status call in `try/except Exception as ex`.
- On exception, log (with context) and post a short issue comment like: "Unable to determine PR merged status due to GitHub API error; please retry." then `return`.
- Optionally, if you want a best-effort fallback, use webhook payload `merged_at` only when present (but still prefer API result when available).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Retry scans all PRs ✓ Resolved 🐞 Bug ➹ Performance
Description
process_cherry_pick_retry_command() loads all open PRs via
list(self.repository.get_pulls(state="open")) and scans them to find a matching cherry-pick PR,
which scales linearly with repository activity and can burn API quota and add noticeable latency.
This can become slow or rate-limit-prone on repositories with many open PRs.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R680-686]

+        open_pulls = await github_api_call(
+            lambda: list(self.repository.get_pulls(state="open")),
+            logger=self.logger,
+            log_prefix=self.log_prefix,
+        )
+        self.logger.debug(f"{self.log_prefix} Cherry-pick retry: found {len(open_pulls)} open PRs to scan")
+        closed_old_pr = False
Evidence
The retry flow explicitly materializes the entire open-PR list into memory before scanning, which
requires fetching all pages from GitHub even though only one matching PR is needed.

webhook_server/libs/handlers/issue_comment_handler.py[671-686]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`/cherry-pick-retry` currently fetches **all** open PRs and then scans them in Python to find a matching PR to close. This is O(N) in open PR count and forces full pagination.

## Issue Context
The code calls `list(self.repository.get_pulls(state="open"))`, which materializes the entire paginated result set.

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[671-686]

## Suggested fix
- Avoid `list(...)`; instead iterate the paginated list and `break` as soon as a match is found (this prevents fetching later pages when a match is early).
- If PyGithub supports it in this repo, prefer server-side filtering (e.g., listing pulls by `head=` or using a search query) to reduce data pulled from GitHub.
- Add a hard cap (max pages / max PRs scanned) with a fallback message if nothing found, to avoid worst-case scans on very large repos.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (11)
12. Unquoted git refs ✓ Resolved 🐞 Bug ☼ Reliability
Description
RunnerHandler.rebase_pr() and cherry_pick() build git/uvx command strings by interpolating
head_ref/base_ref and worktree_path directly, while run_command() tokenizes those strings with
shlex.split(). As a result, whitespace or other shell-significant characters in branch names or
paths can be split into multiple argv tokens, making checkout/fetch/rebase/push or pre-commit runs
fail and rendering /rebase and cherry-pick automation fragile under valid-but-unusual refs or
directory configurations.
Code

webhook_server/libs/handlers/runner_handler.py[R1667-1727]

+        base_ref = await github_api_call(lambda: pull_request.base.ref, logger=self.logger, log_prefix=self.log_prefix)
+        head_ref = await github_api_call(lambda: pull_request.head.ref, logger=self.logger, log_prefix=self.log_prefix)
+        github_token = self.github_webhook.token
+
+        self.logger.info(f"{self.log_prefix} Rebasing {head_ref} onto {base_ref}")
+
+        async with self._checkout_worktree(pull_request=pull_request, skip_merge=True) as (
+            success,
+            worktree_path,
+            out,
+            err,
+        ):
+            if not success:
+                msg = "Failed to prepare worktree for rebase"
+                self.logger.error(f"{self.log_prefix} {msg}: {out} --- {err}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            git_cmd = f"git --work-tree={worktree_path} --git-dir={worktree_path}/.git"
+
+            # Checkout the PR head branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} checkout {head_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
+                redacted_err = _redact_secrets(err, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                msg = f"Failed to checkout branch `{head_ref}`: {redacted_err}"
+                self.logger.error(f"{self.log_prefix} {msg}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            # Fetch the base branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} fetch origin {base_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
+            if not rc:
+                redacted_err = _redact_secrets(err, [github_token], mask_sensitive=self.github_webhook.mask_sensitive)
+                msg = f"Failed to fetch base branch `{base_ref}`: {redacted_err}"
+                self.logger.error(f"{self.log_prefix} {msg}")
+                await github_api_call(
+                    pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
+                )
+                return
+
+            # Rebase onto base branch
+            rc, out, err = await run_command(
+                command=f"{git_cmd} rebase origin/{base_ref}",
+                log_prefix=self.log_prefix,
+                redact_secrets=[github_token],
+                mask_sensitive=self.github_webhook.mask_sensitive,
+            )
Evidence
In rebase_pr(), head_ref and base_ref are inserted into git command strings, and
run_command() then calls shlex.split(command) to produce argv, so any whitespace in those refs
becomes an argument separator and causes git to receive incorrect arguments. Similarly, the newer
code paths interpolate worktree_path into command strings (including the pre-commit invocation in
cherry_pick() and the new rebase git_cmd format), and because run_command() always tokenizes
via shlex.split(), unquoted spaces in the path are treated as separators, breaking uvx/git
execution.

webhook_server/libs/handlers/runner_handler.py[1667-1752]
webhook_server/utils/helpers.py[301-355]
webhook_server/libs/handlers/runner_handler.py[1264-1273]
webhook_server/libs/handlers/runner_handler.py[1687-1695]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RunnerHandler.rebase_pr()` and `RunnerHandler.cherry_pick()` construct command strings that interpolate `head_ref`/`base_ref` and `worktree_path` directly, but `run_command()` tokenizes those strings with `shlex.split()`. This makes operations fail when refs or paths contain whitespace (and potentially other shell-significant characters) because they are split into multiple argv tokens.

## Issue Context
- `head_ref` and `base_ref` come from GitHub PR metadata (`pull_request.head.ref`, `pull_request.base.ref`) and are not escaped before interpolation.
- This PR introduces a new pre-commit command string in `cherry_pick()` and a new `git_cmd` format in `rebase_pr()` that also interpolates `worktree_path`.
- `run_command()` explicitly calls `shlex.split(command)` before executing commands.

## Fix Focus Areas
- webhook_server/libs/handlers/runner_handler.py[1264-1273]
- webhook_server/libs/handlers/runner_handler.py[1667-1752]
- webhook_server/libs/handlers/runner_handler.py[1687-1695]
- webhook_server/utils/helpers.py[301-355]

## Suggested fix
- Apply `shlex.quote()` to interpolated refs and paths anywhere they are inserted into command strings:
 - Quote `head_ref`/`base_ref` for checkout/fetch/rebase/push in `rebase_pr()` (including quoting the full `origin/{base_ref}` token if used).
 - Quote `worktree_path` in the `uvx --directory ...` pre-commit command in `cherry_pick()`.
 - Quote `worktree_path` (or the entire `--work-tree=...` / `--git-dir=...` arguments) anywhere it is interpolated into git command strings.
- Consider a more robust approach by adding a `run_command_argv(argv: list[str], ...)` helper (or equivalent) and passing argv lists directly for critical git operations to avoid string-splitting hazards entirely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. /cherry-pick-retry aborts without app_bot_login 📎 Requirement gap ☼ Reliability
Description
process_cherry_pick_retry_command() returns early when github_webhook.app_bot_login is unset,
preventing /cherry-pick-retry from reaching the step that re-runs the cherry-pick and leaving
owners unable to self-service recovery in environments where the GitHub App bot identity can’t be
resolved. This makes the retry command unusable when bot-login initialization fails, even though the
flow could still safely proceed by skipping only the “close existing PR” step.
Code

webhook_server/libs/handlers/issue_comment_handler.py[R672-685]

+        # Abort early if app_bot_login is not set — we cannot verify PR ownership
+        if not self.github_webhook.app_bot_login:
+            self.logger.error(
+                f"{self.log_prefix} Cannot identify app bot — app_bot_login not set. "
+                "Cherry-pick retry cannot verify PR ownership. Aborting close step."
+            )
+            await github_api_call(
+                pull_request.create_issue_comment,
+                "Cherry-pick retry failed: cannot identify app bot for PR ownership verification. "
+                "Please check GitHub App configuration.",
+                logger=self.logger,
+                log_prefix=self.log_prefix,
+            )
+            return
Evidence
PR Compliance ID 2 requires that the cherry-pick owner can re-trigger cherry-pick recovery via an
automated mechanism, but the added guard if not self.github_webhook.app_bot_login: returns before
the function reaches the later rerun step that would re-invoke the cherry-pick (e.g.,
runner_handler.cherry_pick()). The current behavior contradicts the surrounding
messaging/commentary that implies only the PR-closing portion is being aborted; since
app_bot_login can legitimately remain empty when bot-login initialization fails (exception) or
when no GitHub App API is available, this early-return path is reachable and causes
/cherry-pick-retry to fail instead of retrying.

Cherry-pick owner permissions: allow closing cherry-picked PR and re-triggering cherry-pick
webhook_server/libs/handlers/issue_comment_handler.py[672-685]
webhook_server/libs/handlers/issue_comment_handler.py[671-686]
webhook_server/libs/handlers/issue_comment_handler.py[759-771]
webhook_server/libs/github_api.py[501-524]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`process_cherry_pick_retry_command()` currently aborts the entire `/cherry-pick-retry` flow when `self.github_webhook.app_bot_login` is missing/empty, which prevents the cherry-pick owner from re-triggering recovery and stops execution before the later “Re-run cherry-pick” section.

## Issue Context
The retry flow is intended to support self-service recovery (PR Compliance ID 2) even in environments where the GitHub App bot identity cannot be resolved (e.g., bot-login initialization fails with an exception or no GitHub App API is available). In those cases, the handler should skip only the “close existing PR” scan/close logic that depends on bot ownership verification, but still proceed to re-run the cherry-pick; user-facing messaging should clearly state that ownership could not be verified and therefore no existing PRs were auto-closed (duplicates may remain).

## Fix Focus Areas
- webhook_server/libs/handlers/issue_comment_handler.py[671-686]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Blocking app API init ✓ Resolved 🐞 Bug ☼ Reliability
Description
GithubWebhook.process() calls get_repository_github_app_api() synchronously inside an async method
even though it performs file I/O and GitHub App integration setup, which can block the event loop
during webhook handling. Under concurrent webhook load this can increase latency and risk timeouts
for unrelated requests.
Code

webhook_server/libs/github_api.py[R501-506]

+        # Initialize app bot login for bot-PR identification (async)
+        if not self.app_bot_login:
+            _github_app_api = get_...

@myakove-bot

Copy link
Copy Markdown
Collaborator

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Pre-commit Checks: pre-commit runs automatically if .pre-commit-config.yaml exists
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest python-module-install - Test Python package installation
  • /retest pre-commit - Run pre-commit hooks and checks
  • /retest conventional-title - Validate commit message format
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. LGTM Count: Minimum 1 /lgtm from reviewers
  3. Status Checks: All required status checks must pass
  4. No Blockers: No wip, hold, has-conflicts labels and PR must be mergeable (no conflicts)
  5. Verified: PR must be marked as verified

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove
  • rnetser

Reviewers:

  • myakove
  • rnetser
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge
AI Features
  • Conventional Title: Mode: fix (claude/claude-opus-4-6[1m])
  • Cherry-Pick Conflict Resolution: Enabled (claude/claude-opus-4-6[1m])
  • Test Oracle: Triggers: approved (claude/claude-opus-4-6[1m]); /test-oracle can be used anytime

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is removed on new commits unless the push is detected as a clean rebase
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

Comment thread webhook_server/libs/handlers/runner_handler.py
Comment thread webhook_server/libs/handlers/issue_comment_handler.py
Comment thread webhook_server/libs/handlers/runner_handler.py
Comment thread webhook_server/libs/handlers/runner_handler.py
Comment thread webhook_server/libs/handlers/issue_comment_handler.py
@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e3e4e57

Comment thread webhook_server/libs/handlers/runner_handler.py
@myakove

myakove commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1535 (qodo bug) — Rebase may clobber branches

Addressed: Fixed in commit e3e4e57 — added head.repo.full_name check, rejects fork PRs with comment.

webhook_server/libs/handlers/issue_comment_handler.py:307 (qodo bug) — Rebase authorization too broad

Addressed: Fixed in commit e3e4e57 — non-bot PRs restricted to PR owner or maintainers only.

webhook_server/libs/handlers/runner_handler.py:1244 (qodo bug) — Cherry-pick ignores precommit failures

Addressed: Fixed in commit e3e4e57 — pre-commit failure with no fixable changes now aborts cherry-pick. git add/commit failures also abort.

webhook_server/libs/handlers/runner_handler.py:1273 (qodo bug) — Auto-fix commit unsigned

Addressed: Fixed in commit e3e4e57 — added --signoff to pre-commit auto-fix commit for DCO compliance.

webhook_server/libs/handlers/issue_comment_handler.py:630 (qodo bug) — Retry accepts multiple branches

Addressed: Fixed in commit e3e4e57 — validates single branch name, rejects multi-branch input.

@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 86510ca

Comment thread webhook_server/libs/handlers/runner_handler.py Outdated
@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit eaafc8a

@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9c00066

Comment thread webhook_server/libs/handlers/runner_handler.py Outdated
@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f1d8263

Comment thread webhook_server/libs/handlers/runner_handler.py Outdated
@myakove

myakove commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1607 (qodo bug) — Bot PR check too broad

Addressed: By design — cherry-pick-* labels are only created by our app's cherry_pick() method. Combined with user.type == Bot, this gives sufficient precision. A non-app bot would need both Bot type AND a cherry-pick label matching our naming convention, which is not a realistic attack vector. The previous approach using auto_verified_and_merged_users was worse (missed the GitHub App entirely). Adding app-specific label detection (CHERRY_PICKED_LABEL prefix) tightened the check significantly.

@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 141108d

myakove added 8 commits June 10, 2026 20:24
… rebase

- Store GitHub App bot login on GithubWebhook during init (github_api.py)
- Cherry-pick retry: check PR author login matches app_bot_login instead of user.type
- Rebase: check PR author login matches app_bot_login instead of user.type + label heuristic
- Prevents false matches on Renovate, Dependabot, or other bot PRs
- Replace blocking github_app_api.get_user().login in __init__ with async github_api_call in process()
- Log warning on failure instead of silent empty fallback
- Guard with "if not self.app_bot_login" to avoid redundant API calls
…ncelledError, log missing API

- Use logger.exception instead of logger.warning to preserve traceback
- Add except asyncio.CancelledError: raise before broad except
- Log debug message when GitHub App API is not available
…author check on empty app_bot_login

- github_api.py: get_repository_github_app_api() does file I/O (reads private key)
  and GitHub App setup — was called synchronously in async process(), blocking the
  event loop. Now wrapped with github_api_call() for non-blocking + retry.

- issue_comment_handler.py: when app_bot_login is empty (init failed or fallback
  token used), the author check 'pr_author_login != app_bot_login' would skip ALL
  PRs since no login matches empty string. Now guards the author check behind
  'if self.github_webhook.app_bot_login', falling back to title+body match only.
- issue_comment_handler.py: add early-exit guard before the get_pulls loop
  in cherry-pick retry. If app_bot_login is empty, log ERROR and post a PR
  comment explaining the failure, then return. Reverts the previous silent
  fallback that skipped the author check.

- runner_handler.py: add early-exit guard in rebase_pr() before the
  is_bot_pr check. If app_bot_login is empty, log ERROR and post a PR
  comment, then return. Without this, bot PRs would be treated as regular
  PRs (anyone could rebase) since empty string never matches any login.

No silent fallbacks — if we cannot verify PR ownership, we stop.
- runner_handler.py rebase_pr(): remove top-level abort that blocked ALL
  rebases when app_bot_login was empty (including normal user PRs). Now:
  if app_bot_login is set, classify bot PRs normally; if not set, treat
  PR as user-owned with a warning log. Safe because is_user_valid_to_run_commands
  already gates access to the /rebase command.

- issue_comment_handler.py cherry-pick retry: remove top-level abort that
  blocked the entire retry flow. Move the guard inside the loop after title
  match — if app_bot_login is empty, break out of the close loop with an
  error log (don't close any PR we can't verify ownership of), but still
  proceed to re-run the cherry-pick.
…e git refs, lazy PR iteration

- issue_comment_handler.py: replace hook_data["issue"]["pull_request"]["merged_at"]
  with `await github_api_call(lambda: pull_request.merged, ...)` at both cherry-pick
  (line 578) and cherry-pick-retry (line 644) merge checks. Direct PR object access
  instead of defensive .get() chaining on webhook payload.

- runner_handler.py rebase_pr(): quote all interpolated git refs (head_ref, base_ref)
  and worktree_path with shlex.quote() to prevent shlex.split() breakage on branch
  names with spaces or special chars. Also quote worktree_path in cherry_pick()
  pre-commit command.

- issue_comment_handler.py cherry-pick retry: remove list() wrapper from
  get_pulls(state="open") to iterate lazily via PyGithub's PaginatedList,
  fetching pages on demand and breaking on first match.

- test_issue_comment_handler.py: update all tests to set mock_pull_request.merged
  instead of manipulating hook_data["issue"]["pull_request"]["merged_at"].
… access

- Convert get_pulls(state="open") back to list() inside github_api_call()
  to avoid blocking event loop on PaginatedList page boundary fetches.
- Remove github_api_call lambda wrappers on .title, .user.login, .body —
  after list() materialization these properties are cached and safe to
  access directly.
- Restore "found N open PRs to scan" debug log line.
@myakove myakove force-pushed the fix/issue-1103-1089-cherry-pick branch from b7eb792 to e5044af Compare June 10, 2026 17:24
@qodo-code-review

qodo-code-review Bot commented Jun 10, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e5044af

@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Already addressed — actual code at line 1626 sets is_bot_pr = False when app_bot_login is unset. Does NOT abort. Code_diff is stale.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: github_api_call() with retry is the standard codebase pattern. Persistent API failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Already addressed — actual code uses break not return. Cherry-pick re-run always executes.

webhook_server/libs/handlers/issue_comment_handler.py:705 (qodo rule violation) — open_pr.number unwrapped PyGithub access

Not addressed: Already addressed — PaginatedList converted to list() in github_api_call. Fixed in commit b7eb792.

Comment thread webhook_server/libs/handlers/issue_comment_handler.py
Comment thread webhook_server/libs/handlers/issue_comment_handler.py
@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Already addressed — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort. Code_diff is stale.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: github_api_call() with retry is the standard codebase pattern. Persistent API failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Already addressed — actual code uses break not return. Cherry-pick re-run always executes.

webhook_server/libs/handlers/issue_comment_handler.py:705 (qodo rule violation) — open_pr.number unwrapped PyGithub access

Not addressed: Already addressed — PaginatedList converted to list() in github_api_call. Fixed in commit b7eb792.

…uth message

- issue_comment_handler.py: add explicit empty-string check after .strip()
  in process_cherry_pick_retry_command. Whitespace-only args like
  "/cherry-pick-retry  " bypass the shared arg parser check since
  whitespace is truthy, but strip to empty string.

- owners_files_handler.py: change "not allowed to run retest commands"
  to "not allowed to run commands" — the method is now used for
  /cherry-pick, /rebase, /verified, /wip, not just retest.

- test_owners_files_handler.py: update assertion to match new message.
@qodo-code-review

Copy link
Copy Markdown

Looking for bugs?

Check back in a few minutes. An AI review agent is analyzing this pull request.

@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Already addressed — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort. Code_diff is stale.

webhook_server/libs/handlers/issue_comment_handler.py:223 (qodo bug) — Whitespace args bypass check

Addressed: Fixed — empty branch guard added in process_cherry_pick_retry_command().

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: github_api_call() with retry is the standard codebase pattern. Persistent API failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Already addressed — actual code uses break not return. Cherry-pick re-run always executes.

@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Stale code_diff — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: github_api_call() with retry is the standard codebase pattern. Persistent API failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Stale code_diff — actual code uses break not return. Cherry-pick re-run always executes.

@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Stale code_diff — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: Standard codebase pattern — github_api_call() with retry. Persistent failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Stale code_diff — actual code uses break not return. Cherry-pick re-run always executes.

2 similar comments
@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Stale code_diff — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: Standard codebase pattern — github_api_call() with retry. Persistent failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Stale code_diff — actual code uses break not return. Cherry-pick re-run always executes.

@myakove

myakove commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qodo-code-review[bot]

The following review comments were reviewed and a decision was made:

webhook_server/libs/handlers/runner_handler.py:1605 (qodo bug) — Rebase requires bot login

Not addressed: Stale code_diff — actual code sets is_bot_pr = False when app_bot_login unset. Does NOT abort.

webhook_server/libs/handlers/issue_comment_handler.py:578 (qodo bug) — Merged check can crash

Not addressed: Standard codebase pattern — github_api_call() with retry. Persistent failure = expected fail-fast.

webhook_server/libs/handlers/issue_comment_handler.py:672 (qodo requirement gap) — /cherry-pick-retry aborts without app_bot_login

Not addressed: Stale code_diff — actual code uses break not return. Cherry-pick re-run always executes.

@myakove myakove merged commit ef8d7e6 into main Jun 10, 2026
6 of 8 checks passed
@myakove myakove deleted the fix/issue-1103-1089-cherry-pick branch June 10, 2026 17:52
@myakove-bot

Copy link
Copy Markdown
Collaborator

New container for ghcr.io/myk-org/github-webhook-server:latest published

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow cherry-pick owner to close cherry-picked PR and re-trigger cherry-pick pre-commit fails doesn't auto resolve when cherry-picking

2 participants