Compare commits

...

2 Commits

Author SHA1 Message Date
etalon 39ec958f04 Merge pull request 'fix(jira): migrate to /rest/api/3/search/jql (legacy /search removed)' (#1) from fix/jira-search-jql-migration into main
Reviewed-on: #1
2026-07-15 15:37:39 +00:00
Your Name 097d46886d fix(jira): migrate to /rest/api/3/search/jql (legacy /search removed)
Atlassian removed GET /rest/api/3/search (HTTP 410, CHANGE-2046), which
broke ticket sync for Jira Cloud customers. Migrate the connector's
search() to POST /rest/api/3/search/jql with cursor pagination
(nextPageToken/isLast) instead of the legacy startAt/total scheme.

Verified end-to-end against a live Jira Cloud instance: TestConnection,
search, FetchUpdated and ListAssignedKeys all succeed and parse issues
correctly. Spec §5.3 updated to document the new endpoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:35:23 +02:00
2 changed files with 29 additions and 11 deletions
+28 -10
View File
@@ -2,9 +2,9 @@ package sync
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
) )
@@ -47,23 +47,41 @@ type jiraIssue struct {
} `json:"fields"` } `json:"fields"`
} }
// search runs a JQL query via POST /rest/api/3/search/jql — the replacement
// for the removed GET /rest/api/3/search endpoint (Atlassian CHANGE-2046).
// It uses cursor pagination (nextPageToken/isLast); the legacy startAt/total
// fields no longer exist.
func (j *jiraConnector) search(ctx context.Context, jql, fields string) ([]jiraIssue, error) { func (j *jiraConnector) search(ctx context.Context, jql, fields string) ([]jiraIssue, error) {
fieldList := strings.Split(fields, ",")
u := j.baseURL + "/rest/api/3/search/jql"
var all []jiraIssue var all []jiraIssue
for startAt := 0; ; { nextPageToken := ""
var page struct { for {
Issues []jiraIssue `json:"issues"` reqBody := map[string]any{
Total int `json:"total"` "jql": jql,
"fields": fieldList,
"maxResults": 100,
} }
u := fmt.Sprintf("%s/rest/api/3/search?jql=%s&fields=%s&maxResults=100&startAt=%d", if nextPageToken != "" {
j.baseURL, url.QueryEscape(jql), url.QueryEscape(fields), startAt) reqBody["nextPageToken"] = nextPageToken
if err := getJSON(ctx, j.Authorize, u, &page); err != nil { }
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("jira search: encode request: %w", err)
}
var page struct {
Issues []jiraIssue `json:"issues"`
NextPageToken string `json:"nextPageToken"`
IsLast bool `json:"isLast"`
}
if err := doJSON(ctx, j.Authorize, http.MethodPost, u, body, &page); err != nil {
return nil, fmt.Errorf("jira search: %w", err) return nil, fmt.Errorf("jira search: %w", err)
} }
all = append(all, page.Issues...) all = append(all, page.Issues...)
startAt += len(page.Issues) if page.IsLast || page.NextPageToken == "" || len(page.Issues) == 0 {
if len(page.Issues) == 0 || startAt >= page.Total {
return all, nil return all, nil
} }
nextPageToken = page.NextPageToken
} }
} }
+1 -1
View File
@@ -441,7 +441,7 @@ account** in that system, updated since `lastSyncAt 5 min` (overlap window):
| System | Query mechanism | | System | Query mechanism |
|---|---| |---|---|
| Jira Cloud | `GET /rest/api/3/search?jql=assignee="<email>" AND project=<key> AND updated >= "<ts>"` (Basic auth: email+API token) | | Jira Cloud | `POST /rest/api/3/search/jql` with body `{jql:'assignee="<email>" AND project=<key> AND updated >= "<ts>"', fields, maxResults}`, cursor-paginated via `nextPageToken`/`isLast` (Basic auth: email+API token). The legacy `GET /rest/api/3/search` was removed by Atlassian — CHANGE-2046. |
| Azure DevOps | `POST …/_apis/wit/wiql?api-version=7.1` with WIQL `[System.AssignedTo] = '<email>' AND [System.TeamProject] = '<project>' AND [System.ChangedDate] >= '<ts>'`, then batch `GET workitems` (PAT auth) | | Azure DevOps | `POST …/_apis/wit/wiql?api-version=7.1` with WIQL `[System.AssignedTo] = '<email>' AND [System.TeamProject] = '<project>' AND [System.ChangedDate] >= '<ts>'`, then batch `GET workitems` (PAT auth) |
| YouTrack | `GET /api/issues?query=assignee: <login> project: <key> updated: <ts> .. *` (Bearer permanent token) | | YouTrack | `GET /api/issues?query=assignee: <login> project: <key> updated: <ts> .. *` (Bearer permanent token) |