Skip to content

Commit d65bb49

Browse files
committed
issue: switch to new search API
@dustin-decker reported that the current search APIs will be deprecated on May 1 2025 [1]. Switch to the new search API. [1] https://developer.atlassian.com/changelog/#CHANGE-2046 v2: suggested fixed from @ns-mglaske Signed-off-by: Prarit Bhargava <[email protected]>
1 parent cfa118a commit d65bb49

File tree

2 files changed

+76
-40
lines changed

2 files changed

+76
-40
lines changed

cloud/issue.go

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -515,17 +515,35 @@ type CommentVisibility struct {
515515
// Pagination is used for the Jira REST APIs to conserve server resources and limit
516516
// response size for resources that return potentially large collection of items.
517517
// A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata
518+
// https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get
518519
// Default Pagination options
519520
type SearchOptions struct {
520-
// StartAt: The starting index of the returned projects. Base index: 0.
521-
StartAt int `url:"startAt,omitempty"`
522-
// MaxResults: The maximum number of projects to return per page. Default: 50.
523-
MaxResults int `url:"maxResults,omitempty"`
524-
// Expand: Expand specific sections in the returned issues
525-
Expand string `url:"expand,omitempty"`
521+
// NextPagetoken The token for a page to fetch that is not the first
522+
// page. The first page has a nextPageToken of null. Use the
523+
// nextPageToken to fetch the next page of issues.
524+
// Note: The nextPageToken field is not included in the response for
525+
// the last page, indicating there is no next page.
526+
NextPageToken string
527+
// MaxResults: The maximum number of projects to return per page.
528+
// Default: 50.
529+
MaxResults integer
530+
// Fields: A list of fields to return for each issue
526531
Fields []string
527-
// ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict.
528-
ValidateQuery string `url:"validateQuery,omitempty"`
532+
// Expand: Use expand to include additional information about issues in
533+
// the response.
534+
Expand string
535+
// Properties: A list of up to 5 issue properties to include in the
536+
// results.
537+
Properties []string
538+
// FieldsByKeys: Reference fields by their key (rather than ID). The
539+
// default is false
540+
FieldsByKeys boolean
541+
// FailFast: Fail this request early if we can't retrieve all field
542+
// data. Default false.
543+
FailFast boolean
544+
// ReconcileIssues: Strong consistency issue ids to be reconciled with
545+
// search results. Accepts max 50 ids
546+
ReconcileIssues []integer
529547
}
530548

531549
// searchResult is only a small wrapper around the Search (with JQL) method
@@ -535,6 +553,7 @@ type searchResult struct {
535553
StartAt int `json:"startAt" structs:"startAt"`
536554
MaxResults int `json:"maxResults" structs:"maxResults"`
537555
Total int `json:"total" structs:"total"`
556+
NextPageToken string `json:"nextPageToken" structs:"nextPageToken"`
538557
}
539558

540559
// GetQueryOptions specifies the optional parameters for the Get Issue methods
@@ -1046,28 +1065,43 @@ func (s *IssueService) AddLink(ctx context.Context, issueLink *IssueLink) (*Resp
10461065
// This double check effort is done for v2 - Remove this two lines if this is completed.
10471066
func (s *IssueService) Search(ctx context.Context, jql string, options *SearchOptions) ([]Issue, *Response, error) {
10481067
u := url.URL{
1049-
Path: "rest/api/2/search",
1068+
Path: "rest/api/3/search/jql",
10501069
}
10511070
uv := url.Values{}
10521071
if jql != "" {
10531072
uv.Add("jql", jql)
10541073
}
10551074

10561075
if options != nil {
1057-
if options.StartAt != 0 {
1058-
uv.Add("startAt", strconv.Itoa(options.StartAt))
1076+
if options.NextPageToken != nil {
1077+
uv.Add("nextPageToken", options.NextPageToken)
10591078
}
10601079
if options.MaxResults != 0 {
10611080
uv.Add("maxResults", strconv.Itoa(options.MaxResults))
10621081
}
1082+
if strings.Join(options.Fields, ",") != "" {
1083+
uv.Add("fields", strings.Join(options.Fields, ","))
1084+
}
10631085
if options.Expand != "" {
10641086
uv.Add("expand", options.Expand)
10651087
}
1066-
if strings.Join(options.Fields, ",") != "" {
1067-
uv.Add("fields", strings.Join(options.Fields, ","))
1088+
if len(options.Properties) > 5 {
1089+
return nil, nil, fmt.Errorf("Search option Properties accepts maximum five entries")
1090+
}
1091+
if strings.Join(options.Properties, ",") != "" {
1092+
uv.Add("properties", strings.Join(options.Properties, ","))
10681093
}
1069-
if options.ValidateQuery != "" {
1070-
uv.Add("validateQuery", options.ValidateQuery)
1094+
if options.FieldsByKeys {
1095+
uv.Add("fieldsByKeys", options.FieldsByKeys)
1096+
}
1097+
if options.FailFast {
1098+
uv.Add("failFast", options.FailFast)
1099+
}
1100+
if len(options.ReconcileIssues) > 50 {
1101+
return nil, nil, fmt.Errorf("Search option ReconcileIssue accepts maximum 50 entries")
1102+
}
1103+
if strings.Join(options.ReconcileIssues, ",") != "" {
1104+
uv.Add("reconcileIssues", strings.Join(options.ReconcileIssues, ","))
10711105
}
10721106
}
10731107

@@ -1095,8 +1129,7 @@ func (s *IssueService) Search(ctx context.Context, jql string, options *SearchOp
10951129
func (s *IssueService) SearchPages(ctx context.Context, jql string, options *SearchOptions, f func(Issue) error) error {
10961130
if options == nil {
10971131
options = &SearchOptions{
1098-
StartAt: 0,
1099-
MaxResults: 50,
1132+
MaxResults: 50,
11001133
}
11011134
}
11021135

@@ -1121,16 +1154,19 @@ func (s *IssueService) SearchPages(ctx context.Context, jql string, options *Sea
11211154
}
11221155
}
11231156

1124-
if resp.StartAt+resp.MaxResults >= resp.Total {
1125-
return nil
1157+
if resp.nextPageToken == "" {
1158+
break
11261159
}
11271160

1128-
options.StartAt += resp.MaxResults
1161+
options.NextPageToken = resp.nextPageToken
1162+
11291163
issues, resp, err = s.Search(ctx, jql, options)
11301164
if err != nil {
11311165
return err
11321166
}
11331167
}
1168+
1169+
return nil
11341170
}
11351171

11361172
// GetCustomFields returns a map of customfield_* keys with string values

cloud/issue_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -620,14 +620,14 @@ func TestIssueService_DeleteLink(t *testing.T) {
620620
func TestIssueService_Search(t *testing.T) {
621621
setup()
622622
defer teardown()
623-
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
623+
testMux.HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
624624
testMethod(t, r, http.MethodGet)
625-
testRequestURL(t, r, "/rest/api/2/search?expand=foo&jql=type+%3D+Bug+and+Status+NOT+IN+%28Resolved%29&maxResults=40&startAt=1")
625+
testRequestURL(t, r, "/rest/api/3/search/jql?expand=foo&jql=type+%3D+Bug+and+Status+NOT+IN+%28Resolved%29&maxResults=40&startAt=1")
626626
w.WriteHeader(http.StatusOK)
627-
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
627+
fmt.Fprint(w, `{"expand": "schema,names","maxResults": 40,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
628628
})
629629

630-
opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"}
630+
opt := &SearchOptions{MaxResults: 40}
631631
_, resp, err := testClient.Issue.Search(context.Background(), "type = Bug and Status NOT IN (Resolved)", opt)
632632

633633
if resp == nil {
@@ -651,14 +651,14 @@ func TestIssueService_Search(t *testing.T) {
651651
func TestIssueService_SearchEmptyJQL(t *testing.T) {
652652
setup()
653653
defer teardown()
654-
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
654+
testMux.HandleFunc("/rest/api/3/search/jql", func(w http.ResponseWriter, r *http.Request) {
655655
testMethod(t, r, http.MethodGet)
656-
testRequestURL(t, r, "/rest/api/2/search?expand=foo&maxResults=40&startAt=1")
656+
testRequestURL(t, r, "/rest/api/3/search/jql?expand=foo&maxResults=40&startAt=1")
657657
w.WriteHeader(http.StatusOK)
658-
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
658+
fmt.Fprint(w, `{"expand": "schema,names","maxResults": 40,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
659659
})
660660

661-
opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"}
661+
opt := &SearchOptions{MaxResults: 40}
662662
_, resp, err := testClient.Issue.Search(context.Background(), "", opt)
663663

664664
if resp == nil {
@@ -682,9 +682,9 @@ func TestIssueService_SearchEmptyJQL(t *testing.T) {
682682
func TestIssueService_Search_WithoutPaging(t *testing.T) {
683683
setup()
684684
defer teardown()
685-
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
685+
testMux.HandleFunc("/rest/api/3/search/jql", func(w http.ResponseWriter, r *http.Request) {
686686
testMethod(t, r, http.MethodGet)
687-
testRequestURL(t, r, "/rest/api/2/search?jql=something")
687+
testRequestURL(t, r, "/rest/api/3/search/jql?jql=something")
688688
w.WriteHeader(http.StatusOK)
689689
fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 50,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
690690
})
@@ -711,26 +711,26 @@ func TestIssueService_Search_WithoutPaging(t *testing.T) {
711711
func TestIssueService_SearchPages(t *testing.T) {
712712
setup()
713713
defer teardown()
714-
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
714+
testMux.HandleFunc("/rest/api/3/search/jql", func(w http.ResponseWriter, r *http.Request) {
715715
testMethod(t, r, http.MethodGet)
716-
if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=1&validateQuery=warn" {
716+
if r.URL.String() == "/rest/api/3/search/jql?expand=foo&jql=something&maxResults=2&startAt=1&validateQuery=warn" {
717717
w.WriteHeader(http.StatusOK)
718718
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
719719
return
720-
} else if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=3&validateQuery=warn" {
720+
} else if r.URL.String() == "/rest/api/3/search/jql?expand=foo&jql=something&maxResults=2&startAt=3&validateQuery=warn" {
721721
w.WriteHeader(http.StatusOK)
722722
fmt.Fprint(w, `{"expand": "schema,names","startAt": 3,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
723723
return
724-
} else if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=5&validateQuery=warn" {
724+
} else if r.URL.String() == "/rest/api/3/search/jql?expand=foo&jql=something&maxResults=2&startAt=5&validateQuery=warn" {
725725
w.WriteHeader(http.StatusOK)
726-
fmt.Fprint(w, `{"expand": "schema,names","startAt": 5,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}}]}`)
726+
fmt.Fprint(w, `{"expand": "schema,names","maxResults": 2,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}}]}`)
727727
return
728728
}
729729

730730
t.Errorf("Unexpected URL: %v", r.URL)
731731
})
732732

733-
opt := &SearchOptions{StartAt: 1, MaxResults: 2, Expand: "foo", ValidateQuery: "warn"}
733+
opt := &SearchOptions{MaxResults: 2}
734734
issues := make([]Issue, 0)
735735
err := testClient.Issue.SearchPages(context.Background(), "something", opt, func(issue Issue) error {
736736
issues = append(issues, issue)
@@ -749,19 +749,19 @@ func TestIssueService_SearchPages(t *testing.T) {
749749
func TestIssueService_SearchPages_EmptyResult(t *testing.T) {
750750
setup()
751751
defer teardown()
752-
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
752+
testMux.HandleFunc("/rest/api/3/search/jql", func(w http.ResponseWriter, r *http.Request) {
753753
testMethod(t, r, http.MethodGet)
754-
if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=50&startAt=1&validateQuery=warn" {
754+
if r.URL.String() == "/rest/api/3/search/jql?expand=foo&jql=something&maxResults=50&startAt=1&validateQuery=warn" {
755755
w.WriteHeader(http.StatusOK)
756756
// This is what Jira outputs when the &maxResult= issue occurs. It used to cause SearchPages to go into an endless loop.
757-
fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 0,"total": 6,"issues": []}`)
757+
fmt.Fprint(w, `{"expand": "schema,names","maxResults": 0,"issues": []}`)
758758
return
759759
}
760760

761761
t.Errorf("Unexpected URL: %v", r.URL)
762762
})
763763

764-
opt := &SearchOptions{StartAt: 1, MaxResults: 50, Expand: "foo", ValidateQuery: "warn"}
764+
opt := &SearchOptions{MaxResults: 50}
765765
issues := make([]Issue, 0)
766766
err := testClient.Issue.SearchPages(context.Background(), "something", opt, func(issue Issue) error {
767767
issues = append(issues, issue)

0 commit comments

Comments
 (0)