This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new c5a4fbf79 fix(jira): update epic collector to use new API endpoint and
include (#8547)
c5a4fbf79 is described below
commit c5a4fbf79878961681717108a950e18357f06a30
Author: Bamboo <[email protected]>
AuthorDate: Wed Aug 27 16:28:21 2025 +0800
fix(jira): update epic collector to use new API endpoint and include (#8547)
* fix(jira): update epic collector to use new API endpoint and include all
fields
* fix(jira): enhance epic collector to dynamically select API endpoint
based on JIRA version
* fix(jira): update epic collector to use correct API endpoint for JIRA
Cloud and Server versions
* fix(jira): refactor epic collector to streamline API endpoint selection
and enhance error handling
---
backend/plugins/jira/tasks/epic_collector.go | 102 +++++++++++++++++++++++++--
1 file changed, 96 insertions(+), 6 deletions(-)
diff --git a/backend/plugins/jira/tasks/epic_collector.go
b/backend/plugins/jira/tasks/epic_collector.go
index dcb163309..2e777f92c 100644
--- a/backend/plugins/jira/tasks/epic_collector.go
+++ b/backend/plugins/jira/tasks/epic_collector.go
@@ -82,7 +82,23 @@ func CollectEpics(taskCtx plugin.SubTaskContext)
errors.Error {
jql = buildJQL(*apiCollector.GetSince(), loc)
}
- err = apiCollector.InitCollector(api.ApiCollectorArgs{
+ // Choose API endpoint based on JIRA deployment type
+ if data.JiraServerInfo.DeploymentType == models.DeploymentServer {
+ logger.Info("Using api/2/search for JIRA Server")
+ err = setupApiV2Collector(apiCollector, data, epicIterator, jql)
+ } else {
+ logger.Info("Using api/3/search/jql for JIRA Cloud")
+ err = setupApiV3Collector(apiCollector, data, epicIterator, jql)
+ }
+ if err != nil {
+ return err
+ }
+ return apiCollector.Execute()
+}
+
+// JIRA Server API v2 collector
+func setupApiV2Collector(apiCollector *api.StatefulApiCollector, data
*JiraTaskData, epicIterator api.Iterator, jql string) errors.Error {
+ return apiCollector.InitCollector(api.ApiCollectorArgs{
ApiClient: data.ApiClient,
PageSize: 100,
Incremental: false,
@@ -90,9 +106,18 @@ func CollectEpics(taskCtx plugin.SubTaskContext)
errors.Error {
Query: func(reqData *api.RequestData) (url.Values,
errors.Error) {
query := url.Values{}
epicKeys := []string{}
- for _, e := range reqData.Input.([]interface{}) {
- epicKeys = append(epicKeys, *e.(*string))
+
+ input, ok := reqData.Input.([]interface{})
+ if !ok {
+ return nil, errors.Default.New("invalid input
type, expected []interface{}")
}
+
+ for _, e := range input {
+ if epicKey, ok := e.(*string); ok && epicKey !=
nil {
+ epicKeys = append(epicKeys, *epicKey)
+ }
+ }
+
localJQL := fmt.Sprintf("issue in (%s) and %s",
strings.Join(epicKeys, ","), jql)
query.Set("jql", localJQL)
query.Set("startAt", fmt.Sprintf("%v",
reqData.Pager.Skip))
@@ -117,13 +142,78 @@ func CollectEpics(taskCtx plugin.SubTaskContext)
errors.Error {
}
return data.Issues, nil
},
- // Jira Server returns 400 if the epic is not found
AfterResponse: ignoreHTTPStatus400,
})
+}
+
+// JIRA Cloud API v3 collector
+func setupApiV3Collector(apiCollector *api.StatefulApiCollector, data
*JiraTaskData, epicIterator api.Iterator, jql string) errors.Error {
+ return apiCollector.InitCollector(api.ApiCollectorArgs{
+ ApiClient: data.ApiClient,
+ PageSize: 100,
+ Incremental: false,
+ UrlTemplate: "api/3/search/jql",
+ GetNextPageCustomData: getNextPageCustomDataForV3,
+ Query: func(reqData *api.RequestData) (url.Values,
errors.Error) {
+ query := url.Values{}
+ epicKeys := []string{}
+ for _, e := range reqData.Input.([]interface{}) {
+ epicKeys = append(epicKeys, *e.(*string))
+ }
+ localJQL := fmt.Sprintf("issue in (%s) and %s",
strings.Join(epicKeys, ","), jql)
+ query.Set("jql", localJQL)
+ query.Set("maxResults", fmt.Sprintf("%v",
reqData.Pager.Size))
+ query.Set("expand", "changelog")
+ query.Set("fields", "*all")
+
+ if reqData.CustomData != nil {
+ query.Set("nextPageToken",
reqData.CustomData.(string))
+ }
+
+ return query, nil
+ },
+ Input: epicIterator,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
+ var data struct {
+ Issues []json.RawMessage `json:"issues"`
+ }
+ blob, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, errors.Convert(err)
+ }
+ err = json.Unmarshal(blob, &data)
+ if err != nil {
+ return nil, errors.Convert(err)
+ }
+ return data.Issues, nil
+ },
+ AfterResponse: ignoreHTTPStatus400,
+ })
+}
+
+// Get next page token for API v3
+func getNextPageCustomDataForV3(_ *api.RequestData, prevPageResponse
*http.Response) (interface{}, errors.Error) {
+ var response struct {
+ NextPageToken string `json:"nextPageToken"`
+ }
+
+ blob, err := io.ReadAll(prevPageResponse.Body)
if err != nil {
- return err
+ return nil, errors.Convert(err)
}
- return apiCollector.Execute()
+
+ prevPageResponse.Body = io.NopCloser(strings.NewReader(string(blob)))
+
+ err = json.Unmarshal(blob, &response)
+ if err != nil {
+ return nil, errors.Convert(err)
+ }
+
+ if response.NextPageToken == "" {
+ return nil, api.ErrFinishCollect
+ }
+
+ return response.NextPageToken, nil
}
func GetEpicKeysIterator(db dal.Dal, data *JiraTaskData, batchSize int)
(api.Iterator, errors.Error) {