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 956c9066c fix(github): leave lead time unset when an issue closes 
before creation (#9314)
956c9066c is described below

commit 956c9066cb025a8f9e128373c510b7d3b8d5297d
Author: Mohammad El Naqa <[email protected]>
AuthorDate: Fri Nov 6 10:22:15 2026 +0300

    fix(github): leave lead time unset when an issue closes before creation 
(#9314)
    
    GitHub can report closedAt before createdAt, for example for an issue that
    was closed and then transferred from another repository. Both GitHub issue
    extractors cast the negative duration to uint, which wraps to
    18446744073709551572. The Convert Issues subtask then fails on every run 
with
    MySQL Error 1264 (out of range for issues.lead_time_minutes).
    
    Leave lead time unset in that case, as the Linear plugin already does.
---
 backend/plugins/github/tasks/issue_extractor.go    | 17 +++++++--
 .../plugins/github/tasks/issue_extractor_test.go   | 42 ++++++++++++++++++++++
 .../github_graphql/tasks/issue_extractor.go        |  3 +-
 3 files changed, 58 insertions(+), 4 deletions(-)

diff --git a/backend/plugins/github/tasks/issue_extractor.go 
b/backend/plugins/github/tasks/issue_extractor.go
index 2f7771216..2f80c9286 100644
--- a/backend/plugins/github/tasks/issue_extractor.go
+++ b/backend/plugins/github/tasks/issue_extractor.go
@@ -21,6 +21,7 @@ import (
        "encoding/json"
        "regexp"
        "strings"
+       "time"
 
        "github.com/apache/devlake/core/dal"
        "github.com/apache/devlake/core/errors"
@@ -203,8 +204,7 @@ func convertGithubIssue(issue *IssuesResponse, connectionId 
uint64, repositoryId
                githubIssue.AuthorName = issue.User.Login
        }
        if issue.ClosedAt != nil {
-               temp := 
uint(issue.ClosedAt.ToTime().Sub(issue.GithubCreatedAt.ToTime()).Minutes())
-               githubIssue.LeadTimeMinutes = &temp
+               githubIssue.LeadTimeMinutes = 
IssueLeadTimeMinutes(issue.GithubCreatedAt.ToTime(), issue.ClosedAt.ToTime())
        }
        if issue.Milestone != nil {
                githubIssue.MilestoneId = issue.Milestone.Id
@@ -212,6 +212,19 @@ func convertGithubIssue(issue *IssuesResponse, 
connectionId uint64, repositoryId
        return githubIssue, nil
 }
 
+// IssueLeadTimeMinutes returns the minutes from creation to close.
+// GitHub can report a close time before the creation time, for example for an
+// issue that was closed and then transferred from another repository. A 
negative
+// duration cast to uint wraps to a huge value that the domain issues table 
rejects,
+// which fails the whole Convert Issues subtask, so leave lead time unset 
instead.
+func IssueLeadTimeMinutes(createdAt, closedAt time.Time) *uint {
+       if closedAt.Before(createdAt) {
+               return nil
+       }
+       minutes := uint(closedAt.Sub(createdAt).Minutes())
+       return &minutes
+}
+
 func convertGithubLabels(issueRegexes *IssueRegexes, issue *IssuesResponse, 
githubIssue *models.GithubIssue) ([]interface{}, errors.Error) {
        var results []interface{}
        var joinedLabels []string
diff --git a/backend/plugins/github/tasks/issue_extractor_test.go 
b/backend/plugins/github/tasks/issue_extractor_test.go
new file mode 100644
index 000000000..b8294240a
--- /dev/null
+++ b/backend/plugins/github/tasks/issue_extractor_test.go
@@ -0,0 +1,42 @@
+/*
+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 (
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestIssueLeadTimeMinutes(t *testing.T) {
+       createdAt := time.Date(2026, 11, 1, 9, 49, 53, 0, time.UTC)
+
+       leadTime := IssueLeadTimeMinutes(createdAt, 
createdAt.Add(90*time.Minute))
+       if assert.NotNil(t, leadTime) {
+               assert.Equal(t, uint(90), *leadTime)
+       }
+
+       leadTime = IssueLeadTimeMinutes(createdAt, createdAt)
+       if assert.NotNil(t, leadTime) {
+               assert.Equal(t, uint(0), *leadTime)
+       }
+
+       // Closed before created, as GitHub reports for some transferred issues.
+       assert.Nil(t, IssueLeadTimeMinutes(createdAt, 
createdAt.Add(-44*time.Minute)))
+}
diff --git a/backend/plugins/github_graphql/tasks/issue_extractor.go 
b/backend/plugins/github_graphql/tasks/issue_extractor.go
index 345b6fcfb..3f15bd0d9 100644
--- a/backend/plugins/github_graphql/tasks/issue_extractor.go
+++ b/backend/plugins/github_graphql/tasks/issue_extractor.go
@@ -152,8 +152,7 @@ func convertGithubIssue(milestoneMap map[int]int, issue 
*GraphqlQueryIssue, conn
                githubIssue.AuthorName = author.Login
        }
        if issue.ClosedAt != nil {
-               temp := uint(issue.ClosedAt.Sub(issue.CreatedAt).Minutes())
-               githubIssue.LeadTimeMinutes = &temp
+               githubIssue.LeadTimeMinutes = 
githubTasks.IssueLeadTimeMinutes(issue.CreatedAt, *issue.ClosedAt)
        }
        if issue.Milestone != nil {
                if milestoneId, ok := milestoneMap[issue.Milestone.Number]; ok {

Reply via email to