Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions pr_agent/git_providers/gerrit_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,30 @@ def publish_labels(self, labels):
# but required by the interface
pass

def cleanup(self):
"""Remove the temporary cloned repository from disk."""
if self.repo_path and pathlib.Path(self.repo_path).exists():
try:
shutil.rmtree(self.repo_path, ignore_errors=True)
get_logger().info("Cleaned up temp repo at %s", self.repo_path)
except (OSError, PermissionError) as e:
get_logger().warning(
"Failed to clean up temp repo at %s: %s",
self.repo_path, e
)

def __del__(self):
"""Safety net: clean up temp repo if cleanup() was not called."""
try:
self.cleanup()
except Exception:
pass

def remove_initial_comment(self):
# remove repo, cloned in previous steps
# shutil.rmtree(self.repo_path)
# Do NOT call cleanup() here — this method is invoked during the
# request lifecycle while the cloned repo is still needed by
# subsequent commands. Actual cleanup happens in the server's
# finally block and in __del__ as a safety net.
pass

def remove_comment(self, comment):
Expand Down
43 changes: 37 additions & 6 deletions pr_agent/servers/gerrit_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings, global_settings
from pr_agent.git_providers.gerrit_provider import GerritProvider
from pr_agent.log import get_logger, setup_logger

setup_logger()
Expand All @@ -29,24 +30,54 @@ class Action(str, Enum):
class Item(BaseModel):
refspec: str
project: str
msg: str
msg: str = ""


@router.post("/api/v1/gerrit/{action}")
async def handle_gerrit_request(action: Action, item: Item):
get_logger().debug("Received a Gerrit request")
context["settings"] = copy.deepcopy(global_settings)

# For the "ask" action, the question must come from item.msg.
# For all other actions, use the action path parameter as the command.
if action == Action.ask:
if not item.msg:
return HTTPException(
raise HTTPException(
status_code=400,
detail="msg is required for ask command"
)
await PRAgent().handle_request(
f"{item.project}:{item.refspec}",
f"/{item.msg.strip()}"
)
command = f"/{action.value} {item.msg.strip()}"
else:
command = f"/{action.value}"
Comment on lines +41 to +51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. ask allows blank msg 📘 Rule violation ≡ Correctness

The ask flow only checks if not item.msg, so a whitespace-only msg passes validation and
produces a command like /ask , which can cause unexpected behavior downstream. External inputs
should be normalized/validated (e.g., strip() then empty-check) before use.
Agent Prompt
## Issue description
The `ask` action currently accepts whitespace-only `msg` values because it checks `if not item.msg` before using `item.msg.strip()`.

## Issue Context
`item.msg` is external request input; whitespace-only content should be treated as empty and return HTTP 400.

## Fix Focus Areas
- pr_agent/servers/gerrit_server.py[41-51]

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


try:
await PRAgent().handle_request(
f"{item.project}:{item.refspec}",
command
)
finally:
# Clean up the cloned temp repo created by GerritProvider.
# The provider is cached in the starlette context during
# get_git_provider_with_context().
#
# We guard against two failure modes:
# 1. The starlette context is inaccessible (e.g. middleware not
# active) — caught by the outer try/except.
# 2. The provider was never stored in the context (e.g. an error
# occurred before get_git_provider_with_context ran) — the
# dict will simply be empty, and the GerritProvider.__del__
# safety net handles cleanup on garbage collection.
try:
git_providers = context.get("git_provider", {})
if isinstance(git_providers, dict):
for provider in git_providers.values():
if isinstance(provider, GerritProvider):
provider.cleanup()
Comment on lines +70 to +75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Ask temp repo leak 🐞 Bug ☼ Reliability

The new finally cleanup only cleans GerritProvider instances stored in starlette_context, but
the ask command path constructs its provider via get_git_provider() (not
get_git_provider_with_context()), so its mkdtemp clone is not cleaned by the finally block.
Agent Prompt
### Issue description
Temp repo cleanup in `gerrit_server.handle_gerrit_request` only cleans providers stored in `starlette_context`. However, the `ask` action uses PRQuestions, which creates its GerritProvider via `get_git_provider()` and therefore bypasses the context cache, leaving the mkdtemp clone untracked and not cleaned in the request `finally`.

### Issue Context
- `handle_gerrit_request` cleans only `context.get("git_provider", {})` values.
- PRQuestions (used by `/ask`) constructs providers via `get_git_provider()(pr_url)`.

### Fix Focus Areas
- pr_agent/servers/gerrit_server.py[53-80]
- pr_agent/tools/pr_questions.py[18-25]
- pr_agent/tools/pr_line_questions.py[21-26]
- pr_agent/tools/pr_add_docs.py[20-25]
- pr_agent/tools/pr_config.py[17-22]
- pr_agent/tools/pr_generate_labels.py[27-32]
- pr_agent/tools/pr_update_changelog.py[22-27]

### Suggested change
In server/webhook execution paths, ensure tools use `get_git_provider_with_context(pr_url)` (or a helper that uses it when starlette_context is available) so the created GerritProvider instances are stored in context and reliably cleaned in the server `finally` block.

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

except (LookupError, RuntimeError):
get_logger().debug(
"Could not retrieve GerritProvider for cleanup; "
"temp directory will be cleaned up by __del__"
)


async def get_body(request):
Expand Down