Skip to content

Commit 217d7a7

Browse files
sallyomclaude
andcommitted
feat(bugfix): Enhance Jira integration with descriptions and Gist attachments
- Includes links to GitHub Issue, branches, and PR details - Shows assessment and implementation status with visual indicators - New attachGistsToJira() function fetches and attaches Gist markdown files - Attaches bug-review-issue-{number}.md and implementation-issue-{number}.md - Runs on both create and update to ensure latest content - Makes analysis documents centrally available in Jira (not dependent on individual contributor accounts) - Enhanced GitHub comments with Gist links - Create: "🔗 Jira Task Created" with task link and analysis document links - Update: "🔄 Jira Task Updated" with latest information - Both include links to bug-review and implementation Gists when available 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: sallyom <somalley@redhat.com>
1 parent d1f5b8b commit 217d7a7

File tree

1 file changed

+238
-16
lines changed

1 file changed

+238
-16
lines changed

components/backend/handlers/bugfix/jira_sync.go

Lines changed: 238 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919

2020
"github.com/gin-gonic/gin"
2121
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"k8s.io/client-go/dynamic"
23+
"k8s.io/client-go/kubernetes"
2224
)
2325

2426
// SyncProjectBugFixWorkflowToJira handles POST /api/projects/:projectName/bugfix-workflows/:id/sync-jira
@@ -106,6 +108,9 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
106108
}
107109
} else {
108110
jiraTaskURL = fmt.Sprintf("%s/browse/%s", strings.TrimRight(jiraURL, "/"), jiraTaskKey)
111+
// Refresh attachments for updates (in case new Gists were added)
112+
jiraBase := strings.TrimRight(jiraURL, "/")
113+
attachGistsToJira(c, workflow, jiraBase, jiraTaskKey, authHeader, reqK8s, reqDyn, project)
109114
}
110115
}
111116

@@ -160,7 +165,8 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
160165
// T056: Handle various HTTP status codes properly
161166
switch resp.StatusCode {
162167
case 201:
163-
// Success
168+
// Success - continue to parse response
169+
fmt.Printf("Jira API success (201), response body length: %d bytes\n", len(body))
164170
case 401, 403:
165171
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, "Jira authentication failed")
166172
c.JSON(http.StatusUnauthorized, gin.H{"error": "Jira authentication failed", "details": string(body)})
@@ -175,10 +181,19 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
175181
return
176182
}
177183

184+
// Parse JSON response
178185
var result map[string]interface{}
179186
if err := json.Unmarshal(body, &result); err != nil {
187+
// Log the raw response for debugging
188+
fmt.Printf("ERROR: Failed to parse Jira response as JSON: %v\n", err)
189+
bodyLen := len(body)
190+
fmt.Printf("Response body (first 500 chars): %s\n", string(body[:min(500, bodyLen)]))
180191
websocket.BroadcastBugFixJiraSyncFailed(workflowID, workflow.GithubIssueNumber, "Invalid Jira response")
181-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse Jira response", "details": err.Error()})
192+
c.JSON(http.StatusInternalServerError, gin.H{
193+
"error": "Failed to parse Jira response",
194+
"details": err.Error(),
195+
"responsePreview": string(body[:min(200, bodyLen)]),
196+
})
182197
return
183198
}
184199

@@ -199,19 +214,45 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
199214
fmt.Printf("Warning: Failed to create Jira remote link: %v\n", err)
200215
}
201216

217+
// Attach Gist markdown files to Jira issue
218+
attachGistsToJira(c, workflow, jiraBase, jiraTaskKey, authHeader, reqK8s, reqDyn, project)
219+
202220
// T054: Add comment to GitHub Issue with Jira link
203221
userID, _ := c.Get("userID")
204222
userIDStr, _ := userID.(string)
205223
githubToken, err := git.GetGitHubToken(c.Request.Context(), reqK8s, reqDyn, project, userIDStr)
206224
if err == nil && githubToken != "" {
207225
owner, repo, issueNumber, err := github.ParseGitHubIssueURL(workflow.GithubIssueURL)
208226
if err == nil {
209-
comment := formatGitHubJiraLinkComment(jiraTaskKey, jiraTaskURL)
227+
comment := formatGitHubJiraLinkComment(jiraTaskKey, jiraTaskURL, workflow)
210228
ctx := context.Background()
211229
_, err = github.AddComment(ctx, owner, repo, issueNumber, githubToken, comment)
212230
if err != nil {
213231
// Non-fatal: Log but continue
214232
fmt.Printf("Warning: Failed to add Jira link to GitHub Issue: %v\n", err)
233+
} else {
234+
fmt.Printf("Posted Jira link comment to GitHub Issue #%d\n", issueNumber)
235+
}
236+
}
237+
}
238+
}
239+
240+
// Also post GitHub comment for updates (not just creates)
241+
if isUpdate {
242+
userID, _ := c.Get("userID")
243+
userIDStr, _ := userID.(string)
244+
githubToken, err := git.GetGitHubToken(c.Request.Context(), reqK8s, reqDyn, project, userIDStr)
245+
if err == nil && githubToken != "" {
246+
owner, repo, issueNumber, err := github.ParseGitHubIssueURL(workflow.GithubIssueURL)
247+
if err == nil {
248+
comment := formatGitHubJiraUpdateComment(jiraTaskKey, jiraTaskURL, workflow)
249+
ctx := context.Background()
250+
_, err = github.AddComment(ctx, owner, repo, issueNumber, githubToken, comment)
251+
if err != nil {
252+
// Non-fatal: Log but continue
253+
fmt.Printf("Warning: Failed to add Jira update to GitHub Issue: %v\n", err)
254+
} else {
255+
fmt.Printf("Posted Jira update comment to GitHub Issue #%d\n", issueNumber)
215256
}
216257
}
217258
}
@@ -244,34 +285,165 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
244285
}
245286

246287
// buildJiraDescription builds the Jira issue description from the workflow
288+
// Includes comprehensive information and references to attached Gist files
247289
func buildJiraDescription(workflow *types.BugFixWorkflow) string {
248290
var desc strings.Builder
249291

250-
desc.WriteString("This issue is synchronized from GitHub Issue:\n")
292+
// Header with source
293+
desc.WriteString("h1. Bug Report\n\n")
294+
desc.WriteString("*Source:* [GitHub Issue #")
295+
desc.WriteString(fmt.Sprintf("%d", workflow.GithubIssueNumber))
296+
desc.WriteString("|")
251297
desc.WriteString(workflow.GithubIssueURL)
252-
desc.WriteString("\n\n")
298+
desc.WriteString("]\n\n")
299+
desc.WriteString("----\n\n")
253300

301+
// Description
254302
if workflow.Description != "" {
255-
desc.WriteString("## Description\n")
303+
desc.WriteString("h2. Description\n\n")
256304
desc.WriteString(workflow.Description)
257305
desc.WriteString("\n\n")
258306
}
259307

260-
desc.WriteString("## Details\n")
261-
desc.WriteString(fmt.Sprintf("- GitHub Issue: #%d\n", workflow.GithubIssueNumber))
262-
desc.WriteString(fmt.Sprintf("- Branch: %s\n", workflow.BranchName))
263-
desc.WriteString(fmt.Sprintf("- Created: %s\n", workflow.CreatedAt))
308+
// Repository and branch information
309+
desc.WriteString("h2. Repository Information\n\n")
310+
desc.WriteString(fmt.Sprintf("* *Repository:* %s\n", workflow.ImplementationRepo.URL))
311+
desc.WriteString(fmt.Sprintf("* *Base Branch:* {{%s}}\n", workflow.ImplementationRepo.Branch))
312+
desc.WriteString(fmt.Sprintf("* *Feature Branch:* {{%s}}\n", workflow.BranchName))
313+
desc.WriteString("\n")
314+
315+
// Status information
316+
desc.WriteString("h2. Workflow Status\n\n")
317+
desc.WriteString(fmt.Sprintf("* *Created:* %s\n", workflow.CreatedAt))
318+
desc.WriteString(fmt.Sprintf("* *Phase:* %s\n", workflow.Phase))
319+
320+
if workflow.AssessmentStatus != "" {
321+
desc.WriteString(fmt.Sprintf("* *Assessment:* %s\n", workflow.AssessmentStatus))
322+
}
323+
if workflow.ImplementationCompleted {
324+
desc.WriteString("* *Implementation:* {color:green}✓ Complete{color}\n")
325+
} else {
326+
desc.WriteString("* *Implementation:* Pending\n")
327+
}
328+
desc.WriteString("\n")
329+
330+
// Analysis documents section
331+
hasGists := false
332+
if workflow.Annotations != nil {
333+
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
334+
hasGists = true
335+
}
336+
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
337+
hasGists = true
338+
}
339+
}
340+
341+
if hasGists {
342+
desc.WriteString("h2. Analysis Documents\n\n")
343+
desc.WriteString("_Detailed analysis reports are attached to this issue as markdown files. Original Gist links:_\n\n")
344+
345+
if workflow.Annotations != nil {
346+
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
347+
desc.WriteString("* *Bug Review & Assessment:* [bug-review.md attachment|")
348+
desc.WriteString(bugReviewGist)
349+
desc.WriteString("]\n")
350+
}
351+
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
352+
desc.WriteString("* *Implementation Details:* [implementation.md attachment|")
353+
desc.WriteString(implGist)
354+
desc.WriteString("]\n")
355+
}
356+
}
357+
desc.WriteString("\n")
358+
}
359+
360+
// PR information if available
361+
if workflow.Annotations != nil {
362+
if prURL := workflow.Annotations["github-pr-url"]; prURL != "" {
363+
prNumber := workflow.Annotations["github-pr-number"]
364+
prState := workflow.Annotations["github-pr-state"]
365+
desc.WriteString("h2. Pull Request\n\n")
366+
desc.WriteString(fmt.Sprintf("* *PR:* [#%s|%s]\n", prNumber, prURL))
367+
desc.WriteString(fmt.Sprintf("* *State:* %s\n", prState))
368+
desc.WriteString("\n")
369+
}
370+
}
264371

265-
desc.WriteString("\n---\n")
266-
desc.WriteString("*This issue is automatically synchronized from vTeam BugFix Workspace*\n")
267-
desc.WriteString("*Note: Currently created as Feature Request. Will use proper Bug/Task type after Jira Cloud migration.*\n")
372+
// Footer
373+
desc.WriteString("----\n")
374+
desc.WriteString("_Synchronized from vTeam BugFix Workspace | [View in vTeam|")
375+
desc.WriteString(workflow.GithubIssueURL)
376+
desc.WriteString("]_\n")
268377

269378
return desc.String()
270379
}
271380

272-
// formatGitHubJiraLinkComment formats the comment to post on GitHub Issue
273-
func formatGitHubJiraLinkComment(jiraTaskKey, jiraTaskURL string) string {
274-
return fmt.Sprintf("## 🔗 Jira Task Created\n\nThis bug has been synchronized to Jira:\n- **Task**: [%s](%s)\n\n*Synchronized by vTeam BugFix Workspace*", jiraTaskKey, jiraTaskURL)
381+
// formatGitHubJiraLinkComment formats the comment to post on GitHub Issue when creating new Jira task
382+
func formatGitHubJiraLinkComment(jiraTaskKey, jiraTaskURL string, workflow *types.BugFixWorkflow) string {
383+
var comment strings.Builder
384+
385+
comment.WriteString("## 🔗 Jira Task Created\n\n")
386+
comment.WriteString(fmt.Sprintf("This bug has been synchronized to Jira: [**%s**](%s)\n\n", jiraTaskKey, jiraTaskURL))
387+
388+
// Add links to analysis documents if available
389+
if workflow.Annotations != nil {
390+
hasGists := false
391+
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
392+
if !hasGists {
393+
comment.WriteString("### 📄 Analysis Documents\n\n")
394+
hasGists = true
395+
}
396+
comment.WriteString(fmt.Sprintf("- [Bug Review & Assessment](%s)\n", bugReviewGist))
397+
}
398+
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
399+
if !hasGists {
400+
comment.WriteString("### 📄 Analysis Documents\n\n")
401+
hasGists = true
402+
}
403+
comment.WriteString(fmt.Sprintf("- [Implementation Details](%s)\n", implGist))
404+
}
405+
if hasGists {
406+
comment.WriteString("\n")
407+
}
408+
}
409+
410+
comment.WriteString("*Synchronized by vTeam BugFix Workspace*")
411+
412+
return comment.String()
413+
}
414+
415+
// formatGitHubJiraUpdateComment formats the comment to post on GitHub Issue when updating Jira task
416+
func formatGitHubJiraUpdateComment(jiraTaskKey, jiraTaskURL string, workflow *types.BugFixWorkflow) string {
417+
var comment strings.Builder
418+
419+
comment.WriteString("## 🔄 Jira Task Updated\n\n")
420+
comment.WriteString(fmt.Sprintf("Jira task [**%s**](%s) has been updated with the latest information.\n\n", jiraTaskKey, jiraTaskURL))
421+
422+
// Add links to analysis documents if available
423+
if workflow.Annotations != nil {
424+
hasGists := false
425+
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
426+
if !hasGists {
427+
comment.WriteString("### 📄 Latest Analysis\n\n")
428+
hasGists = true
429+
}
430+
comment.WriteString(fmt.Sprintf("- [Bug Review & Assessment](%s)\n", bugReviewGist))
431+
}
432+
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
433+
if !hasGists {
434+
comment.WriteString("### 📄 Latest Analysis\n\n")
435+
hasGists = true
436+
}
437+
comment.WriteString(fmt.Sprintf("- [Implementation Details](%s)\n", implGist))
438+
}
439+
if hasGists {
440+
comment.WriteString("\n")
441+
}
442+
}
443+
444+
comment.WriteString("*Synchronized by vTeam BugFix Workspace*")
445+
446+
return comment.String()
275447
}
276448

277449
// getSuccessMessage returns appropriate success message
@@ -280,4 +452,54 @@ func getSuccessMessage(created bool, jiraTaskKey string) string {
280452
return fmt.Sprintf("Created Jira task %s", jiraTaskKey)
281453
}
282454
return fmt.Sprintf("Updated Jira task %s", jiraTaskKey)
455+
}
456+
457+
// attachGistsToJira fetches Gist content and attaches it as markdown files to the Jira issue
458+
func attachGistsToJira(c *gin.Context, workflow *types.BugFixWorkflow, jiraBase, jiraTaskKey, authHeader string, reqK8s *kubernetes.Clientset, reqDyn dynamic.Interface, project string) {
459+
if workflow.Annotations == nil {
460+
return
461+
}
462+
463+
// Get GitHub token for fetching Gists
464+
userID, _ := c.Get("userID")
465+
userIDStr, _ := userID.(string)
466+
githubToken, err := git.GetGitHubToken(c.Request.Context(), reqK8s, reqDyn, project, userIDStr)
467+
if err != nil || githubToken == "" {
468+
fmt.Printf("Warning: Cannot attach Gists - failed to get GitHub token: %v\n", err)
469+
return
470+
}
471+
472+
ctx := c.Request.Context()
473+
474+
// Attach bug-review Gist if available
475+
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
476+
fmt.Printf("Fetching bug-review Gist from %s\n", bugReviewGist)
477+
gistContent, err := github.GetGist(ctx, bugReviewGist, githubToken)
478+
if err != nil {
479+
fmt.Printf("Warning: Failed to fetch bug-review Gist: %v\n", err)
480+
} else {
481+
filename := fmt.Sprintf("bug-review-issue-%d.md", workflow.GithubIssueNumber)
482+
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
483+
fmt.Printf("Warning: Failed to attach bug-review.md to %s: %v\n", jiraTaskKey, attachErr)
484+
} else {
485+
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
486+
}
487+
}
488+
}
489+
490+
// Attach implementation Gist if available
491+
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
492+
fmt.Printf("Fetching implementation Gist from %s\n", implGist)
493+
gistContent, err := github.GetGist(ctx, implGist, githubToken)
494+
if err != nil {
495+
fmt.Printf("Warning: Failed to fetch implementation Gist: %v\n", err)
496+
} else {
497+
filename := fmt.Sprintf("implementation-issue-%d.md", workflow.GithubIssueNumber)
498+
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
499+
fmt.Printf("Warning: Failed to attach implementation.md to %s: %v\n", jiraTaskKey, attachErr)
500+
} else {
501+
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
502+
}
503+
}
504+
}
283505
}

0 commit comments

Comments
 (0)