Skip to content

feat: add Jira ticket-context support#2420

Open
dellch wants to merge 8 commits into
The-PR-Agent:mainfrom
dellch:feat/jira-ticket-context
Open

feat: add Jira ticket-context support#2420
dellch wants to merge 8 commits into
The-PR-Agent:mainfrom
dellch:feat/jira-ticket-context

Conversation

@dellch
Copy link
Copy Markdown

@dellch dellch commented Jun 2, 2026

Summary

Adds Jira ticket-context support to the review tool's ticket-analysis flow (require_ticket_analysis_review). Previously only GitHub issues and Azure Boards work items were fetched; teams tracking work in Jira got no ticket context.

The lookup is provider-agnostic — it only uses get_user_description() and get_pr_branch() (plus the PR title), which every git provider implements — so Jira context works on GitHub, GitLab, Bitbucket, and Azure DevOps. It is a no-op when [jira] is not configured.

Jira Cloud only. The base URL is built from a validated site name (jira_sitehttps://<site>.atlassian.net) rather than taken as a free-form URL. This keeps the design simple — config is a site name, not a URL — and is safe by construction: the destination is always *.atlassian.net, so configuration can't point the authenticated request at an arbitrary host. Jira Server / Data Center needs a free-form host, so it isn't supported here yet; it can be added once base-URL handling for the self-hosted case is settled. Happy to revisit the shape if you'd prefer to accept a full URL for Cloud too.

This follows the discussion in #2162, where @naorpeled gave the go-ahead to add Jira support as a focused first PR, deferring /similar_issue and the broader issue-provider abstraction to later work.

What it does

  • find_jira_tickets(): extract Jira keys from the PR title, description and branch name (case-insensitive, normalized to upper case so bugfix/abc-123-x is detected). First-seen order is preserved so the MAX_TICKETS cap is deterministic.
  • _jira_cloud_base_url(): build the base URL from jira_site, validated against JIRA_SITE_PATTERN (a single DNS label) so it cannot contain the characters (. / : @ # ? etc.) needed to escape *.atlassian.net. Returns None (and warns) on a missing/invalid site.
  • _get_jira_client() / extract_jira_tickets(): fetch each key via the atlassian-python-api Jira client (already a dependency; used by the Bitbucket providers). Jira Cloud auth (email + API token). The REST API version is pinned to v2 (JIRA_API_VERSION): v2 returns description and custom fields as plain strings, while v3 returns ADF JSON dicts that would need separate parsing.
  • add_jira_tickets(): provider-agnostic step in extract_tickets() that appends Jira tickets to tickets_content, de-duplicated by URL, respecting the overall MAX_TICKETS cap.
  • jira_requirements_field: maps an instance-specific custom field (e.g. customfield_10127) to the ticket "requirements" section (acceptance criteria). Both the body and the requirements field are truncated to MAX_TICKET_CHARACTERS.
  • Adds a [jira] config section (jira_site, jira_api_email, jira_api_token, jira_requirements_field) and a secrets-template entry. Credentials are read from settings/env, not committed.
  • Documents Jira Cloud setup in the fetching-ticket-context docs, noting Server/Data Center is not yet supported and why.

Verified end-to-end against a live Jira Cloud instance.

Open questions

I am leaving these for maintainer input rather than choosing the API shape in this PR:

  1. Config key naming. For jira_api_token / jira_api_email this PR keeps the names the docs already publish. Note jira_base_url is replaced by jira_site (the site name, not a URL — see the Jira Cloud note above), so the documented contract already changes here. Because the keys sit under the [jira] table, settings resolve to JIRA.JIRA_API_TOKEN and the env override is jira__jira_api_token — the repeated jira_ is redundant. Options: keep the jira_-prefixed names for continuity with the existing docs, or drop the prefix (api_token, site, etc., like [bitbucket_server]) for clarity. Happy to go either way.

  2. A dedicated issue-provider config section. Rather than a [jira] section, would you prefer something like [related_issues] with issue_provider = "jira"? That gives a natural home for future, provider-neutral options (see below) instead of accreting keys under [jira]. This would change the configuration shape, so worth deciding before it ships.

    Relatedly — should more than one issue provider be supported at once? Today extract_tickets() is additive: it runs the provider-native branch (GitHub Issues / Azure Work Items) and then add_jira_tickets(), so a PR referencing both a native issue and a Jira key fetches from both (now bounded by the shared MAX_TICKETS cap). If that is not desired, an issue_providers array (e.g. ["jira"] for one system, or ["jira", "github"] for both in priority order) would let a repo express "one tracker only" or "both, in this order," defaulting to today's additive behavior when unset. This is an API-shape decision, hence flagged here rather than implemented.

  3. Scope of this PR vs. follow-ups. This PR is intentionally minimal (fetch + map + tests). Should any of the following be in this PR, or separate follow-ups?

    • jira_project_keysJira-specific: restrict key matching to configured project prefixes (e.g. DVT, ABC), reducing false positives like utf-8 / sha-1. Doesn't apply to GitHub (numeric #123 refs) or Azure DevOps (native linked work items, no text extraction), so it's named to make the binding explicit. (This differs from Jira issue provider support (minimal) #2162, where valid_project_keys filters JQL search results; here it would filter keys parsed from PR text before fetch.)
    • max_tickets_to_fetch and similar tunables (currently the fixed MAX_TICKETS = 3, matching the existing GitHub path).
    • The broader Issue / IssueProvider abstraction from Jira issue provider support (minimal) #2162 — left out here as it may be premature abstraction with only one consumer.
  4. False-positive handling. Key extraction is heuristic; non-existent keys 404 and are skipped, and candidates are capped at 3 — the same trade-off the existing GitHub #123 extraction already makes. Is the current fetch-and-skip behavior acceptable for the first PR, or should strict project-key filtering (option 3's jira_project_keys) be required before merge?

Out of scope

/similar_issue, embedding client, vector-DB integration, and the issue-provider abstraction layer (all part of #2162) are intentionally excluded.

Test plan

  • pytest tests/unittest/test_jira_ticket_extraction.py (49 tests), including TestJiraSiteInjection covering site-name breakout attempts (query / fragment / path / port / userinfo / encoded-dot / whitespace), asserting they are rejected and no client is built
  • No regressions in tests/unittest/test_extract_issue_from_branch.py
  • Live validation against a Jira Cloud instance

Co-Authored-By: Claude

The review tool's ticket-analysis flow (require_ticket_analysis_review)
only fetched GitHub issues and linked Azure Boards work items. Teams that
track work in Jira got no ticket context. This adds provider-agnostic Jira
ticket lookup.

The lookup uses get_user_description() and get_pr_branch() plus the PR
title, which every git provider implements, so it works on GitHub, GitLab,
Bitbucket, and Azure DevOps. It is a no-op when [jira] is not configured.

- find_jira_tickets(): extract Jira keys from the PR title, description and
  branch name. Matching is case-insensitive and keys are normalized to upper
  case, so lowercased branch names (e.g. bugfix/abc-123-x) are detected.
  First-seen order is preserved so the MAX_TICKETS cap is deterministic.
- _get_jira_client() / extract_jira_tickets(): fetch each key via the
  atlassian-python-api Jira client (already a dependency, used by the
  Bitbucket providers). Supports Jira Cloud (email + token) and Server/Data
  Center (PAT). The REST API version is pinned to v2 (JIRA_API_VERSION):
  v2 returns description and custom fields as plain strings, while v3 returns
  ADF JSON dicts that would need separate parsing.
- add_jira_tickets(): provider-agnostic step in extract_tickets() that
  appends Jira tickets to tickets_content, de-duplicated by URL.
- jira_requirements_field maps an instance-specific custom field (e.g.
  customfield_10127) to the ticket "requirements" section. Both the body and
  the requirements field are truncated to MAX_TICKET_CHARACTERS.
- Add a [jira] config section and secrets-template entry using the key names
  from the existing docs. Credentials are read from settings/env, not
  committed.
- Add unit tests for key extraction, ticket fetching (auth modes, capping,
  skip-on-error, truncation), client construction, and the provider-agnostic
  append. An autouse fixture restores the JIRA.* settings between tests.
- Document Jira support in the fetching-ticket-context docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature 💡 label Jun 2, 2026
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add Jira ticket-context support to ticket analysis flow

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add Jira ticket-context support to PR analysis flow
• Extract Jira keys from PR title, description, branch name
• Fetch ticket details via Jira REST API v2 client
• Support Cloud (email + token) and Server/Data Center (PAT) auth
• Map custom fields to ticket requirements section
• Provider-agnostic implementation works across all git providers
• Comprehensive unit tests for key extraction and ticket fetching
Diagram
flowchart LR
  PR["PR Title/Description/Branch"] -->|find_jira_tickets| Keys["Jira Keys<br/>ABC-123"]
  Keys -->|_get_jira_client| Client["Jira Client<br/>Cloud/Server"]
  Client -->|extract_jira_tickets| Tickets["Ticket Objects<br/>title, body, requirements"]
  Tickets -->|add_jira_tickets| Content["tickets_content<br/>de-duplicated"]
  Content -->|extract_tickets| Review["Ticket Analysis<br/>Review"]

Loading

Grey Divider

File Changes

1. pr_agent/tools/ticket_pr_compliance_check.py ✨ Enhancement +155/-16

Implement Jira ticket extraction and integration

• Add find_jira_tickets() to extract Jira keys from text with case-insensitive matching and
 normalization
• Add _get_jira_client() to build Jira client supporting Cloud and Server/Data Center auth modes
• Add extract_jira_tickets() to fetch ticket details via REST API v2 and map to ticket dict format
• Add _get_pr_title() helper for provider-agnostic PR/MR title access
• Add add_jira_tickets() to append Jira tickets to tickets_content with deduplication
• Extract MAX_TICKETS and MAX_TICKET_CHARACTERS as module-level constants
• Refactor extract_tickets() to call add_jira_tickets() for all providers

pr_agent/tools/ticket_pr_compliance_check.py


2. tests/unittest/test_jira_ticket_extraction.py 🧪 Tests +306/-0

Add unit tests for Jira ticket extraction

• Add comprehensive test suite for Jira key extraction with case-insensitivity and normalization
• Test end-to-end ticket fetching with mocked Jira client across auth modes
• Test requirements field mapping and truncation behavior
• Test error handling for failed fetches and non-existent keys
• Test provider-agnostic ticket append with deduplication
• Add autouse fixture to restore Jira settings between tests

tests/unittest/test_jira_ticket_extraction.py


3. docs/docs/core-abilities/fetching_ticket_context.md 📝 Documentation +18/-3

Document Jira ticket context support

• Update supported platforms note to include Azure DevOps
• Clarify that Jira keys are extracted on all providers while GitHub numeric issues are GitHub-only
• Add Jira Cloud configuration section with base_url, email, and token
• Add optional acceptance criteria / requirements field configuration
• Add Jira Server/Data Center basic auth configuration section

docs/docs/core-abilities/fetching_ticket_context.md


View more (2)
4. pr_agent/settings/.secrets_template.toml ⚙️ Configuration changes +6/-0

Add Jira credentials to secrets template

• Add [jira] section with jira_base_url, jira_api_email, jira_api_token placeholders
• Include comments explaining Cloud vs Server/Data Center usage

pr_agent/settings/.secrets_template.toml


5. pr_agent/settings/configuration.toml ⚙️ Configuration changes +10/-0

Add Jira configuration section

• Add [jira] configuration section with commented examples
• Add jira_requirements_field setting for custom field mapping
• Include documentation for all three auth modes

pr_agent/settings/configuration.toml


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Nondeterministic github_tickets truncation 📘 Rule violation ☼ Reliability
Description
When too many GitHub issue links are found, the code enforces the MAX_TICKETS cap by converting an
unordered set to a list and slicing, making which tickets are kept nondeterministic across runs.
This can lead to inconsistent related ticket context (and thus inconsistent compliance analysis
inputs) and flaky behavior dependent on hash iteration order.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R253-256]

Evidence
PR Compliance ID 21 requires deterministic ordering when truncating collections, but the
implementation collects GitHub ticket URLs into a set and then caps them via
list(github_tickets)[:MAX_TICKETS] (and even set(list(github_tickets)[:MAX_TICKETS])), which has
no defined order and therefore yields an arbitrary subset. The resulting unordered lists are later
merged and sliced again in extract_tickets(), so this nondeterminism can propagate to change which
tickets appear in the final related_tickets context when more than MAX_TICKETS GitHub issues are
referenced.

pr_agent/tools/ticket_pr_compliance_check.py[253-256]
pr_agent/tools/ticket_pr_compliance_check.py[233-261]
pr_agent/tools/ticket_pr_compliance_check.py[263-300]
pr_agent/tools/ticket_pr_compliance_check.py[307-326]
Best Practice: Learned patterns

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

## Issue description
`extract_ticket_links_from_pr_description()` gathers GitHub issue URLs into an unordered `set` and then truncates by converting that set to a list and slicing (e.g., `set(list(github_tickets)[:MAX_TICKETS])` / `list(github_tickets)[:MAX_TICKETS]`). Because set iteration order is not stable, the specific subset of tickets that survives the `MAX_TICKETS` cap can vary between runs, leading to inconsistent downstream `related_tickets` context and ticket compliance analysis inputs.
## Issue Context
Compliance requires deterministic ordering when truncating collections. In this PR, Jira key extraction intentionally preserves first-seen order for determinism, but GitHub issue extraction still truncates arbitrarily; additionally, `extract_tickets()` merges ticket lists and slices again based on list order, so nondeterminism in GitHub ticket ordering can affect which tickets end up in the final context.
## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[233-261]
- pr_agent/tools/ticket_pr_compliance_check.py[263-300]
- pr_agent/tools/ticket_pr_compliance_check.py[307-326]

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


2. Jira URL config injection ✓ Resolved 🐞 Bug ⛨ Security
Description
_get_jira_client() builds an authenticated Jira client using JIRA.JIRA_BASE_URL from settings, so a
PR can override that URL via repo-controlled configuration and redirect Jira requests. If
JIRA.JIRA_API_TOKEN is set in env/.secrets.toml, the agent will send those credentials to the
injected host.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R65-87]

Evidence
The repo’s pyproject.toml is loaded into the same settings object that _get_jira_client() reads
from, and the resulting base_url is used to initialize an authenticated Jira client
(username/password or PAT token). This creates a path for PR-controlled configuration to redirect
authenticated requests.

pr_agent/tools/ticket_pr_compliance_check.py[58-91]
pr_agent/config_loader.py[17-44]
pr_agent/config_loader.py[63-92]

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

## Issue description
`_get_jira_client()` uses `JIRA.JIRA_BASE_URL` from the merged Dynaconf settings to construct an authenticated Jira client. Because repository configuration (`pyproject.toml`) is loaded into the same settings object, an untrusted PR can override `jira_base_url` and redirect authenticated requests (Basic/PAT) to an attacker-controlled host.
### Issue Context
- Global settings load `.secrets.toml` first, then load the repository `pyproject.toml` (tool.pr-agent), enabling repo config to override values that control network destinations.
- `_get_jira_client()` then uses `base_url` + `api_token` to create the client.
### Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[58-91]
- pr_agent/config_loader.py[17-44]
- pr_agent/config_loader.py[89-92]
### Implementation direction
- Ensure `jira_base_url` used for authenticated Jira calls can only come from trusted sources (env / secrets manager / `.secrets.toml`), not from repository-controlled config.
- Practical options:
- Read `jira_base_url`/credentials via a dedicated secrets-only accessor (e.g., env/secrets manager), bypassing repo-loaded Dynaconf layers for these keys.
- Or enforce an allowlist/validation on `jira_base_url` before constructing the client (scheme must be https; host must match configured allowlist set outside the repo), and skip Jira lookup if validation fails.
- Add a log warning explaining why Jira lookup is skipped when an untrusted/invalid base_url is detected (do not log secrets).

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


3. Blocking Jira I/O in async 🐞 Bug ➹ Performance
Description
extract_tickets() is async but calls add_jira_tickets()/extract_jira_tickets() which perform
synchronous Jira HTTP requests (jira_client.issue) inline. This blocks the event loop during
review execution and can reduce FastAPI throughput / cause latency spikes under concurrent reviews.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R380-385]

Evidence
extract_tickets() is awaited during the async PR review run, but it invokes Jira extraction
synchronously; extract_jira_tickets() then calls jira_client.issue() without any async
offloading, which blocks the event loop.

pr_agent/tools/ticket_pr_compliance_check.py[94-157]
pr_agent/tools/ticket_pr_compliance_check.py[273-385]
pr_agent/tools/pr_reviewer.py[120-141]

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

## Issue description
`extract_tickets()` runs in an async pipeline but performs blocking Jira network calls via `atlassian-python-api` (e.g., `jira_client.issue(...)`) directly on the event loop.
### Issue Context
This function is awaited as part of the PR review flow, so blocking I/O here can stall handling of other concurrent requests/tasks.
### Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[94-157]
- pr_agent/tools/ticket_pr_compliance_check.py[273-385]
- pr_agent/tools/pr_reviewer.py[120-141]
### What to change
- Offload Jira network work from the event loop:
- Option A (minimal): in `extract_tickets()`, call Jira extraction via `await asyncio.to_thread(add_jira_tickets, git_provider, tickets_content)` (or `asyncio.get_running_loop().run_in_executor(...)`).
- Option B (more invasive): make `add_jira_tickets()` / `extract_jira_tickets()` async and use an async HTTP client.
- Ensure the returned/updated `tickets_content` is preserved (if using `to_thread`, use the returned list).

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



Remediation recommended

4. JIRA_API_VERSION hardcoded 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The Jira REST API version is hardcoded as JIRA_API_VERSION = "2", which bakes a runtime-behavior
choice into Python code rather than making it configurable via Dynaconf/TOML. This reduces
flexibility for environments that may need v3 (or future versions) without a code change.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R22-26]

Evidence
PR Compliance ID 6 requires runtime/configuration values to be supplied via the repository’s
configuration mechanisms rather than hardcoded in Python. The PR introduces a hardcoded Jira API
version constant (JIRA_API_VERSION = "2") that controls Jira client behavior.

AGENTS.md: Do Not Hardcode Configuration Values; Use .pr_agent.toml or pr_agent/settings Overrides
pr_agent/tools/ticket_pr_compliance_check.py[22-26]

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

## Issue description
`JIRA_API_VERSION` is hardcoded in Python code, but it represents a runtime configuration choice that should be overrideable via `.pr_agent.toml` / `pr_agent/settings/*.toml` (Dynaconf).

## Issue Context
The code currently pins Jira REST API version to `"2"`. This may be a valid default, but it should be configurable so deployments can change the API version without modifying source code.

## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[22-26]
- pr_agent/settings/configuration.toml[321-332]

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


Grey Divider

Previous review results

Review updated until commit 926e33f

Results up to commit 9a39046


🐞 Bugs (3) 📘 Rule violations (2) 🔗 Cross-repo conflicts (0)


Action required
1. Nondeterministic github_tickets truncation 📘 Rule violation ☼ Reliability ⭐ New
Description
When too many GitHub issue links are found, the code enforces the MAX_TICKETS cap by converting an
unordered set to a list and slicing, making which tickets are kept nondeterministic across runs.
This can lead to inconsistent related ticket context (and thus inconsistent compliance analysis
inputs) and flaky behavior dependent on hash iteration order.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R253-256]

Evidence
PR Compliance ID 21 requires deterministic ordering when truncating collections, but the
implementation collects GitHub ticket URLs into a set and then caps them via
list(github_tickets)[:MAX_TICKETS] (and even set(list(github_tickets)[:MAX_TICKETS])), which has
no defined order and therefore yields an arbitrary subset. The resulting unordered lists are later
merged and sliced again in extract_tickets(), so this nondeterminism can propagate to change which
tickets appear in the final related_tickets context when more than MAX_TICKETS GitHub issues are
referenced.

pr_agent/tools/ticket_pr_compliance_check.py[253-256]
pr_agent/tools/ticket_pr_compliance_check.py[233-261]
pr_agent/tools/ticket_pr_compliance_check.py[263-300]
pr_agent/tools/ticket_pr_compliance_check.py[307-326]
Best Practice: Learned patterns

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

## Issue description
`extract_ticket_links_from_pr_description()` gathers GitHub issue URLs into an unordered `set` and then truncates by converting that set to a list and slicing (e.g., `set(list(github_tickets)[:MAX_TICKETS])` / `list(github_tickets)[:MAX_TICKETS]`). Because set iteration order is not stable, the specific subset of tickets that survives the `MAX_TICKETS` cap can vary between runs, leading to inconsistent downstream `related_tickets` context and ticket compliance analysis inputs.

## Issue Context
Compliance requires deterministic ordering when truncating collections. In this PR, Jira key extraction intentionally preserves first-seen order for determinism, but GitHub issue extraction still truncates arbitrarily; additionally, `extract_tickets()` merges ticket lists and slices again based on list order, so nondeterminism in GitHub ticket ordering can affect which tickets end up in the final context.

## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[233-261]
- pr_agent/tools/ticket_pr_compliance_check.py[263-300]
- pr_agent/tools/ticket_pr_compliance_check.py[307-326]

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


2. Jira URL config injection ✓ Resolved 🐞 Bug ⛨ Security
Description
_get_jira_client() builds an authenticated Jira client using JIRA.JIRA_BASE_URL from settings, so a
PR can override that URL via repo-controlled configuration and redirect Jira requests. If
JIRA.JIRA_API_TOKEN is set in env/.secrets.toml, the agent will send those credentials to the
injected host.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R65-87]

Evidence
The repo’s pyproject.toml is loaded into the same settings object that _get_jira_client() reads
from, and the resulting base_url is used to initialize an authenticated Jira client
(username/password or PAT token). This creates a path for PR-controlled configuration to redirect
authenticated requests.

pr_agent/tools/ticket_pr_compliance_check.py[58-91]
pr_agent/config_loader.py[17-44]
pr_agent/config_loader.py[63-92]

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

## Issue description
`_get_jira_client()` uses `JIRA.JIRA_BASE_URL` from the merged Dynaconf settings to construct an authenticated Jira client. Because repository configuration (`pyproject.toml`) is loaded into the same settings object, an untrusted PR can override `jira_base_url` and redirect authenticated requests (Basic/PAT) to an attacker-controlled host.
### Issue Context
- Global settings load `.secrets.toml` first, then load the repository `pyproject.toml` (tool.pr-agent), enabling repo config to override values that control network destinations.
- `_get_jira_client()` then uses `base_url` + `api_token` to create the client.
### Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[58-91]
- pr_agent/config_loader.py[17-44]
- pr_agent/config_loader.py[89-92]
### Implementation direction
- Ensure `jira_base_url` used for authenticated Jira calls can only come from trusted sources (env / secrets manager / `.secrets.toml`), not from repository-controlled config.
- Practical options:
- Read `jira_base_url`/credentials via a dedicated secrets-only accessor (e.g., env/secrets manager), bypassing repo-loaded Dynaconf layers for these keys.
- Or enforce an allowlist/validation on `jira_base_url` before constructing the client (scheme must be https; host must match configured allowlist set outside the repo), and skip Jira lookup if validation fails.
- Add a log warning explaining why Jira lookup is skipped when an untrusted/invalid base_url is detected (do not log secrets).

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


3. Blocking Jira I/O in async 🐞 Bug ➹ Performance
Description
extract_tickets() is async but calls add_jira_tickets()/extract_jira_tickets() which perform
synchronous Jira HTTP requests (jira_client.issue) inline. This blocks the event loop during
review execution and can reduce FastAPI throughput / cause latency spikes under concurrent reviews.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R380-385]

Evidence
extract_tickets() is awaited during the async PR review run, but it invokes Jira extraction
synchronously; extract_jira_tickets() then calls jira_client.issue() without any async
offloading, which blocks the event loop.

pr_agent/tools/ticket_pr_compliance_check.py[94-157]
pr_agent/tools/ticket_pr_compliance_check.py[273-385]
pr_agent/tools/pr_reviewer.py[120-141]

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

## Issue description
`extract_tickets()` runs in an async pipeline but performs blocking Jira network calls via `atlassian-python-api` (e.g., `jira_client.issue(...)`) directly on the event loop.
### Issue Context
This function is awaited as part of the PR review flow, so blocking I/O here can stall handling of other concurrent requests/tasks.
### Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[94-157]
- pr_agent/tools/ticket_pr_compliance_check.py[273-385]
- pr_agent/tools/pr_reviewer.py[120-141]
### What to change
- Offload Jira network work from the event loop:
- Option A (minimal): in `extract_tickets()`, call Jira extraction via `await asyncio.to_thread(add_jira_tickets, git_provider, tickets_content)` (or `asyncio.get_running_loop().run_in_executor(...)`).
- Option B (more invasive): make `add_jira_tickets()` / `extract_jira_tickets()` async and use an async HTTP client.
- Ensure the returned/updated `tickets_content` is preserved (if using `to_thread`, use the returned list).

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


View more (4)
4. Overlong Jira constructor line ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The Jira client initialization is written as a single long line that likely exceeds the repository’s
120-character limit, risking Ruff/formatting failures. This violates the repository Python style
compliance requirement.
Code

pr_agent/tools/ticket_pr_compliance_check.py[72]

Evidence
PR Compliance ID 9 requires Python changes to follow Ruff/line-length (120 chars). The new Jira
constructor call places multiple keyword arguments on one line, which is likely to exceed the
configured limit and fail formatting checks.

AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style: AGENTS.md: Follow Repository Python Style: Ruff/Isort Ordering, 120-Character Lines, Double Quotes, and Existing Docstring Style
pr_agent/tools/ticket_pr_compliance_check.py[71-74]

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

## Issue description
A newly-added Jira client constructor call is on a single very long line and likely exceeds the repo’s 120-character limit.
## Issue Context
The compliance checklist requires adherence to Ruff formatting conventions, including 120-character line length.
## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[71-74]

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


5. Unbounded Azure requirements 🐞 Bug ☼ Reliability
Description
In extract_tickets() for AzureDevopsProvider, the new "requirements" field is populated from
acceptance_criteria without truncation/type checks, so large work items can bloat prompts and
degrade reliability/latency. Only the work item body is capped to MAX_TICKET_CHARACTERS.
Code

pr_agent/tools/ticket_pr_compliance_check.py[1]

Evidence
The Azure DevOps path truncates only ticket_body_str, but then adds requirements directly from
acceptance_criteria. The provider populates that field from the work item’s AcceptanceCriteria
field, which is not bounded by this code path.

pr_agent/tools/ticket_pr_compliance_check.py[352-369]
pr_agent/git_providers/azuredevops_provider.py[663-685]

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

## Issue description
Azure DevOps work items now contribute `requirements` (acceptance criteria) to `tickets_content` without any truncation, which can inject an unbounded blob into downstream prompts. The ticket `body` is truncated, but `requirements` is not.
## Issue Context
`AzureDevopsProvider.get_work_items()` sources acceptance criteria from `Microsoft.VSTS.Common.AcceptanceCriteria`, which can be large (often HTML/text).
## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[352-369]
- pr_agent/git_providers/azuredevops_provider.py[663-685]
## Suggested fix
- Read `acceptance_criteria` into a local `requirements_str`.
- Ensure it’s a string (or default to empty string).
- Apply the same `MAX_TICKET_CHARACTERS` truncation logic used for `ticket_body_str`.
- Use `requirements_str` when populating the ticket dict.

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


6. _get_jira_client() catches broad Exception ✓ Resolved 📘 Rule violation ☼ Reliability
Description
New Jira provider-initialization and ticket-fetch logic uses broad except Exception as e and
continues/returns None, which can silently mask unexpected failures and make misconfiguration
harder to detect. Compliance requires narrow, explicit exception handling (or immediate re-raise
with preserved context) in parsing/credential/provider-init paths.
Code

pr_agent/tools/ticket_pr_compliance_check.py[R70-78]

Evidence
PR Compliance ID 18 requires narrow, explicit exception handling and forbids broad `except
Exception` that swallows errors in provider-init/credential paths. The added Jira integration
catches Exception and returns None during client initialization and continues on fetch errors,
swallowing unexpected failures instead of handling explicit expected exception types or re-raising.

pr_agent/tools/ticket_pr_compliance_check.py[70-78]
pr_agent/tools/ticket_pr_compliance_check.py[103-107]
Best Pra...

Comment on lines +70 to +78
try:
if api_email:
return Jira(url=base_url.rstrip("/"), username=api_email, password=api_token, api_version=JIRA_API_VERSION)
# No email/username: treat the token as a Server/Data Center PAT.
return Jira(url=base_url.rstrip("/"), token=api_token, api_version=JIRA_API_VERSION)
except Exception as e:
get_logger().error(f"Failed to initialize Jira client: {e}",
artifact={"traceback": traceback.format_exc()})
return None
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. _get_jira_client() catches broad exception 📘 Rule violation ☼ Reliability

New Jira provider-initialization and ticket-fetch logic uses broad except Exception as e and
continues/returns None, which can silently mask unexpected failures and make misconfiguration
harder to detect. Compliance requires narrow, explicit exception handling (or immediate re-raise
with preserved context) in parsing/credential/provider-init paths.
Agent Prompt
## Issue description
Jira client initialization and issue fetching catch broad `Exception` and then return `None`/`continue`, which can hide unexpected failures.

## Issue Context
This code is in provider-initialization / credential-handling paths, where compliance requires narrow exception types or immediate re-raise while preserving context.

## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[70-78]
- pr_agent/tools/ticket_pr_compliance_check.py[103-107]
- pr_agent/tools/ticket_pr_compliance_check.py[168-171]

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Expanding on why I am not narrowing the exception handling here.

_get_jira_client() catches broad Exception 📘 Rule violation ☼ Reliability
New Jira provider-initialization and ticket-fetch logic uses broad except Exception as e and continues/returns None ... Compliance requires narrow, explicit exception handling (or immediate re-raise with preserved context) in parsing/credential/provider-init paths.

Two reasons, one about consistency and one about what the library actually raises.

1. Broad except Exception is the established convention in this exact file. On main, before this PR, ticket_pr_compliance_check.py already has 7 broad except Exception as e: handlers (lines 62, 140, 167, 170, 178, 212, 219), and the same "log and continue / return None" shape is already the idiom in extract_tickets():

  • GitHub path (main L138-143): try: issue_main = ...get_issue(...) except Exception as e: get_logger().error(...); continue
  • Azure DevOps path (main L212-217): except Exception as e: get_logger().error("Error processing Azure DevOps ticket: ...")
  • top-level wrapper (main L219): except Exception as e: get_logger().error("Error extracting tickets ...")

The new Jira handlers deliberately match that shape. Narrowing only the new code would make this file inconsistent with its own neighbours (and with ~312 except Exception across ~50 files under pr_agent/).

2. The library does not give a clean typed contract to narrow to on these paths. Jira.issue() in atlassian-python-api (3.41.4) is a bare return self.get(...) with no custom exceptions; it relies on AtlassianRestAPI.raise_for_status(), which raises requests.HTTPError (e.g. HTTPError("Unauthorized (401)")), not the typed ApiError / ApiNotFoundError / ApiPermissionError classes. Those typed classes exist but are only raised by a few specific helpers, not the generic .get() path. So the most precise correct catch would be (requests.HTTPError, requests.RequestException) — and misconfiguration can still surface other ways — which is why the broad catch (with {e} logged) is the safer choice here.

If tighter exception handling is wanted, I would suggest it as a separate, file-wide cleanup so the convention stays uniform, rather than singling out the new Jira code. Happy to do that if you would like.

Comment thread pr_agent/tools/ticket_pr_compliance_check.py
extract_tickets() capped provider-native tickets to MAX_TICKETS, then
add_jira_tickets() appended up to MAX_TICKETS more, so a PR could carry up
to 2 * MAX_TICKETS ticket bodies into the review prompt.

Treat MAX_TICKETS as the combined per-PR cap: provider-native tickets
already in tickets_content count against it, and Jira tickets are appended
only until the total reaches MAX_TICKETS, keeping existing tickets first.
Skip the Jira lookup entirely (no client init) when the cap is already met.

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

extract_jira_tickets() built the Jira client before checking whether the
text contained any Jira keys. Since add_jira_tickets() runs for every
provider, a configured-but-unreferenced Jira would construct a client (and
log an init failure if misconfigured) on every keyless PR.

Extract keys first and return early when there are none, so the client is
only built when there is something to fetch.

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

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

Comment thread pr_agent/tools/ticket_pr_compliance_check.py Outdated
@dellch dellch force-pushed the feat/jira-ticket-context branch from d33bc57 to cb9ee43 Compare June 2, 2026 21:06
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

Thanks for the review. On finding #5 (nondeterministic GitHub PR-description ticket cap): this is a real bug, but it is in the pre-existing extract_ticket_links_from_pr_description() and unrelated to Jira ticket support. To keep this PR scoped, I will fix it in a separate PR rather than here. (The fix mirrors the first-seen-order dedup this PR already uses for Jira keys.)

For the other findings: #2 (combined MAX_TICKETS cap) and #4 (skip Jira client init when no keys) are fixed in this PR. #1 (broad except Exception) matches the existing exception-handling style throughout this file. #3 (MAX_TICKETS/JIRA_API_VERSION as constants) is noted as an open question in the PR description — happy to move the tunables to config if you prefer.

_get_jira_client() returned None silently whenever base_url + api_token
were not both set. If a user set some [jira] values but not the required
pair, the misconfiguration was invisible and Jira lookup just never ran.

Log a warning naming the missing keys when Jira is partially configured,
while staying silent when nothing is set (Jira simply not in use).

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

Follow-up on finding #1 (broad except Exception in _get_jira_client() / fetch paths), with the concrete examples I referred to.

The Jira code deliberately matches the existing convention in this exact file. On main, before this PR, ticket_pr_compliance_check.py already has 7 broad except Exception as e: handlers — at lines 62, 140, 167, 170, 178, 212, and 219 — and the same "log and continue / return None" pattern the finding flags is already the idiom in extract_tickets():

  • extract_tickets() GitHub path (main L138-143): try: issue_main = ...get_issue(...) except Exception as e: get_logger().error(...); continue
  • extract_tickets() Azure DevOps path (main L212-217): except Exception as e: get_logger().error("Error processing Azure DevOps ticket: ...")
  • top-level extract_tickets() wrapper (main L219): except Exception as e: get_logger().error("Error extracting tickets ...")

Our _get_jira_client() and extract_jira_tickets() handlers follow that same shape so the file stays internally consistent. Repo-wide this is also the norm (~312 except Exception across ~50 files under pr_agent/), so narrowing only the new Jira handlers would make this file inconsistent with both its neighbours and the codebase.

Happy to tighten exception handling here if you would like, but I would suggest doing it as a separate, file-wide cleanup rather than singling out the new code, so the convention stays uniform. Let me know your preference.

Hoist the base-url normalization into a local so the two Jira(...) calls
fit comfortably within the 120-char line limit, and drop the duplicated
.rstrip("/").

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code review by qodo was updated up to the latest commit 1bc29cd

Comment on lines +380 to +385
# Provider-agnostic Jira lookup. Tickets are often referenced by key in the PR
# title, description or branch name rather than via a provider-native link, so
# this runs for every provider and is a no-op when Jira is not configured.
add_jira_tickets(git_provider, tickets_content)

return tickets_content
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. Blocking jira i/o in async 🐞 Bug ➹ Performance

extract_tickets() is async but calls add_jira_tickets()/extract_jira_tickets() which perform
synchronous Jira HTTP requests (jira_client.issue) inline. This blocks the event loop during
review execution and can reduce FastAPI throughput / cause latency spikes under concurrent reviews.
Agent Prompt
### Issue description
`extract_tickets()` runs in an async pipeline but performs blocking Jira network calls via `atlassian-python-api` (e.g., `jira_client.issue(...)`) directly on the event loop.

### Issue Context
This function is awaited as part of the PR review flow, so blocking I/O here can stall handling of other concurrent requests/tasks.

### Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[94-157]
- pr_agent/tools/ticket_pr_compliance_check.py[273-385]
- pr_agent/tools/pr_reviewer.py[120-141]

### What to change
- Offload Jira network work from the event loop:
  - Option A (minimal): in `extract_tickets()`, call Jira extraction via `await asyncio.to_thread(add_jira_tickets, git_provider, tickets_content)` (or `asyncio.get_running_loop().run_in_executor(...)`).
  - Option B (more invasive): make `add_jira_tickets()` / `extract_jira_tickets()` async and use an async HTTP client.
- Ensure the returned/updated `tickets_content` is preserved (if using `to_thread`, use the returned list).

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Accurate that jira_client.issue() is synchronous, but I would not address it in this PR — the new Jira call follows the pre-existing pattern of this function rather than introducing a new one.

Verified against main (before this PR):

  • extract_tickets() is already declared async def (ticket_pr_compliance_check.py L108).
  • It already performs synchronous, blocking network calls inline:
    • GitHub path: git_provider.repo_obj.get_issue(...) (L139, L156) and git_provider.fetch_sub_issues(...) (L152) — PyGithub, blocking.
    • Azure path: git_provider.get_linked_work_items() (L194) — blocking.
  • Those provider methods are plain def (synchronous): e.g. def get_linked_work_items and def fetch_sub_issues, not async def.

So the event loop is already blocked on synchronous ticket I/O for the existing GitHub and Azure providers; the Jira call added here is consistent with that. Wrapping only the Jira call in asyncio.to_thread would not actually fix the concern (the GitHub/Azure calls in the same function would still block) and would make the new code stylistically inconsistent with its neighbours.

A proper fix — offloading all the ticket I/O in extract_tickets() (GitHub, Azure, and Jira together), or making the providers async — is a refactor of pre-existing code that is out of scope for adding Jira support. Happy to open a separate PR for that if it is wanted, but I would prefer not to single out the Jira call here.

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

On finding:

  1. MAX_TICKETS hardcoded in code 📘 Rule violation ⚙ Maintainability

Worth clarifying that this PR did not introduce these limits — it codified pre-existing ones as named constants. On main, before this PR, the same ticket_pr_compliance_check.py already had:

  • the per-PR ticket cap as bare literals: if len(github_tickets) > 3 / github_tickets[:3] (main L58, L61) and if len(merged) > 3 / merged[:3] (main L126, L128);
  • MAX_TICKET_CHARACTERS = 10000 as a local variable inside extract_tickets() (main L109).

This PR lifts those scattered literals into module-level MAX_TICKETS and MAX_TICKET_CHARACTERS constants and reuses them, which is a readability improvement over the prior state, not a new hardcoded knob.

On making them configurable: I agree these two are legitimate tunables, and exposing them is already raised as open question #3 (max_tickets_to_fetch). I have deferred it deliberately, because where the key should live depends on open question #2 (keep it under [jira], or introduce a shared [related_issues] section). Wiring it now would pre-empt that config-shape decision and risk being reworked. Once the section is decided, these constants are trivial to route through settings.

JIRA_API_VERSION is a different case: it is a correctness contract, not a tunable. The parsing logic depends on the REST v2 response shape (plain strings); v3 returns ADF JSON dicts we do not parse, so exposing it as config would let someone set "3" and silently get empty ticket bodies. I would keep that one as a constant.

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

On finding:

  1. Missing Cloud email guard 🐞 Bug ☼ Reliability ⭐ New

I do not think this is a bug — token-without-email is a documented, first-class auth mode, not a Cloud misconfiguration.

Per Atlassian's official docs on Using Personal Access Tokens (Jira Data Center / Server):

To use a personal access token for authentication, you have to pass it as a bearer token in the Authorization header of a REST API call.
curl -H "Authorization: Bearer <yourToken>" https://{baseUrlOfYourInstance}/rest/api/content
... nor should you need to inform which user is making the request.

So base_url + token with no email is a valid PAT configuration. That is exactly the branch _get_jira_client() takes when api_email is empty — Jira(url=..., token=...), which atlassian-python-api sends as the Authorization: Bearer header. The repo docs cover this too, under "Using a Personal Access Token (PAT) for Jira Data Center/Server" (base_url + token, no email).

Adding a "Cloud requires email" guard would actually break that documented PAT setup. There is also no reliable way to distinguish "Cloud user who forgot their email" from "Server PAT user" up front — both present as base_url + token, no email; the credentials carry no signal to tell them apart, so any hard guard would produce a false positive against valid PAT users.

On the "late, per-ticket auth failure" concern: a genuinely bad credential is not silent — the per-ticket fetch is wrapped and logs Failed to fetch Jira ticket {key}: .... We also already warn on partial configuration (missing base_url or token) in a prior commit. So I would keep the current behavior and not add the email guard.

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

Follow-up on finding #6, to be precise about it.

To be fair to the finding: Atlassian's Cloud basic-auth docs do require email:api_token for Cloud, so a Cloud user who sets base_url + token but omits the email is indeed misconfigured.

The problem is the proposed guard can't be implemented without breaking valid setups. Server / Data Center users authenticating with a PAT do not supply an email at allbase_url + token with no email is their correct, documented configuration (Atlassian PAT docs: the bearer token alone identifies the user). So base_url + token, no email is simultaneously:

  • a valid Server/DC PAT config, and
  • a broken Cloud config.

The credentials carry no signal to tell those two apart, so any "Cloud requires email" guard would false-positive against every valid PAT user. That is why I would not add it.

On the "late, per-ticket auth failure" concern: the failure is not silent. extract_jira_tickets() already logs Failed to fetch Jira ticket {key}: {e}, and the underlying atlassian-python-api .issue() call raises requests.HTTPError — for this exact case the message is Unauthorized (401). So a Cloud-without-email mistake surfaces as Failed to fetch Jira ticket ABC-123: Unauthorized (401), which points right at the auth problem. Including {e} in the message is what makes that diagnosable, and it is already there.

The Azure DevOps branch of extract_tickets() truncated the work-item body
to MAX_TICKET_CHARACTERS but passed acceptance_criteria into "requirements"
unbounded, so a large field could inject an oversized blob into the review
prompt. This mirrors the truncation the Jira path in this PR already applies.

Cap requirements to MAX_TICKET_CHARACTERS and guard against non-string
values, matching the body handling.

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code review by qodo was updated up to the latest commit 8d6bac2

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

On finding:

  1. Unbounded Azure requirements 🐞 Bug ☼ Reliability
    In extract_tickets() for AzureDevopsProvider, the new "requirements" field is populated from acceptance_criteria without truncation/type checks ... Only the work item body is capped to MAX_TICKET_CHARACTERS.

Fixed in 8d6bac2. One clarification: the Azure requirements line is actually pre-existing (it has been on main since the Azure work-item support landed, not added by this PR). But the observation is fair — this PR added acceptance-criteria truncation on the Jira side, so leaving the parallel Azure path unbounded was an inconsistency worth removing. The Azure branch now caps acceptance_criteria to MAX_TICKET_CHARACTERS and guards against non-string values, matching both the body handling and the Jira path. Added two tests (TestAzureRequirementsTruncation).

Comment thread pr_agent/tools/ticket_pr_compliance_check.py Outdated
Building the Jira client from a free-form jira_base_url let repo-controlled
configuration (e.g. pyproject.toml [tool.pr-agent]) redirect the authenticated
request, and the API token with it, to an arbitrary host. PR-Agent merges repo
config into the same settings object that supplies the base URL, so an untrusted
PR could point Jira at an attacker server and exfiltrate the token.

Replace jira_base_url with jira_site (the "<site>" in https://<site>.atlassian.net)
and build the base URL from it. jira_site is validated as a single DNS label, so it
cannot contain the characters (. / : @ # ? etc.) needed to escape *.atlassian.net.
The destination is therefore fixed by construction regardless of what untrusted
config supplies.

This scopes the feature to Jira Cloud. Jira Server / Data Center, which needs a
free-form host, is not supported for now; it can return once base-URL trust is
handled (e.g. a secrets-only source). Cloud authenticates with email + API token,
so the partial-config warning now requires site + email + token.

- Add JIRA_SITE_PATTERN + _jira_cloud_base_url(); validate and build the URL.
- Cloud-only auth in _get_jira_client() (drop the Server/DC PAT branch).
- Update [jira] config + secrets template: jira_site instead of jira_base_url.
- Rewrite the docs Jira section Cloud-only, noting Server/DC is not yet supported
  and why.
- Add TestJiraSiteInjection covering breakout attempts (query/fragment/path/port/
  userinfo/encoded-dot/whitespace), asserting they are rejected and no client is built.

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

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

Comment on lines +253 to +256
if len(github_tickets) > MAX_TICKETS:
get_logger().info(f"Too many tickets found in PR description: {len(github_tickets)}")
# Limit the number of tickets to 3
github_tickets = set(list(github_tickets)[:3])
# Limit the number of tickets
github_tickets = set(list(github_tickets)[:MAX_TICKETS])
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. Nondeterministic github_tickets truncation 📘 Rule violation ☼ Reliability

When too many GitHub issue links are found, the code enforces the MAX_TICKETS cap by converting an
unordered set to a list and slicing, making which tickets are kept nondeterministic across runs.
This can lead to inconsistent related ticket context (and thus inconsistent compliance analysis
inputs) and flaky behavior dependent on hash iteration order.
Agent Prompt
## Issue description
`extract_ticket_links_from_pr_description()` gathers GitHub issue URLs into an unordered `set` and then truncates by converting that set to a list and slicing (e.g., `set(list(github_tickets)[:MAX_TICKETS])` / `list(github_tickets)[:MAX_TICKETS]`). Because set iteration order is not stable, the specific subset of tickets that survives the `MAX_TICKETS` cap can vary between runs, leading to inconsistent downstream `related_tickets` context and ticket compliance analysis inputs.

## Issue Context
Compliance requires deterministic ordering when truncating collections. In this PR, Jira key extraction intentionally preserves first-seen order for determinism, but GitHub issue extraction still truncates arbitrarily; additionally, `extract_tickets()` merges ticket lists and slices again based on list order, so nondeterminism in GitHub ticket ordering can affect which tickets end up in the final context.

## Fix Focus Areas
- pr_agent/tools/ticket_pr_compliance_check.py[233-261]
- pr_agent/tools/ticket_pr_compliance_check.py[263-300]
- pr_agent/tools/ticket_pr_compliance_check.py[307-326]

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

On finding:

  1. Nondeterministic github_tickets truncation 📘 Rule violation ☼ Reliability

This is a real bug, but it is pre-existing in extract_ticket_links_from_pr_description() and unrelated to Jira. On main, that function collects GitHub issue URLs into a set and caps via set(list(github_tickets)[:3]), which has no defined order. This PR only renamed the literal cap ([:3][:MAX_TICKETS]) when introducing the shared constant; it did not introduce, and does not change, the set-based nondeterminism.

To keep this PR scoped to Jira support, I have split the fix out separately:

Happy to rebase this PR on top of that once it merges if you would prefer the constant rename not to touch that line here in the meantime.

Reframe the "why Cloud only" note around the design (URL derived from a
validated site name, always an Atlassian host) rather than detailing a
configuration-injection scenario, which overstated this feature's role.

Co-Authored-By: Claude
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Jun 2, 2026

Code review by qodo was updated up to the latest commit 926e33f

@dellch
Copy link
Copy Markdown
Author

dellch commented Jun 2, 2026

@naorpeled Here is the initial implementation of Jira (Cloud) integration.

I would focus first on the PR's description, particularly the open questions, because they affect what else, if anything, belongs in this PR's scope.

@naorpeled
Copy link
Copy Markdown
Member

naorpeled commented Jun 3, 2026

@dellch thanks for this!
I'll go over this during the weekend 🙏🏻


@pytest.mark.parametrize("bad_site", INJECTION_ATTEMPTS)
def test_injection_attempt_rejected(self, bad_site):
import pr_agent.tools.ticket_pr_compliance_check as m
def test_valid_site_only_ever_targets_atlassian_net(self):
"""Any accepted site name resolves to a host under .atlassian.net."""
from urllib.parse import urlparse
import pr_agent.tools.ticket_pr_compliance_check as m
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants