diff --git a/internal/sync/jira.go b/internal/sync/jira.go index 5f63243..7d09d2d 100644 --- a/internal/sync/jira.go +++ b/internal/sync/jira.go @@ -2,9 +2,9 @@ package sync import ( "context" + "encoding/json" "fmt" "net/http" - "net/url" "strings" "time" ) @@ -47,23 +47,41 @@ type jiraIssue struct { } `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) { + fieldList := strings.Split(fields, ",") + u := j.baseURL + "/rest/api/3/search/jql" var all []jiraIssue - for startAt := 0; ; { - var page struct { - Issues []jiraIssue `json:"issues"` - Total int `json:"total"` + nextPageToken := "" + for { + reqBody := map[string]any{ + "jql": jql, + "fields": fieldList, + "maxResults": 100, } - u := fmt.Sprintf("%s/rest/api/3/search?jql=%s&fields=%s&maxResults=100&startAt=%d", - j.baseURL, url.QueryEscape(jql), url.QueryEscape(fields), startAt) - if err := getJSON(ctx, j.Authorize, u, &page); err != nil { + if nextPageToken != "" { + reqBody["nextPageToken"] = nextPageToken + } + 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) } all = append(all, page.Issues...) - startAt += len(page.Issues) - if len(page.Issues) == 0 || startAt >= page.Total { + if page.IsLast || page.NextPageToken == "" || len(page.Issues) == 0 { return all, nil } + nextPageToken = page.NextPageToken } } diff --git a/specification.md b/specification.md index 860ce79..87fda43 100644 --- a/specification.md +++ b/specification.md @@ -441,7 +441,7 @@ account** in that system, updated since `lastSyncAt − 5 min` (overlap window): | System | Query mechanism | |---|---| -| Jira Cloud | `GET /rest/api/3/search?jql=assignee="" AND project= AND updated >= ""` (Basic auth: email+API token) | +| Jira Cloud | `POST /rest/api/3/search/jql` with body `{jql:'assignee="" AND project= AND updated >= ""', 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] = '' AND [System.TeamProject] = '' AND [System.ChangedDate] >= ''`, then batch `GET workitems` (PAT auth) | | YouTrack | `GET /api/issues?query=assignee: project: updated: .. *` (Bearer permanent token) |