Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .code-reviewer.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ comment_mode: notes

# update_description: false # Inject review summary into MR/PR description

# output_json: false # Output results as JSON instead of posting

# summarize: false # Generate MR summary instead of review
# summary_update_description: false # Update MR description with the generated summary

# intent_review: true # Enable two-pass intent-aware review (default: true in CI)

# How to handle diffs that exceed the model's context window.
# fail: error out with a helpful message (default, forces smaller MRs)
# split: auto-split into chunks and merge results
Expand Down Expand Up @@ -80,5 +87,4 @@ extra_rules: |
# --models / --consensus-threshold (multi-model consensus)
# --api-key / REVIEW_API_KEY
# --no-context, --incremental, --sarif, --no-color
# --summarize
# See README.md for the full CLI flags and env vars reference.
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.6.0] — 2026-07-27

### Added
- Multi-line comment and suggestion support (#46)
- Inject review summary into MR/PR description (#45)
- Platform-specific code suggestion rendering (#42)
- Opt-in resolve mode for previous review cleanup (`cleanup_mode`) (#43)
- GitLab Draft Notes for single-notification reviews
- GitHub VCS client and platform auto-detection (#35)
- GitHub Review API hardening against production edge cases (#41)

### Changed
- Add `SubmitReview` to `VCSClient`, move orchestration into client
- Address tech debt from CodeRabbit reviews (#36, #37, #38, #39)

### Fixed
- Clear `GITHUB_ACTIONS` in `ci_without_project_id` test

## [0.5.2] — 2026-07-25

### Added
- 10 integration tests for end-to-end pipeline verification (#33)

## [0.5.1] — 2026-07-25

### Added
- Pre-push hook with install/uninstall commands
- Core.hooksPath test coverage

### Changed
- Reduce false positives with 5 prompt quality improvements

### Fixed
- CI lint failures and CodeRabbit review findings

## [0.5.0] — 2026-07-20

### Added
- `--fix` mode — auto-apply suggestions to working tree
- `--explain` mode — explain diffs instead of reviewing
- Two-pass intent-aware review (v0.6 preview)
- Auto-summary mode (`--summarize`)
- `REVIEW.md` — repo-level review instructions with highest prompt priority

### Changed
- Consolidate fix tests into table-driven format

### Fixed
- Improve suggestion quality with prompt rules and sanitization
- Critical bugs in suggestion sanitizer
- Unconditional count assertions and fail on `ReadFile` error
- Security: adversarial input guardrails + terminal sanitization
10 changes: 8 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Roadmap

Current status: **v0.5.1Prompt quality, pre-push hook, intent-aware review, explain, and fix**
Current status: **v0.6.0GitHub support, multi-line comments, code suggestions, and review lifecycle**

## ✅ v0.1 — Foundation (Done)

Expand Down Expand Up @@ -52,7 +52,13 @@ Current status: **v0.5.1 — Prompt quality, pre-push hook, intent-aware review,

## 🔜 v0.6 — Platform Expansion

- [ ] **GitHub support** — `internal/github/` client implementing `VCSClient` interface, PR review comments, GitHub Actions integration
- [x] **GitHub support** — `internal/github/` client implementing `VCSClient` interface, PR review comments, GitHub Actions integration
- [x] **Code suggestions** — Platform-specific suggestion rendering
- [x] **Resolve discussions mode** — Opt-in `cleanup_mode`
- [x] **MR/PR description update** — Update with review summary
- [x] **Multi-line comments and suggestions**
- [x] **GitLab Draft Notes API** — For single-notification reviews
- [x] **SSRF hardening and tech debt cleanup**
- [ ] **GitHub Actions workflow** — Drop-in `.github/workflows/code-review.yml` example
- [ ] **Pre-commit.com listing** — Register in the pre-commit hook registry for discovery

Expand Down
30 changes: 30 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -965,3 +965,33 @@ func TestLoad_InvalidCleanupMode(t *testing.T) {
t.Errorf("error = %q, want mention of 'invalid cleanup-mode'", err.Error())
}
}

func TestLoadGitHubCIEnv(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"code-reviewer", "--ci"}

t.Setenv("GOOGLE_CLOUD_PROJECT", "test-project")
t.Setenv("CI_PROJECT_ID", "") // Clear competing GitLab CI variable.
t.Setenv("GITHUB_ACTIONS", "true")
t.Setenv("GITHUB_REPOSITORY", "owner/repo")
t.Setenv("GITHUB_EVENT_NAME", "pull_request")
t.Setenv("GITHUB_TOKEN", "ghp_test")
// Also need GITHUB_REF so it parses MR IID
t.Setenv("GITHUB_REF", "refs/pull/42/merge")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

cfg, err := Load()
if err != nil {
t.Fatalf("Load() unexpected error: %v", err)
}

if cfg.Platform != "github" {
t.Errorf("Platform = %q, want 'github'", cfg.Platform)
}
if cfg.CIProjectID != "owner/repo" {
t.Errorf("CIProjectID = %q, want 'owner/repo'", cfg.CIProjectID)
}
if cfg.CIMergeRequestID != "42" {
t.Errorf("CIMergeRequestID = %q, want '42'", cfg.CIMergeRequestID)
}
}
127 changes: 127 additions & 0 deletions internal/github/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -847,3 +847,130 @@ func TestGetPRChanges_PaginatedFiles(t *testing.T) {
}
}

func TestGetDescription_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"body": "PR description"}`))
}))
defer srv.Close()

client := NewClient(srv.URL, "token")
desc, err := client.GetDescription(context.Background(), "owner/repo", "1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use cancellable test contexts instead of context.Background().

All four client calls in this test function use a non-cancellable root context. Use a cancellable test context here so request cancellation and deadlines can propagate through the HTTP requests.

Also applies to: 883, 916, 957.

🤖 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/github/client_test.go` at line 858, Update the test function
containing the GetDescription call and the other client calls at the referenced
locations to use a cancellable test context instead of context.Background().
Create and manage the context with the test’s cleanup lifecycle, then pass it to
each client method so cancellation and deadlines propagate through the HTTP
requests.

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if desc != "PR description" {
t.Errorf("GetDescription() = %q, want 'PR description'", desc)
}
}

func TestSetDescription_Success(t *testing.T) {
var gotBody string
var gotMethod string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
var req struct {
Body string `json:"body"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
gotBody = req.Body
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()

client := NewClient(srv.URL, "token")
err := client.SetDescription(context.Background(), "owner/repo", "1", "new desc")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotMethod != http.MethodPatch {
t.Errorf("Method = %q, want PATCH", gotMethod)
}
if gotBody != "new desc" {
t.Errorf("Body = %q, want 'new desc'", gotBody)
}
}

func TestSubmitReview_MultiLine(t *testing.T) {
var gotReq CreateReviewRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
return
}
_ = json.NewDecoder(r.Body).Decode(&gotReq)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()

client := NewClient(srv.URL, "token")
req := vcs.SubmitReviewRequest{
Summary: "Summary",
Comments: []vcs.ReviewComment{
{Path: "a.go", Line: 10, EndLine: 15, Body: "msg1"},
},
}
err := client.SubmitReview(context.Background(), "owner/repo", "1", req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(gotReq.Comments) != 1 {
t.Fatalf("expected 1 comment, got %d", len(gotReq.Comments))
}
comment := gotReq.Comments[0]
if comment.Line != 15 {
t.Errorf("Line = %d, want 15", comment.Line)
}
if comment.StartLine == nil || *comment.StartLine != 10 {
t.Errorf("StartLine = %v, want 10", comment.StartLine)
}
if comment.Side != "RIGHT" {
t.Errorf("Side = %q, want RIGHT", comment.Side)
}
}

func TestSubmitReview_SingleLine(t *testing.T) {
var gotReq CreateReviewRequest
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
return
}
_ = json.NewDecoder(r.Body).Decode(&gotReq)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()

client := NewClient(srv.URL, "token")
req := vcs.SubmitReviewRequest{
Summary: "Summary",
Comments: []vcs.ReviewComment{
{Path: "a.go", Line: 10, EndLine: 0, Body: "msg1"},
},
}
err := client.SubmitReview(context.Background(), "owner/repo", "1", req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(gotReq.Comments) != 1 {
t.Fatalf("expected 1 comment, got %d", len(gotReq.Comments))
}
comment := gotReq.Comments[0]
if comment.Line != 10 {
t.Errorf("Line = %d, want 10", comment.Line)
}
if comment.StartLine != nil {
t.Errorf("StartLine = %v, want nil", comment.StartLine)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if comment.Side != "RIGHT" {
t.Errorf("Side = %q, want RIGHT", comment.Side)
}
}

Loading
Loading