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 bcfdfb69d fix(circleci): skip HTTP 500 on workflow/job collectors and 
filter by time range (#8956)
bcfdfb69d is described below

commit bcfdfb69d7a7509bff344c9100880e749c086f4f
Author: Joshua Smith <[email protected]>
AuthorDate: Wed Jun 24 23:56:04 2026 -0600

    fix(circleci): skip HTTP 500 on workflow/job collectors and filter by time 
range (#8956)
    
    * Skip 500 (corrupt CircleCI Server records) alongside 404 in AfterResponse 
hook so a single bad pipeline does not abort the entire subtask
      * Apply SyncPolicy.TimeAfter to workflow and job DB iterators on full 
sync to avoid calling the API for every historical tool-layer row
    
    Signed-off-by: Joshua Smith <[email protected]>
---
 backend/plugins/circleci/tasks/job_collector.go    | 10 ++--
 backend/plugins/circleci/tasks/shared.go           | 13 +++--
 backend/plugins/circleci/tasks/shared_test.go      | 62 ++++++++++++++++++++++
 .../plugins/circleci/tasks/workflow_collector.go   | 10 ++--
 4 files changed, 83 insertions(+), 12 deletions(-)

diff --git a/backend/plugins/circleci/tasks/job_collector.go 
b/backend/plugins/circleci/tasks/job_collector.go
index 00fd23452..ca158c1b9 100644
--- a/backend/plugins/circleci/tasks/job_collector.go
+++ b/backend/plugins/circleci/tasks/job_collector.go
@@ -73,8 +73,10 @@ func CollectJobs(taskCtx plugin.SubTaskContext) errors.Error 
{
                                        dal.Where("connection_id = ? and 
project_slug = ?", data.Options.ConnectionId, data.Options.ProjectSlug),
                                }
 
-                               if isIncremental {
-                                       clauses = append(clauses, 
dal.Where("created_date > ?", createdAfter))
+                               // Incremental: workflows newer than last 
successful collectJobs.
+                               // Full sync: workflows within 
SyncPolicy.TimeAfter window.
+                               if createdAfter != nil {
+                                       clauses = append(clauses, 
dal.Where("created_date >= ?", createdAfter))
                                }
 
                                db := taskCtx.GetDal()
@@ -88,7 +90,7 @@ func CollectJobs(taskCtx plugin.SubTaskContext) errors.Error {
                                UrlTemplate:    "/v2/workflow/{{ .Input.Id 
}}/job",
                                Query:          BuildQueryParamsWithPageToken,
                                ResponseParser: ParseCircleciPageTokenResp,
-                               AfterResponse:  ignoreDeletedBuilds, // Ignore 
the 404 response if a workflow has been deleted
+                               AfterResponse:  ignoreDeletedOrBrokenBuilds,
                        },
                        GetCreated: func(item json.RawMessage) (time.Time, 
errors.Error) {
                                var job struct { // Individual job response 
lacks created_at field, so have to use started_at
@@ -105,7 +107,7 @@ func CollectJobs(taskCtx plugin.SubTaskContext) 
errors.Error {
                                UrlTemplate:    "/v2/workflow/{{ .Input.Id 
}}/job", // The individual job endpoint has different fields so need to 
recollect all jobs for a workflow
                                Query:          BuildQueryParamsWithPageToken,
                                ResponseParser: ParseCircleciPageTokenResp,
-                               AfterResponse:  ignoreDeletedBuilds,
+                               AfterResponse:  ignoreDeletedOrBrokenBuilds,
                        },
                        BuildInputIterator: func() (api.Iterator, errors.Error) 
{
                                db := taskCtx.GetDal()
diff --git a/backend/plugins/circleci/tasks/shared.go 
b/backend/plugins/circleci/tasks/shared.go
index 6e235ecd6..d6bbee812 100644
--- a/backend/plugins/circleci/tasks/shared.go
+++ b/backend/plugins/circleci/tasks/shared.go
@@ -123,10 +123,15 @@ func ParseCircleciPageTokenResp(res *http.Response) 
([]json.RawMessage, errors.E
        return data.Items, err
 }
 
-func ignoreDeletedBuilds(res *http.Response) errors.Error {
-       // CircleCI API will return a 404 response for a workflow/job that has 
been deleted
-       // due to their data retention policy. We should ignore these errors.
-       if res.StatusCode == http.StatusNotFound {
+// ignoreDeletedOrBrokenBuilds skips per-item API failures that should not
+// abort an entire collector subtask. 404 means the resource was deleted
+// (retention); 500 means the CircleCI Server record is corrupt/stuck
+// (e.g. pipeline exists but its /workflow endpoint errors).
+func ignoreDeletedOrBrokenBuilds(res *http.Response) errors.Error {
+       switch res.StatusCode {
+       case http.StatusNotFound:
+               return api.ErrIgnoreAndContinue
+       case http.StatusInternalServerError:
                return api.ErrIgnoreAndContinue
        }
        return nil
diff --git a/backend/plugins/circleci/tasks/shared_test.go 
b/backend/plugins/circleci/tasks/shared_test.go
new file mode 100644
index 000000000..a6c55e9c9
--- /dev/null
+++ b/backend/plugins/circleci/tasks/shared_test.go
@@ -0,0 +1,62 @@
+/*
+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 (
+       "bytes"
+       "io"
+       "net/http"
+       "testing"
+
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/stretchr/testify/assert"
+)
+
+func makeResponse(statusCode int) *http.Response {
+       return &http.Response{
+               StatusCode: statusCode,
+               Body:       io.NopCloser(bytes.NewBufferString("")),
+               Request:    &http.Request{},
+       }
+}
+
+func TestIgnoreDeletedOrBrokenBuilds(t *testing.T) {
+       tests := []struct {
+               name       string
+               statusCode int
+               want       error
+       }{
+               {"404 returns ErrIgnoreAndContinue", http.StatusNotFound, 
api.ErrIgnoreAndContinue},
+               {"500 returns ErrIgnoreAndContinue", 
http.StatusInternalServerError, api.ErrIgnoreAndContinue},
+               {"200 returns nil", http.StatusOK, nil},
+               {"403 returns nil", http.StatusForbidden, nil},
+               {"502 returns nil", http.StatusBadGateway, nil},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       res := makeResponse(tt.statusCode)
+                       got := ignoreDeletedOrBrokenBuilds(res)
+                       if tt.want == nil {
+                               assert.Nil(t, got)
+                       } else {
+                               assert.Equal(t, tt.want, got)
+                       }
+               })
+       }
+}
+
diff --git a/backend/plugins/circleci/tasks/workflow_collector.go 
b/backend/plugins/circleci/tasks/workflow_collector.go
index f0f3aebe5..25c5c727d 100644
--- a/backend/plugins/circleci/tasks/workflow_collector.go
+++ b/backend/plugins/circleci/tasks/workflow_collector.go
@@ -60,8 +60,10 @@ func CollectWorkflows(taskCtx plugin.SubTaskContext) 
errors.Error {
                                        dal.Where("connection_id = ? AND 
project_slug = ?", data.Options.ConnectionId, data.Options.ProjectSlug),
                                }
 
-                               if isIncremental {
-                                       clauses = append(clauses, 
dal.Where("created_date > ?", createdAfter))
+                               // Incremental: pipelines newer than last 
successful collectWorkflows.
+                               // Full sync: pipelines within 
SyncPolicy.TimeAfter window.
+                               if createdAfter != nil {
+                                       clauses = append(clauses, 
dal.Where("created_date >= ?", createdAfter))
                                }
 
                                db := taskCtx.GetDal()
@@ -75,7 +77,7 @@ func CollectWorkflows(taskCtx plugin.SubTaskContext) 
errors.Error {
                                UrlTemplate:    "/v2/pipeline/{{ .Input.Id 
}}/workflow",
                                Query:          BuildQueryParamsWithPageToken,
                                ResponseParser: ParseCircleciPageTokenResp,
-                               AfterResponse:  ignoreDeletedBuilds, // Ignore 
the 404 response if a workflow has been deleted
+                               AfterResponse:  ignoreDeletedOrBrokenBuilds,
                        },
                        GetCreated: extractCreatedAt,
                },
@@ -88,7 +90,7 @@ func CollectWorkflows(taskCtx plugin.SubTaskContext) 
errors.Error {
                                        err := api.UnmarshalResponse(res, &data)
                                        return []json.RawMessage{data}, err
                                },
-                               AfterResponse: ignoreDeletedBuilds,
+                               AfterResponse: ignoreDeletedOrBrokenBuilds,
                        },
                        BuildInputIterator: func() (api.Iterator, errors.Error) 
{
                                clauses := []dal.Clause{

Reply via email to