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) {

Reply via email to