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/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 1c8dfe08 [feat-1681][jira]: Collect the epic issue in an issue's epic
link (#2401)
1c8dfe08 is described below
commit 1c8dfe085d6d1440b97f45beb4934c231b2c45fd
Author: Keon Amini <[email protected]>
AuthorDate: Thu Jul 28 08:49:03 2022 -0500
[feat-1681][jira]: Collect the epic issue in an issue's epic link (#2401)
* feat(jira): collect the epic issue in an issue's epic link
* test: added e2e test for external epic
* fix: addressing changes per PR feedback
* fix: add new subtasks
* fix: added batching via iterators to the epic collector + improvements to
iterator framework
* fix: troubleshooting
* fix: cyclic imports eliminated
---
api/pipelines/pipelines.go | 4 +-
helpers/e2ehelper/data_flow_tester.go | 16 +-
plugins/helper/iterator.go | 66 ++++---
plugins/helper/list.go | 11 +-
plugins/helper/queue.go | 35 +++-
plugins/jenkins/models/job.go | 2 +-
plugins/jira/e2e/epic_test.go | 130 ++++++++++++++
.../jira/e2e/raw_tables/_raw_jira_api_boards.csv | 1 +
.../e2e/raw_tables/_raw_jira_api_issue_types.csv | 44 +++++
.../jira/e2e/raw_tables/_raw_jira_api_issues.csv | 3 +
.../e2e/raw_tables/_raw_jira_external_epics.csv | 4 +
.../_tool_jira_board_issues_for_external_epics.csv | 4 +
.../_tool_jira_boards_for_external_epics.csv | 2 +
.../_tool_jira_issues_for_external_epics.csv | 5 +
plugins/jira/impl/impl.go | 11 +-
plugins/jira/tasks/epic_collector.go | 135 ++++++++++++++
plugins/jira/tasks/epic_extractor.go | 63 +++++++
plugins/jira/tasks/issue_extractor.go | 199 +++++++++++----------
18 files changed, 604 insertions(+), 131 deletions(-)
diff --git a/api/pipelines/pipelines.go b/api/pipelines/pipelines.go
index 990101ba..931fa853 100644
--- a/api/pipelines/pipelines.go
+++ b/api/pipelines/pipelines.go
@@ -51,10 +51,10 @@ POST /pipelines
// @Description }
// @Tags pipelines
// @Accept application/json
-// @Param pipeline body string true "json"
+// @Param pipeline body models.NewPipeline true "json"
// @Success 200 {object} models.Pipeline
// @Failure 400 {string} errcode.Error "Bad Request"
-// @Failure 500 {string} errcode.Error "Internel Error"
+// @Failure 500 {string} errcode.Error "Internal Error"
// @Router /pipelines [post]
func Post(c *gin.Context) {
newPipeline := &models.NewPipeline{}
diff --git a/helpers/e2ehelper/data_flow_tester.go
b/helpers/e2ehelper/data_flow_tester.go
index 378ccba0..60a30f81 100644
--- a/helpers/e2ehelper/data_flow_tester.go
+++ b/helpers/e2ehelper/data_flow_tester.go
@@ -180,13 +180,18 @@ func (t *DataFlowTester) FlushTabler(dst schema.Tabler) {
// Subtask executes specified subtasks
func (t *DataFlowTester) Subtask(subtaskMeta core.SubTaskMeta, taskData
interface{}) {
- subtaskCtx := helper.NewStandaloneSubTaskContext(context.Background(),
t.Cfg, t.Log, t.Db, t.Name, taskData)
+ subtaskCtx := t.SubtaskContext(taskData)
err := subtaskMeta.EntryPoint(subtaskCtx)
if err != nil {
panic(err)
}
}
+// SubtaskContext creates a subtask context
+func (t *DataFlowTester) SubtaskContext(taskData interface{})
core.SubTaskContext {
+ return helper.NewStandaloneSubTaskContext(context.Background(), t.Cfg,
t.Log, t.Db, t.Name, taskData)
+}
+
func filterColumn(column dal.ColumnMeta, opts TableOptions) bool {
for _, ignore := range opts.IgnoreFields {
if column.Name() == ignore {
@@ -241,6 +246,8 @@ func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler,
opts TableOptions) {
forScanValues[i] = new(sql.NullTime)
} else if columnType.ScanType().Name() == `bool` {
forScanValues[i] = new(bool)
+ } else if columnType.ScanType().Name() == `RawBytes` {
+ forScanValues[i] = new(sql.NullString)
} else {
forScanValues[i] = new(string)
}
@@ -267,6 +274,13 @@ func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler,
opts TableOptions) {
} else {
values[i] = `0`
}
+ case *sql.NullString:
+ value := *forScanValues[i].(*sql.NullString)
+ if value.Valid {
+ values[i] = value.String
+ } else {
+ values[i] = ``
+ }
case *string:
values[i] =
fmt.Sprint(*forScanValues[i].(*string))
}
diff --git a/plugins/helper/iterator.go b/plugins/helper/iterator.go
index 7c8953b2..873b9adf 100644
--- a/plugins/helper/iterator.go
+++ b/plugins/helper/iterator.go
@@ -34,27 +34,41 @@ type Iterator interface {
// DalCursorIterator FIXME ...
type DalCursorIterator struct {
- db dal.Dal
- cursor *sql.Rows
- elemType reflect.Type
+ db dal.Dal
+ cursor *sql.Rows
+ elemType reflect.Type
+ batchSize int
}
// NewDalCursorIterator FIXME ...
func NewDalCursorIterator(db dal.Dal, cursor *sql.Rows, elemType reflect.Type)
(*DalCursorIterator, error) {
+ return NewBatchedDalCursorIterator(db, cursor, elemType, -1)
+}
+
+// NewBatchedDalCursorIterator FIXME ...
+func NewBatchedDalCursorIterator(db dal.Dal, cursor *sql.Rows, elemType
reflect.Type, batchSize int) (*DalCursorIterator, error) {
return &DalCursorIterator{
- db: db,
- cursor: cursor,
- elemType: elemType,
+ db: db,
+ cursor: cursor,
+ elemType: elemType,
+ batchSize: batchSize,
}, nil
}
-// HasNext FIXME ...
+// HasNext increments the row curser. If we're at the end, it'll return false.
func (c *DalCursorIterator) HasNext() bool {
return c.cursor.Next()
}
-// Fetch FIXME ...
+// Fetch if batching is disabled, it'll read a single row, otherwise it'll
read as many rows up to the batch size, and the
+// runtime return type will be []interface{}. Note, HasNext needs to have been
called before invoking this.
func (c *DalCursorIterator) Fetch() (interface{}, error) {
+ if c.batchSize > 0 {
+ return c.batchedFetch()
+ }
+ if c.batchSize != -1 {
+ panic("invalid batch size")
+ }
elem := reflect.New(c.elemType).Interface()
err := c.db.Fetch(c.cursor, elem)
if err != nil {
@@ -63,7 +77,23 @@ func (c *DalCursorIterator) Fetch() (interface{}, error) {
return elem, nil
}
-// Close interator
+func (c *DalCursorIterator) batchedFetch() (interface{}, error) {
+ var elems []interface{}
+ for i := 1; ; i++ {
+ elem := reflect.New(c.elemType).Interface()
+ err := c.cursor.Scan(elem)
+ if err != nil {
+ return nil, err
+ }
+ elems = append(elems, elem)
+ if i == c.batchSize || !c.HasNext() {
+ break
+ }
+ }
+ return elems, nil
+}
+
+// Close iterator
func (c *DalCursorIterator) Close() error {
return c.cursor.Close()
}
@@ -114,22 +144,6 @@ func NewDateIterator(days int) (*DateIterator, error) {
}, nil
}
-type QueueIteratorNode struct {
- data interface{}
- next *QueueIteratorNode
-}
-
-func (q *QueueIteratorNode) Next() interface{} {
- if q.next == nil {
- return nil
- }
- return q.next
-}
-
-func (q *QueueIteratorNode) SetNext(next interface{}) {
- q.next, _ = next.(*QueueIteratorNode)
-}
-
type QueueIterator struct {
queue *Queue
}
@@ -143,7 +157,7 @@ func (q *QueueIterator) Fetch() (interface{}, error) {
}
func (q *QueueIterator) Push(data QueueNode) {
- q.queue.PushWitouLock(data)
+ q.queue.PushWithoutLock(data)
}
func (q *QueueIterator) Close() error {
diff --git a/plugins/helper/list.go b/plugins/helper/list.go
index b3a1381b..a130bfa2 100644
--- a/plugins/helper/list.go
+++ b/plugins/helper/list.go
@@ -17,8 +17,13 @@ limitations under the License.
package helper
+// ListBaseNode 'abstract' base struct for Nodes that are chained in a linked
list manner
type ListBaseNode struct {
- next interface{}
+ next *ListBaseNode
+}
+
+func (l *ListBaseNode) Data() interface{} {
+ panic("list node Data() needs to be implemented by subclasses")
}
func (l *ListBaseNode) Next() interface{} {
@@ -29,10 +34,10 @@ func (l *ListBaseNode) Next() interface{} {
}
func (l *ListBaseNode) SetNext(next interface{}) {
- l.next = next
+ l.next = next.(*ListBaseNode)
}
-// NewListBaseNode create and init a new node
+// NewListBaseNode create and init a new node (only to be called by subclasses)
func NewListBaseNode() *ListBaseNode {
return &ListBaseNode{
next: nil,
diff --git a/plugins/helper/queue.go b/plugins/helper/queue.go
index b1e74783..edf58d62 100644
--- a/plugins/helper/queue.go
+++ b/plugins/helper/queue.go
@@ -25,6 +25,7 @@ import (
type QueueNode interface {
Next() interface{}
SetNext(next interface{})
+ Data() interface{}
}
type Queue struct {
@@ -38,7 +39,7 @@ type Queue struct {
func (q *Queue) Push(node QueueNode) {
q.mux.Lock()
defer q.mux.Unlock()
- q.PushWitouLock(node)
+ q.PushWithoutLock(node)
}
// Pull get a node from queue
@@ -57,8 +58,8 @@ func (q *Queue) Pull(add *int64) QueueNode {
return node
}
-// PushWitouLock is no lock mode of Push
-func (q *Queue) PushWitouLock(node QueueNode) {
+// PushWithoutLock is no lock mode of Push
+func (q *Queue) PushWithoutLock(node QueueNode) {
if q.tail == nil {
q.head = node
q.tail = node
@@ -70,7 +71,7 @@ func (q *Queue) PushWitouLock(node QueueNode) {
}
}
-// PullWitouLock is no lock mode of Pull
+// PullWithOutLock is no lock mode of Pull
func (q *Queue) PullWithOutLock() QueueNode {
var node QueueNode = nil
@@ -125,3 +126,29 @@ func NewQueue() *Queue {
mux: sync.Mutex{},
}
}
+
+type QueueIteratorNode struct {
+ next *QueueIteratorNode
+ data interface{}
+}
+
+func (q *QueueIteratorNode) Next() interface{} {
+ if q.next == nil {
+ return nil
+ }
+ return q.next
+}
+
+func (q *QueueIteratorNode) SetNext(next interface{}) {
+ q.next, _ = next.(*QueueIteratorNode)
+}
+
+func (q *QueueIteratorNode) Data() interface{} {
+ return q.data
+}
+
+func NewQueueIteratorNode(data interface{}) *QueueIteratorNode {
+ return &QueueIteratorNode{
+ data: data,
+ }
+}
diff --git a/plugins/jenkins/models/job.go b/plugins/jenkins/models/job.go
index 13b0fd9d..0a406999 100644
--- a/plugins/jenkins/models/job.go
+++ b/plugins/jenkins/models/job.go
@@ -44,8 +44,8 @@ func (JenkinsJob) TableName() string {
}
type FolderInput struct {
- Path string
*helper.ListBaseNode
+ Path string
}
func NewFolderInput(path string) *FolderInput {
diff --git a/plugins/jira/e2e/epic_test.go b/plugins/jira/e2e/epic_test.go
new file mode 100644
index 00000000..e0f3ef3f
--- /dev/null
+++ b/plugins/jira/e2e/epic_test.go
@@ -0,0 +1,130 @@
+/*
+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 e2e
+
+import (
+ "github.com/apache/incubator-devlake/helpers/e2ehelper"
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/plugins/jira/impl"
+ "github.com/apache/incubator-devlake/plugins/jira/models"
+ "github.com/apache/incubator-devlake/plugins/jira/tasks"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestEpicDataflow(t *testing.T) {
+ var plugin impl.Jira
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "jira", plugin)
+ taskData := &tasks.JiraTaskData{
+ Options: &tasks.JiraOptions{
+ ConnectionId: 1,
+ BoardId: 93,
+ TransformationRules:
tasks.TransformationRules{StoryPointField: "customfield_10024"},
+ },
+ }
+
+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_jira_api_issue_types.csv",
"_raw_jira_api_issue_types")
+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_jira_api_issues.csv",
"_raw_jira_api_issues")
+
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_jira_external_epics.csv",
"_raw_jira_api_epics")
+
+ dataflowTester.FlushTabler(&models.JiraIssue{})
+ dataflowTester.FlushTabler(&models.JiraBoardIssue{})
+ dataflowTester.FlushTabler(&models.JiraSprintIssue{})
+ dataflowTester.FlushTabler(&models.JiraIssueChangelogs{})
+ dataflowTester.FlushTabler(&models.JiraIssueChangelogItems{})
+ dataflowTester.FlushTabler(&models.JiraWorklog{})
+ dataflowTester.FlushTabler(&models.JiraAccount{})
+ dataflowTester.FlushTabler(&models.JiraIssueType{})
+
+ ctx := dataflowTester.SubtaskContext(taskData)
+
+ // run pre-req subtasks
+ require.NoError(t, tasks.ExtractIssueTypesMeta.EntryPoint(ctx))
+ require.NoError(t, tasks.ExtractIssuesMeta.EntryPoint(ctx))
+ dataflowTester.VerifyTableWithOptions(
+ models.JiraIssue{}, e2ehelper.TableOptions{
+ CSVRelPath:
"./snapshot_tables/_tool_jira_issues_for_external_epics.csv",
+ TargetFields: nil,
+ IgnoreFields: nil,
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ },
+ )
+ dataflowTester.VerifyTableWithOptions(
+ models.JiraBoardIssue{}, e2ehelper.TableOptions{
+ CSVRelPath:
"./snapshot_tables/_tool_jira_board_issues_for_external_epics.csv",
+ TargetFields: []string{"connection_id", "board_id",
"issue_id"},
+ IgnoreFields: nil,
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ },
+ )
+ t.Run("batch_single", func(t *testing.T) {
+ // run the part of the collector that queries tools data
+ iter, err := tasks.GetEpicKeysIterator(ctx.GetDal(), taskData,
1)
+ require.NoError(t, err)
+ require.True(t, iter.HasNext())
+ e1, err := iter.Fetch()
+ require.NoError(t, err)
+ require.True(t, iter.HasNext())
+ e2, err := iter.Fetch()
+ require.NoError(t, err)
+ require.False(t, iter.HasNext())
+ require.Equal(t, 1, len(e1.([]interface{})))
+ require.Equal(t, 1, len(e2.([]interface{})))
+ epicKeys := []string{
+ *(e1.([]interface{})[0].(*string)),
+ *(e2.([]interface{})[0].(*string)),
+ }
+ require.Contains(t, epicKeys, "K5-1")
+ require.Contains(t, epicKeys, "K5-4")
+ })
+ t.Run("batch_multiple", func(t *testing.T) {
+ // run the part of the collector that queries tools data
+ iter, err := tasks.GetEpicKeysIterator(ctx.GetDal(), taskData,
2)
+ require.NoError(t, err)
+ require.True(t, iter.HasNext())
+ e, err := iter.Fetch()
+ require.NoError(t, err)
+ require.False(t, iter.HasNext())
+ require.Equal(t, 2, len(e.([]interface{})))
+ epicKeys := []string{
+ *(e.([]interface{})[0].(*string)),
+ *(e.([]interface{})[1].(*string)),
+ }
+ require.Contains(t, epicKeys, "K5-1")
+ require.Contains(t, epicKeys, "K5-4")
+ })
+
+ require.NoError(t, tasks.ExtractEpicsMeta.EntryPoint(ctx))
+
+ dataflowTester.VerifyTableWithOptions(
+ models.JiraBoardIssue{}, e2ehelper.TableOptions{
+ CSVRelPath:
"./snapshot_tables/_tool_jira_board_issues_for_external_epics.csv",
+ TargetFields: nil,
+ IgnoreFields: nil,
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ },
+ )
+ dataflowTester.VerifyTableWithOptions(
+ models.JiraIssue{}, e2ehelper.TableOptions{
+ CSVRelPath:
"./snapshot_tables/_tool_jira_issues_for_external_epics.csv",
+ TargetFields: nil,
+ IgnoreFields: nil,
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ },
+ )
+}
diff --git a/plugins/jira/e2e/raw_tables/_raw_jira_api_boards.csv
b/plugins/jira/e2e/raw_tables/_raw_jira_api_boards.csv
index 99f53629..7705a32d 100644
--- a/plugins/jira/e2e/raw_tables/_raw_jira_api_boards.csv
+++ b/plugins/jira/e2e/raw_tables/_raw_jira_api_boards.csv
@@ -1,2 +1,3 @@
"id","params","data","url","input","created_at"
7,"{""ConnectionId"":2,""BoardId"":8}","{""id"": 8, ""name"": ""迭代开发看板(停用)"",
""self"": ""https://merico.atlassian.net/rest/agile/1.0/board/8"", ""type"":
""scrum"", ""location"": {""name"": ""Enterprise Edition (EE)"", ""avatarURI"":
""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10552?size=small"",
""projectId"": 10003, ""projectKey"": ""EE"", ""displayName"": ""Enterprise
Edition (EE)"", ""projectName"": ""Enterprise Edition"", ""projectTypeKey"":
[...]
+1,"{""ConnectionId"":1,""BoardId"":93}","{""id"":93,""self"":""https://merico.atlassian.net/rest/agile/1.0/board/93"",""name"":""test_board"",""type"":""kanban"",""location"":{""projectId"":10050,""displayName"":""Keon_5
(K5)"",""projectName"":""Keon_5"",""projectKey"":""K5"",""projectTypeKey"":""software"",""avatarURI"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/project/avatar/10410?size=small"",""name"":""Keon_5
(K5)""}}",https://merico.atlassian.net/rest/agil [...]
diff --git a/plugins/jira/e2e/raw_tables/_raw_jira_api_issue_types.csv
b/plugins/jira/e2e/raw_tables/_raw_jira_api_issue_types.csv
index 37062b59..11934417 100644
--- a/plugins/jira/e2e/raw_tables/_raw_jira_api_issue_types.csv
+++ b/plugins/jira/e2e/raw_tables/_raw_jira_api_issue_types.csv
@@ -43,3 +43,47 @@ id,params,data,url,input,created_at
2,"{""ConnectionId"":2,""BoardId"":8}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10128"",""id"":""10128"",""description"":""Subtasks
track small pieces of work that are part of a larger
task."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Subtask"",""untranslatedName"":""Subtask"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{""
[...]
37,"{""ConnectionId"":2,""BoardId"":8}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10132"",""id"":""10132"",""description"":""问题或错误。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""缺陷"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10046""}}}",https://merico.atlassian.net/rest/api/3/issuetype,
[...]
10,"{""ConnectionId"":2,""BoardId"":8}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10133"",""id"":""10133"",""description"":""表述为用户目标的功能。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""故事"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10046""}}}",https://merico.atlassian.net/rest/api/3/iss
[...]
+45,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10077"",""id"":""10077"",""description"":""Subtasks
track small pieces of work that are part of a larger
task."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Subtask"",""untranslatedName"":""Subtask"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{
[...]
+46,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10128"",""id"":""10128"",""description"":""Subtasks
track small pieces of work that are part of a larger
task."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Subtask"",""untranslatedName"":""Subtask"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{
[...]
+47,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10091"",""id"":""10091"",""description"":""非业务的测试开发工作,例如开发测试平台"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""测试开发任务"",""untranslatedName"":""测试开发任务"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+48,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10069"",""id"":""10069"",""description"":"""",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10300?size=medium"",""name"":""疑问"",""untranslatedName"":""疑问"",""subtask"":false,""avatarId"":10300,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+49,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10074"",""id"":""10074"",""description"":""Tasks
track small, distinct pieces of
work."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""Task"",""untranslatedName"":""Task"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10033""}}}",https://m
[...]
+50,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10051"",""id"":""10051"",""description"":""Tasks
track small, distinct pieces of
work."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""任务"",""untranslatedName"":""任务"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10022""}}}",https://meric
[...]
+51,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10040"",""id"":""10040"",""description"":""For
system outages or incidents. Created by Jira Service
Desk."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10553?size=medium"",""name"":""Incident"",""untranslatedName"":""Incident"",""subtask"":false,""avatarId"":10553,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuet
[...]
+52,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10032"",""id"":""10032"",""description"":""对产品的任何疑问或建议,问题由产品经理定期进行处理。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10300?size=medium"",""name"":""Question"",""untranslatedName"":""Question"",""subtask"":false,""avatarId"":10300,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+53,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10073"",""id"":""10073"",""description"":""Stories
track functionality or features expressed as user
goals."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""Story"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":
[...]
+54,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10133"",""id"":""10133"",""description"":""Stories
track functionality or features expressed as user
goals."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""Story"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":
[...]
+55,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10072"",""id"":""10072"",""description"":""技术债需求"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10321?size=medium"",""name"":""Tech
Story"",""untranslatedName"":""Tech
Story"",""subtask"":false,""avatarId"":10321,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+56,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10000"",""id"":""10000"",""description"":""讲述一个业务,包含多个Story,可能垮Sprint的工作任务,例如效率报表。"",""iconUrl"":""https://merico.atlassian.net/images/icons/issuetypes/epic.svg"",""name"":""Epic"",""untranslatedName"":""Epic"",""subtask"":false,""hierarchyLevel"":1}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+57,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10107"",""id"":""10107"",""description"":""This
is the Xray Sub Test Execution Issue Type. Used to execute test cases already
defined. A Sub Test Execution can be created for a parent issue like a
requirement in order to execute the test cases associated with
it."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10566?size=medium"",""na
[...]
+58,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10078"",""id"":""10078"",""description"":"""",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""测试任务"",""untranslatedName"":""测试任务"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+59,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10090"",""id"":""10090"",""description"":""Subtasks
track small pieces of work that are part of a larger
task."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Subtask"",""untranslatedName"":""Subtask"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{
[...]
+60,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10004"",""id"":""10004"",""description"":""测试发现的系统缺陷。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""Bug"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+61,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10075"",""id"":""10075"",""description"":""Bugs
track problems or
errors."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""Bug"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10033""}}}",https://merico.atlassian
[...]
+62,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10047"",""id"":""10047"",""description"":""客户提出的一种改进方案。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10322?size=medium"",""name"":""Suggestion"",""untranslatedName"":""Suggestion"",""subtask"":false,""avatarId"":10322,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+63,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10052"",""id"":""10052"",""description"":""长篇故事追踪相关缺陷、故事和任务集。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium"",""name"":""项目"",""untranslatedName"":""项目"",""subtask"":false,""avatarId"":10307,""hierarchyLevel"":1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10022""}}}",https://merico.atlassian.net/rest/api/
[...]
+64,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10001"",""id"":""10001"",""description"":""具体一个功能点,以用户角度,大小适中,可以满足某一个需要为原则。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""Story"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+65,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10102"",""id"":""10102"",""description"":""This
is the Xray Test Issue Type. Used to define test cases of different types that
can be executed multiple times using Test Execution
issues."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10561?size=medium"",""name"":""Test"",""untranslatedName"":""Test"",""subtask"":false,""avatarId"":10
[...]
+66,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10031"",""id"":""10031"",""description"":""技术性任务或需求。包括技术债和用户故事的拆解。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""Task"",""untranslatedName"":""Task"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+67,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10106"",""id"":""10106"",""description"":""This
is the Xray Precondition Issue Type. Used to abstract common actions that must
be ensured before the test case execution. A Precondition can be associated
with multiple test
cases."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10565?size=medium"",""name"":""Precondition"",""untranslate
[...]
+68,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10053"",""id"":""10053"",""description"":""子任务跟踪大任务中的小任务。"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""子任务"",""untranslatedName"":""子任务"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10022""}}}",https://merico.atlassian.net/rest/api/3/
[...]
+69,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10101"",""id"":""10101"",""description"":""Subtasks
track small pieces of work that are part of a larger
task."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Subtask"",""untranslatedName"":""Subtask"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1,""scope"":{""type"":""PROJECT"",""project"":{
[...]
+70,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10082"",""id"":""10082"",""description"":""讲述一个完整的模块、场景、任务,包含多了Epic,可能跨Project、跨Team。例如:Dashboard、人才发展场景、设置重构"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10306?size=medium"",""name"":""Initiative"",""untranslatedName"":""Initiative"",""subtask"":false,""avatarId"":10306,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/
[...]
+71,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10097"",""id"":""10097"",""description"":""Stories
track functionality or features expressed as user
goals."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""Story"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":
[...]
+72,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10068"",""id"":""10068"",""description"":"""",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10300?size=medium"",""name"":""风险"",""untranslatedName"":""风险"",""subtask"":false,""avatarId"":10300,""hierarchyLevel"":0}",https://merico.atlassian.net/rest/api/3/issuetype,null,2022-07-22T00:20:26.871+00:00
+73,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10103"",""id"":""10103"",""description"":""This
is the Xray Test Set Issue Type. Creates a group of test cases. Used to
associate all included Tests with other Xray issue types like Test Execution
and Test Plan. A Test Set can also be associated with a requirement issue to
provide coverage and test
status."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/ty
[...]
+74,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10100"",""id"":""10100"",""description"":""Epics
track collections of related bugs, stories, and
tasks."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium"",""name"":""Epic"",""untranslatedName"":""Epic"",""subtask"":false,""avatarId"":10307,""hierarchyLevel"":1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""1004
[...]
+75,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10054"",""id"":""10054"",""description"":""在新一代项目中创建的一个用来测试的类型。看看是不是只在新一代项目中可以看见"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10304?size=medium"",""name"":""新一代测试类型"",""untranslatedName"":""新一代测试类型"",""subtask"":false,""avatarId"":10304,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10022""}}}",https://m
[...]
+76,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10086"",""id"":""10086"",""description"":""Stories
track functionality or features expressed as user
goals."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium"",""name"":""Story"",""untranslatedName"":""Story"",""subtask"":false,""avatarId"":10315,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":
[...]
+77,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10104"",""id"":""10104"",""description"":""This
is the Xray Test Plan Issue Type. Used to define the scope of test cases for a
given test campaign and to aggregate all executions for those tests displaying
the latest result for each test
case."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10563?size=medium"",""name"":""Test
Plan""," [...]
+78,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10089"",""id"":""10089"",""description"":""Epics
track collections of related bugs, stories, and
tasks."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium"",""name"":""Epic"",""untranslatedName"":""Epic"",""subtask"":false,""avatarId"":10307,""hierarchyLevel"":1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""1003
[...]
+79,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10099"",""id"":""10099"",""description"":""Bugs
track problems or
errors."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""Bug"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10041""}}}",https://merico.atlassian
[...]
+80,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10127"",""id"":""10127"",""description"":""Epics
track collections of related bugs, stories, and
tasks."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium"",""name"":""Epic"",""untranslatedName"":""Epic"",""subtask"":false,""avatarId"":10307,""hierarchyLevel"":1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""1004
[...]
+81,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10132"",""id"":""10132"",""description"":""Bugs
track problems or
errors."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""Bug"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10046""}}}",https://merico.atlassian
[...]
+82,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10087"",""id"":""10087"",""description"":""Tasks
track small, distinct pieces of
work."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""Task"",""untranslatedName"":""Task"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10038""}}}",https://m
[...]
+83,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10098"",""id"":""10098"",""description"":""Tasks
track small, distinct pieces of
work."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""Task"",""untranslatedName"":""Task"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10041""}}}",https://m
[...]
+84,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10126"",""id"":""10126"",""description"":""Tasks
track small, distinct pieces of
work."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium"",""name"":""Task"",""untranslatedName"":""Task"",""subtask"":false,""avatarId"":10318,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10046""}}}",https://m
[...]
+85,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10088"",""id"":""10088"",""description"":""Bugs
track problems or
errors."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"",""name"":""Bug"",""untranslatedName"":""Bug"",""subtask"":false,""avatarId"":10303,""hierarchyLevel"":0,""scope"":{""type"":""PROJECT"",""project"":{""id"":""10038""}}}",https://merico.atlassian
[...]
+86,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10076"",""id"":""10076"",""description"":""Epics
track collections of related bugs, stories, and
tasks."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10307?size=medium"",""name"":""Epic"",""untranslatedName"":""Epic"",""subtask"":false,""avatarId"":10307,""hierarchyLevel"":1,""scope"":{""type"":""PROJECT"",""project"":{""id"":""1003
[...]
+87,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10003"",""id"":""10003"",""description"":""拆分Task、Story或Bug,最小执行单位,通常是具体的测试任务或编写测试用例。一般最大为8h,极特殊情况允许16h"",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium"",""name"":""Sub-task"",""untranslatedName"":""Sub-task"",""subtask"":true,""avatarId"":10316,""hierarchyLevel"":-1}",https://merico.atlassian.net/rest/api/3/issuetyp
[...]
+88,"{""ConnectionId"":1,""BoardId"":93}","{""self"":""https://merico.atlassian.net/rest/api/3/issuetype/10105"",""id"":""10105"",""description"":""This
is the Xray Test Execution Issue Type. Used to execute test cases already
defined."",""iconUrl"":""https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10564?size=medium"",""name"":""Test
Execution"",""untranslatedName"":""Test
Execution"",""subtask"":false,""avatarId"":10564,""hierarchyLevel"":0}",https://m
[...]
diff --git a/plugins/jira/e2e/raw_tables/_raw_jira_api_issues.csv
b/plugins/jira/e2e/raw_tables/_raw_jira_api_issues.csv
index 7387436f..feeca3dc 100644
--- a/plugins/jira/e2e/raw_tables/_raw_jira_api_issues.csv
+++ b/plugins/jira/e2e/raw_tables/_raw_jira_api_issues.csv
@@ -29,3 +29,6 @@
12468,"{""ConnectionId"":2,""BoardId"":8}","{""id"": ""10097"", ""key"":
""EE-35"", ""self"":
""https://merico.atlassian.net/rest/agile/1.0/issue/10097"", ""expand"":
""operations,versionedRepresentations,editmeta,changelog,renderedFields"",
""fields"": {""epic"": null, ""votes"": {""self"":
""https://merico.atlassian.net/rest/api/2/issue/EE-35/votes"", ""votes"": 0,
""hasVoted"": false}, ""labels"": [], ""parent"": {""id"": ""10065"", ""key"":
""EE-3"", ""self"": ""https://merico.atlass [...]
12469,"{""ConnectionId"":2,""BoardId"":8}","{""id"": ""10098"", ""key"":
""EE-36"", ""self"":
""https://merico.atlassian.net/rest/agile/1.0/issue/10098"", ""expand"":
""operations,versionedRepresentations,editmeta,changelog,renderedFields"",
""fields"": {""epic"": null, ""votes"": {""self"":
""https://merico.atlassian.net/rest/api/2/issue/EE-36/votes"", ""votes"": 0,
""hasVoted"": false}, ""labels"": [], ""parent"": {""id"": ""10065"", ""key"":
""EE-3"", ""self"": ""https://merico.atlass [...]
12470,"{""ConnectionId"":2,""BoardId"":8}","{""id"": ""10099"", ""key"":
""EE-37"", ""self"":
""https://merico.atlassian.net/rest/agile/1.0/issue/10099"", ""expand"":
""operations,versionedRepresentations,editmeta,changelog,renderedFields"",
""fields"": {""epic"": null, ""votes"": {""self"":
""https://merico.atlassian.net/rest/api/2/issue/EE-37/votes"", ""votes"": 0,
""hasVoted"": false}, ""labels"": [], ""parent"": {""id"": ""10065"", ""key"":
""EE-3"", ""self"": ""https://merico.atlass [...]
+13000,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20708"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20708"",""key"":""K5-2"",""changelog"":{""startAt"":0,""maxResults"":2,""total"":2,""histories"":[{""id"":""422892"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
+13001,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20709"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20709"",""key"":""K5-3"",""changelog"":{""startAt"":0,""maxResults"":2,""total"":2,""histories"":[{""id"":""422897"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
+13002,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20710"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20710"",""key"":""K5-4"",""changelog"":{""startAt"":0,""maxResults"":1,""total"":1,""histories"":[{""id"":""422896"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
\ No newline at end of file
diff --git a/plugins/jira/e2e/raw_tables/_raw_jira_external_epics.csv
b/plugins/jira/e2e/raw_tables/_raw_jira_external_epics.csv
new file mode 100644
index 00000000..f30df960
--- /dev/null
+++ b/plugins/jira/e2e/raw_tables/_raw_jira_external_epics.csv
@@ -0,0 +1,4 @@
+"id","params","data","url","input","created_at"
+13000,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20708"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20708"",""key"":""K5-2"",""changelog"":{""startAt"":0,""maxResults"":2,""total"":2,""histories"":[{""id"":""422892"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
+13001,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20709"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20709"",""key"":""K5-3"",""changelog"":{""startAt"":0,""maxResults"":2,""total"":2,""histories"":[{""id"":""422897"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
+13002,"{""ConnectionId"":1,""BoardId"":93}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""20710"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/20710"",""key"":""K5-4"",""changelog"":{""startAt"":0,""maxResults"":1,""total"":1,""histories"":[{""id"":""422896"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=62a2d08d1be00a0068af1945"",""accountId"":""62a2d08d1be00a0068af1945"",""emailAddress"":"
[...]
diff --git
a/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv
b/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv
new file mode 100644
index 00000000..cd050491
--- /dev/null
+++
b/plugins/jira/e2e/snapshot_tables/_tool_jira_board_issues_for_external_epics.csv
@@ -0,0 +1,4 @@
+connection_id,board_id,issue_id
+1,93,20708
+1,93,20709
+1,93,20710
diff --git
a/plugins/jira/e2e/snapshot_tables/_tool_jira_boards_for_external_epics.csv
b/plugins/jira/e2e/snapshot_tables/_tool_jira_boards_for_external_epics.csv
new file mode 100644
index 00000000..5dbb4dfa
--- /dev/null
+++ b/plugins/jira/e2e/snapshot_tables/_tool_jira_boards_for_external_epics.csv
@@ -0,0 +1,2 @@
+connection_id,board_id,created_at,updated_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark,project_id,name,self,type
+1,93,2022-07-19T19:36:30.254+00:00,2022-07-19T19:36:30.254+00:00,"{""ConnectionId"":1,""BoardId"":93}",_raw_jira_api_boards,1,,10050,test_board,https://merico.atlassian.net/rest/agile/1.0/board/93,kanban
diff --git
a/plugins/jira/e2e/snapshot_tables/_tool_jira_issues_for_external_epics.csv
b/plugins/jira/e2e/snapshot_tables/_tool_jira_issues_for_external_epics.csv
new file mode 100644
index 00000000..d1f5c8cb
--- /dev/null
+++ b/plugins/jira/e2e/snapshot_tables/_tool_jira_issues_for_external_epics.csv
@@ -0,0 +1,5 @@
+connection_id,issue_id,project_id,self,icon_url,issue_key,summary,type,epic_key,status_name,status_key,story_point,original_estimate_minutes,aggregate_estimate_minutes,remaining_estimate_minutes,creator_account_id,creator_account_type,creator_display_name,assignee_account_id,assignee_account_type,assignee_display_name,priority_id,priority_name,parent_id,parent_key,sprint_id,sprint_name,resolution_date,created,updated,spent_minutes,lead_time_minutes,std_story_point,std_type,std_status,all
[...]
+1,20707,10050,https://merico.atlassian.net/rest/api/2/issue/20707,https://merico.atlassian.net/images/icons/issuetypes/epic.svg,K5-1,test
epic,Epic,,To Do,new,0,0,0,0,62a2d08d1be00a0068af1945,,Keon
Amini,,,,3,Medium,0,,0,,,2022-07-15T22:15:43.810+00:00,2022-07-15T22:54:52.078+00:00,0,0,0,EPIC,TODO,,2022-07-20T00:16:37.030+00:00,2022-07-20T00:16:37.030+00:00,"{""ConnectionId"":1,""BoardId"":93}",_raw_jira_external_epics,9,
+1,20708,10050,https://merico.atlassian.net/rest/agile/1.0/issue/20708,https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium,K5-2,first
story,Story,K5-1,To Do,new,0,0,0,0,62a2d08d1be00a0068af1945,,Keon
Amini,,,,3,Medium,20707,K5-1,175,K5 Sprint
1,,2022-07-15T22:29:49.026+00:00,2022-07-15T22:30:23.341+00:00,0,0,0,STORY,TODO,,2022-07-20T00:15:41.988+00:00,2022-07-20T00:15:41.988+00:00,"{""ConnectionId"":1,""BoardId"":93}",_raw_jira_api_issues,1,
+1,20709,10050,https://merico.atlassian.net/rest/agile/1.0/issue/20709,https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium,K5-3,second
story,Story,K5-4,To Do,new,0,0,0,0,62a2d08d1be00a0068af1945,,Keon
Amini,,,,3,Medium,20710,K5-4,175,K5 Sprint
1,,2022-07-15T22:30:43.178+00:00,2022-07-15T22:31:38.612+00:00,0,0,0,STORY,TODO,,2022-07-20T00:15:41.988+00:00,2022-07-20T00:15:41.988+00:00,"{""ConnectionId"":1,""BoardId"":93}",_raw_jira_api_issues,2,
+1,20710,10050,https://merico.atlassian.net/rest/agile/1.0/issue/20710,https://merico.atlassian.net/images/icons/issuetypes/epic.svg,K5-4,K5
epic,Epic,,To Do,new,0,0,0,0,62a2d08d1be00a0068af1945,,Keon
Amini,,,,3,Medium,0,,0,,,2022-07-15T22:31:15.981+00:00,2022-07-15T22:31:38.598+00:00,0,0,0,EPIC,TODO,,2022-07-20T00:15:41.988+00:00,2022-07-20T00:15:41.988+00:00,"{""ConnectionId"":1,""BoardId"":93}",_raw_jira_api_issues,3,
diff --git a/plugins/jira/impl/impl.go b/plugins/jira/impl/impl.go
index 9a368abd..cedd9f29 100644
--- a/plugins/jira/impl/impl.go
+++ b/plugins/jira/impl/impl.go
@@ -100,6 +100,9 @@ func (plugin Jira) SubTaskMetas() []core.SubTaskMeta {
tasks.ExtractAccountsMeta,
tasks.ConvertAccountsMeta,
+
+ tasks.CollectEpicsMeta,
+ tasks.ExtractEpicsMeta,
}
}
@@ -110,10 +113,10 @@ func (plugin Jira) PrepareTaskData(taskCtx
core.TaskContext, options map[string]
logger.Debug("%v", options)
err = mapstructure.Decode(options, &op)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not decode Jira options: %v", err)
}
if op.ConnectionId == 0 {
- return nil, fmt.Errorf("connectionId is invalid")
+ return nil, fmt.Errorf("jira connectionId is invalid")
}
connection := &models.JiraConnection{}
connectionHelper := helper.NewConnectionHelper(
@@ -121,11 +124,11 @@ func (plugin Jira) PrepareTaskData(taskCtx
core.TaskContext, options map[string]
nil,
)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not get connection API instance
for Jira: %v", err)
}
err = connectionHelper.FirstById(connection, op.ConnectionId)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to get Jira connection: %v", err)
}
var since time.Time
diff --git a/plugins/jira/tasks/epic_collector.go
b/plugins/jira/tasks/epic_collector.go
new file mode 100644
index 00000000..082b0398
--- /dev/null
+++ b/plugins/jira/tasks/epic_collector.go
@@ -0,0 +1,135 @@
+/*
+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"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ "reflect"
+ "strings"
+
+ "encoding/json"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+const RAW_EPIC_TABLE = "jira_api_epics"
+
+var _ core.SubTaskEntryPoint = CollectEpics
+
+var CollectEpicsMeta = core.SubTaskMeta{
+ Name: "collectEpics",
+ EntryPoint: CollectEpics,
+ EnabledByDefault: true,
+ Description: "collect Jira epics from all boards",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET,
core.DOMAIN_TYPE_CROSS},
+}
+
+func CollectEpics(taskCtx core.SubTaskContext) error {
+ db := taskCtx.GetDal()
+ data := taskCtx.GetData().(*JiraTaskData)
+ epicIterator, err := GetEpicKeysIterator(db, data, 100)
+ if err != nil {
+ return err
+ }
+ since := data.Since
+ jql := "ORDER BY created ASC"
+ if since != nil {
+ // prepend a time range criteria if `since` was specified,
either by user or from database
+ jql = fmt.Sprintf("updated >= '%s' %s",
since.Format("2006/01/02 15:04"), jql)
+ }
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: JiraApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ BoardId: data.Options.BoardId,
+ },
+ Table: RAW_EPIC_TABLE,
+ },
+ ApiClient: data.ApiClient,
+ PageSize: 100,
+ Incremental: false,
+ UrlTemplate: "api/2/search",
+ Query: func(reqData *helper.RequestData) (url.Values, error) {
+ query := url.Values{}
+ epicKeys := []string{}
+ for _, e := range reqData.Input.([]interface{}) {
+ epicKeys = append(epicKeys, *e.(*string))
+ }
+ localJQL := fmt.Sprintf("issue in (%s) %s",
strings.Join(epicKeys, ","), jql)
+ query.Set("jql", localJQL)
+ query.Set("startAt", fmt.Sprintf("%v",
reqData.Pager.Skip))
+ query.Set("maxResults", fmt.Sprintf("%v",
reqData.Pager.Size))
+ query.Set("expand", "changelog")
+ return query, nil
+ },
+ Input: epicIterator,
+ GetTotalPages: GetTotalPagesFromResponse,
+ Concurrency: 10,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage,
error) {
+ var data struct {
+ Issues []json.RawMessage `json:"issues"`
+ }
+ blob, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(blob, &data)
+ if err != nil {
+ return nil, err
+ }
+ return data.Issues, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return collector.Execute()
+}
+
+func GetEpicKeysIterator(db dal.Dal, data *JiraTaskData, batchSize int)
(helper.Iterator, error) {
+ cursor, err := db.RawCursor(`
+ SELECT
+ DISTINCT epic_key
+ FROM
+ _tool_jira_issues i
+ LEFT JOIN _tool_jira_board_issues bi ON (
+ i.connection_id = bi.connection_id
+ AND
+ i.issue_id = bi.issue_id
+ )
+ WHERE
+ i.connection_id = ?
+ AND
+ bi.board_id = ?
+ AND
+ i.epic_key != ''
+ `, data.Options.ConnectionId, data.Options.BoardId)
+ if err != nil {
+ return nil, fmt.Errorf("unable to query for external epics:
%v", err)
+ }
+ iter, err := helper.NewBatchedDalCursorIterator(db, cursor,
reflect.TypeOf(""), batchSize)
+ if err != nil {
+ return nil, err
+ }
+ return iter, nil
+}
diff --git a/plugins/jira/tasks/epic_extractor.go
b/plugins/jira/tasks/epic_extractor.go
new file mode 100644
index 00000000..d6379c5e
--- /dev/null
+++ b/plugins/jira/tasks/epic_extractor.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 (
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/helper"
+)
+
+var _ core.SubTaskEntryPoint = ExtractEpics
+
+var ExtractEpicsMeta = core.SubTaskMeta{
+ Name: "extractEpics",
+ EntryPoint: ExtractEpics,
+ EnabledByDefault: true,
+ Description: "extract Jira epics from all boards",
+ DomainTypes: []string{core.DOMAIN_TYPE_TICKET,
core.DOMAIN_TYPE_CROSS},
+}
+
+func ExtractEpics(taskCtx core.SubTaskContext) error {
+ data := taskCtx.GetData().(*JiraTaskData)
+ db := taskCtx.GetDal()
+ connectionId := data.Options.ConnectionId
+ boardId := data.Options.BoardId
+ logger := taskCtx.GetLogger()
+ logger.Info("extract external epic Issues, connection_id=%d,
board_id=%d", connectionId, boardId)
+ mappings, err := getTypeMappings(data, db)
+ if err != nil {
+ return err
+ }
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: JiraApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ BoardId: data.Options.BoardId,
+ },
+ Table: RAW_EPIC_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, error) {
+ return extractIssues(data, mappings, true, row)
+ },
+ })
+ if err != nil {
+ return err
+ }
+ return extractor.Execute()
+}
diff --git a/plugins/jira/tasks/issue_extractor.go
b/plugins/jira/tasks/issue_extractor.go
index 6fc28bbe..207bb33b 100644
--- a/plugins/jira/tasks/issue_extractor.go
+++ b/plugins/jira/tasks/issue_extractor.go
@@ -40,6 +40,11 @@ var ExtractIssuesMeta = core.SubTaskMeta{
DomainTypes: []string{core.DOMAIN_TYPE_TICKET,
core.DOMAIN_TYPE_CROSS},
}
+type typeMappings struct {
+ typeIdMappings map[string]string
+ stdTypeMappings map[string]string
+}
+
func ExtractIssues(taskCtx core.SubTaskContext) error {
data := taskCtx.GetData().(*JiraTaskData)
db := taskCtx.GetDal()
@@ -47,24 +52,10 @@ func ExtractIssues(taskCtx core.SubTaskContext) error {
boardId := data.Options.BoardId
logger := taskCtx.GetLogger()
logger.Info("extract Issues, connection_id=%d, board_id=%d",
connectionId, boardId)
- typeIdMapping := make(map[string]string)
- issueTypes := make([]models.JiraIssueType, 0)
- clauses := []dal.Clause{
- dal.From(&models.JiraIssueType{}),
- dal.Where("connection_id = ?", connectionId),
- }
- err := db.All(&issueTypes, clauses...)
+ mappings, err := getTypeMappings(data, db)
if err != nil {
return err
}
- for _, issueType := range issueTypes {
- typeIdMapping[issueType.Id] = issueType.UntranslatedName
- }
-
- stdTypeMappings := make(map[string]string)
- for userType, stdType := range
data.Options.TransformationRules.TypeMappings {
- stdTypeMappings[userType] =
strings.ToUpper(stdType.StandardType)
- }
extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
Ctx: taskCtx,
@@ -82,85 +73,113 @@ func ExtractIssues(taskCtx core.SubTaskContext) error {
Table: RAW_ISSUE_TABLE,
},
Extract: func(row *helper.RawData) ([]interface{}, error) {
- var apiIssue apiv2models.Issue
- err := json.Unmarshal(row.Data, &apiIssue)
- if err != nil {
- return nil, err
- }
- err = apiIssue.SetAllFields(row.Data)
- if err != nil {
- return nil, err
- }
- var results []interface{}
- sprints, issue, worklogs, changelogs, changelogItems,
users := apiIssue.ExtractEntities(data.Options.ConnectionId)
- for _, sprintId := range sprints {
- sprintIssue := &models.JiraSprintIssue{
- ConnectionId:
data.Options.ConnectionId,
- SprintId: sprintId,
- IssueId: issue.IssueId,
- IssueCreatedDate: &issue.Created,
- ResolutionDate: issue.ResolutionDate,
- }
- results = append(results, sprintIssue)
- }
- if issue.ResolutionDate != nil {
- issue.LeadTimeMinutes =
uint(issue.ResolutionDate.Unix()-issue.Created.Unix()) / 60
- }
- if data.Options.TransformationRules.StoryPointField !=
"" {
- strStoryPoint, _ :=
apiIssue.Fields.AllFields[data.Options.TransformationRules.StoryPointField].(string)
- if strStoryPoint != "" {
- issue.StoryPoint, _ =
strconv.ParseFloat(strStoryPoint, 32)
- }
- }
- issue.Type = typeIdMapping[issue.Type]
- issue.StdStoryPoint = int64(issue.StoryPoint)
- issue.StdType = stdTypeMappings[issue.Type]
- if issue.StdType == "" {
- issue.StdType = strings.ToUpper(issue.Type)
- }
- issue.StdStatus = getStdStatus(issue.StatusKey)
- results = append(results, issue)
- for _, worklog := range worklogs {
- results = append(results, worklog)
- }
- var issueUpdated *time.Time
- // likely this issue has more changelogs to be collected
- if len(changelogs) == 100 {
- issueUpdated = nil
- } else {
- issueUpdated = &issue.Updated
- }
- for _, changelog := range changelogs {
- changelog.IssueUpdated = issueUpdated
- results = append(results, changelog)
- }
- for _, changelogItem := range changelogItems {
- results = append(results, changelogItem)
- }
- for _, user := range users {
- results = append(results, user)
- }
- results = append(results, &models.JiraBoardIssue{
- ConnectionId: connectionId,
- BoardId: boardId,
- IssueId: issue.IssueId,
- })
- labels := apiIssue.Fields.Labels
- for _, v := range labels {
- issueLabel := &models.JiraIssueLabel{
- IssueId: issue.IssueId,
- LabelName: v,
- ConnectionId: data.Options.ConnectionId,
- }
- results = append(results, issueLabel)
- }
- return results, nil
+ return extractIssues(data, mappings, false, row)
},
})
-
if err != nil {
return err
}
-
return extractor.Execute()
}
+
+func extractIssues(data *JiraTaskData, mappings *typeMappings, ignoreBoard
bool, row *helper.RawData) ([]interface{}, error) {
+ var apiIssue apiv2models.Issue
+ err := json.Unmarshal(row.Data, &apiIssue)
+ if err != nil {
+ return nil, err
+ }
+ err = apiIssue.SetAllFields(row.Data)
+ if err != nil {
+ return nil, err
+ }
+ var results []interface{}
+ sprints, issue, worklogs, changelogs, changelogItems, users :=
apiIssue.ExtractEntities(data.Options.ConnectionId)
+ for _, sprintId := range sprints {
+ sprintIssue := &models.JiraSprintIssue{
+ ConnectionId: data.Options.ConnectionId,
+ SprintId: sprintId,
+ IssueId: issue.IssueId,
+ IssueCreatedDate: &issue.Created,
+ ResolutionDate: issue.ResolutionDate,
+ }
+ results = append(results, sprintIssue)
+ }
+ if issue.ResolutionDate != nil {
+ issue.LeadTimeMinutes =
uint(issue.ResolutionDate.Unix()-issue.Created.Unix()) / 60
+ }
+ if data.Options.TransformationRules.StoryPointField != "" {
+ strStoryPoint, _ :=
apiIssue.Fields.AllFields[data.Options.TransformationRules.StoryPointField].(string)
+ if strStoryPoint != "" {
+ issue.StoryPoint, _ = strconv.ParseFloat(strStoryPoint,
32)
+ }
+ }
+ issue.Type = mappings.typeIdMappings[issue.Type]
+ issue.StdStoryPoint = int64(issue.StoryPoint)
+ issue.StdType = mappings.stdTypeMappings[issue.Type]
+ if issue.StdType == "" {
+ issue.StdType = strings.ToUpper(issue.Type)
+ }
+ issue.StdStatus = getStdStatus(issue.StatusKey)
+ results = append(results, issue)
+ for _, worklog := range worklogs {
+ results = append(results, worklog)
+ }
+ var issueUpdated *time.Time
+ // likely this issue has more changelogs to be collected
+ if len(changelogs) == 100 {
+ issueUpdated = nil
+ } else {
+ issueUpdated = &issue.Updated
+ }
+ for _, changelog := range changelogs {
+ changelog.IssueUpdated = issueUpdated
+ results = append(results, changelog)
+ }
+ for _, changelogItem := range changelogItems {
+ results = append(results, changelogItem)
+ }
+ for _, user := range users {
+ results = append(results, user)
+ }
+ if !ignoreBoard {
+ results = append(results, &models.JiraBoardIssue{
+ ConnectionId: data.Options.ConnectionId,
+ BoardId: data.Options.BoardId,
+ IssueId: issue.IssueId,
+ })
+ }
+ labels := apiIssue.Fields.Labels
+ for _, v := range labels {
+ issueLabel := &models.JiraIssueLabel{
+ IssueId: issue.IssueId,
+ LabelName: v,
+ ConnectionId: data.Options.ConnectionId,
+ }
+ results = append(results, issueLabel)
+ }
+ return results, nil
+}
+
+func getTypeMappings(data *JiraTaskData, db dal.Dal) (*typeMappings, error) {
+ typeIdMapping := make(map[string]string)
+ issueTypes := make([]models.JiraIssueType, 0)
+ clauses := []dal.Clause{
+ dal.From(&models.JiraIssueType{}),
+ dal.Where("connection_id = ?", data.Options.ConnectionId),
+ }
+ err := db.All(&issueTypes, clauses...)
+ if err != nil {
+ return nil, err
+ }
+ for _, issueType := range issueTypes {
+ typeIdMapping[issueType.Id] = issueType.UntranslatedName
+ }
+ stdTypeMappings := make(map[string]string)
+ for userType, stdType := range
data.Options.TransformationRules.TypeMappings {
+ stdTypeMappings[userType] =
strings.ToUpper(stdType.StandardType)
+ }
+ return &typeMappings{
+ typeIdMappings: typeIdMapping,
+ stdTypeMappings: stdTypeMappings,
+ }, nil
+}