Skip to content

feat: SARIF 2.1.0 output format - #12

Merged
brucearctor merged 3 commits into
mainfrom
feat/sarif-output
Jul 9, 2026
Merged

feat: SARIF 2.1.0 output format#12
brucearctor merged 3 commits into
mainfrom
feat/sarif-output

Conversation

@brucearctor

@brucearctor brucearctor commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SARIF (Static Analysis Results Interchange Format) 2.1.0 JSON output support, enabling CI systems (GitHub Code Scanning, GitLab Security Dashboard) to display code review findings in their native security/code scanning tabs.

Usage

# Via flag
code-reviewer --diff --sarif results.sarif

# Via environment variable
export SARIF_OUTPUT=results.sarif
code-reviewer --diff

Changes

  • internal/config/config.go: Added SARIFOutput field, SARIF_OUTPUT env var, and --sarif <path> flag
  • internal/reviewer/sarif.go: SARIF 2.1.0 report builder with severity mapping (CRITICAL/HIGH→error, MEDIUM→warning, LOW→note), rule deduplication by category, and line clamping
  • internal/reviewer/sarif_test.go: Tests for severity mapping, empty findings, file I/O, and empty category fallback
  • internal/reviewer/reviewer.go: Wired SARIF output into the review pipeline (runs after terminal/JSON output)

SARIF output details

  • Conforms to SARIF v2.1.0 schema
  • Rules deduplicated by finding category
  • Empty categories default to "general"
  • Line 0 clamped to 1 (SARIF requires positive line numbers)
  • Compatible with github/codeql-action/upload-sarif

Summary by CodeRabbit

  • New Features
    • Added incremental review mode that limits checks to files changed between the latest two pushes, with automatic fallback to the full diff when data isn’t available.
    • Added SARIF 2.1.0 export for review results via new CLI flags and environment variables.
    • Enhanced CI support with automatic commit-range handling for improved incremental operation.
  • Bug Fixes
    • Improved file-path matching for incremental selection.
    • Ensured findings with line number 0 are exported with a valid SARIF start line.
  • Tests
    • Added coverage for SARIF generation/writing and incremental review scenarios (including first-push and fallback cases).

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ecaea169-ed79-40e5-bd74-c0b9a088f0fd

📥 Commits

Reviewing files that changed from the base of the PR and between 0faa9e4 and b8d4634.

📒 Files selected for processing (1)
  • internal/gitlab/client.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/gitlab/client.go

📝 Walkthrough

Walkthrough

This PR adds incremental MR review support and SARIF 2.1.0 output. Config gains Incremental, SARIFOutput, and CICommitBeforeSHA fields with env/flag wiring. The reviewer filters diffs to changed files via GitLab commit comparison, and a new SARIF module builds and writes reports with accompanying tests.

Changes

Incremental review and SARIF output

Layer / File(s) Summary
Config and compare API wiring
internal/config/config.go, internal/reviewer/interfaces.go, internal/gitlab/client.go, internal/reviewer/output_test.go
Adds Incremental, SARIFOutput, and CICommitBeforeSHA; loads env vars and CLI flags; extends the VCS interface and GitLab client with commit comparison; updates the output mock.
Incremental filtering and SARIF emission
internal/reviewer/reviewer.go
Caches MR versions, filters diffs to changed files in incremental CI mode, writes SARIF output when configured, and reuses cached versions for GitLab inline comments.
SARIF report model and writer
internal/reviewer/sarif.go, internal/reviewer/sarif_test.go
Defines SARIF 2.1.0 types, builds reports with deduped rules and severity mapping, writes indented JSON to disk, and tests report construction and file output.
Incremental review tests
internal/reviewer/reviewer_test.go
Expands the mock VCS and adds incremental review tests for normal flow, first-push handling, and fallback behavior on version or compare failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReviewerRun
  participant GitLabClient
  participant FilterByFiles
  participant WriteSARIF

  ReviewerRun->>GitLabClient: fetch MR versions
  GitLabClient-->>ReviewerRun: previous/current head SHA
  ReviewerRun->>GitLabClient: CompareCommits(previous, current)
  GitLabClient-->>ReviewerRun: changed file paths
  ReviewerRun->>FilterByFiles: filterByFiles(diffs, changedFiles)
  FilterByFiles-->>ReviewerRun: filtered diffs
  ReviewerRun->>WriteSARIF: WriteSARIF(SARIFOutput, reviewResult)
  WriteSARIF-->>ReviewerRun: write status
Loading

Possibly related PRs

  • OpticDiff/code-reviewer#5: Both PRs modify internal/reviewer/reviewer.go’s Run() flow around diff acquisition and filtering.
  • OpticDiff/code-reviewer#7: Both PRs modify internal/config/config.go to populate Config.Models from REVIEW_MODELS and adjust related CLI parsing.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding SARIF 2.1.0 output support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sarif-output

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/reviewer/reviewer.go (1)

175-211: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

SARIF output is skipped on GitLab post failure, and its error path loses the findings count.

Two issues in this block:

  1. If PostToGitLab fails, the function returns early at line 200, so the SARIF block (204-211) never runs — the CI security-tab artifact is lost even though the review itself succeeded. Given SARIF output is meant to feed CI code-scanning independently of comment posting, consider writing SARIF before/independently of the GitLab-post step (or on a best-effort basis even if posting fails).
  2. When WriteSARIF fails, the function returns 0 for the finding count (line 207) instead of len(allFindings), unlike the sibling error path at line 200 (return len(allFindings), fmt.Errorf(...)). Callers relying on the returned count (e.g., for exit-code gating) would incorrectly see zero findings despite err being non-nil.
🐛 Proposed fix
 	// Write SARIF if requested.
 	if r.cfg.SARIFOutput != "" {
 		if err := WriteSARIF(r.cfg.SARIFOutput, result); err != nil {
-			return 0, fmt.Errorf("writing SARIF: %w", err)
+			return len(allFindings), fmt.Errorf("writing SARIF: %w", err)
 		}
 		slog.Info("SARIF output written", "path", r.cfg.SARIFOutput)
 	}

Consider moving the SARIF-write block ahead of the GitLab-posting step (or wrapping the post in a way that doesn't early-return before SARIF is written) so CI code-scanning output isn't dependent on comment-posting success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewer/reviewer.go` around lines 175 - 211, SARIF generation is
currently blocked by an early return from the GitLab posting path, and the SARIF
error path drops the findings count. Update the output flow in reviewer.go’s
post-processing so WriteSARIF runs independently of PostToGitLab (or before it)
and is still attempted even if posting fails, and change the WriteSARIF failure
return to preserve len(allFindings) just like the PostToGitLab error path. Use
the existing PostToGitLab and WriteSARIF branches in the Step 7 output section
as the place to adjust the control flow.
🧹 Nitpick comments (2)
internal/reviewer/reviewer.go (1)

189-197: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant GetMRVersions call.

GetMRVersions is already fetched at line 80 for incremental filtering; it's fetched again here for discussion-mode comment placement. Cache and reuse the first result (when available) to avoid a duplicate GitLab API round-trip on every CI run.

♻️ Proposed fix: reuse versions fetched during incremental filtering
-	var diffs []diff.FileDiff
+	var diffs []diff.FileDiff
+	var mrVersions []gitlab.DiffVersion
 ...
 	if r.cfg.Incremental && r.cfg.CIMode && r.glClient != nil {
 		versions, verr := r.glClient.GetMRVersions(ctx, r.cfg.CIProjectID, r.cfg.CIMergeRequestID)
+		mrVersions = versions
 		...
 	}
 ...
 		var version *gitlab.DiffVersion
 		if r.cfg.CommentMode == config.CommentModeDiscussions {
-			versions, err := r.glClient.GetMRVersions(ctx, r.cfg.CIProjectID, r.cfg.CIMergeRequestID)
-			if err != nil {
-				slog.Warn("could not fetch MR versions, inline comments may fail", "error", err)
-			} else if len(versions) > 0 {
-				version = &versions[0]
-			}
+			if mrVersions == nil {
+				var err error
+				mrVersions, err = r.glClient.GetMRVersions(ctx, r.cfg.CIProjectID, r.cfg.CIMergeRequestID)
+				if err != nil {
+					slog.Warn("could not fetch MR versions, inline comments may fail", "error", err)
+				}
+			}
+			if len(mrVersions) > 0 {
+				version = &mrVersions[0]
+			}
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewer/reviewer.go` around lines 189 - 197, The review placement
logic in reviewer.go is making a redundant GetMRVersions call in the
comment-mode discussions branch. Reuse the versions already fetched earlier for
incremental filtering in reviewer logic instead of calling
r.glClient.GetMRVersions again, and pass the cached result into the version
selection used for inline/discussion comments. Keep the existing fallback
behavior when no versions are available, but avoid the extra GitLab API
round-trip.
internal/config/config.go (1)

109-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or reword the unused CI fields internal/config/config.go:109-112
CIDiffBaseSHA and CICommitBeforeSHA are only populated from environment variables and never consumed elsewhere; the “reserved for future incremental review” note is stale now that incremental review uses MR versions plus CompareCommits. Either delete the fields or update the comment to match the current behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/config.go` around lines 109 - 112, The CI config struct
includes stale unused fields, so either remove CIDiffBaseSHA and
CICommitBeforeSHA from the config definition or update their documentation to
reflect how incremental review actually works now. Check the config struct in
internal/config/config.go and any references to these symbols to confirm they
are not consumed elsewhere, then keep only the fields and comments that match
the current CompareCommits-based behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/reviewer/reviewer.go`:
- Around line 78-102: The incremental review path in reviewer.go now calls
CompareCommits on VCSClient, but the interface and test doubles are missing that
method. Add CompareCommits to VCSClient in interfaces.go, implement it on the
GitLab client, and update the reviewer_test.go and output_test.go mocks so they
satisfy the expanded interface and the package builds cleanly.

---

Outside diff comments:
In `@internal/reviewer/reviewer.go`:
- Around line 175-211: SARIF generation is currently blocked by an early return
from the GitLab posting path, and the SARIF error path drops the findings count.
Update the output flow in reviewer.go’s post-processing so WriteSARIF runs
independently of PostToGitLab (or before it) and is still attempted even if
posting fails, and change the WriteSARIF failure return to preserve
len(allFindings) just like the PostToGitLab error path. Use the existing
PostToGitLab and WriteSARIF branches in the Step 7 output section as the place
to adjust the control flow.

---

Nitpick comments:
In `@internal/config/config.go`:
- Around line 109-112: The CI config struct includes stale unused fields, so
either remove CIDiffBaseSHA and CICommitBeforeSHA from the config definition or
update their documentation to reflect how incremental review actually works now.
Check the config struct in internal/config/config.go and any references to these
symbols to confirm they are not consumed elsewhere, then keep only the fields
and comments that match the current CompareCommits-based behavior.

In `@internal/reviewer/reviewer.go`:
- Around line 189-197: The review placement logic in reviewer.go is making a
redundant GetMRVersions call in the comment-mode discussions branch. Reuse the
versions already fetched earlier for incremental filtering in reviewer logic
instead of calling r.glClient.GetMRVersions again, and pass the cached result
into the version selection used for inline/discussion comments. Keep the
existing fallback behavior when no versions are available, but avoid the extra
GitLab API round-trip.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1db3e234-7702-44c8-924b-5052c0abf0e7

📥 Commits

Reviewing files that changed from the base of the PR and between 71e27cc and 0e92bc9.

📒 Files selected for processing (4)
  • internal/config/config.go
  • internal/reviewer/reviewer.go
  • internal/reviewer/sarif.go
  • internal/reviewer/sarif_test.go

Comment on lines +78 to +102
// Step 2b: Incremental review — filter to only files changed in latest push.
if r.cfg.Incremental && r.cfg.CIMode && r.glClient != nil {
versions, verr := r.glClient.GetMRVersions(ctx, r.cfg.CIProjectID, r.cfg.CIMergeRequestID)
if verr != nil {
slog.Warn("failed to get MR versions for incremental review, falling back to full review", "error", verr)
} else if len(versions) > 1 {
// Compare previous version's head to current version's head.
prevHead := versions[1].HeadSHA
currHead := versions[0].HeadSHA
changedFiles, cerr := r.glClient.CompareCommits(ctx, r.cfg.CIProjectID, prevHead, currHead)
if cerr != nil {
slog.Warn("failed to compare commits for incremental review, falling back to full review", "error", cerr)
} else {
before := len(diffs)
diffs = filterByFiles(diffs, changedFiles)
slog.Info("incremental review",
"total_files", before,
"changed_files", len(changedFiles),
"reviewing", len(diffs),
)
}
} else {
slog.Info("first push to MR, performing full review")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the VCSClient interface and existing CompareCommits usages/mocks.
rg -n 'type VCSClient interface' -A 15 --type=go
rg -n 'CompareCommits' --type=go

Repository: OpticDiff/code-reviewer

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the interface and the reviewer/output mocks, then inspect only the relevant slices.
fd -a 'reviewer.go|reviewer_test.go|output_test.go' internal

printf '\n--- VCSClient / CompareCommits search ---\n'
rg -n 'type VCSClient interface|CompareCommits|mockVCS|outputMockVCS' internal --type=go || true

printf '\n--- reviewer.go relevant lines ---\n'
sed -n '1,180p' internal/reviewer/reviewer.go

printf '\n--- reviewer_test.go relevant lines ---\n'
sed -n '1,260p' internal/reviewer/reviewer_test.go

printf '\n--- output_test.go relevant lines ---\n'
sed -n '1,260p' internal/output_test.go

Repository: OpticDiff/code-reviewer

Length of output: 17527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- internal/reviewer/interfaces.go ---\n'
sed -n '1,220p' internal/reviewer/interfaces.go

printf '\n--- CompareCommits implementation search ---\n'
rg -n 'func .*CompareCommits|CompareCommits\(' internal --type=go || true

printf '\n--- internal/reviewer/output_test.go relevant slice ---\n'
sed -n '280,380p' internal/reviewer/output_test.go

printf '\n--- compile-time interface checks in tests ---\n'
rg -n 'var _ VCSClient = ' internal/reviewer --type=go || true

Repository: OpticDiff/code-reviewer

Length of output: 4459


Add CompareCommits to VCSClient
internal/reviewer/reviewer.go calls r.glClient.CompareCommits(...), but internal/reviewer/interfaces.go does not declare it and the test doubles in internal/reviewer/reviewer_test.go and internal/reviewer/output_test.go don’t implement it. Add the method to VCSClient and implement/update the GitLab client and mocks, or this package will not build.

🧰 Tools
🪛 GitHub Actions: CI / 1_Test.txt

[error] 87-87: go test failed due to compile error: r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🪛 GitHub Actions: CI / 2_Build.txt

[error] 87-87: go build failed: r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🪛 GitHub Actions: CI / Build

[error] 87-87: go build failed: r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🪛 GitHub Actions: CI / Test

[error] 87-87: go test failed: internal/reviewer/reviewer.go:87:37: r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🪛 GitHub Check: Build

[failure] 87-87:
r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🪛 GitHub Check: Lint

[failure] 87-87:
r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)) (typecheck)

🪛 GitHub Check: Test

[failure] 87-87:
r.glClient.CompareCommits undefined (type VCSClient has no field or method CompareCommits)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewer/reviewer.go` around lines 78 - 102, The incremental review
path in reviewer.go now calls CompareCommits on VCSClient, but the interface and
test doubles are missing that method. Add CompareCommits to VCSClient in
interfaces.go, implement it on the GitLab client, and update the
reviewer_test.go and output_test.go mocks so they satisfy the expanded interface
and the package builds cleanly.

Sources: Path instructions, Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/reviewer/reviewer_test.go (1)

1033-1241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a table-driven test for the incremental-review scenarios.

The four TestRun_Incremental* functions share the same cfg/ds scaffolding and differ only in mockVCS setup and assertions. Consolidating into a table-driven test would reduce duplication and make it easier to add new fallback scenarios later.

As per path instructions for **/*_test.go: "Verify table-driven tests, proper cleanup, and race condition safety."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewer/reviewer_test.go` around lines 1033 - 1241, The incremental
review tests are repetitive across the four TestRun_Incremental* cases, so
consolidate them into a table-driven test to reduce duplicated cfg and
mockDiffSource setup. Keep the shared Run/NewWithDiffSource scaffolding in one
helper or loop, and vary only the mockVCS behavior, expected calls, and
assertions for each scenario. Use the existing symbols
TestRun_IncrementalReview, TestRun_IncrementalReview_FirstPush,
TestRun_IncrementalReview_VersionErrorFallback, and
TestRun_IncrementalReview_CompareErrorFallback as the cases to fold together.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/gitlab/client.go`:
- Around line 53-76: The CompareCommits method in Client currently only checks
compare_timeout, so it can return an incomplete file list when GitLab marks
diffs as collapsed or too_large. Update the response struct in CompareCommits to
capture those diff-level flags from Diffs, and fail closed with an error if any
diff is incomplete before building the returned files slice. Keep the existing
behavior in c.get, resp.CompareTimeout handling, and the final file collection
logic, but add the new completeness check inside CompareCommits.

---

Nitpick comments:
In `@internal/reviewer/reviewer_test.go`:
- Around line 1033-1241: The incremental review tests are repetitive across the
four TestRun_Incremental* cases, so consolidate them into a table-driven test to
reduce duplicated cfg and mockDiffSource setup. Keep the shared
Run/NewWithDiffSource scaffolding in one helper or loop, and vary only the
mockVCS behavior, expected calls, and assertions for each scenario. Use the
existing symbols TestRun_IncrementalReview, TestRun_IncrementalReview_FirstPush,
TestRun_IncrementalReview_VersionErrorFallback, and
TestRun_IncrementalReview_CompareErrorFallback as the cases to fold together.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 58ef3368-3645-498f-99a1-3b5c2a3ff032

📥 Commits

Reviewing files that changed from the base of the PR and between 0e92bc9 and 0faa9e4.

📒 Files selected for processing (6)
  • internal/config/config.go
  • internal/gitlab/client.go
  • internal/reviewer/interfaces.go
  • internal/reviewer/output_test.go
  • internal/reviewer/reviewer.go
  • internal/reviewer/reviewer_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/reviewer/reviewer.go
  • internal/config/config.go

Comment thread internal/gitlab/client.go
@brucearctor
brucearctor merged commit c50c36f into main Jul 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant