Skip to content

Adapt to the atlassian.net API change effective 2025-05-01 - #273

Open
torrresagus wants to merge 1 commit into
DevOpsMadDog:features/intermediate-stagefrom
tiltmeter-dev:fix/jira-search-jql
Open

torrresagus wants to merge 1 commit into
DevOpsMadDog:features/intermediate-stagefrom
tiltmeter-dev:fix/jira-search-jql

Conversation

@torrresagus

Copy link
Copy Markdown

What changed in the API

Notice jira-cloud-change-2046-search-jql about atlassian.net's API describes this change, published 2024-10-31 and effective 2025-05-01. It is not signed by atlassian.net: it was read from a file the person running Tiltmeter trusts.

The notice's summary:

Jira Cloud removed GET and POST /rest/api/3/search, which now answer 410 Gone, in favor of
/rest/api/3/search/jql. The new endpoint pages with nextPageToken instead of startAt, returns no
total, and returns only issue ids unless the request names the fields it needs. Source:
https://developer.atlassian.com/changelog/#CHANGE-2046

GET https://your-domain.atlassian.net/rest/api/3/search

Severity Change Path Before After
breaking endpoint_removed $ — (gone), moved to GET https://your-domain.atlassian.net/rest/api/3/search/jql
breaking field_removed $.startAt — (absent)
breaking field_removed $.total — (absent)
breaking field_removed $.maxResults — (absent)

POST https://your-domain.atlassian.net/rest/api/3/search

Severity Change Path Before After
breaking endpoint_removed $ — (gone), moved to POST https://your-domain.atlassian.net/rest/api/3/search/jql
breaking field_removed $.startAt — (absent)
breaking field_removed $.total — (absent)
breaking field_removed $.maxResults — (absent)

The notice's notes:

GET /rest/api/3/search: Query parameters: jql, nextPageToken instead of startAt, maxResults, fields,
expand. fields defaults to id only: pass the fields the code reads, such as fields=*navigable.
$.startAt: Pages are fetched with nextPageToken: pass the nextPageToken of the previous page; isLast
is true on the last page.
$.total: To count matching issues, POST /rest/api/3/search/approximate-count with body {"jql":
"..."}; it returns {"count": n}.
POST /rest/api/3/search: Body: jql, nextPageToken instead of startAt, maxResults, fields (a list),
expand (a string). fields defaults to id only: send the fields the code reads, such as
["*navigable"].

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.py
  • suite-core/connectors/bidirectional_sync.py
  • suite-core/connectors/sdlc_connectors.py
  • suite-core/core/connectors.py
  • suite-core/core/jira_cloud_engine.py
  • suite-core/core/jira_sync.py
  • tests/test_jira_cloud_router.py

Left for a human

  • The total count is gone. connectors.py now reports the number of issues fetched as 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.
  • bidirectional_sync.py pulls only the first page (no pagination before or after), so results beyond maxResults=100 are still not fetched.
  • The router's public SearchRequest now takes nextPageToken instead of startAt, so API clients that send startAt need updating.
  • The internal route /rest/api/3/search in jira_cloud_router.py and the endpoint catalog in jira_cloud_engine.py still use the old path. That path is this app's own proxy route, so I left it. Rename it if you want it to match upstream.
  • The shell scripts under scripts/ had no matches for the old search path or startAt, so I didn't change them.

This pull request was generated automatically by Tiltmeter from the unsigned notice jira-cloud-change-2046-search-jql about atlassian.net's API. An AI agent (Claude Code with claude-sonnet-5-5) wrote these edits; review them like any other change.

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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T05:16:17.317606Z a722239 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

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",

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.

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

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.

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")

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.

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>
Suggested change
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")

Comment on lines +554 to 555
self._url("search/jql"),
json={"jql": jql, "maxResults": max_results, "fields": ["*all"]},

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.

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>
Suggested change
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),

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.

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>
Suggested change
"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 {}

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.

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"],

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.

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>
Suggested change
"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", [])),

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.

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>
Suggested change
"total": len(data.get("issues", [])),
"fetched": len(data.get("issues", [])),

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +941 to 942
"total": len(all_issues),
"fetched": len(all_issues),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant