Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4d9829d
docs: fix automations note to reflect self-hosted deployments (#3201)
shashankvarma499 Sep 8, 2026
349d106
chore: remove dead bandit configuration (#3203)
asarakhatun17-lgtm Sep 8, 2026
b11a1db
TYP: ship a py.typed marker for PEP 561 compliance (#3205)
utsab345 Sep 8, 2026
11c1442
chore(deps): update yanked Python dependencies (#3211)
PeterDaveHello Sep 8, 2026
5e41d97
ci: run the pre-commit workflow on pull requests (#3212)
utsab345 Sep 8, 2026
f7656a9
docs: drop stale README release entries (#3208)
Shaisolaris Sep 8, 2026
1dcd346
chore: pin uv 0.12.10 in pyproject.toml (#3153)
PeterDaveHello Sep 8, 2026
29709d4
test(providers): enforce capability predicate truth (#3156)
auroraxo Sep 8, 2026
6f2e0ff
fix(polling): bound active workers across polling iterations (#3063)
PeterDaveHello Sep 9, 2026
d9395ee
fix(describe): retain successful chunks after partial failure (#3163)
oleksii-tumanov Sep 9, 2026
bf05b46
fix(deps): declare certifi as a range per the security-pin policy (#3…
7487 Sep 9, 2026
172d261
refactor(help): select help rendering by provider capability (#3213)
7487 Sep 9, 2026
00b8c9d
fix(providers): report code-suggestion publication failure (#3219)
auroraxo Sep 9, 2026
d5c15c4
fix(bitbucket): restore persistent comment links (#3226)
junnhwan Sep 9, 2026
a648424
fix(review): preserve findings after failed chunks (#3229)
junnhwan Sep 9, 2026
8bd1532
fix(github): report total inline suggestion fallback failure (#3232)
yashaskn8 Sep 9, 2026
70d3f27
Harden Dockerfiles for stable and predictable builds (#3218)
PeterDaveHello Sep 9, 2026
37b9db5
feat(telemetry): expose a native prometheus scrape endpoint
utsab345 Sep 9, 2026
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
6 changes: 4 additions & 2 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# disabled. We might run it manually if needed.
# Runs every pre-commit hook (ruff, actionlint, YAML/TOML checks, ...) on
# pull requests, so the hooks are enforced in CI rather than manual-only.
name: pre-commit

on:
workflow_dispatch:
# pull_request:
pull_request:
# push:
# branches: [main]

Expand All @@ -13,5 +14,6 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
# SEE https://github.com/pre-commit/action
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
8 changes: 0 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,3 @@ repos:
language: system
types_or: [python, pyi]
args: [--fix, --show-fixes]

# - repo: https://github.com/PyCQA/bandit
# rev: 1.7.10
# hooks:
# - id: bandit
# args: [
# "-c", "pyproject.toml",
# ]
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Ruff is the single linting tool: `pyproject.toml` configures it and the pre-comm
- Keep Python lines within the 120-character limit declared in `pyproject.toml`.
- `pyproject.toml` configures Ruff rules `E`, `F`, `B` and `I`; `I001` (import sorting), `F401` (unused imports), and `F541` (f-strings without placeholders) are fixable. The `lint.ignore` list defers pre-existing violations — treat it as a debt ledger: fix the code and drop entries rather than adding new ones.
- Before committing, run `uv run ruff check --fix` on the files you touched and fix every issue it reports. Keep fixes mechanical (rename, remove unused imports, sort imports); do not alter program logic while cleaning up — if a lint fix would change behavior, surface it instead of applying it silently.
- `.pre-commit-config.yaml` is the source of truth for enabled pre-commit hooks. Run them on the files you touched with `uv run pre-commit run --files <paths>` and review the automatic edits so unrelated changes are not included. The pre-commit GitHub Actions workflow is manual-only (`workflow_dispatch`); the hooks are not enforced in CI.
- `.pre-commit-config.yaml` is the source of truth for enabled pre-commit hooks. Run them on the files you touched with `uv run pre-commit run --files <paths>` and review the automatic edits so unrelated changes are not included. The pre-commit GitHub Actions workflow runs on pull requests, so the hooks are enforced in CI.
- No general-purpose Python formatter is currently enforced (`ruff format` is deliberately not adopted yet). Preserve the surrounding file's formatting and avoid unrelated rewrites or repository-wide formatting.
- Prefer double quotes for Python strings where consistent with the surrounding file.
- Match existing docstring and comment style—concise English comments using imperative phrasing only where necessary.
Expand Down
19 changes: 0 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,6 @@ pr-agent --pr_url https://github.com/owner/repo/pull/123 review

Full notes for every release are on the [Releases page](https://github.com/the-pr-agent/pr-agent/releases).

### Jul 26, 2026 — [v0.41.0](https://github.com/the-pr-agent/pr-agent/releases/tag/v0.41.0)

Claude Opus 5 support, and `docker/mosaico` became a self-contained deployment bundle.

### Jul 25, 2026 — [v0.40.0](https://github.com/the-pr-agent/pr-agent/releases/tag/v0.40.0)

Default model moved to GPT-5.6, Gemini 3.6 support, persistent inline comments (no more duplicate
suggestions across runs), an OpenRouter provider-routing/reasoning config, a tokenless
[plain-diff provider](https://docs.pr-agent.ai/usage-guide/plain_diff_mode/), and CI artifact
context injection.

### Jul 5, 2026 — [v0.39.0](https://github.com/the-pr-agent/pr-agent/releases/tag/v0.39.0)

`AGENTS.md` and friends are now fed to `/review`, `/describe` and `/improve` **by default**, so the
model picks up your project's conventions out of the box. Also:
[Agent Skills (`SKILL.md`)](https://docs.pr-agent.ai/core-abilities/agent_skills/),
[organization-level settings](https://docs.pr-agent.ai/usage-guide/configuration_options/),
[restricted mode](https://docs.pr-agent.ai/usage-guide/additional_configurations/#restricted-mode)
for reduced GitHub permissions, GitHub Checks as an output target, and Claude Sonnet 5 support.

## Why Use PR-Agent?

Expand Down
36 changes: 18 additions & 18 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,64 +1,64 @@
FROM python:3.12.14-slim AS base
COPY --from=ghcr.io/astral-sh/uv:0.12.10 /uv /uvx /bin/

RUN apt update && apt install --no-install-recommends -y git curl && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install --no-install-recommends -y git curl && apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /app
ENV UV_COMPILE_BYTECODE=1
ADD pyproject.toml uv.lock ./
ADD docs docs
COPY pyproject.toml uv.lock ./
COPY docs docs
RUN uv sync --frozen --no-install-project --no-dev
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONPATH=/app

FROM base AS github_app
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "-m", "gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "pr_agent/servers/gunicorn_config.py", "--forwarded-allow-ips", "*", "pr_agent.servers.github_app:app"]

FROM base AS bitbucket_app
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_app.py"]

FROM base AS bitbucket_server_webhook
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_server_webhook.py"]

FROM base AS github_polling
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"]

FROM base AS github_action
ADD pr_agent pr_agent
ADD github_action/entrypoint.sh /
COPY pr_agent pr_agent
COPY github_action/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

FROM base AS gitlab_webhook
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "-m", "gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "pr_agent/servers/gunicorn_config.py", "--forwarded-allow-ips", "*", "pr_agent.servers.gitlab_webhook:app"]

FROM base AS azure_devops_webhook
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "pr_agent/servers/azuredevops_server_webhook.py"]

FROM base AS gitea_app
ADD pr_agent pr_agent
COPY pr_agent pr_agent
CMD ["python", "-m", "gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "pr_agent/servers/gunicorn_config.py","pr_agent.servers.gitea_app:app"]

FROM base AS mosaico_agent
ADD pr_agent pr_agent
COPY pr_agent pr_agent
EXPOSE 9000
CMD ["python", "pr_agent/mosaico/server.py"]


FROM base AS test
RUN uv sync --frozen --no-install-project
ADD pr_agent pr_agent
ADD tests tests
ADD .github/workflows/publish.yml .github/workflows/publish.yml
ADD docker/mosaico docker/mosaico
COPY pr_agent pr_agent
COPY tests tests
COPY .github/workflows/publish.yml .github/workflows/publish.yml
COPY docker/mosaico docker/mosaico

FROM base AS cli
ADD pr_agent pr_agent
COPY pr_agent pr_agent
RUN uv sync --frozen --no-dev && ln -s /app/.venv/bin/pr-agent /usr/local/bin/pr-agent
ENTRYPOINT ["python", "pr_agent/cli.py"]
3 changes: 2 additions & 1 deletion docker/Dockerfile.lambda
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ RUN dnf update -y && \
# Build the locked environment at /opt/venv with uv; put its site-packages
# on the Lambda runtime's import path alongside the task root via PYTHONPATH.
ENV UV_COMPILE_BYTECODE=1 UV_PROJECT_ENVIRONMENT=/opt/venv
ADD pyproject.toml uv.lock ./
WORKDIR ${LAMBDA_TASK_ROOT}
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev --extra lambda --python /var/lang/bin/python3.12
ENV PYTHONPATH="/opt/venv/lib/python3.12/site-packages:${LAMBDA_TASK_ROOT}"
COPY pr_agent/ ${LAMBDA_TASK_ROOT}/pr_agent/
Expand Down
20 changes: 19 additions & 1 deletion docs/docs/installation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ behavior, not Requests-specific environment settings.

## Sizing a self-hosted webhook server

The GitHub, GitLab and Gitea webhook servers (the `github_app`, `gitlab_webhook` and `gitea_app` Docker targets) run under gunicorn with multiple worker processes, so that a worker busy handling a request cannot block the health check served by another. The other deployments — Bitbucket, Azure DevOps, GitHub polling, and the Lambda variants — run a single process and are unaffected by this section.
The GitHub, GitLab and Gitea webhook servers (the `github_app`, `gitlab_webhook` and `gitea_app` Docker targets) run under gunicorn with multiple worker processes, so that a worker busy handling a request cannot block the health check served by another. The other deployments — Bitbucket, Azure DevOps, GitHub polling, and the Lambda variants — do not use this gunicorn worker configuration.

| Variable | Default | Description |
|----------|---------|-------------|
Expand All @@ -40,6 +40,24 @@ If the container is OOMKilled during startup, lower the worker count:
GUNICORN_WORKERS=2
```

## GitHub polling workers

GitHub polling uses one polling process and a separate child process for each
accepted comment. At most 10 of these child workers may be active across all
polling iterations. If an earlier batch still fills the limit, accepted work
waits for a free slot without blocking the event loop. This can delay the next
notification poll under sustained load.

The existing per-batch limit is unchanged: only the first 10 queued comments in
a batch are accepted; excess work is logged and dropped. Waiting work is held in
memory only. Notification acknowledgement is unchanged, so shutdown or startup
failures do not guarantee redelivery. Dispatch interruptions log the remaining
count. A worker startup failure stops polling rather than risking further
dispatch with an untracked child; inspect the failure before restarting.
Completed children are joined without waiting and closed each iteration;
live children are not terminated on cancellation. This is not a durable queue,
a model-call deadline, or a host-wide memory limit.

!!! note "Docker Hub namespace migration"
Releases **`0.34.2` and later** are published under [`pragent/pr-agent`](https://hub.docker.com/r/pragent/pr-agent). Older releases (up to and including `v0.31`) remain at the legacy [`codiumai/pr-agent`](https://hub.docker.com/r/codiumai/pr-agent) namespace as a frozen archive — no new images are pushed there. The examples on this site reference the new namespace; if you are pinning to a release before `0.34.2`, swap `pragent/pr-agent` for `codiumai/pr-agent` in your `image:` / `docker pull` / `uses: docker://` references.

Expand Down
26 changes: 25 additions & 1 deletion docs/docs/usage-guide/additional_configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ Telemetry is disabled by default. To enable it, set in `configuration.toml`:
```toml
[otel]
is_enabled = true
exporter_type = "console" # "console", "otlp", or "none"
exporter_type = "console" # "console", "otlp", "prometheus", or "none"
service_name = "pr-agent"
environment = "development" # e.g. "development", "staging", "production"
```
Expand All @@ -362,6 +362,29 @@ otlp_protocol = "grpc" # default: "http"

This is the recommended topology for fleets: point every PR-Agent instance at the same collector and aggregate there. Each process creates its own exporter connection; use the `service_name` and `environment` resource attributes to slice instances apart on the backend.

### Exposing native Prometheus metrics

Instead of pushing to a collector, set `exporter_type = "prometheus"` to expose a native `GET /metrics` scrape endpoint on the gunicorn-served apps (`github_app`, `gitlab_webhook`, `azuredevops_server_webhook`, `gitea_app`). The command counter is translated into the Prometheus text format, and every gunicorn worker's values are merged at scrape time, so counters stay correct across the process workers:

```toml
[otel]
exporter_type = "prometheus"
prometheus_multiproc_dir = "/tmp/pr-agent-prometheus" # shared, writable by every worker
```

1. The exporter is metrics-only: command spans are not exported in this mode.
2. `/metrics` is mounted only when this exporter is selected, so nothing is exposed by default. It does not depend on an OTLP collector or endpoint.
3. gunicorn registers and deregisters workers' state files automatically (`when_ready`/`child_exit`); a worker that dies mid-scrape leaves only a stale file, which is ignored once it is marked dead.
4. Metric families are created from the first data point's label set. Later attributes that do not fit the family are dropped, and missing ones are back-filled with an empty string, so a scrape never breaks on drifting label cardinality.
5. The exporter ships with PR-Agent (it depends on `prometheus-client`); no extra package is required. Scrape it like any exporter:

```yaml
scrape_configs:
- job_name: pr-agent
static_configs:
- targets: ["pr-agent:3000"]
```

Privacy controls (both off by default):

- `include_pr_url = true` attaches PR URLs to spans. Off by default because URLs expose private repo names.
Expand All @@ -373,6 +396,7 @@ Notes:
- PR-Agent keeps its own OpenTelemetry providers and never registers the process-global one, so embedding PR-Agent in an application that already uses OpenTelemetry will not interfere with the host's telemetry. Pending spans and metrics are flushed automatically on process exit.
- Each OTLP export call is bounded by `otlp_timeout` (default 3 seconds, retries included), so an unreachable collector cannot hang CLI exit or request completion. Raise it for slow collectors at the cost of longer worst-case stalls.
- If `exporter_type = "otlp"` is set but no endpoint is configured, telemetry is disabled entirely (fail closed) — it never falls back to another exporter, so a missing secret cannot redirect telemetry into process logs.
- With `exporter_type = "prometheus"`, `prometheus_multiproc_dir` must be a shared directory writable by every worker; it defaults to `/tmp/pr-agent-prometheus`. In non-gunicorn (single-process) deployments the exporter works without it and simply serves the process's own registry.
- **Serverless deployments** (e.g. the AWS Lambda webhooks) are supported: buffered spans and metrics are force-flushed at the end of every handled request, because frozen execution environments stop background export threads and are reaped without running exit handlers. No extra configuration is needed.

## Bringing per-repo context files to PR-Agent
Expand Down
3 changes: 1 addition & 2 deletions docs/docs/usage-guide/automations_and_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ When this parameter is set to `true`, PR-Agent will not run any automatic tools
### GitHub App

!!! note "Configurations for PR-Agent"
PR-Agent for GitHub is an App, hosted by Codium. So all the instructions below are relevant for PR-Agent users.
Same goes for [GitLab webhook](#gitlab-webhook) and [BitBucket App](#bitbucket-app) sections.
These settings apply to self-hosted GitHub App, GitLab webhook, and Bitbucket App deployments.

#### GitHub app automatic tools when a new PR is opened

Expand Down
4 changes: 4 additions & 0 deletions pr_agent/git_providers/bitbucket_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ def _get_cloud_comment(self, comment):
return comment._cloud_comment
return comment

def get_comment_url(self, comment):
comment = self._get_cloud_comment(comment)
return comment.data["links"]["html"]["href"]

def supports_review_comment_identity(self) -> bool:
return True

Expand Down
4 changes: 4 additions & 0 deletions pr_agent/git_providers/bitbucket_server_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ def is_supported(self, capability: str) -> bool:
return False
return True

def supports_markdown_tables(self) -> bool:
# Bitbucket Data Center renders Markdown tables in comments, but not GFM.
return True

def set_pr(self, pr_url: str):
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
Expand Down
11 changes: 7 additions & 4 deletions pr_agent/git_providers/codecommit_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,15 @@ def publish_comment(self, pr_comment: str, is_temporary: bool = False):

def publish_code_suggestions(self, code_suggestions: list) -> bool:
counter = 1
publishable_count = 0
published_count = 0
for suggestion in code_suggestions:
# Verify that each suggestion has the required keys
if not all(key in suggestion for key in ["body", "relevant_file", "relevant_lines_start"]):
get_logger().warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys")
continue

publishable_count += 1
target_contexts = self._get_target_contexts_for_file(suggestion["relevant_file"])
for target in target_contexts:
try:
Expand All @@ -236,15 +239,15 @@ def publish_code_suggestions(self, code_suggestions: list) -> bool:
annotation_file=suggestion["relevant_file"],
annotation_line=suggestion["relevant_lines_start"],
)
published_count += 1
except Exception as e:
raise ValueError(f"CodeCommit Cannot publish code suggestions for PR: {self.pr_num}") from e

counter += 1

# The calling function passes in a list of code suggestions, and this function publishes each suggestion one at a time.
# If we were to return False here, the calling function will attempt to publish the same list of code suggestions again, one at a time.
# Since this function publishes the suggestions one at a time anyway, we always return True here to avoid the retry.
return True
# A partial failure must not report failure: the caller republishes the whole
# list, which would post the already-accepted suggestions a second time.
return published_count > 0 or publishable_count == 0

def publish_labels(self, labels):
return [""] # not implemented yet
Expand Down
15 changes: 12 additions & 3 deletions pr_agent/git_providers/gerrit_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,10 @@ def split_suggestion(self, msg) -> tuple[str, str]:
'\n'.join(context) + '\n' if context else ''
)

def publish_code_suggestions(self, code_suggestions: list):
def publish_code_suggestions(self, code_suggestions: list) -> bool:
msg = []
publishable_count = 0
published_count = 0
repo_root = pathlib.Path(self.repo_path).resolve()
for suggestion in code_suggestions:
# Validate suggestion structure before accessing keys
Expand All @@ -402,6 +404,8 @@ def publish_code_suggestions(self, code_suggestions: list):
except ValueError:
get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
continue

publishable_count += 1
description, code = self.split_suggestion(suggestion['body'])
add_suggestion(
target_path,
Expand All @@ -417,8 +421,13 @@ def publish_code_suggestions(self, code_suggestions: list):
msg.append(f'* {description}\n{full_path}')

if msg:
add_comment(self.parsed_url, self.refspec, "\n".join(msg))
return True
try:
add_comment(self.parsed_url, self.refspec, "\n".join(msg))
published_count += 1
except Exception as e:
get_logger().exception("Failed to publish Gerrit code suggestions: {}", e)

return published_count > 0 or publishable_count == 0

def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if not is_temporary:
Expand Down
15 changes: 15 additions & 0 deletions pr_agent/git_providers/git_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ def supports_threaded_pr_questions(self) -> bool:
def supports_line_question_history(self) -> bool:
return False

def supports_checkbox_commands(self) -> bool:
"""Whether a published comment renders command checkboxes as checkboxes.

Providers that render `- [ ]` as a tickable box override this; the default is no
support, so tools render commands as text instead."""
return False

def supports_markdown_tables(self) -> bool:
"""Whether comments render pipe-table markdown.

Only consulted for providers without `gfm_markdown`, so that tools can degrade to
a plain table instead of refusing to render. Providers that render Markdown tables
but not GitHub-flavored markdown override this."""
return False

#Given a url (issues or PR/MR) - get the .git repo url to which they belong. Needs to be implemented by the provider.
def get_git_repo_url(self, issues_or_pr_url: str) -> str:
get_logger().warning("Not implemented! Returning empty url")
Expand Down
Loading
Loading