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 {