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 <[email protected]>
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 {

Reply via email to