You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2025/09/03 06:29:17 UTC

(incubator-devlake) branch main updated: fix(jira): fix type for Jira issue descriptions (#8559)

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 314124abd fix(jira): fix type for Jira issue descriptions (#8559)
314124abd is described below

commit 314124abd9dab70c171389c22b5be75ec28bbb48
Author: Bamboo <12...@users.noreply.github.com>
AuthorDate: Wed Sep 3 14:29:12 2025 +0800

    fix(jira): fix type for Jira issue descriptions (#8559)
    
    * fix(jira): update epic collector to use new API endpoint and include all fields
    
    * fix(jira): enhance epic collector to dynamically select API endpoint based on JIRA version
    
    * fix(jira): update epic collector to use correct API endpoint for JIRA Cloud and Server versions
    
    * fix(jira): refactor epic collector to streamline API endpoint selection and enhance error handling
    
    * fix(jira): fix type for Jira issue descriptions
    
    * refactor(jira): update comment and worklog models to use FlexibleDescription type for comments
    
    * docs(jira): add ADF reference for FlexibleDescription type in issue model
---
 backend/plugins/jira/tasks/apiv2models/comment.go |  18 +--
 backend/plugins/jira/tasks/apiv2models/issue.go   | 135 +++++++++++++++++++++-
 backend/plugins/jira/tasks/apiv2models/worklog.go |  22 ++--
 3 files changed, 152 insertions(+), 23 deletions(-)

diff --git a/backend/plugins/jira/tasks/apiv2models/comment.go b/backend/plugins/jira/tasks/apiv2models/comment.go
index b41feb847..c2cd8e1ed 100644
--- a/backend/plugins/jira/tasks/apiv2models/comment.go
+++ b/backend/plugins/jira/tasks/apiv2models/comment.go
@@ -25,14 +25,14 @@ import (
 )
 
 type Comment struct {
-	Self         string             `json:"self"`
-	Id           string             `json:"id"`
-	Author       *Account           `json:"author"`
-	Body         string             `json:"body"`
-	UpdateAuthor *Account           `json:"updateAuthor"`
-	Created      common.Iso8601Time `json:"created"`
-	Updated      common.Iso8601Time `json:"updated"`
-	JsdPublic    bool               `json:"jsdPublic"`
+	Self         string              `json:"self"`
+	Id           string              `json:"id"`
+	Author       *Account            `json:"author"`
+	Body         FlexibleDescription `json:"body"`
+	UpdateAuthor *Account            `json:"updateAuthor"`
+	Created      common.Iso8601Time  `json:"created"`
+	Updated      common.Iso8601Time  `json:"updated"`
+	JsdPublic    bool                `json:"jsdPublic"`
 }
 
 func (c Comment) ToToolLayer(connectionId uint64, issueId uint64, issueUpdated *time.Time) *models.JiraIssueComment {
@@ -41,7 +41,7 @@ func (c Comment) ToToolLayer(connectionId uint64, issueId uint64, issueUpdated *
 		IssueId:      issueId,
 		ComentId:     c.Id,
 		Self:         c.Self,
-		Body:         c.Body,
+		Body:         c.Body.Value,
 		Created:      c.Updated.ToTime(),
 		Updated:      c.Updated.ToTime(),
 		IssueUpdated: issueUpdated,
diff --git a/backend/plugins/jira/tasks/apiv2models/issue.go b/backend/plugins/jira/tasks/apiv2models/issue.go
index 4fcc85780..ab54efb95 100644
--- a/backend/plugins/jira/tasks/apiv2models/issue.go
+++ b/backend/plugins/jira/tasks/apiv2models/issue.go
@@ -19,6 +19,7 @@ package apiv2models
 
 import (
 	"encoding/json"
+	"strings"
 	"time"
 
 	"github.com/apache/incubator-devlake/core/errors"
@@ -26,6 +27,134 @@ import (
 	"github.com/apache/incubator-devlake/plugins/jira/models"
 )
 
+// FlexibleDescription supports both plain text and ADF (Atlassian Document Format) for Jira description field
+// ADF reference: https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
+type FlexibleDescription struct {
+	Value string
+}
+
+// ADFNode represents a node in Atlassian Document Format
+type ADFNode struct {
+	Type    string                 `json:"type"`
+	Text    string                 `json:"text,omitempty"`
+	Content []ADFNode              `json:"content,omitempty"`
+	Attrs   map[string]interface{} `json:"attrs,omitempty"`
+}
+
+// UnmarshalJSON implements custom JSON unmarshaling for FlexibleDescription
+func (fd *FlexibleDescription) UnmarshalJSON(data []byte) error {
+	// Handle null values
+	if string(data) == "null" {
+		fd.Value = ""
+		return nil
+	}
+
+	// Try to unmarshal as string first
+	var str string
+	if err := json.Unmarshal(data, &str); err == nil {
+		fd.Value = str
+		return nil
+	}
+
+	// Try to unmarshal as ADF document
+	var adfDoc ADFNode
+	if err := json.Unmarshal(data, &adfDoc); err == nil {
+		fd.Value = extractTextFromADF(adfDoc)
+		return nil
+	}
+
+	// Fallback: keep raw JSON as string for debugging
+	fd.Value = string(data)
+	return nil
+}
+
+// extractTextFromADF recursively extracts plain text from ADF document
+func extractTextFromADF(node ADFNode) string {
+	var result strings.Builder
+
+	switch node.Type {
+	case "text":
+		result.WriteString(node.Text)
+	case "hardBreak":
+		result.WriteString("\n")
+	case "paragraph":
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+		result.WriteString("\n")
+	case "heading":
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+		result.WriteString("\n")
+	case "listItem":
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+	case "bulletList", "orderedList":
+		for _, child := range node.Content {
+			result.WriteString("• ")
+			result.WriteString(extractTextFromADF(child))
+			result.WriteString("\n")
+		}
+	case "table":
+		for _, row := range node.Content {
+			if row.Type == "tableRow" {
+				for j, cell := range row.Content {
+					if j > 0 {
+						result.WriteString(" | ")
+					}
+					result.WriteString(extractTextFromADF(cell))
+				}
+				result.WriteString("\n")
+			}
+		}
+	case "tableCell", "tableHeader":
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+	case "codeBlock":
+		result.WriteString("```\n")
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+		result.WriteString("\n```\n")
+	case "blockquote":
+		result.WriteString("> ")
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+		result.WriteString("\n")
+	case "doc":
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+	case "inlineCard", "mention":
+		// Extract text from attrs or content for links and mentions
+		if attrs, ok := node.Attrs["text"]; ok {
+			if text, ok := attrs.(string); ok {
+				result.WriteString(text)
+			}
+		} else {
+			for _, child := range node.Content {
+				result.WriteString(extractTextFromADF(child))
+			}
+		}
+	default:
+		// For unknown types, extract content recursively
+		for _, child := range node.Content {
+			result.WriteString(extractTextFromADF(child))
+		}
+	}
+
+	return result.String()
+}
+
+// String returns the string value
+func (fd FlexibleDescription) String() string {
+	return fd.Value
+}
+
 type Issue struct {
 	Expand string `json:"expand"`
 	ID     uint64 `json:"id,string"`
@@ -121,8 +250,8 @@ type Issue struct {
 			ID   string `json:"id"`
 			Name string `json:"name"`
 		} `json:"components"`
-		Timeoriginalestimate *int64 `json:"timeoriginalestimate"`
-		Description          string `json:"description"`
+		Timeoriginalestimate *int64              `json:"timeoriginalestimate"`
+		Description          FlexibleDescription `json:"description"`
 		Timetracking         *struct {
 			RemainingEstimate        string `json:"remainingEstimate"`
 			TimeSpent                string `json:"timeSpent"`
@@ -233,7 +362,7 @@ func (i Issue) toToolLayer(connectionId uint64) *models.JiraIssue {
 		IssueKey:           i.Key,
 		StoryPoint:         &workload,
 		Summary:            i.Fields.Summary,
-		Description:        i.Fields.Description,
+		Description:        i.Fields.Description.Value,
 		Type:               i.Fields.Issuetype.ID,
 		StatusName:         i.Fields.Status.Name,
 		StatusKey:          i.Fields.Status.StatusCategory.Key,
diff --git a/backend/plugins/jira/tasks/apiv2models/worklog.go b/backend/plugins/jira/tasks/apiv2models/worklog.go
index 83bbe0c7b..5fdb23778 100644
--- a/backend/plugins/jira/tasks/apiv2models/worklog.go
+++ b/backend/plugins/jira/tasks/apiv2models/worklog.go
@@ -25,17 +25,17 @@ import (
 )
 
 type Worklog struct {
-	Self             string             `json:"self"`
-	Author           *Account           `json:"author"`
-	UpdateAuthor     *Account           `json:"updateAuthor"`
-	Comment          string             `json:"comment"`
-	Created          string             `json:"created"`
-	Updated          common.Iso8601Time `json:"updated"`
-	Started          common.Iso8601Time `json:"started"`
-	TimeSpent        string             `json:"timeSpent"`
-	TimeSpentSeconds int                `json:"timeSpentSeconds"`
-	ID               string             `json:"id"`
-	IssueID          uint64             `json:"issueId,string"`
+	Self             string              `json:"self"`
+	Author           *Account            `json:"author"`
+	UpdateAuthor     *Account            `json:"updateAuthor"`
+	Comment          FlexibleDescription `json:"comment"`
+	Created          string              `json:"created"`
+	Updated          common.Iso8601Time  `json:"updated"`
+	Started          common.Iso8601Time  `json:"started"`
+	TimeSpent        string              `json:"timeSpent"`
+	TimeSpentSeconds int                 `json:"timeSpentSeconds"`
+	ID               string              `json:"id"`
+	IssueID          uint64              `json:"issueId,string"`
 }
 
 func (w Worklog) ToToolLayer(connectionId uint64, issueUpdated *time.Time) *models.JiraWorklog {