Repository navigation
Adapt to the atlassian.net API change effective 2025-05-01 - #273
torrresagus wants to merge 1 commit into
Conversation
Moved all Jira Cloud JQL searches from the removed /rest/api/3/search to /rest/api/3/search/jql. Pagination now uses nextPageToken/isLast instead of startAt/total, and requests ask for fields=*navigable because the new endpoint returns only ids by default. I updated the jira-cloud router test and its stub to match; I didn't run any tests. Notice jira-cloud-change-2046-search-jql about atlassian.net's API describes the change. It was read from a file and is not signed by the provider.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
8 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/test_jira_cloud_router.py">
<violation number="1" location="tests/test_jira_cloud_router.py:259">
P2: Because this request provides `fields`, the test never verifies the new `*navigable` default. Add a no-fields search case and assert that the upstream body contains `fields == ["*navigable"]`.</violation>
<violation number="2" location="tests/test_jira_cloud_router.py:285">
P2: This case never supplies `nextPageToken`, so it would still pass if page-2 tokens were dropped before the upstream request. Add a token-bearing request and assert `sent["nextPageToken"]` to cover the new pagination contract.</violation>
</file>
<file name="suite-api/apps/api/jira_cloud_router.py">
<violation number="1" location="suite-api/apps/api/jira_cloud_router.py:76">
P2: Removing `startAt` without 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 on `SearchRequest` so stale clients fail loudly, for example with `model_config = {"extra": "forbid"}`.</violation>
</file>
<file name="suite-core/core/jira_cloud_engine.py">
<violation number="1" location="suite-core/core/jira_cloud_engine.py:254">
P3: An empty `fields` list is sent as-is, so `search/jql` returns only `id` instead of the `*navigable` default. Use a truthiness check so empty and None get the same default, matching `connectors.py`.</violation>
<violation number="2" location="suite-core/core/jira_cloud_engine.py:258">
P2: The new enhanced-search endpoint rejects unbounded JQL with 400, but this API still accepts and forwards unrestricted `jql` strings. Validate or require a bounded query before calling the new endpoint, and apply the same contract to the Jira connector paths.</violation>
</file>
<file name="suite-core/core/jira_sync.py">
<violation number="1" location="suite-core/core/jira_sync.py:554">
P2: The URL moves to `search/jql`, but the request body still asks for `*all`. The PR description and every other caller of this endpoint use `*navigable`, so this path is inconsistent with the migration and may be rejected or behave differently on the new endpoint. Change the fields to `*navigable`.</violation>
</file>
<file name="suite-core/core/connectors.py">
<violation number="1" location="suite-core/core/connectors.py:801">
P3: `search_issues` reports the page size under `total`, which reads as the match count. Rename the key to `fetched` to match `bulk_search`.</violation>
<violation number="2" location="suite-core/core/connectors.py:941">
P2: `bulk_search` now reports `total` equal to `fetched`, so a result capped at `max_results` looks complete. Return the final `next_page_token` as well so callers can tell more results exist and resume.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| stub.set( | ||
| "POST", | ||
| "search", | ||
| "search/jql", |
There was a problem hiding this comment.
P2: Because this request provides fields, the test never verifies the new *navigable default. Add a no-fields search case and assert that the upstream body contains fields == ["*navigable"].
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_jira_cloud_router.py, line 259:
<comment>Because this request provides `fields`, the test never verifies the new `*navigable` default. Add a no-fields search case and assert that the upstream body contains `fields == ["*navigable"]`.</comment>
<file context>
@@ -256,14 +256,11 @@ def test_get_issue_passes_fields_and_expand(configured_engine: JiraCloudEngine,
stub.set(
"POST",
- "search",
+ "search/jql",
_StubResponse(
200,
</file context>
| sent = stub.calls[0]["json"] | ||
| assert sent["jql"] == "project = SEC AND status = 'In Progress'" | ||
| assert sent["startAt"] == 0 | ||
| assert "startAt" not in sent |
There was a problem hiding this comment.
P2: This case never supplies nextPageToken, so it would still pass if page-2 tokens were dropped before the upstream request. Add a token-bearing request and assert sent["nextPageToken"] to cover the new pagination contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_jira_cloud_router.py, line 285:
<comment>This case never supplies `nextPageToken`, so it would still pass if page-2 tokens were dropped before the upstream request. Add a token-bearing request and assert `sent["nextPageToken"]` to cover the new pagination contract.</comment>
<file context>
@@ -275,19 +272,17 @@ def test_search_jql(configured_engine: JiraCloudEngine, stub: StubHTTPXClient) -
sent = stub.calls[0]["json"]
assert sent["jql"] == "project = SEC AND status = 'In Progress'"
- assert sent["startAt"] == 0
+ assert "startAt" not in sent
assert sent["maxResults"] == 50
assert sent["fields"] == ["summary", "status"]
</file context>
| 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.
P2: Removing startAt without 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 on SearchRequest so stale clients fail loudly, for example with model_config = {"extra": "forbid"}.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-api/apps/api/jira_cloud_router.py, line 76:
<comment>Removing `startAt` without 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 on `SearchRequest` so stale clients fail loudly, for example with `model_config = {"extra": "forbid"}`.</comment>
<file context>
@@ -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")
maxResults: int = Field(50, ge=1, le=1000, description="Max results to return")
fields: Optional[List[str]] = Field(None, description="Field selector — None = all")
</file context>
| nextPageToken: Optional[str] = Field(None, description="Token of the page to fetch") | |
| model_config = {"extra": "forbid"} | |
| nextPageToken: Optional[str] = Field(None, description="Token of the page to fetch") |
| self._url("search/jql"), | ||
| json={"jql": jql, "maxResults": max_results, "fields": ["*all"]}, |
There was a problem hiding this comment.
P2: The URL moves to search/jql, but the request body still asks for *all. The PR description and every other caller of this endpoint use *navigable, so this path is inconsistent with the migration and may be rejected or behave differently on the new endpoint. Change the fields to *navigable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-core/core/jira_sync.py, line 554:
<comment>The URL moves to `search/jql`, but the request body still asks for `*all`. The PR description and every other caller of this endpoint use `*navigable`, so this path is inconsistent with the migration and may be rejected or behave differently on the new endpoint. Change the fields to `*navigable`.</comment>
<file context>
@@ -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"]},
auth=self._auth(),
</file context>
| self._url("search/jql"), | |
| json={"jql": jql, "maxResults": max_results, "fields": ["*all"]}, | |
| self._url("search/jql"), | |
| json={"jql": jql, "maxResults": max_results, "fields": ["*navigable"]}, |
| "fetched", | ||
| { | ||
| "total": total or len(all_issues), | ||
| "total": len(all_issues), |
There was a problem hiding this comment.
P2: bulk_search now reports total equal to fetched, so a result capped at max_results looks complete. Return the final next_page_token as well so callers can tell more results exist and resume.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-core/core/connectors.py, line 941:
<comment>`bulk_search` now reports `total` equal to `fetched`, so a result capped at `max_results` looks complete. Return the final `next_page_token` as well so callers can tell more results exist and resume.</comment>
<file context>
@@ -925,22 +928,21 @@ def bulk_search(
"fetched",
{
- "total": total or len(all_issues),
+ "total": len(all_issues),
"fetched": len(all_issues),
"issues": all_issues,
</file context>
| "total": len(all_issues), | |
| "total": len(all_issues), | |
| "next_page_token": next_page_token, |
| 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 {} |
There was a problem hiding this comment.
P2: The new enhanced-search endpoint rejects unbounded JQL with 400, but this API still accepts and forwards unrestricted jql strings. Validate or require a bounded query before calling the new endpoint, and apply the same contract to the Jira connector paths.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-core/core/jira_cloud_engine.py, line 258:
<comment>The new enhanced-search endpoint rejects unbounded JQL with 400, but this API still accepts and forwards unrestricted `jql` strings. Validate or require a bounded query before calling the new endpoint, and apply the same contract to the Jira connector paths.</comment>
<file context>
@@ -243,18 +243,19 @@ def get_issue(
- 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 {}
def get_transitions(self, issue_key: str) -> Dict[str, Any]:
</file context>
| "startAt": start_at, | ||
| "maxResults": max_results, | ||
| # fields defaults to id only on search/jql | ||
| "fields": fields if fields is not None else ["*navigable"], |
There was a problem hiding this comment.
P3: An empty fields list is sent as-is, so search/jql returns only id instead of the *navigable default. Use a truthiness check so empty and None get the same default, matching connectors.py.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-core/core/jira_cloud_engine.py, line 254:
<comment>An empty `fields` list is sent as-is, so `search/jql` returns only `id` instead of the `*navigable` default. Use a truthiness check so empty and None get the same default, matching `connectors.py`.</comment>
<file context>
@@ -243,18 +243,19 @@ def get_issue(
- "startAt": start_at,
"maxResults": max_results,
+ # fields defaults to id only on search/jql
+ "fields": fields if fields is not None else ["*navigable"],
}
- if fields is not None:
</file context>
| "fields": fields if fields is not None else ["*navigable"], | |
| "fields": fields or ["*navigable"], |
| { | ||
| "endpoint": endpoint, | ||
| "total": data.get("total", 0), | ||
| "total": len(data.get("issues", [])), |
There was a problem hiding this comment.
P3: search_issues reports the page size under total, which reads as the match count. Rename the key to fetched to match bulk_search.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At suite-core/core/connectors.py, line 801:
<comment>`search_issues` reports the page size under `total`, which reads as the match count. Rename the key to `fetched` to match `bulk_search`.</comment>
<file context>
@@ -797,8 +798,9 @@ def search_issues(
{
"endpoint": endpoint,
- "total": data.get("total", 0),
+ "total": len(data.get("issues", [])),
"issues": data.get("issues", []),
+ "next_page_token": data.get("nextPageToken"),
</file context>
| "total": len(data.get("issues", [])), | |
| "fetched": len(data.get("issues", [])), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7222390a1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| 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.
Reject legacy offsets instead of silently resetting pagination
When an existing /api/v1/jira-cloud/rest/api/3/search client sends startAt > 0, BaseModel's default extra='ignore' behavior drops that now-unknown property, so req.nextPageToken remains None and the handler fetches page one. Because the v1 route is unchanged, this turns an upgrade into silent duplicate or missing search results rather than a validation failure; retain a compatibility adapter or explicitly forbid the legacy field.
Useful? React with 👍 / 👎.
| "total": len(all_issues), | ||
| "fetched": len(all_issues), |
There was a problem hiding this comment.
Surface truncation when bulk search reaches max_results
When matching issues exceed max_results, the loop exits on its length guard with a nonempty next_page_token, but this result makes total identical to fetched and omits that token. The bulk_search action can no longer determine that the list is truncated or resume it (the old query-wide total exposed this), so return a continuation/has_more signal or obtain the query count.
Useful? React with 👍 / 👎.
What changed in the API
Notice
jira-cloud-change-2046-search-jqlaboutatlassian.net's API describes this change, published 2024-10-31 and effective 2025-05-01. It is not signed byatlassian.net: it was read from a file the person running Tiltmeter trusts.The notice's summary:
GET https://your-domain.atlassian.net/rest/api/3/search$GET https://your-domain.atlassian.net/rest/api/3/search/jql$.startAt$.total$.maxResultsPOST https://your-domain.atlassian.net/rest/api/3/search$POST https://your-domain.atlassian.net/rest/api/3/search/jql$.startAt$.total$.maxResultsThe notice's notes:
What this pull request changes
Moved all Jira Cloud JQL searches from the removed /rest/api/3/search to /rest/api/3/search/jql. Pagination now uses nextPageToken/isLast instead of startAt/total, and requests ask for fields=*navigable because the new endpoint returns only ids by default. I updated the jira-cloud router test and its stub to match; I didn't run any tests.
suite-api/apps/api/jira_cloud_router.pysuite-core/connectors/bidirectional_sync.pysuite-core/connectors/sdlc_connectors.pysuite-core/core/connectors.pysuite-core/core/jira_cloud_engine.pysuite-core/core/jira_sync.pytests/test_jira_cloud_router.pyLeft for a human
total(search_issues: this page only; bulk_search: all pages). A true count needs POST /rest/api/3/search/approximate-count, which I didn't add.This pull request was generated automatically by Tiltmeter from the unsigned notice
jira-cloud-change-2046-search-jqlabout atlassian.net's API. An AI agent (Claude Code with claude-sonnet-5-5) wrote these edits; review them like any other change.