Skip to content
Merged
6 changes: 5 additions & 1 deletion .github/workflows/osv-scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest
- name: Install osv-scanner
run: |
curl -fsSL https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64 -o /usr/local/bin/osv-scanner
chmod +x /usr/local/bin/osv-scanner
osv-scanner --version
- run: mkdir -p security_issues
- run: osv-scanner scan source --recursive --format json --config osv-scanner.toml --no-call-analysis=go --experimental-exclude=debug --experimental-exclude=scripts --experimental-exclude=tests --experimental-exclude=.livereview_pgdata --experimental-exclude=.lrdata --experimental-exclude=livereview_pgdata --experimental-exclude=lrdata . > security_issues/osv-scanner-ci.json
- uses: actions/upload-artifact@v4
Expand Down
49 changes: 49 additions & 0 deletions docs/integrations/gitea/gitea_issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Gitea Issues

This doc tracks Gitea integration issues and their implemented solutions.

## Resolved Issues

### 1. Missing Severity Level in Initial Comments
**Issue:**
When the AI posted code reviews on a Gitea PR, the severity level (e.g., `**Severity: critical**`) was missing from the comment block.
**Solution:**
The Gitea provider previously posted raw comment content. This was fixed by introducing `formatGiteaComment` in [internal/providers/gitea/gitea_provider.go#L274](internal/providers/gitea/gitea_provider.go#L274). This function standardizes the format to match the GitHub/GitLab providers, ensuring that severity and suggestions are properly injected and sanitized before the comment is posted.

### 2. General Replies Displacing Inline Threading
**Issue:**
When a user replied to an inline review comment on Gitea, the bot's subsequent reply broke out of the inline discussion and was posted at the very bottom of the PR timeline as a quoted general comment.
**Solution:**
Gitea webhooks for replies often omit the exact `position` line data while retaining the `review_id`. The reply routing logic in [internal/provider_output/gitea/api_client.go#L59](internal/provider_output/gitea/api_client.go#L59) was updated to aggressively trigger metadata enrichment whenever an inline reply lacks `position`. The [enrichCommentMetadata](internal/provider_output/gitea/api_client.go#L220) function now dynamically fetches the parent comment's line coordinates to ensure the bot responds properly within the inline thread rather than falling back to a general quote block.

### 3. Inline Comments Disregarded as PR Requests
**Issue:**
Whenever a user tried to comment inline on the code, Gitea would send a webhook that the system incorrectly disregarded as a general PR request, completely ignoring the comment content.
**Solution:**
This occurred because Gitea assigns the `reviewed` action to `pull_request_review_comment` webhooks when an inline comment is created. The webhook parser ([internal/provider_input/gitea/gitea_conversion.go#L83](internal/provider_input/gitea/gitea_conversion.go#L83)) was strictly filtering out any action other than `created`. The logic was updated to explicitly process `reviewed` actions and intelligently fall back to the `Review` object if the raw `Comment` body was omitted in the payload. The unified processor also handles this special case in [internal/api/unified_processor_v2.go#L88](internal/api/unified_processor_v2.go#L88).
## TODO

### 4. Race Condition Handling for Multiple Bot Comments
**Issue:**
When livereview posts multiple comments in quick succession in response to a single user comment, the enrichment logic may select an earlier, incomplete comment instead of the most recent and comprehensive one. This can lead to processing outdated or less detailed responses.

**Current Status:**
- ✅ **Implemented:** Enhanced enrichment logic to collect all suitable comments and prioritize by timestamp
- ✅ **Fixed:** Removed early break after finding first suitable comment
- ✅ **Added:** Content comparison to handle duplicate comment bodies
- 🔄 **In Progress:** Testing and monitoring for race condition scenarios

**Solution Details:**
1. **Collect all suitable comments** instead of breaking after first match
2. **Prioritize by latest timestamp** using `UpdatedAt` field comparison
3. **Skip duplicate content** using `seenContents` map to prevent race processing
4. **Select most comprehensive** response when multiple comments contain bot mention

**Files Modified:**
- `internal/provider_input/gitea/gitea_provider.go` - Updated enrichment logic in `FetchMergeRequestData`
- `internal/provider_input/gitea/gitea_types.go` - Added `DiffHunk` field to `GiteaReviewComment` struct

**Next Steps:**
- Monitor webhook processing logs for race condition scenarios
- Verify that latest/most comprehensive comment is consistently selected
- Consider additional heuristics if timestamp-based selection proves insufficient
62 changes: 39 additions & 23 deletions internal/api/unified_processor_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
gitlabmentions "github.com/livereview/internal/providers/gitlab"
gl "github.com/livereview/internal/providers/gitlab"
"github.com/livereview/internal/reviewmodel"
networkbitbucket "github.com/livereview/network/providers/bitbucket"
networkgithub "github.com/livereview/network/providers/github"
networkgitea "github.com/livereview/network/providers/gitea"
)

Expand Down Expand Up @@ -85,6 +87,16 @@ func (p *UnifiedProcessorV2Impl) CheckResponseWarrant(event UnifiedWebhookEventV

commentBody := strings.TrimSpace(event.Comment.Body)
if commentBody == "" {
// Gitea Special Case: 'reviewed' action often has empty body but carries inline comments
if event.Provider == "gitea" && event.Comment.Metadata != nil && event.Comment.Metadata["action"] == "reviewed" {
log.Printf("[DEBUG] Empty body for Gitea 'reviewed' action; allowing warrant for review scan")
return true, ResponseScenarioV2{
Type: "review_submission",
Reason: "Gitea review submission (requires scan of inline comments)",
Confidence: 0.5, // Low confidence until we find a mention in the scan
Comment thread
Amazing-Stardom marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this confidence value? where it is used and why it is 0.5?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitea doesn't have proper webhooks.

whenever new comment posted, it doesn't have any data in the webhook.

So, there is a 50% probability whether the reply is needed or not based on fetching the recent comment posted in the PR.

So, it is set up to 0.5 to identify the confidence level of the comment.

Metadata: map[string]interface{}{"action": "reviewed"},
}
}
return hardFailure("comment body empty; cannot evaluate warrant", "event.comment.body")
}

Expand Down Expand Up @@ -714,7 +726,7 @@ func (p *UnifiedProcessorV2Impl) checkGitHubParentCommentAuthor(event UnifiedWeb
apiURL = fmt.Sprintf("https://api.github.com/repos/%s/issues/comments/%s", repoFullName, parentID)
}

req, err := http.NewRequest("GET", apiURL, nil)
req, err := networkgithub.NewRequest(http.MethodGet, apiURL, nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change to networkGitHub? What is the difference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be related to Gitea issues, but this change has to be done due to rule 2 in Copilot instructions and security audits.

  1. All the network operations should be defined in a single module with proper security doc mentioned in the network. md
  2. This rule mainly focuses on any HTTP request that should be done through a single model.

Rule 2: https://github.com/HexmosTech/LiveReview/blob/master/.github/copilot-instructions.md#rule-2-code-organization-and-status-docs

if err != nil {
return false, err
}
Expand All @@ -723,8 +735,8 @@ func (p *UnifiedProcessorV2Impl) checkGitHubParentCommentAuthor(event UnifiedWeb
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "LiveReview-Bot")

client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
client := networkgithub.NewHTTPClient(30 * time.Second)
resp, err := networkgithub.Do(client, req)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -811,16 +823,16 @@ func (p *UnifiedProcessorV2Impl) checkBitbucketParentCommentAuthor(event Unified
}

apiURL := fmt.Sprintf("https://api.bitbucket.org/2.0/repositories/%s/%s/pullrequests/%s/comments/%s", workspace, repository, prNumber, parentID)
req, err := http.NewRequest("GET", apiURL, nil)
req, err := networkbitbucket.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
if err != nil {
return false, err
}

req.SetBasicAuth(email, token.PatToken)
req.Header.Set("Accept", "application/json")

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
client := networkbitbucket.NewHTTPClient(10 * time.Second)
resp, err := networkbitbucket.Do(client, req)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -1586,32 +1598,36 @@ func (p *UnifiedProcessorV2Impl) buildGiteaArtifactFromEvent(ctx context.Context
log.Printf("[DEBUG] Building Gitea artifact for repo=%s org_id=%d", event.Repository.FullName, orgID)

patchURL := ""
if event.MergeRequest.Metadata != nil {
if rawPatchURL, ok := event.MergeRequest.Metadata["patch_url"].(string); ok {
patchURL = strings.TrimSpace(rawPatchURL)
}
// Construct the official API patch URL which reliably accepts PAT authentication.
// We prioritize this over the UI-based patch_url often found in webhook metadata.
baseURL := strings.TrimRight(token.ProviderURL, "/")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference here from previous patchURL construction?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old version was directly fetching the patch URL in the UI, which was added to send in the webhook.
which may need auth to view the page.

As I checked a few cases, the path URL was not sent in the webhook from the Gitea side.
So, I just reordered to use an actual API call with a PAT token to view diff.

repoFullName := event.Repository.FullName
prNumber := event.MergeRequest.Number

if baseURL != "" && repoFullName != "" && prNumber > 0 {
patchURL = fmt.Sprintf("%s/api/v1/repos/%s/pulls/%d.patch",
baseURL, repoFullName, prNumber)
}
if patchURL == "" {
// Use provider URL and metadata to construct API patch URL reliably.
// We avoid brittle WebURL path manipulation by using known metadata.
baseURL := strings.TrimRight(token.ProviderURL, "/")
repoFullName := event.Repository.FullName
prNumber := event.MergeRequest.Number

if baseURL != "" && repoFullName != "" && prNumber > 0 {
patchURL = fmt.Sprintf("%s/api/v1/repos/%s/pulls/%d.patch",
baseURL, repoFullName, prNumber)
} else if webURL := strings.TrimSpace(event.MergeRequest.WebURL); webURL != "" {
// Fallback: Gitea supports appending .patch to the UI URL.
// This is a safe last-resort if API-specific metadata is incomplete.
patchURL = strings.TrimRight(webURL, "/") + ".patch"
// Fallback to metadata or web URL if API URL construction is not possible
if patchURL == "" {
if event.MergeRequest.Metadata != nil {
if rawPatchURL, ok := event.MergeRequest.Metadata["patch_url"].(string); ok {
patchURL = strings.TrimSpace(rawPatchURL)
}
}
if patchURL == "" {
if webURL := strings.TrimSpace(event.MergeRequest.WebURL); webURL != "" {
patchURL = strings.TrimRight(webURL, "/") + ".patch"
}
}
}
if patchURL == "" {
return nil, fmt.Errorf("missing gitea patch URL")
}

client := networkgitea.NewHTTPClient(20 * time.Second)
log.Printf("[DEBUG] Fetching Gitea patch from URL: %s", patchURL)
patchContent, err := networkgitea.FetchPatchContent(ctx, client, patchURL, token.PatToken)
if err != nil {
return nil, fmt.Errorf("failed to fetch gitea patch: %w", err)
Expand Down
9 changes: 7 additions & 2 deletions internal/api/webhook_orchestrator_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ func (wo *WebhookOrchestratorV2) processEventAsync(ctx context.Context, event *U
case "discussion_reply":
log.Printf("[INFO] Discussion reply scenario - handling as comment reply")
wo.handleCommentReplyFlow(processingCtx, event, provider, timeline, orgID)
case "content_trigger":
Comment thread
Amazing-Stardom marked this conversation as resolved.
log.Printf("[INFO] Content trigger scenario - handling as comment reply")
case "content_trigger", "review_submission":
log.Printf("[INFO] %s scenario - handling as comment reply", scenario.Type)
wo.handleCommentReplyFlow(processingCtx, event, provider, timeline, orgID)
default:
log.Printf("[WARN] Unknown response scenario: %s", scenario.Type)
Expand All @@ -325,6 +325,11 @@ func (wo *WebhookOrchestratorV2) processEventAsync(ctx context.Context, event *U
func (wo *WebhookOrchestratorV2) handleCommentReplyFlow(ctx context.Context, event *UnifiedWebhookEventV2, provider WebhookProviderV2, timeline *UnifiedTimelineV2, orgID int64) {
log.Printf("[INFO] Processing comment reply flow for event %s/%s", event.EventType, event.Provider)

if event.Comment == nil || strings.TrimSpace(event.Comment.Body) == "" {
Comment thread
Amazing-Stardom marked this conversation as resolved.
log.Printf("[INFO] Skipping comment reply flow: comment body is empty (even after potential enrichment)")
return
}

// Generate AI response
response, learning, usage, err := wo.unifiedProcessor.ProcessCommentReply(ctx, *event, timeline, orgID)
if err != nil {
Expand Down
40 changes: 35 additions & 5 deletions internal/provider_input/gitea/gitea_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,26 @@ func ConvertGiteaPullRequestReviewCommentEvent(body []byte) (*UnifiedWebhookEven
return nil, fmt.Errorf("failed to parse Gitea PR review comment webhook: %w", err)
}

if payload.Action != "created" {
Comment thread
Amazing-Stardom marked this conversation as resolved.
if payload.Action != "created" && payload.Action != "reviewed" {
log.Printf("[DEBUG] Ignoring Gitea pull_request_review_comment action: %s", payload.Action)
return nil, fmt.Errorf("pull_request_review_comment event ignored (action=%s)", payload.Action)
}

if payload.Comment == nil {
return nil, fmt.Errorf("comment is nil in payload")
Comment thread
Amazing-Stardom marked this conversation as resolved.
if payload.Action == "reviewed" && payload.Review != nil {
// Fallback to review content if it's a review event and comment is not explicitly provided
payload.Comment = &GiteaV2Comment{
ID: payload.Review.ID,
HTMLURL: payload.Review.HTMLURL,
User: payload.Review.User,
Body: payload.Review.Content,
CreatedAt: payload.Review.CreatedAt,
UpdatedAt: payload.Review.UpdatedAt,
ReviewID: payload.Review.ID,
}
} else {
return nil, fmt.Errorf("comment is nil in payload")
}
}
if payload.PullRequest == nil {
return nil, fmt.Errorf("pull_request is nil in payload")
Expand All @@ -108,6 +121,11 @@ func ConvertGiteaPullRequestReviewCommentEvent(body []byte) (*UnifiedWebhookEven
Actor: convertGiteaUserToUnified(payload.Sender),
}

// Tag the action in metadata so the processor knows how to handle empty summaries
Comment thread
Amazing-Stardom marked this conversation as resolved.
if event.Comment != nil && event.Comment.Metadata != nil {
event.Comment.Metadata["action"] = payload.Action
}

return event, nil
}

Expand Down Expand Up @@ -182,6 +200,13 @@ func convertGiteaCommentToUnified(comment *GiteaV2Comment) *UnifiedCommentV2 {
}

unified.Metadata["comment_type"] = "review_comment"
} else if comment.ReviewID > 0 || comment.InReplyTo > 0 {
Comment thread
Amazing-Stardom marked this conversation as resolved.
// No path/position, but associated with a review or replying to a review comment.
// This happens when a user clicks "Reply" inside an inline review discussion —
// Gitea fires an issue_comment event, but the reply belongs to the review thread.
unified.Metadata["comment_type"] = "review_reply"
log.Printf("[DEBUG] Gitea comment %d tagged as review_reply: review_id=%d, in_reply_to=%d",
comment.ID, comment.ReviewID, comment.InReplyTo)
} else {
unified.Metadata["comment_type"] = "issue_comment"
}
Expand All @@ -190,6 +215,7 @@ func convertGiteaCommentToUnified(comment *GiteaV2Comment) *UnifiedCommentV2 {
if comment.InReplyTo > 0 {
inReplyTo := strconv.FormatInt(comment.InReplyTo, 10)
unified.InReplyToID = &inReplyTo
unified.Metadata["in_reply_to"] = comment.InReplyTo
}

// Review context
Expand Down Expand Up @@ -287,12 +313,16 @@ func convertGiteaUserToUnified(user *GiteaV2User) UnifiedUserV2 {
// Gitea uses "LEFT" (old/base) and "RIGHT" (new/head)
// Unified types use "old", "new", "context"
func convertGiteaSideToLineType(side string) string {
log.Printf("[DEBUG] convertGiteaSideToLineType: input side='%s'", side)
result := "new" // Default to new side
switch strings.ToUpper(side) {
case "LEFT":
return "old"
result = "old"
case "RIGHT":
return "new"
result = "new"
default:
return "new" // Default to new side
result = "new" // Default to new side
}
log.Printf("[DEBUG] convertGiteaSideToLineType: result='%s' for input='%s'", result, side)
return result
}
Loading
Loading