Skip to content
Open
13 changes: 13 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ auto-verify-cherry-picked-prs: true # Default: true - automatically verify cher

create-issue-for-new-pr: true # Global default: create tracking issues for new PRs

# Commands allowed on draft PRs (optional)
# If not set: commands are blocked on draft PRs (default behavior)
# If empty list []: all commands allowed on draft PRs
# If list with values: only those commands allowed on draft PRs
# allow-commands-on-draft-prs: [] # Uncomment to allow all commands on draft PRs
# allow-commands-on-draft-prs: # Or allow only specific commands:
# - build-and-push-container
# - retest

# Labels configuration - control which labels are enabled and their colors
# If not set, all labels are enabled with default colors
labels:
Expand Down Expand Up @@ -186,6 +195,10 @@ repositories:
minimum-lgtm: 0 # The minimum PR lgtm required before approve the PR
create-issue-for-new-pr: true # Override global setting: create tracking issues for new PRs (default: true)

# Allow commands on draft PRs (overrides global setting)
allow-commands-on-draft-prs:
- build-and-push-container # Allow building containers on draft PRs

# Repository-specific labels configuration (overrides global)
labels:
enabled-labels:
Expand Down
18 changes: 18 additions & 0 deletions webhook_server/config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ properties:
type: boolean
description: Create a tracking issue for new pull requests (global default)
default: true
allow-commands-on-draft-prs:
type: array
items:
type: string
description: |
List of commands allowed on draft PRs.
- Not set (default): commands blocked on draft PRs
- Empty list []: all commands allowed on draft PRs
- List with commands: only specified commands allowed (e.g., ["build-and-push-container", "retest"])
labels:
type: object
description: Configure which labels are enabled and their colors
Expand Down Expand Up @@ -332,6 +341,15 @@ properties:
type: boolean
description: Create a tracking issue for new pull requests
default: true
allow-commands-on-draft-prs:
type: array
items:
type: string
description: |
List of commands allowed on draft PRs.
- Not set (default): commands blocked on draft PRs
- Empty list []: all commands allowed on draft PRs
- List with commands: only specified commands allowed (e.g., ["build-and-push-container", "retest"])
labels:
type: object
description: Configure which labels are enabled and their colors
Expand Down
31 changes: 24 additions & 7 deletions webhook_server/libs/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,14 +472,31 @@ async def process(self) -> Any:
self.logger.debug(f"{self.log_prefix} {event_log}")

if await asyncio.to_thread(lambda: pull_request.draft):
self.logger.debug(f"{self.log_prefix} Pull request is draft, doing nothing")
token_metrics = await self._get_token_metrics()
self.logger.info(
f"{self.log_prefix} Webhook processing completed successfully: "
f"draft PR (skipped) - {token_metrics}",
allow_commands_on_draft = self.config.get_value("allow-commands-on-draft-prs")

# Validate type: must be a list, treat invalid types as None (default-deny)
if allow_commands_on_draft is not None and not isinstance(allow_commands_on_draft, list):
self.logger.warning(
f"{self.log_prefix} allow-commands-on-draft-prs has invalid type "
f"{type(allow_commands_on_draft).__name__}, expected list. Treating as not configured."
)
allow_commands_on_draft = None

# Only allow issue_comment events when config is explicitly set
if allow_commands_on_draft is None or self.github_event != "issue_comment":
self.logger.debug(f"{self.log_prefix} Pull request is draft, doing nothing")
token_metrics = await self._get_token_metrics()
self.logger.info(
f"{self.log_prefix} Webhook processing completed successfully: "
f"draft PR (skipped) - {token_metrics}",
)
await self._update_context_metrics()
return None

self.logger.debug(
f"{self.log_prefix} Pull request is draft, but allow-commands-on-draft-prs is "
"configured, processing issue_comment"
)
await self._update_context_metrics()
return None

self.last_commit = await self._get_last_commit(pull_request=pull_request)
self.parent_committer = pull_request.user.login
Expand Down
25 changes: 24 additions & 1 deletion webhook_server/libs/handlers/issue_comment_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ async def process_comment_webhook_data(self, pull_request: PullRequest) -> None:

# Execute all commands in parallel
if _user_commands:
# Cache draft status once to avoid repeated API calls
is_draft = await asyncio.to_thread(lambda: pull_request.draft)

tasks: list[Coroutine[Any, Any, Any] | Task[Any]] = []
for user_command in _user_commands:
task = asyncio.create_task(
Expand All @@ -98,6 +101,7 @@ async def process_comment_webhook_data(self, pull_request: PullRequest) -> None:
command=user_command,
reviewed_user=user_login,
issue_comment_id=self.hook_data["comment"]["id"],
is_draft=is_draft,
)
)
tasks.append(task)
Expand Down Expand Up @@ -143,7 +147,7 @@ async def process_comment_webhook_data(self, pull_request: PullRequest) -> None:
raise

async def user_commands(
self, pull_request: PullRequest, command: str, reviewed_user: str, issue_comment_id: int
self, pull_request: PullRequest, command: str, reviewed_user: str, issue_comment_id: int, *, is_draft: bool
) -> None:
available_commands: list[str] = [
COMMAND_RETEST_STR,
Expand All @@ -161,6 +165,25 @@ async def user_commands(
_command = command_and_args[0]
_args: str = command_and_args[1] if len(command_and_args) > 1 else ""

# Check if command is allowed on draft PRs
if is_draft:
allow_commands_on_draft = self.github_webhook.config.get_value("allow-commands-on-draft-prs")
# Empty list means all commands allowed; non-empty list means only those commands
if isinstance(allow_commands_on_draft, list) and len(allow_commands_on_draft) > 0:
# Sanitize: ensure all entries are strings for safe join and comparison
allow_commands_on_draft = [str(cmd) for cmd in allow_commands_on_draft]
if _command not in allow_commands_on_draft:
self.logger.debug(
f"{self.log_prefix} Command {_command} is not allowed on draft PRs. "
f"Allowed commands: {allow_commands_on_draft}"
)
await asyncio.to_thread(
pull_request.create_issue_comment,
f"Command `/{_command}` is not allowed on draft PRs.\n"
f"Allowed commands on draft PRs: {', '.join(allow_commands_on_draft)}",
)
return
Comment on lines +168 to +185
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HIGH: Non-list config values fail open and allow all draft commands.

If allow-commands-on-draft-prs is unset or mistyped (e.g., a string), the current guard is skipped and all commands run on draft PRs—contradicting the default “block” behavior and widening permissions on misconfig. Consider explicitly blocking when unset and rejecting non-lists.

🛠️ Proposed fix
         if is_draft:
             allow_commands_on_draft = self.github_webhook.config.get_value("allow-commands-on-draft-prs")
+            if allow_commands_on_draft is None:
+                self.logger.debug(
+                    f"{self.log_prefix} Draft PR commands are disabled (allow-commands-on-draft-prs not set)."
+                )
+                return
+            if not isinstance(allow_commands_on_draft, list):
+                raise ValueError(
+                    f"allow-commands-on-draft-prs must be a list of command strings, "
+                    f"got {type(allow_commands_on_draft).__name__}"
+                )
             # Empty list means all commands allowed; non-empty list means only those commands
-            if isinstance(allow_commands_on_draft, list) and len(allow_commands_on_draft) > 0:
+            if len(allow_commands_on_draft) > 0:
                 # Sanitize: ensure all entries are strings for safe join and comparison
                 allow_commands_on_draft = [str(cmd) for cmd in allow_commands_on_draft]
                 if _command not in allow_commands_on_draft:
                     self.logger.debug(
                         f"{self.log_prefix} Command {_command} is not allowed on draft PRs. "
                         f"Allowed commands: {allow_commands_on_draft}"
                     )
🤖 Prompt for AI Agents
In `@webhook_server/libs/handlers/issue_comment_handler.py` around lines 168 -
185, The draft-PR command check currently treats non-list or missing
allow-commands-on-draft-prs as "no restriction" and thus allows commands —
change it so only a proper list is accepted and any missing/mistyped value
blocks commands: fetch allow_commands_on_draft via
github_webhook.config.get_value, if it is None treat as "no commands allowed on
drafts" and post a comment via pull_request.create_issue_comment rejecting the
command; if it exists but is not a list, log a warning with
self.logger.debug/self.log_prefix and likewise post a rejection comment telling
admins to set a list; only when allow_commands_on_draft is a list should you
sanitize entries to strings and apply the current membership check against
_command (with the existing early return when not allowed).


self.logger.debug(
f"{self.log_prefix} User: {reviewed_user}, Command: {_command}, Command args: {_args or 'None'}"
)
Expand Down
Loading