Skip to content

Commit c3a54ae

Browse files
committed
fix(jira): refactor epic collector to streamline API endpoint selection and enhance error handling
1 parent ecb4feb commit c3a54ae

1 file changed

Lines changed: 94 additions & 27 deletions

File tree

backend/plugins/jira/tasks/epic_collector.go

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,45 +82,47 @@ func CollectEpics(taskCtx plugin.SubTaskContext) errors.Error {
8282
jql = buildJQL(*apiCollector.GetSince(), loc)
8383
}
8484

85-
// Choose API endpoint based on JIRA version
86-
var urlTemplate string
87-
var shouldAddFieldsParam bool
88-
89-
// Use api/2 for JIRA Server <= v8, api/3 for newer versions
90-
if data.JiraServerInfo.DeploymentType == models.DeploymentServer &&
91-
len(data.JiraServerInfo.VersionNumbers) == 3 &&
92-
data.JiraServerInfo.VersionNumbers[0] <= 8 {
93-
// JIRA Server <= v8
94-
urlTemplate = "api/2/search"
95-
shouldAddFieldsParam = false
96-
logger.Info("Using api/2/search for JIRA Server version <= 8")
85+
// Choose API endpoint based on JIRA deployment type
86+
if data.JiraServerInfo.DeploymentType == models.DeploymentServer {
87+
logger.Info("Using api/2/search for JIRA Server")
88+
err = setupApiV2Collector(apiCollector, data, epicIterator, jql)
9789
} else {
98-
// JIRA Cloud and Server > v8 (api/2 deprecated in Cloud)
99-
urlTemplate = "api/3/search/jql"
100-
shouldAddFieldsParam = true
101-
logger.Info("Using api/3/search/jql for JIRA Cloud or JIRA Server version > 8")
90+
logger.Info("Using api/3/search/jql for JIRA Cloud")
91+
err = setupApiV3Collector(apiCollector, data, epicIterator, jql)
10292
}
93+
if err != nil {
94+
return err
95+
}
96+
return apiCollector.Execute()
97+
}
10398

104-
err = apiCollector.InitCollector(api.ApiCollectorArgs{
99+
// JIRA Server API v2 collector
100+
func setupApiV2Collector(apiCollector *api.StatefulApiCollector, data *JiraTaskData, epicIterator api.Iterator, jql string) errors.Error {
101+
return apiCollector.InitCollector(api.ApiCollectorArgs{
105102
ApiClient: data.ApiClient,
106103
PageSize: 100,
107104
Incremental: false,
108-
UrlTemplate: urlTemplate,
105+
UrlTemplate: "api/2/search",
109106
Query: func(reqData *api.RequestData) (url.Values, errors.Error) {
110107
query := url.Values{}
111108
epicKeys := []string{}
112-
for _, e := range reqData.Input.([]interface{}) {
113-
epicKeys = append(epicKeys, *e.(*string))
109+
110+
input, ok := reqData.Input.([]interface{})
111+
if !ok {
112+
return nil, errors.Default.New("invalid input type, expected []interface{}")
114113
}
114+
115+
for _, e := range input {
116+
if epicKey, ok := e.(*string); ok && epicKey != nil {
117+
epicKeys = append(epicKeys, *epicKey)
118+
}
119+
}
120+
115121
localJQL := fmt.Sprintf("issue in (%s) and %s", strings.Join(epicKeys, ","), jql)
116122
query.Set("jql", localJQL)
117123
query.Set("startAt", fmt.Sprintf("%v", reqData.Pager.Skip))
118124
query.Set("maxResults", fmt.Sprintf("%v", reqData.Pager.Size))
119125
query.Set("expand", "changelog")
120-
// api/3 requires fields parameter, api/2 does not
121-
if shouldAddFieldsParam {
122-
query.Set("fields", "*all")
123-
}
124126
return query, nil
125127
},
126128
Input: epicIterator,
@@ -140,13 +142,78 @@ func CollectEpics(taskCtx plugin.SubTaskContext) errors.Error {
140142
}
141143
return data.Issues, nil
142144
},
143-
// Jira Server returns 400 if the epic is not found
144145
AfterResponse: ignoreHTTPStatus400,
145146
})
147+
}
148+
149+
// JIRA Cloud API v3 collector
150+
func setupApiV3Collector(apiCollector *api.StatefulApiCollector, data *JiraTaskData, epicIterator api.Iterator, jql string) errors.Error {
151+
return apiCollector.InitCollector(api.ApiCollectorArgs{
152+
ApiClient: data.ApiClient,
153+
PageSize: 100,
154+
Incremental: false,
155+
UrlTemplate: "api/3/search/jql",
156+
GetNextPageCustomData: getNextPageCustomDataForV3,
157+
Query: func(reqData *api.RequestData) (url.Values, errors.Error) {
158+
query := url.Values{}
159+
epicKeys := []string{}
160+
for _, e := range reqData.Input.([]interface{}) {
161+
epicKeys = append(epicKeys, *e.(*string))
162+
}
163+
localJQL := fmt.Sprintf("issue in (%s) and %s", strings.Join(epicKeys, ","), jql)
164+
query.Set("jql", localJQL)
165+
query.Set("maxResults", fmt.Sprintf("%v", reqData.Pager.Size))
166+
query.Set("expand", "changelog")
167+
query.Set("fields", "*all")
168+
169+
if reqData.CustomData != nil {
170+
query.Set("nextPageToken", reqData.CustomData.(string))
171+
}
172+
173+
return query, nil
174+
},
175+
Input: epicIterator,
176+
ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
177+
var data struct {
178+
Issues []json.RawMessage `json:"issues"`
179+
}
180+
blob, err := io.ReadAll(res.Body)
181+
if err != nil {
182+
return nil, errors.Convert(err)
183+
}
184+
err = json.Unmarshal(blob, &data)
185+
if err != nil {
186+
return nil, errors.Convert(err)
187+
}
188+
return data.Issues, nil
189+
},
190+
AfterResponse: ignoreHTTPStatus400,
191+
})
192+
}
193+
194+
// Get next page token for API v3
195+
func getNextPageCustomDataForV3(_ *api.RequestData, prevPageResponse *http.Response) (interface{}, errors.Error) {
196+
var response struct {
197+
NextPageToken string `json:"nextPageToken"`
198+
}
199+
200+
blob, err := io.ReadAll(prevPageResponse.Body)
146201
if err != nil {
147-
return err
202+
return nil, errors.Convert(err)
148203
}
149-
return apiCollector.Execute()
204+
205+
prevPageResponse.Body = io.NopCloser(strings.NewReader(string(blob)))
206+
207+
err = json.Unmarshal(blob, &response)
208+
if err != nil {
209+
return nil, errors.Convert(err)
210+
}
211+
212+
if response.NextPageToken == "" {
213+
return nil, api.ErrFinishCollect
214+
}
215+
216+
return response.NextPageToken, nil
150217
}
151218

152219
func GetEpicKeysIterator(db dal.Dal, data *JiraTaskData, batchSize int) (api.Iterator, errors.Error) {

0 commit comments

Comments
 (0)