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 e6152f8e8 fix(tempo): add the connection id to worklog domain ids
(#9331)
e6152f8e8 is described below
commit e6152f8e80970efa3b6a852d3d568b84b1f70804
Author: Shivam Sharma <[email protected]>
AuthorDate: Sun Nov 8 19:05:37 2026 +0530
fix(tempo): add the connection id to worklog domain ids (#9331)
convert_worklogs built the issue_worklogs id from the Tempo worklog id
alone (tempo:TempoWorklog:<id>), while the tool layer keys worklogs by
(connection_id, tempo_worklog_id). Two connections with the same worklog
id converted to the same domain row, so the connection converted last
overwrote the other's worklog.
Use tempo:TempoWorklog:<connectionId>:<tempoWorklogId>, the id didgen
generates from the TempoWorklog primary key. The converter is a
full-sync DataConverter, so it deletes a scope's rows with the old ids
before it saves the new ones; no migration is needed.
Closes #9330
Generated-by: Claude Code (Claude Opus 5.5)
Signed-off-by: Shivam Sharma
<[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../tempo/e2e/snapshot_tables/issue_worklogs.csv | 4 +-
backend/plugins/tempo/tasks/worklog_convertor.go | 89 ++++++++++++----------
.../plugins/tempo/tasks/worklog_convertor_test.go | 63 +++++++++++++++
3 files changed, 112 insertions(+), 44 deletions(-)
diff --git a/backend/plugins/tempo/e2e/snapshot_tables/issue_worklogs.csv
b/backend/plugins/tempo/e2e/snapshot_tables/issue_worklogs.csv
index 417b0f122..29632131d 100644
--- a/backend/plugins/tempo/e2e/snapshot_tables/issue_worklogs.csv
+++ b/backend/plugins/tempo/e2e/snapshot_tables/issue_worklogs.csv
@@ -1,3 +1,3 @@
id,author_id,comment,time_spent_minutes,logged_date,started_date,issue_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
-tempo:TempoWorklog:12345,jira:JiraAccount:1:abc123,Working on
task,60,2024-01-15T10:00:00.000+00:00,2024-01-15T09:00:00.000+00:00,jira:JiraIssue:1:10001,,,0,
-tempo:TempoWorklog:12346,jira:JiraAccount:1:abc123,Internal
meeting,120,2024-01-16T16:00:00.000+00:00,2024-01-16T14:00:00.000+00:00,jira:JiraIssue:1:10002,,,0,
+tempo:TempoWorklog:1:12345,jira:JiraAccount:1:abc123,Working on
task,60,2024-01-15T10:00:00.000+00:00,2024-01-15T09:00:00.000+00:00,jira:JiraIssue:1:10001,,,0,
+tempo:TempoWorklog:1:12346,jira:JiraAccount:1:abc123,Internal
meeting,120,2024-01-16T16:00:00.000+00:00,2024-01-16T14:00:00.000+00:00,jira:JiraIssue:1:10002,,,0,
diff --git a/backend/plugins/tempo/tasks/worklog_convertor.go
b/backend/plugins/tempo/tasks/worklog_convertor.go
index bd6c4f255..a128f9a60 100644
--- a/backend/plugins/tempo/tasks/worklog_convertor.go
+++ b/backend/plugins/tempo/tasks/worklog_convertor.go
@@ -73,48 +73,7 @@ func ConvertWorklogs(taskCtx plugin.SubTaskContext)
errors.Error {
InputRowType: reflect.TypeOf(models.TempoWorklog{}),
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
- tempoWorklog := inputRow.(*models.TempoWorklog)
-
- // Must match the domain ids the jira plugin generates
with didgen
- // for JiraIssue / JiraAccount (plugins may not import
each other),
- // so worklogs join issues and accounts. Like before,
this assumes
- // the Tempo connection id equals the Jira connection
id.
- domainIssueId := fmt.Sprintf("jira:JiraIssue:%d:%d",
connectionId, tempoWorklog.IssueId)
- authorId := ""
- if tempoWorklog.AuthorAccountId != "" {
- authorId =
fmt.Sprintf("jira:JiraAccount:%d:%s", connectionId,
tempoWorklog.AuthorAccountId)
- }
-
- domainWorklogId := fmt.Sprintf("tempo:TempoWorklog:%d",
tempoWorklog.TempoWorklogId)
- timeSpentMinutes := tempoWorklog.TimeSpentSeconds / 60
-
- var startedDate *time.Time
- if tempoWorklog.StartDate != "" &&
tempoWorklog.StartTime != "" {
- if t, err := time.Parse("2006-01-02 15:04:05",
tempoWorklog.StartDate+" "+tempoWorklog.StartTime); err == nil {
- startedDate = &t
- }
- }
-
- var loggedDate *time.Time
- if tempoWorklog.CreatedAt != "" {
- if t, err := time.Parse(time.RFC3339,
tempoWorklog.CreatedAt); err == nil {
- loggedDate = &t
- }
- }
-
- worklog := &ticket.IssueWorklog{
- DomainEntity: domainlayer.DomainEntity{
- Id: domainWorklogId,
- },
- IssueId: domainIssueId,
- AuthorId: authorId,
- TimeSpentMinutes: timeSpentMinutes,
- StartedDate: startedDate,
- LoggedDate: loggedDate,
- Comment: tempoWorklog.Description,
- }
-
- return []interface{}{worklog}, nil
+ return []interface{}{convertWorklog(connectionId,
inputRow.(*models.TempoWorklog))}, nil
},
})
@@ -124,3 +83,49 @@ func ConvertWorklogs(taskCtx plugin.SubTaskContext)
errors.Error {
return converter.Execute()
}
+
+// convertWorklog maps a Tempo worklog to the domain layer.
+func convertWorklog(connectionId uint64, tempoWorklog *models.TempoWorklog)
*ticket.IssueWorklog {
+ // Must match the domain ids the jira plugin generates with didgen
+ // for JiraIssue / JiraAccount (plugins may not import each other),
+ // so worklogs join issues and accounts. Like before, this assumes
+ // the Tempo connection id equals the Jira connection id.
+ domainIssueId := fmt.Sprintf("jira:JiraIssue:%d:%d", connectionId,
tempoWorklog.IssueId)
+ authorId := ""
+ if tempoWorklog.AuthorAccountId != "" {
+ authorId = fmt.Sprintf("jira:JiraAccount:%d:%s", connectionId,
tempoWorklog.AuthorAccountId)
+ }
+
+ // Tempo worklog ids are only unique within one Tempo instance, so the
id
+ // carries the connection id like the tool layer primary key (and like
+ // didgen would generate it): otherwise two connections that share a
+ // worklog id overwrite each other's issue_worklogs row.
+ domainWorklogId := fmt.Sprintf("tempo:TempoWorklog:%d:%d",
connectionId, tempoWorklog.TempoWorklogId)
+ timeSpentMinutes := tempoWorklog.TimeSpentSeconds / 60
+
+ var startedDate *time.Time
+ if tempoWorklog.StartDate != "" && tempoWorklog.StartTime != "" {
+ if t, err := time.Parse("2006-01-02 15:04:05",
tempoWorklog.StartDate+" "+tempoWorklog.StartTime); err == nil {
+ startedDate = &t
+ }
+ }
+
+ var loggedDate *time.Time
+ if tempoWorklog.CreatedAt != "" {
+ if t, err := time.Parse(time.RFC3339, tempoWorklog.CreatedAt);
err == nil {
+ loggedDate = &t
+ }
+ }
+
+ return &ticket.IssueWorklog{
+ DomainEntity: domainlayer.DomainEntity{
+ Id: domainWorklogId,
+ },
+ IssueId: domainIssueId,
+ AuthorId: authorId,
+ TimeSpentMinutes: timeSpentMinutes,
+ StartedDate: startedDate,
+ LoggedDate: loggedDate,
+ Comment: tempoWorklog.Description,
+ }
+}
diff --git a/backend/plugins/tempo/tasks/worklog_convertor_test.go
b/backend/plugins/tempo/tasks/worklog_convertor_test.go
new file mode 100644
index 000000000..80079bf99
--- /dev/null
+++ b/backend/plugins/tempo/tasks/worklog_convertor_test.go
@@ -0,0 +1,63 @@
+/*
+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/apache/devlake/plugins/tempo/models"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConvertWorklog(t *testing.T) {
+ tempoWorklog := &models.TempoWorklog{
+ ConnectionId: 1,
+ TempoWorklogId: 12345,
+ IssueId: 10001,
+ AuthorAccountId: "abc123",
+ TimeSpentSeconds: 3600,
+ StartDate: "2024-01-15",
+ StartTime: "09:00:00",
+ Description: "Working on task",
+ CreatedAt: "2024-01-15T10:00:00Z",
+ }
+
+ worklog := convertWorklog(1, tempoWorklog)
+ assert.Equal(t, "tempo:TempoWorklog:1:12345", worklog.Id)
+ assert.Equal(t, "jira:JiraIssue:1:10001", worklog.IssueId)
+ assert.Equal(t, "jira:JiraAccount:1:abc123", worklog.AuthorId)
+ assert.Equal(t, 60, worklog.TimeSpentMinutes)
+ assert.Equal(t, "Working on task", worklog.Comment)
+ if assert.NotNil(t, worklog.StartedDate) {
+ assert.Equal(t, time.Date(2024, 1, 15, 9, 0, 0, 0, time.UTC),
*worklog.StartedDate)
+ }
+ if assert.NotNil(t, worklog.LoggedDate) {
+ assert.Equal(t, time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC),
*worklog.LoggedDate)
+ }
+}
+
+func TestConvertWorklogIdsAreUniquePerConnection(t *testing.T) {
+ // Tempo worklog ids are only unique within one Tempo instance: two
+ // connections can both have worklog 12345, and their domain rows must
+ // not share a primary key.
+ first := convertWorklog(1, &models.TempoWorklog{ConnectionId: 1,
TempoWorklogId: 12345, IssueId: 10001})
+ second := convertWorklog(2, &models.TempoWorklog{ConnectionId: 2,
TempoWorklogId: 12345, IssueId: 20001})
+ assert.NotEqual(t, first.Id, second.Id)
+ assert.Equal(t, "tempo:TempoWorklog:2:12345", second.Id)
+}