Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ public record AiRequestPreviousIssueDTO(
String id,
String type, // security|quality|performance|style
String severity, // critical|high|medium|low|info
String title,
String reason,
String suggestedFixDescription,
String suggestedFixDiff,
String file,
Integer line,
String branch,
String pullRequestId,
String status, // open|resolved|ignored
String issueCategory
String category
) {
public static AiRequestPreviousIssueDTO fromEntity(CodeAnalysisIssue issue) {
String categoryStr = issue.getIssueCategory() != null
Expand All @@ -23,9 +24,10 @@ public static AiRequestPreviousIssueDTO fromEntity(CodeAnalysisIssue issue) {
return new AiRequestPreviousIssueDTO(
String.valueOf(issue.getId()),
categoryStr,
issue.getSeverity() != null ? issue.getSeverity().name().toLowerCase() : null,
issue.getSeverity() != null ? issue.getSeverity().name() : null,
issue.getReason(),
issue.getSuggestedFixDescription(),
issue.getSuggestedFixDiff(),
issue.getFilePath(),
issue.getLineNumber(),
issue.getAnalysis() == null ? null : issue.getAnalysis().getBranchName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,15 +372,15 @@ private void mapCodeAnalysisIssuesToBranch(Set<String> changedFiles, Branch bran
if (existing.isPresent()) {
bc = existing.get();
bc.setSeverity(issue.getSeverity());
branchIssueRepository.save(bc);
branchIssueRepository.saveAndFlush(bc);
} else {
bc = new BranchIssue();
bc.setBranch(branch);
bc.setCodeAnalysisIssue(issue);
bc.setResolved(issue.isResolved());
bc.setSeverity(issue.getSeverity());
bc.setFirstDetectedPrNumber(issue.getAnalysis() != null ? issue.getAnalysis().getPrNumber() : null);
branchIssueRepository.save(bc);
branchIssueRepository.saveAndFlush(bc);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Add INFO to the severity check constraint on code_analysis_issue table
-- This is needed because INFO was added to IssueSeverity enum but the constraint wasn't updated

-- Drop the existing constraint and recreate with INFO included
ALTER TABLE code_analysis_issue DROP CONSTRAINT IF EXISTS code_analysis_issue_severity_check;

ALTER TABLE code_analysis_issue
ADD CONSTRAINT code_analysis_issue_severity_check
CHECK (severity IN ('HIGH', 'MEDIUM', 'LOW', 'INFO', 'RESOLVED'));
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public CommentOnBitbucketCloudAction(
}

public void postSummaryResult(String textContent) throws IOException {
postSummaryResultWithId(textContent);
}

/**
* Post a summary result comment and return the comment ID.
* @param textContent The markdown content to post
* @return The ID of the created comment
*/
public String postSummaryResultWithId(String textContent) throws IOException {
String workspace = vcsRepoInfo.getRepoWorkspace();
String repoSlug = vcsRepoInfo.getRepoSlug();

Expand Down Expand Up @@ -65,6 +74,15 @@ public void postSummaryResult(String textContent) throws IOException {

try (Response response = authorizedOkHttpClient.newCall(req).execute()) {
validate(response);
// Parse response to get comment ID
if (response.body() != null) {
String responseBody = response.body().string();
JsonNode jsonNode = objectMapper.readTree(responseBody);
if (jsonNode.has("id")) {
return String.valueOf(jsonNode.get("id").asInt());
}
}
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ public class MarkdownAnalysisFormatter implements AnalysisFormatter {
);

private final boolean useGitHubSpoilers;
private final boolean includeDetailedIssues;

/**
* Default constructor - no spoilers (for Bitbucket)
* Default constructor - no spoilers (for Bitbucket), no detailed issues in summary
*/
public MarkdownAnalysisFormatter() {
this.useGitHubSpoilers = false;
this.includeDetailedIssues = false;
}

/**
Expand All @@ -44,6 +46,17 @@ public MarkdownAnalysisFormatter() {
*/
public MarkdownAnalysisFormatter(boolean useGitHubSpoilers) {
this.useGitHubSpoilers = useGitHubSpoilers;
this.includeDetailedIssues = false;
}

/**
* Full constructor with all options
* @param useGitHubSpoilers true for GitHub (uses details/summary), false for Bitbucket
* @param includeDetailedIssues true to include detailed issues in summary, false for separate comment
*/
public MarkdownAnalysisFormatter(boolean useGitHubSpoilers, boolean includeDetailedIssues) {
this.useGitHubSpoilers = useGitHubSpoilers;
this.includeDetailedIssues = includeDetailedIssues;
}

@Override
Expand Down Expand Up @@ -104,15 +117,19 @@ public String format(AnalysisSummary summary) {

md.append("\n");

md.append("### Detailed Issues\n\n");
// Only include detailed issues if explicitly requested
if (includeDetailedIssues) {
md.append("### Detailed Issues\n\n");

appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.HIGH, EMOJI_HIGH, "High Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.MEDIUM, EMOJI_MEDIUM, "Medium Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.LOW, EMOJI_LOW, "Low Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.INFO, EMOJI_INFO, "Informational Notes");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.HIGH, EMOJI_HIGH, "High Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.MEDIUM, EMOJI_MEDIUM, "Medium Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.LOW, EMOJI_LOW, "Low Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.INFO, EMOJI_INFO, "Informational Notes");
}
}

if (!summary.getFileIssueCount().isEmpty()) {
// Only include files affected if detailed issues are included
if (includeDetailedIssues && !summary.getFileIssueCount().isEmpty()) {
md.append("### Files Affected\n");
summary.getFileIssueCount().entrySet().stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
Expand Down Expand Up @@ -144,6 +161,58 @@ public String format(AnalysisSummary summary) {
return md.toString();
}

/**
* Format only the detailed issues section for posting as a separate comment.
* For GitHub, wraps all issues in a single collapsible spoiler.
* @param summary The analysis summary containing issues
* @return Markdown-formatted string with detailed issues, or empty string if no issues
*/
public String formatDetailedIssues(AnalysisSummary summary) {
if (summary.getIssues() == null || summary.getIssues().isEmpty()) {
return "";
}

StringBuilder md = new StringBuilder();

int totalIssues = summary.getIssues().size();

if (useGitHubSpoilers) {
// GitHub: wrap ALL issues in a single collapsible section
md.append("<details>\n");
md.append(String.format("<summary><b>📋 Detailed Issues (%d)</b></summary>\n\n", totalIssues));
} else {
// Bitbucket: regular header (no spoiler support)
md.append("## 📋 Detailed Issues\n\n");
}

appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.HIGH, EMOJI_HIGH, "High Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.MEDIUM, EMOJI_MEDIUM, "Medium Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.LOW, EMOJI_LOW, "Low Severity Issues");
appendIssuesBySevertiy(md, summary.getIssues(), IssueSeverity.INFO, EMOJI_INFO, "Informational Notes");

if (!summary.getFileIssueCount().isEmpty()) {
md.append("### Files Affected\n");
summary.getFileIssueCount().entrySet().stream()
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
.limit(10)
.forEach(entry -> {
String fileName = getShortFileName(entry.getKey());
md.append(String.format("- **%s**: %d issue%s\n",
fileName,
entry.getValue(),
entry.getValue() == 1 ? "" : "s"));
});
md.append("\n");
}

if (useGitHubSpoilers) {
// Close the details tag for GitHub
md.append("</details>\n");
}

return md.toString();
}

private void appendIssuesBySevertiy(StringBuilder md, List<AnalysisSummary.IssueSummary> issues,
IssueSeverity severity, String emoji, String title) {
List<AnalysisSummary.IssueSummary> severityIssues = issues.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ public String createMarkdownSummary(CodeAnalysis analysis, AnalysisSummary summa
}
}

/**
* Creates markdown-formatted detailed issues for posting as a separate comment.
*
* @param summary The analysis summary
* @param useGitHubSpoilers true for GitHub (uses details/summary for collapsible fixes), false for Bitbucket
* @return Markdown-formatted string with detailed issues, or empty string if no issues
*/
public String createDetailedIssuesMarkdown(AnalysisSummary summary, boolean useGitHubSpoilers) {
try {
return new MarkdownAnalysisFormatter(useGitHubSpoilers).formatDetailedIssues(summary);
} catch (Exception e) {
log.error("Error creating detailed issues markdown: {}", e.getMessage(), e);
return "";
}
}

/**
* Creates a plain text summary for pull request comments
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,66 @@ public void deletePreviousComments(String owner, String repo, int prNumber, Stri
}
}
}

/**
* Create a Pull Request Review with a summary body and optional inline comments.
* This creates a review that appears in the "Conversation" tab with connected comments.
*
* @param owner Repository owner
* @param repo Repository name
* @param pullRequestNumber PR number
* @param commitId The SHA of the commit to review
* @param body The main review body/summary (appears as the review comment)
* @param event Review event: COMMENT, APPROVE, REQUEST_CHANGES
* @param comments List of inline comments, each with: path, line, body
* @return The review ID
*/
public String createPullRequestReview(String owner, String repo, int pullRequestNumber,
String commitId, String body, String event,
List<Map<String, Object>> comments) throws IOException {
String apiUrl = String.format("%s/repos/%s/%s/pulls/%d/reviews",
GitHubConfig.API_BASE, owner, repo, pullRequestNumber);

Map<String, Object> payload = new HashMap<>();
payload.put("commit_id", commitId);
payload.put("body", body);
payload.put("event", event); // COMMENT, APPROVE, or REQUEST_CHANGES

if (comments != null && !comments.isEmpty()) {
payload.put("comments", comments);
}

Request req = new Request.Builder()
.url(apiUrl)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.post(RequestBody.create(objectMapper.writeValueAsString(payload), JSON))
.build();

log.debug("Creating PR review on GitHub: {}", apiUrl);

try (Response resp = authorizedOkHttpClient.newCall(req).execute()) {
if (!resp.isSuccessful()) {
String respBody = resp.body() != null ? resp.body().string() : "";
String msg = String.format("Failed to create PR review: %d - %s", resp.code(), respBody);
log.warn(msg);
throw new IOException(msg);
}

String respBody = resp.body() != null ? resp.body().string() : "";
Map<String, Object> responseMap = objectMapper.readValue(respBody, new TypeReference<Map<String, Object>>() {});
Number id = (Number) responseMap.get("id");
log.info("Created PR review {} on PR #{}", id, pullRequestNumber);
return id != null ? String.valueOf(id.longValue()) : null;
}
}

/**
* Create a simplified PR review with just summary and detailed issues body.
* Both will appear as connected review comments.
*/
public String createReviewWithSummary(String owner, String repo, int pullRequestNumber,
String commitId, String summaryBody) throws IOException {
return createPullRequestReview(owner, repo, pullRequestNumber, commitId, summaryBody, "COMMENT", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class BitbucketReportingService implements VcsReportingService {
private static final Logger log = LoggerFactory.getLogger(BitbucketReportingService.class);

private static final String CODECROW_REVIEW_MARKER = "<!-- codecrow-review -->";
private static final String CODECROW_ISSUES_MARKER = "<!-- codecrow-issues -->";

private final ReportGenerator reportGenerator;
private final VcsClientProvider vcsClientProvider;
Expand Down Expand Up @@ -121,6 +122,7 @@ public void postAnalysisResults(

AnalysisSummary summary = reportGenerator.createAnalysisSummary(codeAnalysis, platformPrEntityId);
String markdownSummary = reportGenerator.createMarkdownSummary(codeAnalysis, summary);
String detailedIssuesMarkdown = reportGenerator.createDetailedIssuesMarkdown(summary, false);
CodeInsightsReport report = reportGenerator.createCodeInsightsReport(
summary,
codeAnalysis
Expand All @@ -136,14 +138,21 @@ public void postAnalysisResults(
vcsRepoInfo.getVcsConnection()
);

postOrUpdateComment(httpClient, vcsRepoInfo, pullRequestNumber, markdownSummary, placeholderCommentId);
// Post summary comment (or update placeholder)
String summaryCommentId = postOrUpdateComment(httpClient, vcsRepoInfo, pullRequestNumber, markdownSummary, placeholderCommentId);

// Post detailed issues as a separate comment reply if there are issues
if (detailedIssuesMarkdown != null && !detailedIssuesMarkdown.isEmpty() && summaryCommentId != null) {
postDetailedIssuesReply(httpClient, vcsRepoInfo, pullRequestNumber, summaryCommentId, detailedIssuesMarkdown);
}

postReport(httpClient, vcsRepoInfo, codeAnalysis.getCommitHash(), report);
postAnnotations(httpClient, vcsRepoInfo, codeAnalysis.getCommitHash(), annotations);

log.info("Successfully posted analysis results to Bitbucket");
}

private void postOrUpdateComment(
private String postOrUpdateComment(
OkHttpClient httpClient,
VcsRepoInfo vcsRepoInfo,
Long pullRequestNumber,
Expand All @@ -165,8 +174,34 @@ private void postOrUpdateComment(
String fullContent = markdownSummary + "\n\n" + CODECROW_REVIEW_MARKER;
commentAction.updateComment(placeholderCommentId, fullContent);
log.debug("Updated placeholder comment {} with analysis results", placeholderCommentId);
return placeholderCommentId;
} else {
commentAction.postSummaryResult(markdownSummary);
return commentAction.postSummaryResultWithId(markdownSummary);
}
}

private void postDetailedIssuesReply(
OkHttpClient httpClient,
VcsRepoInfo vcsRepoInfo,
Long pullRequestNumber,
String parentCommentId,
String detailedIssuesMarkdown
) throws IOException {
try {
log.debug("Posting detailed issues as reply to comment {} on PR {}", parentCommentId, pullRequestNumber);

CommentOnBitbucketCloudAction commentAction = new CommentOnBitbucketCloudAction(
httpClient,
vcsRepoInfo,
pullRequestNumber
);

String content = detailedIssuesMarkdown + "\n\n" + CODECROW_ISSUES_MARKER;
commentAction.postCommentReply(parentCommentId, content);

log.debug("Posted detailed issues reply to PR {}", pullRequestNumber);
} catch (Exception e) {
log.warn("Failed to post detailed issues as reply, will be included in annotations: {}", e.getMessage());
}
}

Expand Down
Loading