You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by zh...@apache.org on 2022/06/15 08:57:24 UTC

[incubator-devlake] branch release-v0.11-hotfix updated: feat: collect jira status (#2182) (#2196)

This is an automated email from the ASF dual-hosted git repository.

zhangliang2022 pushed a commit to branch release-v0.11-hotfix
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/release-v0.11-hotfix by this push:
     new 94c41258 feat: collect jira status (#2182) (#2196)
94c41258 is described below

commit 94c41258b83f541e95cc4ea4933b313746e4f95b
Author: NaRro <co...@merico.dev>
AuthorDate: Wed Jun 15 16:57:19 2022 +0800

    feat: collect jira status (#2182) (#2196)
    
    * feat: collect jira status (#2182)
    
    * feat: collect jira status
    
    * feat: add license header
    
    * feat: register migrations
    
    * fix: jira connection table name
    
    * feat: convert changelogs' standard status
    
    * fix: sql error
    
    * fix: review request
    
    * fix: apache license
    
    * feat: update ticket.changelogs
    
    * fix: changelog migration
    
    * fix: tablename
    
    * fix: changelog convertor
---
 models/domainlayer/ticket/changelog.go             | 18 +++---
 models/migrationscripts/register.go                |  5 +-
 models/migrationscripts/updateSchemas2022061402.go | 60 ++++++++++++++++++
 plugins/jira/jira.go                               |  4 ++
 plugins/jira/models/connection.go                  | 16 ++---
 .../migrationscripts/updateSchemas20220614.go      | 46 ++++++++++----
 plugins/jira/{tasks/shared.go => models/status.go} | 28 ++++-----
 .../jira/tasks/apiv2models/status.go               | 40 ++++++------
 plugins/jira/tasks/changelog_convertor.go          | 41 ++++++++----
 plugins/jira/tasks/issue_collector.go              |  6 --
 plugins/jira/tasks/issue_convertor.go              |  2 +-
 plugins/jira/tasks/issue_extractor.go              | 14 +----
 plugins/jira/tasks/shared.go                       | 26 ++++++++
 .../jira/tasks/{shared.go => status_collector.go}  | 38 +++++++++---
 plugins/jira/tasks/status_extractor.go             | 72 ++++++++++++++++++++++
 plugins/tapd/tasks/bug_changelog_converter.go      | 21 ++++---
 plugins/tapd/tasks/story_changelog_converter.go    | 16 ++---
 plugins/tapd/tasks/task_changelog_converter.go     | 16 ++---
 18 files changed, 336 insertions(+), 133 deletions(-)

diff --git a/models/domainlayer/ticket/changelog.go b/models/domainlayer/ticket/changelog.go
index 8ad5bd7e..70499ac3 100644
--- a/models/domainlayer/ticket/changelog.go
+++ b/models/domainlayer/ticket/changelog.go
@@ -27,12 +27,14 @@ type Changelog struct {
 	domainlayer.DomainEntity
 
 	// collected fields
-	IssueId     string `gorm:"index;type:varchar(255)"`
-	AuthorId    string `gorm:"type:varchar(255)"`
-	AuthorName  string `gorm:"type:varchar(255)"`
-	FieldId     string `gorm:"type:varchar(255)"`
-	FieldName   string `gorm:"type:varchar(255)"`
-	FromValue   string
-	ToValue     string
-	CreatedDate time.Time
+	IssueId           string `gorm:"index;type:varchar(255)"`
+	AuthorId          string `gorm:"type:varchar(255)"`
+	AuthorName        string `gorm:"type:varchar(255)"`
+	FieldId           string `gorm:"type:varchar(255)"`
+	FieldName         string `gorm:"type:varchar(255)"`
+	OriginalFromValue string
+	OriginalToValue   string
+	FromValue         string
+	ToValue           string
+	CreatedDate       time.Time
 }
diff --git a/models/migrationscripts/register.go b/models/migrationscripts/register.go
index e014cc4e..ad45e755 100644
--- a/models/migrationscripts/register.go
+++ b/models/migrationscripts/register.go
@@ -25,8 +25,7 @@ func RegisterAll() {
 		new(initSchemas),
 		new(updateSchemas20220505), new(updateSchemas20220507), new(updateSchemas20220510),
 		new(updateSchemas20220513), new(updateSchemas20220524), new(updateSchemas20220526),
-		new(updateSchemas20220527),
-		new(updateSchemas20220528),
-		new(updateSchemas20220602),
+		new(updateSchemas20220527), new(updateSchemas20220528), new(updateSchemas20220602),
+		new(updateSchemas2022061402),
 	}, "Framework")
 }
diff --git a/models/migrationscripts/updateSchemas2022061402.go b/models/migrationscripts/updateSchemas2022061402.go
new file mode 100644
index 00000000..1e013ec3
--- /dev/null
+++ b/models/migrationscripts/updateSchemas2022061402.go
@@ -0,0 +1,60 @@
+/*
+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 migrationscripts
+
+import (
+	"context"
+	"time"
+
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"gorm.io/gorm"
+)
+
+type Changelog20220614 struct {
+	archived.DomainEntity
+
+	// collected fields
+	IssueId           string `gorm:"index;type:varchar(255)"`
+	AuthorId          string `gorm:"type:varchar(255)"`
+	AuthorName        string `gorm:"type:varchar(255)"`
+	FieldId           string `gorm:"type:varchar(255)"`
+	FieldName         string `gorm:"type:varchar(255)"`
+	OriginalFromValue string
+	OriginalToValue   string
+	FromValue         string
+	ToValue           string
+	CreatedDate       time.Time
+}
+
+func (Changelog20220614) TableName() string {
+	return "changelogs"
+}
+
+type updateSchemas2022061402 struct{}
+
+func (*updateSchemas2022061402) Up(ctx context.Context, db *gorm.DB) error {
+	return db.Migrator().AutoMigrate(&Changelog20220614{})
+}
+
+func (*updateSchemas2022061402) Version() uint64 {
+	return 20220614091600
+}
+
+func (*updateSchemas2022061402) Name() string {
+	return "update table: changelogs, add standard_from, standard_to"
+}
diff --git a/plugins/jira/jira.go b/plugins/jira/jira.go
index 9f758190..6fe00730 100644
--- a/plugins/jira/jira.go
+++ b/plugins/jira/jira.go
@@ -54,6 +54,9 @@ func (plugin Jira) Description() string {
 
 func (plugin Jira) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
+		{Name: "collectStatus", EntryPoint: tasks.CollectStatus, EnabledByDefault: true, Description: "collect Jira status"},
+		{Name: "extractStatus", EntryPoint: tasks.ExtractStatus, EnabledByDefault: true, Description: "extract Jira status"},
+
 		{Name: "collectProjects", EntryPoint: tasks.CollectProjects, EnabledByDefault: true, Description: "collect Jira projects"},
 		{Name: "extractProjects", EntryPoint: tasks.ExtractProjects, EnabledByDefault: true, Description: "extract Jira projects"},
 
@@ -153,6 +156,7 @@ func (plugin Jira) MigrationScripts() []migration.Script {
 		new(migrationscripts.UpdateSchemas20220525),
 		new(migrationscripts.UpdateSchemas20220526),
 		new(migrationscripts.UpdateSchemas20220527),
+		new(migrationscripts.UpdateSchemas20220614),
 	}
 }
 
diff --git a/plugins/jira/models/connection.go b/plugins/jira/models/connection.go
index edb6b88f..04cf8e62 100644
--- a/plugins/jira/models/connection.go
+++ b/plugins/jira/models/connection.go
@@ -51,12 +51,20 @@ type JiraConnection struct {
 	RateLimit                  int    `comment:"api request rate limt per hour" json:"rateLimit"`
 }
 
+func (JiraConnection) TableName() string {
+	return "_tool_jira_connections"
+}
+
 type JiraIssueTypeMapping struct {
 	ConnectionID uint64 `gorm:"primaryKey" json:"jiraConnectionId" validate:"required"`
 	UserType     string `gorm:"type:varchar(50);primaryKey" json:"userType" validate:"required"`
 	StandardType string `gorm:"type:varchar(50)" json:"standardType" validate:"required"`
 }
 
+func (JiraIssueTypeMapping) TableName() string {
+	return "_tool_jira_issue_type_mappings"
+}
+
 type JiraIssueStatusMapping struct {
 	ConnectionID   uint64 `gorm:"primaryKey" json:"jiraConnectionId" validate:"required"`
 	UserType       string `gorm:"type:varchar(50);primaryKey" json:"userType" validate:"required"`
@@ -69,14 +77,6 @@ type JiraConnectionDetail struct {
 	TypeMappings map[string]map[string]interface{} `json:"typeMappings"`
 }
 
-func (JiraConnection) TableName() string {
-	return "_tool_jira_connections"
-}
-
-func (JiraIssueTypeMapping) TableName() string {
-	return "_tool_jira_issue_type_mappings"
-}
-
 func (JiraIssueStatusMapping) TableName() string {
 	return "_tool_jira_issue_status_mappings"
 }
diff --git a/models/migrationscripts/register.go b/plugins/jira/models/migrationscripts/updateSchemas20220614.go
similarity index 52%
copy from models/migrationscripts/register.go
copy to plugins/jira/models/migrationscripts/updateSchemas20220614.go
index e014cc4e..7d64d376 100644
--- a/models/migrationscripts/register.go
+++ b/plugins/jira/models/migrationscripts/updateSchemas20220614.go
@@ -17,16 +17,38 @@ limitations under the License.
 
 package migrationscripts
 
-import "github.com/apache/incubator-devlake/migration"
-
-// RegisterAll register all the migration scripts of framework
-func RegisterAll() {
-	migration.Register([]migration.Script{
-		new(initSchemas),
-		new(updateSchemas20220505), new(updateSchemas20220507), new(updateSchemas20220510),
-		new(updateSchemas20220513), new(updateSchemas20220524), new(updateSchemas20220526),
-		new(updateSchemas20220527),
-		new(updateSchemas20220528),
-		new(updateSchemas20220602),
-	}, "Framework")
+import (
+	"context"
+
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"gorm.io/gorm"
+)
+
+type JiraStatus struct {
+	archived.NoPKModel
+	ConnectionId   uint64 `gorm:"primaryKey"`
+	ID             string `gorm:"primaryKey"`
+	Name           string
+	Self           string
+	StatusCategory string
+}
+
+func (JiraStatus) TableName() string {
+	return "_tool_jira_statuses"
+}
+
+type UpdateSchemas20220614 struct{}
+
+func (*UpdateSchemas20220614) Up(ctx context.Context, db *gorm.DB) error {
+	return db.Migrator().AutoMigrate(
+		&JiraStatus{},
+	)
+}
+
+func (*UpdateSchemas20220614) Version() uint64 {
+	return 20220614112900
+}
+
+func (*UpdateSchemas20220614) Name() string {
+	return "add jira status"
 }
diff --git a/plugins/jira/tasks/shared.go b/plugins/jira/models/status.go
similarity index 65%
copy from plugins/jira/tasks/shared.go
copy to plugins/jira/models/status.go
index 236b9387..0791b100 100644
--- a/plugins/jira/tasks/shared.go
+++ b/plugins/jira/models/status.go
@@ -15,23 +15,19 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package tasks
+package models
 
-import (
-	"net/http"
+import "github.com/apache/incubator-devlake/models/common"
 
-	"github.com/apache/incubator-devlake/plugins/helper"
-)
+type JiraStatus struct {
+	common.NoPKModel
+	ConnectionId   uint64 `gorm:"primaryKey"`
+	ID             string `gorm:"primaryKey"`
+	Name           string
+	Self           string
+	StatusCategory string
+}
 
-func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs) (int, error) {
-	body := &JiraPagination{}
-	err := helper.UnmarshalResponse(res, body)
-	if err != nil {
-		return 0, err
-	}
-	pages := body.Total / args.PageSize
-	if body.Total%args.PageSize > 0 {
-		pages++
-	}
-	return pages, nil
+func (JiraStatus) TableName() string {
+	return "_tool_jira_statuses"
 }
diff --git a/models/domainlayer/ticket/changelog.go b/plugins/jira/tasks/apiv2models/status.go
similarity index 54%
copy from models/domainlayer/ticket/changelog.go
copy to plugins/jira/tasks/apiv2models/status.go
index 8ad5bd7e..7dd00a41 100644
--- a/models/domainlayer/ticket/changelog.go
+++ b/plugins/jira/tasks/apiv2models/status.go
@@ -15,24 +15,26 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package ticket
+package apiv2models
 
-import (
-	"time"
-
-	"github.com/apache/incubator-devlake/models/domainlayer"
-)
-
-type Changelog struct {
-	domainlayer.DomainEntity
-
-	// collected fields
-	IssueId     string `gorm:"index;type:varchar(255)"`
-	AuthorId    string `gorm:"type:varchar(255)"`
-	AuthorName  string `gorm:"type:varchar(255)"`
-	FieldId     string `gorm:"type:varchar(255)"`
-	FieldName   string `gorm:"type:varchar(255)"`
-	FromValue   string
-	ToValue     string
-	CreatedDate time.Time
+type Status struct {
+	Description string `json:"description"`
+	IconURL     string `json:"iconUrl"`
+	ID          string `json:"id"`
+	Name        string `json:"name"`
+	Scope       *struct {
+		Type    string `json:"type"`
+		Project struct {
+			ID string `json:"id"`
+		} `json:"project"`
+	} `json:"scope"`
+	Self           string `json:"self"`
+	StatusCategory struct {
+		ColorName string `json:"colorName"`
+		ID        int    `json:"id"`
+		Key       string `json:"key"`
+		Name      string `json:"name"`
+		Self      string `json:"self"`
+	} `json:"statusCategory"`
+	UntranslatedName string `json:"untranslatedName"`
 }
diff --git a/plugins/jira/tasks/changelog_convertor.go b/plugins/jira/tasks/changelog_convertor.go
index 31e9f9ea..e2801ad5 100644
--- a/plugins/jira/tasks/changelog_convertor.go
+++ b/plugins/jira/tasks/changelog_convertor.go
@@ -46,6 +46,11 @@ func ConvertChangelogs(taskCtx core.SubTaskContext) error {
 	logger := taskCtx.GetLogger()
 	db := taskCtx.GetDb()
 	logger.Info("covert changelog")
+	statusMap, err := GetStatusInfo(db)
+	if err != nil {
+		logger.Error(err.Error())
+		return err
+	}
 	// select all changelogs belongs to the board
 	cursor, err := db.Table("_tool_jira_changelog_items").
 		Joins(`left join _tool_jira_changelogs on (
@@ -60,7 +65,7 @@ func ConvertChangelogs(taskCtx core.SubTaskContext) error {
 		Where("_tool_jira_changelog_items.connection_id = ? AND _tool_jira_board_issues.board_id = ?", connectionId, boardId).
 		Rows()
 	if err != nil {
-		logger.Info(err.Error())
+		logger.Error(err.Error())
 		return err
 	}
 	defer cursor.Close()
@@ -87,33 +92,43 @@ func ConvertChangelogs(taskCtx core.SubTaskContext) error {
 					row.ChangelogId,
 					row.Field,
 				)},
-				IssueId:     issueIdGenerator.Generate(row.ConnectionId, row.IssueId),
-				AuthorId:    userIdGen.Generate(connectionId, row.AuthorAccountId),
-				AuthorName:  row.AuthorDisplayName,
-				FieldId:     row.FieldId,
-				FieldName:   row.Field,
-				FromValue:   row.FromString,
-				ToValue:     row.ToString,
-				CreatedDate: row.Created,
+				IssueId:           issueIdGenerator.Generate(row.ConnectionId, row.IssueId),
+				AuthorId:          userIdGen.Generate(connectionId, row.AuthorAccountId),
+				AuthorName:        row.AuthorDisplayName,
+				FieldId:           row.FieldId,
+				FieldName:         row.Field,
+				OriginalFromValue: row.FromString,
+				OriginalToValue:   row.ToString,
+				CreatedDate:       row.Created,
 			}
 			if row.Field == "assignee" {
 				if row.ToValue != "" {
-					changelog.ToValue = userIdGen.Generate(connectionId, row.ToValue)
+					changelog.OriginalToValue = userIdGen.Generate(connectionId, row.ToValue)
 				}
 				if row.FromValue != "" {
-					changelog.FromValue = userIdGen.Generate(connectionId, row.FromValue)
+					changelog.OriginalFromValue = userIdGen.Generate(connectionId, row.FromValue)
 				}
 			}
 			if row.Field == "Sprint" {
-				changelog.FromValue, err = convertIds(row.FromValue, connectionId, sprintIdGenerator)
+				changelog.OriginalFromValue, err = convertIds(row.FromValue, connectionId, sprintIdGenerator)
 				if err != nil {
 					return nil, err
 				}
-				changelog.ToValue, err = convertIds(row.ToValue, connectionId, sprintIdGenerator)
+				changelog.OriginalToValue, err = convertIds(row.ToValue, connectionId, sprintIdGenerator)
 				if err != nil {
 					return nil, err
 				}
 			}
+			if row.Field == "status" {
+				fromStatus, ok := statusMap[row.FromString]
+				if ok {
+					changelog.FromValue = GetStdStatus(fromStatus.StatusCategory)
+				}
+				toStatus, ok := statusMap[row.ToString]
+				if ok {
+					changelog.ToValue = GetStdStatus(toStatus.StatusCategory)
+				}
+			}
 			return []interface{}{changelog}, nil
 		},
 	})
diff --git a/plugins/jira/tasks/issue_collector.go b/plugins/jira/tasks/issue_collector.go
index b7850420..40be8c11 100644
--- a/plugins/jira/tasks/issue_collector.go
+++ b/plugins/jira/tasks/issue_collector.go
@@ -27,7 +27,6 @@ import (
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 	"github.com/apache/incubator-devlake/plugins/jira/models"
-	"github.com/apache/incubator-devlake/plugins/jira/tasks/apiv2models"
 )
 
 const RAW_ISSUE_TABLE = "jira_api_issues"
@@ -138,11 +137,6 @@ func CollectIssues(taskCtx core.SubTaskContext) error {
 			if err != nil {
 				return nil, err
 			}
-			var issue apiv2models.Issue
-			err = json.Unmarshal(blob, &issue)
-			if err != nil {
-				return nil, err
-			}
 			return data.Issues, nil
 		},
 	})
diff --git a/plugins/jira/tasks/issue_convertor.go b/plugins/jira/tasks/issue_convertor.go
index f5d316c5..81ded160 100644
--- a/plugins/jira/tasks/issue_convertor.go
+++ b/plugins/jira/tasks/issue_convertor.go
@@ -79,7 +79,7 @@ func ConvertIssues(taskCtx core.SubTaskContext) error {
 				EpicKey:                 jiraIssue.EpicKey,
 				Type:                    jiraIssue.StdType,
 				Status:                  jiraIssue.StdStatus,
-				OriginalStatus:          jiraIssue.StatusKey,
+				OriginalStatus:          jiraIssue.StatusName,
 				StoryPoint:              jiraIssue.StdStoryPoint,
 				OriginalEstimateMinutes: jiraIssue.OriginalEstimateMinutes,
 				ResolutionDate:          jiraIssue.ResolutionDate,
diff --git a/plugins/jira/tasks/issue_extractor.go b/plugins/jira/tasks/issue_extractor.go
index 92d14687..62acc4f7 100644
--- a/plugins/jira/tasks/issue_extractor.go
+++ b/plugins/jira/tasks/issue_extractor.go
@@ -22,8 +22,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
-
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 	"github.com/apache/incubator-devlake/plugins/jira/models"
@@ -57,6 +55,7 @@ func ExtractIssues(taskCtx core.SubTaskContext) error {
 		return strings.ToUpper(stdType)
 	}
 	// prepare getStdStatus function
+	// TODO: status mapping is now not used
 	var statusMappingRows []*models.JiraIssueStatusMapping
 	err = db.Find(&statusMappingRows, "connection_id = ?", connectionId).Error
 	if err != nil {
@@ -70,15 +69,6 @@ func ExtractIssues(taskCtx core.SubTaskContext) error {
 		k := makeStatusMappingKey(statusMappingRow.UserType, statusMappingRow.UserStatus)
 		statusMappings[k] = statusMappingRow.StandardStatus
 	}
-	getStdStatus := func(statusKey string) string {
-		if statusKey == "done" {
-			return ticket.DONE
-		} else if statusKey == "new" {
-			return ticket.TODO
-		} else {
-			return ticket.IN_PROGRESS
-		}
-	}
 
 	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
 		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
@@ -123,7 +113,7 @@ func ExtractIssues(taskCtx core.SubTaskContext) error {
 			}
 			issue.StdStoryPoint = uint(issue.StoryPoint)
 			issue.StdType = getStdType(issue.Type)
-			issue.StdStatus = getStdStatus(issue.StatusKey)
+			issue.StdStatus = GetStdStatus(issue.StatusKey)
 			if len(changelogs) < 100 {
 				issue.ChangelogUpdated = &row.CreatedAt
 			}
diff --git a/plugins/jira/tasks/shared.go b/plugins/jira/tasks/shared.go
index 236b9387..9f6dfc99 100644
--- a/plugins/jira/tasks/shared.go
+++ b/plugins/jira/tasks/shared.go
@@ -20,7 +20,10 @@ package tasks
 import (
 	"net/http"
 
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
 	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/jira/models"
+	"gorm.io/gorm"
 )
 
 func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs) (int, error) {
@@ -35,3 +38,26 @@ func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs
 	}
 	return pages, nil
 }
+
+func GetStdStatus(statusKey string) string {
+	if statusKey == "done" {
+		return ticket.DONE
+	} else if statusKey == "new" {
+		return ticket.TODO
+	} else {
+		return ticket.IN_PROGRESS
+	}
+}
+
+func GetStatusInfo(db *gorm.DB) (map[string]models.JiraStatus, error) {
+	data := make([]models.JiraStatus, 0)
+	err := db.Model(&models.JiraStatus{}).Scan(&data).Error
+	if err != nil {
+		return nil, err
+	}
+	statusMap := make(map[string]models.JiraStatus)
+	for _, v := range data {
+		statusMap[v.Name] = v
+	}
+	return statusMap, nil
+}
diff --git a/plugins/jira/tasks/shared.go b/plugins/jira/tasks/status_collector.go
similarity index 51%
copy from plugins/jira/tasks/shared.go
copy to plugins/jira/tasks/status_collector.go
index 236b9387..7ea12793 100644
--- a/plugins/jira/tasks/shared.go
+++ b/plugins/jira/tasks/status_collector.go
@@ -18,20 +18,40 @@ limitations under the License.
 package tasks
 
 import (
+	"encoding/json"
 	"net/http"
 
+	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 )
 
-func GetTotalPagesFromResponse(res *http.Response, args *helper.ApiCollectorArgs) (int, error) {
-	body := &JiraPagination{}
-	err := helper.UnmarshalResponse(res, body)
+const RAW_STATUS_TABLE = "jira_api_status"
+
+func CollectStatus(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*JiraTaskData)
+
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: JiraApiParams{
+				ConnectionId: data.Connection.ID,
+				BoardId:      data.Options.BoardId,
+			},
+			Table: RAW_STATUS_TABLE,
+		},
+		ApiClient:     data.ApiClient,
+		UrlTemplate:   "api/2/status",
+		GetTotalPages: GetTotalPagesFromResponse,
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, error) {
+			var data []json.RawMessage
+			err := helper.UnmarshalResponse(res, &data)
+			return data, err
+		},
+	})
+
 	if err != nil {
-		return 0, err
+		return err
 	}
-	pages := body.Total / args.PageSize
-	if body.Total%args.PageSize > 0 {
-		pages++
-	}
-	return pages, nil
+
+	return collector.Execute()
 }
diff --git a/plugins/jira/tasks/status_extractor.go b/plugins/jira/tasks/status_extractor.go
new file mode 100644
index 00000000..9e92b22f
--- /dev/null
+++ b/plugins/jira/tasks/status_extractor.go
@@ -0,0 +1,72 @@
+/*
+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 (
+	"encoding/json"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/jira/models"
+	"github.com/apache/incubator-devlake/plugins/jira/tasks/apiv2models"
+)
+
+func ExtractStatus(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*JiraTaskData)
+	connectionId := data.Connection.ID
+	boardId := data.Options.BoardId
+	logger := taskCtx.GetLogger()
+	logger.Info("extract Status, connection_id=%d, board_id=%d", connectionId, boardId)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: JiraApiParams{
+				ConnectionId: connectionId,
+				BoardId:      boardId,
+			},
+			Table: RAW_STATUS_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, error) {
+			var apiStatus apiv2models.Status
+			err := json.Unmarshal(row.Data, &apiStatus)
+			if err != nil {
+				return nil, err
+			}
+			if apiStatus.Scope != nil {
+				// FIXME: skip scope status
+				return nil, nil
+			}
+			var jiraStatus = &models.JiraStatus{
+				ConnectionId:   connectionId,
+				ID:             apiStatus.ID,
+				Name:           apiStatus.Name,
+				Self:           apiStatus.Self,
+				StatusCategory: apiStatus.StatusCategory.Key,
+			}
+			var result []interface{}
+			result = append(result, jiraStatus)
+			return result, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/tapd/tasks/bug_changelog_converter.go b/plugins/tapd/tasks/bug_changelog_converter.go
index 8f430262..4aabd2e5 100644
--- a/plugins/tapd/tasks/bug_changelog_converter.go
+++ b/plugins/tapd/tasks/bug_changelog_converter.go
@@ -18,6 +18,9 @@ limitations under the License.
 package tasks
 
 import (
+	"reflect"
+	"time"
+
 	"github.com/apache/incubator-devlake/models/common"
 	"github.com/apache/incubator-devlake/models/domainlayer"
 	"github.com/apache/incubator-devlake/models/domainlayer/didgen"
@@ -25,8 +28,6 @@ import (
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 	"github.com/apache/incubator-devlake/plugins/tapd/models"
-	"reflect"
-	"time"
 )
 
 type BugChangelogItemResult struct {
@@ -82,14 +83,14 @@ func ConvertBugChangelog(taskCtx core.SubTaskContext) error {
 				DomainEntity: domainlayer.DomainEntity{
 					Id: clIdGen.Generate(data.Connection.ID, cl.ID, cl.Field),
 				},
-				IssueId:     IssueIdGen.Generate(data.Connection.ID, cl.BugID),
-				AuthorId:    UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Author),
-				AuthorName:  cl.Author,
-				FieldId:     cl.Field,
-				FieldName:   cl.Field,
-				FromValue:   cl.ValueBeforeParsed,
-				ToValue:     cl.ValueAfterParsed,
-				CreatedDate: cl.Created,
+				IssueId:           IssueIdGen.Generate(data.Connection.ID, cl.BugID),
+				AuthorId:          UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Author),
+				AuthorName:        cl.Author,
+				FieldId:           cl.Field,
+				FieldName:         cl.Field,
+				OriginalFromValue: cl.ValueBeforeParsed,
+				OriginalToValue:   cl.ValueAfterParsed,
+				CreatedDate:       cl.Created,
 			}
 
 			return []interface{}{
diff --git a/plugins/tapd/tasks/story_changelog_converter.go b/plugins/tapd/tasks/story_changelog_converter.go
index cca20956..ddf19297 100644
--- a/plugins/tapd/tasks/story_changelog_converter.go
+++ b/plugins/tapd/tasks/story_changelog_converter.go
@@ -86,14 +86,14 @@ func ConvertStoryChangelog(taskCtx core.SubTaskContext) error {
 				DomainEntity: domainlayer.DomainEntity{
 					Id: fmt.Sprintf("%s:%s", clIdGen.Generate(data.Connection.ID, cl.ID), cl.Field),
 				},
-				IssueId:     IssueIdGen.Generate(data.Connection.ID, cl.StoryID),
-				AuthorId:    UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Creator),
-				AuthorName:  cl.Creator,
-				FieldId:     cl.Field,
-				FieldName:   cl.Field,
-				FromValue:   cl.ValueBeforeParsed,
-				ToValue:     cl.ValueAfterParsed,
-				CreatedDate: cl.Created,
+				IssueId:           IssueIdGen.Generate(data.Connection.ID, cl.StoryID),
+				AuthorId:          UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Creator),
+				AuthorName:        cl.Creator,
+				FieldId:           cl.Field,
+				FieldName:         cl.Field,
+				OriginalFromValue: cl.ValueBeforeParsed,
+				OriginalToValue:   cl.ValueAfterParsed,
+				CreatedDate:       cl.Created,
 			}
 
 			return []interface{}{
diff --git a/plugins/tapd/tasks/task_changelog_converter.go b/plugins/tapd/tasks/task_changelog_converter.go
index 9ce44fb7..ced0ae82 100644
--- a/plugins/tapd/tasks/task_changelog_converter.go
+++ b/plugins/tapd/tasks/task_changelog_converter.go
@@ -87,14 +87,14 @@ func ConvertTaskChangelog(taskCtx core.SubTaskContext) error {
 				DomainEntity: domainlayer.DomainEntity{
 					Id: fmt.Sprintf("%s:%s", clIdGen.Generate(data.Connection.ID, cl.ID), cl.Field),
 				},
-				IssueId:     IssueIdGen.Generate(data.Connection.ID, cl.TaskID),
-				AuthorId:    UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Creator),
-				AuthorName:  cl.Creator,
-				FieldId:     cl.Field,
-				FieldName:   cl.Field,
-				FromValue:   cl.ValueBeforeParsed,
-				ToValue:     cl.ValueAfterParsed,
-				CreatedDate: cl.Created,
+				IssueId:           IssueIdGen.Generate(data.Connection.ID, cl.TaskID),
+				AuthorId:          UserIdGen.Generate(data.Connection.ID, data.Options.WorkspaceID, cl.Creator),
+				AuthorName:        cl.Creator,
+				FieldId:           cl.Field,
+				FieldName:         cl.Field,
+				OriginalFromValue: cl.ValueBeforeParsed,
+				OriginalToValue:   cl.ValueAfterParsed,
+				CreatedDate:       cl.Created,
 			}
 
 			return []interface{}{