Skip to content

Commit 3538c53

Browse files
committed
fix(github): bootstrap workflow runs incremental window from tool state
1 parent d991671 commit 3538c53

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

backend/plugins/github/tasks/cicd_run_collector.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import (
2424
"net/url"
2525
"time"
2626

27+
"github.com/apache/incubator-devlake/core/dal"
2728
"github.com/apache/incubator-devlake/core/errors"
2829
"github.com/apache/incubator-devlake/core/log"
2930
"github.com/apache/incubator-devlake/core/plugin"
3031
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
32+
"github.com/apache/incubator-devlake/plugins/github/models"
3133
)
3234

3335
func init() {
@@ -89,6 +91,24 @@ func CollectRuns(taskCtx plugin.SubTaskContext) errors.Error {
8991
// `windowStart` past the previously collected second (inclusive-both-ends), while
9092
// fullsync + TimeAfter keeps the user-specified bound inclusive.
9193
createdAfter := manager.GetSince()
94+
sinceSource := "state_since"
95+
syncPolicy := taskCtx.TaskContext().SyncPolicy()
96+
if createdAfter == nil {
97+
sinceSource = "none"
98+
}
99+
if createdAfter == nil && (syncPolicy == nil || !syncPolicy.FullSync) {
100+
fallbackSince, err := loadLatestRunUpdatedAt(taskCtx, data.Options.ConnectionId, data.Options.GithubId)
101+
if err != nil {
102+
return err
103+
}
104+
if fallbackSince != nil {
105+
createdAfter = fallbackSince
106+
sinceSource = "tool_runs_fallback"
107+
logger.Info("cicd_run_collector: collector state missing; bootstrapping since from existing _tool_github_runs at %s", fallbackSince.UTC().Format(time.RFC3339))
108+
} else {
109+
logger.Debug("cicd_run_collector: collector state missing and no _tool_github_runs timestamp found")
110+
}
111+
}
92112
untilPtr := manager.GetUntil()
93113
*untilPtr = untilPtr.Truncate(time.Second)
94114
until := *untilPtr
@@ -102,12 +122,14 @@ func CollectRuns(taskCtx plugin.SubTaskContext) errors.Error {
102122
} else {
103123
// 2018-01-01 conservatively predates GitHub Actions' late-2019 GA.
104124
windowStart = time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC)
125+
sinceSource = "epoch_fullsync"
105126
}
106127

107-
logger.Info("cicd_run_collector: collecting workflow runs in [%s, %s] (incremental=%v)",
128+
logger.Info("cicd_run_collector: collecting workflow runs in [%s, %s] (incremental=%v, since_source=%s)",
108129
windowStart.Format(githubTimeLayout),
109130
until.Format(githubTimeLayout),
110-
manager.IsIncremental())
131+
manager.IsIncremental(),
132+
sinceSource)
111133

112134
leafWindows, err := newLeafWindowBuilder(taskCtx, data).build(windowStart, until)
113135
if err != nil {
@@ -122,6 +144,28 @@ func CollectRuns(taskCtx plugin.SubTaskContext) errors.Error {
122144
return manager.Execute()
123145
}
124146

147+
func loadLatestRunUpdatedAt(taskCtx plugin.SubTaskContext, connectionId uint64, repoId int) (*time.Time, errors.Error) {
148+
db := taskCtx.GetDal()
149+
latest := &models.GithubRun{}
150+
err := db.First(
151+
latest,
152+
dal.Where("connection_id = ? AND repo_id = ? AND github_updated_at IS NOT NULL", connectionId, repoId),
153+
dal.Orderby("github_updated_at DESC"),
154+
dal.Limit(1),
155+
)
156+
if err != nil {
157+
if db.IsErrorNotFound(err) {
158+
return nil, nil
159+
}
160+
return nil, err
161+
}
162+
if latest.GithubUpdatedAt == nil {
163+
return nil, nil
164+
}
165+
fallback := latest.GithubUpdatedAt.UTC()
166+
return &fallback, nil
167+
}
168+
125169
// buildRunsQuery assembles the filtered-mode query for a single leaf TimeWindow.
126170
// Shared between registerCollectorForLeafWindows and tests.
127171
func buildRunsQuery(reqData *helper.RequestData) (url.Values, errors.Error) {

backend/plugins/github/tasks/cicd_run_collector_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,57 @@ import (
4040
"github.com/stretchr/testify/require"
4141
)
4242

43+
func TestCicdRunLoadLatestRunUpdatedAt_ReturnsLatestTimestamp(t *testing.T) {
44+
connectionId := uint64(1)
45+
repoId := 101
46+
latestTs := time.Date(2025, 4, 1, 10, 11, 12, 0, time.UTC)
47+
48+
mockDal := new(mockdal.Dal)
49+
mockDal.On("First", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
50+
dst := args.Get(0).(*models.GithubRun)
51+
dst.ConnectionId = connectionId
52+
dst.RepoId = repoId
53+
dst.GithubUpdatedAt = &latestTs
54+
}).Return(nil).Once()
55+
56+
ctx := unithelper.DummySubTaskContext(mockDal)
57+
since, err := loadLatestRunUpdatedAt(ctx, connectionId, repoId)
58+
59+
require.Nil(t, err)
60+
require.NotNil(t, since)
61+
assert.True(t, since.Equal(latestTs))
62+
mockDal.AssertExpectations(t)
63+
}
64+
65+
func TestCicdRunLoadLatestRunUpdatedAt_NotFoundReturnsNil(t *testing.T) {
66+
mockDal := new(mockdal.Dal)
67+
notFoundErr := errors.Default.New("record not found")
68+
mockDal.On("First", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(notFoundErr).Once()
69+
mockDal.On("IsErrorNotFound", notFoundErr).Return(true).Once()
70+
71+
ctx := unithelper.DummySubTaskContext(mockDal)
72+
since, err := loadLatestRunUpdatedAt(ctx, 1, 101)
73+
74+
require.Nil(t, err)
75+
assert.Nil(t, since)
76+
mockDal.AssertExpectations(t)
77+
}
78+
79+
func TestCicdRunLoadLatestRunUpdatedAt_PropagatesError(t *testing.T) {
80+
mockDal := new(mockdal.Dal)
81+
dbErr := errors.Default.New("db unavailable")
82+
mockDal.On("First", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dbErr).Once()
83+
mockDal.On("IsErrorNotFound", dbErr).Return(false).Once()
84+
85+
ctx := unithelper.DummySubTaskContext(mockDal)
86+
since, err := loadLatestRunUpdatedAt(ctx, 1, 101)
87+
88+
assert.Nil(t, since)
89+
require.NotNil(t, err)
90+
assert.Contains(t, err.Error(), "db unavailable")
91+
mockDal.AssertExpectations(t)
92+
}
93+
4394
// newTestBuilder constructs a leafWindowBuilder with a stubbed probe for unit testing.
4495
func newTestBuilder(probe probeFunc) *leafWindowBuilder {
4596
mockDal := new(mockdal.Dal)

0 commit comments

Comments
 (0)