Repository navigation
Adapt to the atlassian.net API change effective 2025-05-01 #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: features/intermediate-stage
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,7 +73,7 @@ class IssueCreateResponse(BaseModel): | |
|
|
||
| class SearchRequest(BaseModel): | ||
| jql: str = Field(..., description="JQL query string") | ||
| startAt: int = Field(0, ge=0, description="Pagination offset") | ||
| nextPageToken: Optional[str] = Field(None, description="Token of the page to fetch") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an existing Useful? React with 👍 / 👎. |
||
| maxResults: int = Field(50, ge=1, le=1000, description="Max results to return") | ||
| fields: Optional[List[str]] = Field(None, description="Field selector — None = all") | ||
|
|
||
|
|
@@ -195,7 +195,7 @@ def search_issues(req: SearchRequest) -> Dict[str, Any]: | |
| try: | ||
| return engine.search( | ||
| jql=req.jql, | ||
| start_at=req.startAt, | ||
| next_page_token=req.nextPageToken, | ||
| max_results=req.maxResults, | ||
| fields=req.fields, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -748,7 +748,7 @@ def search_issues( | |||||||
| self, | ||||||||
| jql: str, | ||||||||
| max_results: int = 50, | ||||||||
| start_at: int = 0, | ||||||||
| next_page_token: Optional[str] = None, | ||||||||
| fields: Optional[List[str]] = None, | ||||||||
| ) -> ConnectorOutcome: | ||||||||
| """Search Jira issues using JQL (Agent READ operation).""" | ||||||||
|
|
@@ -757,14 +757,15 @@ def search_issues( | |||||||
| "skipped", {"reason": "jira connector not fully configured"} | ||||||||
| ) | ||||||||
|
|
||||||||
| endpoint = urljoin(self.base_url + "/", "rest/api/3/search") | ||||||||
| endpoint = urljoin(self.base_url + "/", "rest/api/3/search/jql") | ||||||||
| payload: Dict[str, Any] = { | ||||||||
| "jql": jql, | ||||||||
| "maxResults": max_results, | ||||||||
| "startAt": start_at, | ||||||||
| # fields defaults to id only on search/jql | ||||||||
| "fields": fields or ["*navigable"], | ||||||||
| } | ||||||||
| if fields: | ||||||||
| payload["fields"] = fields | ||||||||
| if next_page_token: | ||||||||
| payload["nextPageToken"] = next_page_token | ||||||||
|
|
||||||||
| try: | ||||||||
| response = self._request( | ||||||||
|
|
@@ -797,8 +798,9 @@ def search_issues( | |||||||
| "fetched", | ||||||||
| { | ||||||||
| "endpoint": endpoint, | ||||||||
| "total": data.get("total", 0), | ||||||||
| "total": len(data.get("issues", [])), | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents
Suggested change
|
||||||||
| "issues": data.get("issues", []), | ||||||||
| "next_page_token": data.get("nextPageToken"), | ||||||||
| "data": data, | ||||||||
| "operation": "search_issues", | ||||||||
| }, | ||||||||
|
|
@@ -874,27 +876,28 @@ def bulk_search( | |||||||
| """Paginated JQL bulk query — fetches all matching issues up to max_results. | ||||||||
|
|
||||||||
| Returns all issues in a single ConnectorOutcome with automatic pagination. | ||||||||
| Uses POST /rest/api/3/search with startAt advancement. | ||||||||
| Uses POST /rest/api/3/search/jql with nextPageToken advancement. | ||||||||
| """ | ||||||||
| if not self.configured: | ||||||||
| return ConnectorOutcome( | ||||||||
| "skipped", {"reason": "jira connector not fully configured"} | ||||||||
| ) | ||||||||
|
|
||||||||
| all_issues: List[Dict[str, Any]] = [] | ||||||||
| start_at = 0 | ||||||||
| total = None | ||||||||
| next_page_token: Optional[str] = None | ||||||||
| pages_fetched = 0 | ||||||||
|
|
||||||||
| while len(all_issues) < max_results: | ||||||||
| batch_size = min(page_size, max_results - len(all_issues)) | ||||||||
| endpoint = urljoin(self.base_url + "/", "rest/api/3/search") | ||||||||
| endpoint = urljoin(self.base_url + "/", "rest/api/3/search/jql") | ||||||||
| payload: Dict[str, Any] = { | ||||||||
| "jql": jql, | ||||||||
| "maxResults": batch_size, | ||||||||
| "startAt": start_at, | ||||||||
| # fields defaults to id only on search/jql | ||||||||
| "fields": fields or ["*navigable"], | ||||||||
| } | ||||||||
| if fields: | ||||||||
| payload["fields"] = fields | ||||||||
| if next_page_token: | ||||||||
| payload["nextPageToken"] = next_page_token | ||||||||
|
|
||||||||
| try: | ||||||||
| response = self._request( | ||||||||
|
|
@@ -925,22 +928,21 @@ def bulk_search( | |||||||
| break | ||||||||
|
|
||||||||
| issues = data.get("issues", []) | ||||||||
| if total is None: | ||||||||
| total = data.get("total", 0) | ||||||||
| all_issues.extend(issues) | ||||||||
| start_at += len(issues) | ||||||||
| pages_fetched += 1 | ||||||||
| next_page_token = data.get("nextPageToken") | ||||||||
|
|
||||||||
| if not issues or start_at >= (total or 0): | ||||||||
| if not issues or data.get("isLast", True) or not next_page_token: | ||||||||
| break | ||||||||
|
|
||||||||
| return ConnectorOutcome( | ||||||||
| "fetched", | ||||||||
| { | ||||||||
| "total": total or len(all_issues), | ||||||||
| "total": len(all_issues), | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents
Suggested change
|
||||||||
| "fetched": len(all_issues), | ||||||||
|
Comment on lines
+941
to
942
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When matching issues exceed Useful? React with 👍 / 👎. |
||||||||
| "issues": all_issues, | ||||||||
| "operation": "bulk_search", | ||||||||
| "pages_fetched": (start_at // page_size) + (1 if start_at % page_size else 0), | ||||||||
| "pages_fetched": pages_fetched, | ||||||||
| }, | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -243,18 +243,19 @@ def get_issue( | |||||
| def search( | ||||||
| self, | ||||||
| jql: str, | ||||||
| start_at: int = 0, | ||||||
| next_page_token: Optional[str] = None, | ||||||
| max_results: int = 50, | ||||||
| fields: Optional[List[str]] = None, | ||||||
| ) -> Dict[str, Any]: | ||||||
| body: Dict[str, Any] = { | ||||||
| "jql": jql, | ||||||
| "startAt": start_at, | ||||||
| "maxResults": max_results, | ||||||
| # fields defaults to id only on search/jql | ||||||
| "fields": fields if fields is not None else ["*navigable"], | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: An empty Prompt for AI agents
Suggested change
|
||||||
| } | ||||||
| if fields is not None: | ||||||
| body["fields"] = fields | ||||||
| return self._request("POST", "search", json_body=body) or {} | ||||||
| if next_page_token: | ||||||
| body["nextPageToken"] = next_page_token | ||||||
| return self._request("POST", "search/jql", json_body=body) or {} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The new enhanced-search endpoint rejects unbounded JQL with 400, but this API still accepts and forwards unrestricted Prompt for AI agents |
||||||
|
|
||||||
| def get_transitions(self, issue_key: str) -> Dict[str, Any]: | ||||||
| return self._request("GET", f"issue/{issue_key}/transitions") or {"transitions": []} | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -551,7 +551,7 @@ def transition_issue(self, issue_key: str, transition_id: str) -> None: | |||||||||
|
|
||||||||||
| def search_issues(self, jql: str, max_results: int = 50) -> List[Dict[str, Any]]: | ||||||||||
| resp = self._session.post( | ||||||||||
| self._url("search"), | ||||||||||
| self._url("search/jql"), | ||||||||||
| json={"jql": jql, "maxResults": max_results, "fields": ["*all"]}, | ||||||||||
|
Comment on lines
+554
to
555
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The URL moves to Prompt for AI agents
Suggested change
|
||||||||||
| auth=self._auth(), | ||||||||||
| headers=self._headers(), | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -256,14 +256,11 @@ def test_get_issue_passes_fields_and_expand(configured_engine: JiraCloudEngine, | |
| def test_search_jql(configured_engine: JiraCloudEngine, stub: StubHTTPXClient) -> None: | ||
| stub.set( | ||
| "POST", | ||
| "search", | ||
| "search/jql", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Because this request provides Prompt for AI agents |
||
| _StubResponse( | ||
| 200, | ||
| { | ||
| "expand": "schema,names", | ||
| "startAt": 0, | ||
| "maxResults": 50, | ||
| "total": 1, | ||
| "isLast": True, | ||
| "issues": [ | ||
| {"id": "10001", "key": "SEC-42", "fields": {"summary": "CVE-2025-9999"}}, | ||
| ], | ||
|
|
@@ -275,19 +272,17 @@ def test_search_jql(configured_engine: JiraCloudEngine, stub: StubHTTPXClient) - | |
| "/api/v1/jira-cloud/rest/api/3/search", | ||
| json={ | ||
| "jql": "project = SEC AND status = 'In Progress'", | ||
| "startAt": 0, | ||
| "maxResults": 50, | ||
| "fields": ["summary", "status"], | ||
| }, | ||
| ) | ||
| assert resp.status_code == 200 | ||
| body = resp.json() | ||
| assert body["total"] == 1 | ||
| assert body["issues"][0]["key"] == "SEC-42" | ||
| # Body forwarded with camelCase | ||
| sent = stub.calls[0]["json"] | ||
| assert sent["jql"] == "project = SEC AND status = 'In Progress'" | ||
| assert sent["startAt"] == 0 | ||
| assert "startAt" not in sent | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This case never supplies Prompt for AI agents |
||
| assert sent["maxResults"] == 50 | ||
| assert sent["fields"] == ["summary", "status"] | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Removing
startAtwithout rejecting it means Pydantic silently drops it, so old clients that send it get page 1 with HTTP 200 on every call. Reject unknown keys onSearchRequestso stale clients fail loudly, for example withmodel_config = {"extra": "forbid"}.Prompt for AI agents