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/devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 0133b9ed0 fix(taiga): page through epics, issues, tasks and user 
stories (#9341)
0133b9ed0 is described below

commit 0133b9ed01960e9f9f46f254b75cbe5bd8c48773
Author: Shivam Sharma <[email protected]>
AuthorDate: Sun Nov 8 19:58:04 2026 +0530

    fix(taiga): page through epics, issues, tasks and user stories (#9341)
    
    The four collectors sent only the project filter. Taiga list endpoints
    are paginated (30 items per page by default) and return page 1 when no
    page is given, so only the first 30 items of a project were collected,
    and the parallel page fetches stored the same page ten times. Send page
    and page_size, and stop when Taiga reports no next page, since a page
    past the last one returns 404.
    
    Closes #9340
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    Signed-off-by: Shivam Sharma 
<[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 backend/plugins/taiga/tasks/epic_collector.go      | 15 ++----
 backend/plugins/taiga/tasks/issue_collector.go     | 15 ++----
 backend/plugins/taiga/tasks/shared.go              | 53 ++++++++++++++++++++++
 backend/plugins/taiga/tasks/shared_test.go         | 50 ++++++++++++++++++++
 backend/plugins/taiga/tasks/task_collector.go      | 15 ++----
 .../plugins/taiga/tasks/user_story_collector.go    | 16 ++-----
 6 files changed, 123 insertions(+), 41 deletions(-)

diff --git a/backend/plugins/taiga/tasks/epic_collector.go 
b/backend/plugins/taiga/tasks/epic_collector.go
index 4deec5c09..40a5445f7 100644
--- a/backend/plugins/taiga/tasks/epic_collector.go
+++ b/backend/plugins/taiga/tasks/epic_collector.go
@@ -19,9 +19,7 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "net/http"
-       "net/url"
 
        "github.com/apache/devlake/core/errors"
        "github.com/apache/devlake/core/plugin"
@@ -54,14 +52,11 @@ func CollectEpics(taskCtx plugin.SubTaskContext) 
errors.Error {
                        },
                        Table: RAW_EPIC_TABLE,
                },
-               ApiClient:   data.ApiClient,
-               PageSize:    1000,
-               UrlTemplate: "epics",
-               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
-                       query := url.Values{}
-                       query.Set("project", fmt.Sprintf("%d", 
data.Options.ProjectId))
-                       return query, nil
-               },
+               ApiClient:             data.ApiClient,
+               PageSize:              taigaPageSize,
+               UrlTemplate:           "epics",
+               Query:                 
taigaProjectListQuery(data.Options.ProjectId),
+               GetNextPageCustomData: getNextTaigaPage,
                ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
                        var result []json.RawMessage
                        err := api.UnmarshalResponse(res, &result)
diff --git a/backend/plugins/taiga/tasks/issue_collector.go 
b/backend/plugins/taiga/tasks/issue_collector.go
index 937baeab7..99e4f2d66 100644
--- a/backend/plugins/taiga/tasks/issue_collector.go
+++ b/backend/plugins/taiga/tasks/issue_collector.go
@@ -19,9 +19,7 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "net/http"
-       "net/url"
 
        "github.com/apache/devlake/core/errors"
        "github.com/apache/devlake/core/plugin"
@@ -54,14 +52,11 @@ func CollectIssues(taskCtx plugin.SubTaskContext) 
errors.Error {
                        },
                        Table: RAW_ISSUE_TABLE,
                },
-               ApiClient:   data.ApiClient,
-               PageSize:    1000,
-               UrlTemplate: "issues",
-               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
-                       query := url.Values{}
-                       query.Set("project", fmt.Sprintf("%d", 
data.Options.ProjectId))
-                       return query, nil
-               },
+               ApiClient:             data.ApiClient,
+               PageSize:              taigaPageSize,
+               UrlTemplate:           "issues",
+               Query:                 
taigaProjectListQuery(data.Options.ProjectId),
+               GetNextPageCustomData: getNextTaigaPage,
                ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
                        var result []json.RawMessage
                        err := api.UnmarshalResponse(res, &result)
diff --git a/backend/plugins/taiga/tasks/shared.go 
b/backend/plugins/taiga/tasks/shared.go
new file mode 100644
index 000000000..8e3a96dee
--- /dev/null
+++ b/backend/plugins/taiga/tasks/shared.go
@@ -0,0 +1,53 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "fmt"
+       "net/http"
+       "net/url"
+
+       "github.com/apache/devlake/core/errors"
+       "github.com/apache/devlake/helpers/pluginhelper/api"
+)
+
+// taigaPageSize is the largest page size Taiga accepts (MAX_PAGINATE_BY).
+const taigaPageSize = 1000
+
+// taigaProjectListQuery builds the query for a Taiga list endpoint filtered by
+// project. Taiga paginates list endpoints (30 items per page by default), so
+// the page and page size are always sent.
+func taigaProjectListQuery(projectId uint64) func(reqData *api.RequestData) 
(url.Values, errors.Error) {
+       return func(reqData *api.RequestData) (url.Values, errors.Error) {
+               query := url.Values{}
+               query.Set("project", fmt.Sprintf("%d", projectId))
+               query.Set("page", fmt.Sprintf("%d", reqData.Pager.Page))
+               query.Set("page_size", fmt.Sprintf("%d", reqData.Pager.Size))
+               return query, nil
+       }
+}
+
+// getNextTaigaPage stops the collection when Taiga reports no next page.
+// Taiga answers a page past the last one with 404, so a full last page must
+// not lead to another request.
+func getNextTaigaPage(_ *api.RequestData, prevPageResponse *http.Response) 
(interface{}, errors.Error) {
+       if prevPageResponse.Header.Get("X-Pagination-Next") == "" {
+               return nil, api.ErrFinishCollect
+       }
+       return nil, nil
+}
diff --git a/backend/plugins/taiga/tasks/shared_test.go 
b/backend/plugins/taiga/tasks/shared_test.go
new file mode 100644
index 000000000..2d8dd645f
--- /dev/null
+++ b/backend/plugins/taiga/tasks/shared_test.go
@@ -0,0 +1,50 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "net/http"
+       "testing"
+
+       "github.com/apache/devlake/helpers/pluginhelper/api"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestTaigaProjectListQueryRequestsThePage(t *testing.T) {
+       query := taigaProjectListQuery(42)
+
+       got, err := query(&api.RequestData{Pager: &api.Pager{Page: 3, Size: 
1000}})
+
+       assert.Nil(t, err)
+       assert.Equal(t, "42", got.Get("project"))
+       // Taiga paginates list endpoints (30 items by default) and only returns
+       // the page asked for, so the collector must send page and page_size.
+       assert.Equal(t, "3", got.Get("page"))
+       assert.Equal(t, "1000", got.Get("page_size"))
+}
+
+func TestGetNextTaigaPage(t *testing.T) {
+       more := &http.Response{Header: http.Header{}}
+       more.Header.Set("X-Pagination-Next", 
"https://taiga.example/api/v1/issues?page=2&page_size=1000&project=42";)
+       _, err := getNextTaigaPage(nil, more)
+       assert.Nil(t, err)
+
+       last := &http.Response{Header: http.Header{}}
+       _, err = getNextTaigaPage(nil, last)
+       assert.ErrorIs(t, err, api.ErrFinishCollect)
+}
diff --git a/backend/plugins/taiga/tasks/task_collector.go 
b/backend/plugins/taiga/tasks/task_collector.go
index 4d0f6a6f4..ca5a90557 100644
--- a/backend/plugins/taiga/tasks/task_collector.go
+++ b/backend/plugins/taiga/tasks/task_collector.go
@@ -19,9 +19,7 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "net/http"
-       "net/url"
 
        "github.com/apache/devlake/core/errors"
        "github.com/apache/devlake/core/plugin"
@@ -54,14 +52,11 @@ func CollectTasks(taskCtx plugin.SubTaskContext) 
errors.Error {
                        },
                        Table: RAW_TASK_TABLE,
                },
-               ApiClient:   data.ApiClient,
-               PageSize:    1000,
-               UrlTemplate: "tasks",
-               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
-                       query := url.Values{}
-                       query.Set("project", fmt.Sprintf("%d", 
data.Options.ProjectId))
-                       return query, nil
-               },
+               ApiClient:             data.ApiClient,
+               PageSize:              taigaPageSize,
+               UrlTemplate:           "tasks",
+               Query:                 
taigaProjectListQuery(data.Options.ProjectId),
+               GetNextPageCustomData: getNextTaigaPage,
                ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
                        var result []json.RawMessage
                        err := api.UnmarshalResponse(res, &result)
diff --git a/backend/plugins/taiga/tasks/user_story_collector.go 
b/backend/plugins/taiga/tasks/user_story_collector.go
index 1fa69f86c..206ae72c4 100644
--- a/backend/plugins/taiga/tasks/user_story_collector.go
+++ b/backend/plugins/taiga/tasks/user_story_collector.go
@@ -19,9 +19,7 @@ package tasks
 
 import (
        "encoding/json"
-       "fmt"
        "net/http"
-       "net/url"
 
        "github.com/apache/devlake/core/errors"
        "github.com/apache/devlake/core/plugin"
@@ -54,15 +52,11 @@ func CollectUserStories(taskCtx plugin.SubTaskContext) 
errors.Error {
                        },
                        Table: RAW_USER_STORY_TABLE,
                },
-               ApiClient:   data.ApiClient,
-               PageSize:    1000, // Fetch all in one page - Taiga returns all 
user stories for a project
-               UrlTemplate: "userstories",
-               Query: func(reqData *api.RequestData) (url.Values, 
errors.Error) {
-                       query := url.Values{}
-                       query.Set("project", fmt.Sprintf("%d", 
data.Options.ProjectId))
-                       // Don't specify page - get all results
-                       return query, nil
-               },
+               ApiClient:             data.ApiClient,
+               PageSize:              taigaPageSize,
+               UrlTemplate:           "userstories",
+               Query:                 
taigaProjectListQuery(data.Options.ProjectId),
+               GetNextPageCustomData: getNextTaigaPage,
                ResponseParser: func(res *http.Response) ([]json.RawMessage, 
errors.Error) {
                        var result []json.RawMessage
                        err := api.UnmarshalResponse(res, &result)

Reply via email to