Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd6cd93bf0 | |||
| c889724dda | |||
| 1ac51669ed | |||
| 2e6f46f28d | |||
| 3fc31c0822 | |||
| 2b611dbd0b | |||
| 3abb611baf |
@@ -0,0 +1,268 @@
|
||||
# GitHub Support for review-bot
|
||||
|
||||
## Goal
|
||||
|
||||
AI code reviews on GitHub PRs using SAP AI Core as the LLM provider.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Auto-detection of platform (explicit `--provider` flag is fine)
|
||||
- Unifying into one abstraction layer for its own sake
|
||||
|
||||
## Constraints
|
||||
|
||||
1. **Same features on both platforms** — anything review-bot does on Gitea should work on GitHub
|
||||
2. **Testable** — small interfaces, dependency injection, no global state
|
||||
3. **Interface from working code** — extract from gitea/, don't invent in vacuum
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Feature Inventory
|
||||
|
||||
What does review-bot actually do?
|
||||
|
||||
### Core Review Flow
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Get PR metadata | Title, body, head SHA, base ref |
|
||||
| Get PR diff | Unified diff format |
|
||||
| Get PR files | List of changed files with status |
|
||||
| Get file content | Raw file at ref |
|
||||
| List directory | Enumerate files in path |
|
||||
| Post review | Body + inline comments + verdict |
|
||||
|
||||
### Review Management
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| List reviews | Get existing reviews on PR |
|
||||
| Delete review | Remove old review before re-posting |
|
||||
| Get authenticated user | Who am I? |
|
||||
|
||||
### Platform-Specific (not in shared interface)
|
||||
|
||||
| Feature | Gitea | GitHub |
|
||||
|---------|-------|--------|
|
||||
| Resolve comment | Yes | No equivalent |
|
||||
| Timeline API | Yes | No equivalent |
|
||||
|
||||
These stay on gitea.Client directly. Callers that need them type-assert.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: GitHub API Mapping
|
||||
|
||||
| Feature | Gitea API | GitHub API |
|
||||
|---------|-----------|------------|
|
||||
| Get PR | `GET /api/v1/repos/.../pulls/{n}` | `GET /repos/.../pulls/{n}` |
|
||||
| Get diff | `.diff` suffix | `Accept: application/vnd.github.diff` header |
|
||||
| Get files | `GET .../pulls/{n}/files` | Same |
|
||||
| Get file content | `GET .../raw/{path}?ref=` | `GET .../contents/{path}?ref=` + base64 decode |
|
||||
| List directory | `GET .../contents/{path}` | Same |
|
||||
| Post review | `POST .../pulls/{n}/reviews` | Same (adapter handles comment schema) |
|
||||
| List reviews | `GET .../pulls/{n}/reviews` | Same |
|
||||
| Delete review | `DELETE .../pulls/{n}/reviews/{id}` | Same |
|
||||
| Get user | `GET /api/v1/user` | `GET /user` |
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Interface Design
|
||||
|
||||
**Principle:** Extract from working gitea/ code. The interface is discovered, not invented.
|
||||
|
||||
### Small, role-based interfaces
|
||||
|
||||
```go
|
||||
// vcs/interfaces.go
|
||||
|
||||
type PRReader interface {
|
||||
GetPullRequest(ctx context.Context, owner, repo string, number int) (*PullRequest, error)
|
||||
GetPullRequestDiff(ctx context.Context, owner, repo string, number int) (string, error)
|
||||
GetPullRequestFiles(ctx context.Context, owner, repo string, number int) ([]ChangedFile, error)
|
||||
}
|
||||
|
||||
type FileReader interface {
|
||||
GetFileContent(ctx context.Context, owner, repo, path, ref string) (string, error)
|
||||
ListContents(ctx context.Context, owner, repo, path string) ([]ContentEntry, error)
|
||||
}
|
||||
|
||||
type Reviewer interface {
|
||||
PostReview(ctx context.Context, owner, repo string, number int, req ReviewRequest) (*Review, error)
|
||||
ListReviews(ctx context.Context, owner, repo string, number int) ([]Review, error)
|
||||
DeleteReview(ctx context.Context, owner, repo string, number int, reviewID int64) error
|
||||
}
|
||||
|
||||
type Identity interface {
|
||||
GetAuthenticatedUser(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// Client combines all for callers that need everything
|
||||
type Client interface {
|
||||
PRReader
|
||||
FileReader
|
||||
Reviewer
|
||||
Identity
|
||||
}
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
Use what gitea/ already has. Move to vcs/types.go or re-export.
|
||||
|
||||
```go
|
||||
type PullRequest struct { ... } // from gitea.PullRequest
|
||||
type ChangedFile struct { ... } // from gitea.ChangedFile
|
||||
type ContentEntry struct { ... } // from gitea.ContentEntry
|
||||
type Review struct { ... } // from gitea.Review
|
||||
type ReviewRequest struct { ... } // new, for PostReview input
|
||||
type ReviewComment struct { ... } // from gitea.ReviewComment
|
||||
```
|
||||
|
||||
### Adapter responsibilities
|
||||
|
||||
Each adapter (gitea, github) handles:
|
||||
- API URL construction
|
||||
- Auth header format (`token` vs `Bearer`)
|
||||
- Request/response mapping
|
||||
- Comment schema translation (line numbers, commit IDs, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Test Plan
|
||||
|
||||
### Unit Tests (mock HTTP)
|
||||
|
||||
```
|
||||
github/
|
||||
pr_test.go # TestGetPullRequest, TestGetDiff, TestGetFiles
|
||||
files_test.go # TestGetFileContent, TestListContents
|
||||
review_test.go # TestPostReview, TestListReviews, TestDeleteReview
|
||||
identity_test.go # TestGetAuthenticatedUser
|
||||
```
|
||||
|
||||
Per method: happy path, 404, 401, 429, malformed response.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Against github.com/aweiker/ai-core-review-bot:
|
||||
- Fetch real PR
|
||||
- Fetch real file
|
||||
- Post + delete review (clean up)
|
||||
|
||||
### End-to-End
|
||||
|
||||
Open PR on test repo, run full review-bot, verify review appears.
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Implementation Phases
|
||||
|
||||
### Phase 1: Extract interfaces from gitea/
|
||||
|
||||
**Work:**
|
||||
- Create `vcs/interfaces.go` with interfaces extracted from gitea/client.go signatures
|
||||
- Create `vcs/types.go` — move or alias types from gitea/
|
||||
- Verify gitea.Client satisfies vcs.Client (compile-time check)
|
||||
|
||||
**Exit criteria:** `var _ vcs.Client = (*gitea.Client)(nil)` compiles.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Gitea adapter (if needed)
|
||||
|
||||
**Work:**
|
||||
- If gitea.Client method signatures don't match exactly, create wrapper
|
||||
- Keep gitea/ working exactly as before
|
||||
|
||||
**Exit criteria:** Existing tests pass. No behavior change.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: GitHub client — PRReader
|
||||
|
||||
**Work:**
|
||||
- `github/client.go` — struct, constructor, HTTP helpers
|
||||
- `github/pr.go` — GetPullRequest, GetPullRequestDiff, GetPullRequestFiles
|
||||
- Unit tests
|
||||
|
||||
**Exit criteria:** `go test ./github/...` passes for PR methods.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: GitHub client — FileReader
|
||||
|
||||
**Work:**
|
||||
- `github/files.go` — GetFileContent, ListContents
|
||||
- Unit tests
|
||||
|
||||
**Exit criteria:** Unit tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: GitHub client — Reviewer + Identity
|
||||
|
||||
**Work:**
|
||||
- `github/review.go` — PostReview, ListReviews, DeleteReview
|
||||
- `github/identity.go` — GetAuthenticatedUser
|
||||
- Unit tests
|
||||
|
||||
**Exit criteria:** Unit tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Integration tests
|
||||
|
||||
**Work:**
|
||||
- `integration/github_test.go`
|
||||
- Test against real GitHub
|
||||
|
||||
**Exit criteria:** All integration tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Wire into cmd/review-bot
|
||||
|
||||
**Work:**
|
||||
- Add `--provider github|gitea` flag (default: gitea for backward compat)
|
||||
- Select client based on flag
|
||||
- Update to use vcs interfaces where it makes sense
|
||||
|
||||
**Exit criteria:**
|
||||
- `./review-bot --provider github ...` works
|
||||
- `./review-bot --provider gitea ...` works (same as before)
|
||||
- Existing Gitea workflows unchanged
|
||||
|
||||
---
|
||||
|
||||
### Phase 8: GitHub Actions workflow + releases
|
||||
|
||||
**Work:**
|
||||
- `.github/workflows/ci.yml` — test on PR
|
||||
- `.github/workflows/release.yml` — publish binary to GitHub releases
|
||||
- `.github/actions/review/action.yml` — composite action
|
||||
- Action downloads binary from github.com/aweiker/ai-core-review-bot releases
|
||||
|
||||
**Exit criteria:**
|
||||
- CI runs on github.com/aweiker/ai-core-review-bot
|
||||
- Release creates downloadable binary
|
||||
- Review action posts review successfully
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Decisions
|
||||
|
||||
| Question | Decision |
|
||||
|----------|----------|
|
||||
| Auth token | Workflow `GITHUB_TOKEN` (automatic) |
|
||||
| Binary distribution | GitHub releases on aweiker/ai-core-review-bot |
|
||||
| Comment schema | Adapter's job — translate ReviewComment to platform format |
|
||||
| Default provider | `gitea` for backward compatibility |
|
||||
| Shared types | vcs/types.go (extracted from gitea/) |
|
||||
| Platform-specific features | Stay on concrete client, not interface |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
8 phases. Start by extracting interfaces from working gitea/ code, not inventing them. GitHub implements the same interfaces. Each phase has clear exit criteria.
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build phase2
|
||||
|
||||
package vcs_test
|
||||
|
||||
import (
|
||||
"gitea.weiker.me/rodin/review-bot/gitea"
|
||||
"gitea.weiker.me/rodin/review-bot/vcs"
|
||||
)
|
||||
|
||||
// Compile-time assertion: documents the gap between gitea.Client and vcs.Client.
|
||||
// Guarded by the "phase2" build tag — enable once the Gitea adapter bridges these gaps:
|
||||
//
|
||||
// 1. PostReview signature mismatch:
|
||||
// gitea.Client: PostReview(ctx, owner, repo, number, event, body string, comments []gitea.ReviewComment)
|
||||
// vcs.Reviewer: PostReview(ctx, owner, repo, number, req vcs.ReviewRequest)
|
||||
//
|
||||
// 2. GetFileContent signature mismatch:
|
||||
// gitea.Client: GetFileContent(ctx, owner, repo, filepath string) [no ref; uses default branch]
|
||||
// vcs.FileReader: GetFileContent(ctx, owner, repo, path, ref string)
|
||||
// (gitea.Client has GetFileContentRef for the ref variant)
|
||||
//
|
||||
// 3. ReviewComment type mismatch:
|
||||
// gitea.ReviewComment uses NewPosition int64 (Gitea line-number convention)
|
||||
// vcs.ReviewComment uses Position int (GitHub diff-position convention)
|
||||
//
|
||||
// The Gitea adapter (Phase 2) will wrap gitea.Client to bridge these gaps.
|
||||
var _ vcs.Client = (*gitea.Client)(nil)
|
||||
@@ -0,0 +1,40 @@
|
||||
// Package vcs defines the shared VCS client interface and supporting types.
|
||||
// Platform adapters (gitea, github) implement these interfaces so the core
|
||||
// review logic can work with any VCS platform without platform-specific code.
|
||||
package vcs
|
||||
|
||||
import "context"
|
||||
|
||||
// PRReader can fetch pull request metadata, diffs, and changed files.
|
||||
type PRReader interface {
|
||||
GetPullRequest(ctx context.Context, owner, repo string, number int) (*PullRequest, error)
|
||||
GetPullRequestDiff(ctx context.Context, owner, repo string, number int) (string, error)
|
||||
GetPullRequestFiles(ctx context.Context, owner, repo string, number int) ([]ChangedFile, error)
|
||||
}
|
||||
|
||||
// FileReader can fetch file contents and list directory entries.
|
||||
type FileReader interface {
|
||||
GetFileContent(ctx context.Context, owner, repo, path, ref string) (string, error)
|
||||
ListContents(ctx context.Context, owner, repo, path string) ([]ContentEntry, error)
|
||||
}
|
||||
|
||||
// Reviewer can post, list, and delete pull request reviews.
|
||||
type Reviewer interface {
|
||||
PostReview(ctx context.Context, owner, repo string, number int, req ReviewRequest) (*Review, error)
|
||||
ListReviews(ctx context.Context, owner, repo string, number int) ([]Review, error)
|
||||
DeleteReview(ctx context.Context, owner, repo string, number int, reviewID int64) error
|
||||
}
|
||||
|
||||
// Identity can report who the authenticated user is.
|
||||
type Identity interface {
|
||||
GetAuthenticatedUser(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// Client is the full VCS interface: PR reads, file reads, review management, and identity.
|
||||
// Platform adapters (gitea, github) implement this interface.
|
||||
type Client interface {
|
||||
PRReader
|
||||
FileReader
|
||||
Reviewer
|
||||
Identity
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package vcs
|
||||
|
||||
// ReviewEvent is the event type for a pull request review action.
|
||||
// Adapters must translate these action constants to/from platform-native values.
|
||||
// For example, Gitea uses "APPROVED" as both action and state, while GitHub
|
||||
// uses "APPROVE" for the action and returns "approved" as the state.
|
||||
type ReviewEvent string
|
||||
|
||||
const (
|
||||
// ReviewEventApprove approves the pull request.
|
||||
ReviewEventApprove ReviewEvent = "APPROVE"
|
||||
// ReviewEventRequestChanges requests changes to the pull request.
|
||||
ReviewEventRequestChanges ReviewEvent = "REQUEST_CHANGES"
|
||||
// ReviewEventComment posts a review comment without approval or rejection.
|
||||
ReviewEventComment ReviewEvent = "COMMENT"
|
||||
)
|
||||
|
||||
// HeadRef identifies the source branch and latest commit of a pull request.
|
||||
type HeadRef struct {
|
||||
SHA string `json:"sha"`
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
|
||||
// UserInfo identifies a user by login name.
|
||||
type UserInfo struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
// PullRequest holds relevant PR metadata.
|
||||
type PullRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Head HeadRef `json:"head"`
|
||||
}
|
||||
|
||||
// ChangedFile represents a file modified in a PR.
|
||||
type ChangedFile struct {
|
||||
Filename string `json:"filename"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ContentEntry represents a file or directory entry from the contents API.
|
||||
type ContentEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"` // "file" or "dir"
|
||||
}
|
||||
|
||||
// Review represents a pull request review.
|
||||
type Review struct {
|
||||
ID int64 `json:"id"`
|
||||
Body string `json:"body"`
|
||||
User UserInfo `json:"user"`
|
||||
State string `json:"state"`
|
||||
Stale bool `json:"stale"`
|
||||
CommitID string `json:"commit_id"`
|
||||
}
|
||||
|
||||
// ReviewComment represents an inline comment in a review.
|
||||
// All adapters use GitHub diff-position convention:
|
||||
// - Position is a 1-indexed offset from the @@ hunk line in the unified diff.
|
||||
// - CommitID identifies the commit the comment is anchored to.
|
||||
// It is optional; omit (empty string) for review-level comments that are
|
||||
// not attached to a specific commit.
|
||||
//
|
||||
// Adapters are responsible for translating to/from platform-native formats
|
||||
// (e.g. Gitea uses line numbers; GitHub uses diff positions natively).
|
||||
type ReviewComment struct {
|
||||
Path string `json:"path"`
|
||||
Position int `json:"position"` // diff-position: 1-indexed offset from @@ hunk line
|
||||
CommitID string `json:"commit_id"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// ReviewRequest is the payload for posting a review.
|
||||
type ReviewRequest struct {
|
||||
// Body is the top-level review comment.
|
||||
Body string `json:"body"`
|
||||
// Event is the review action (approve, request changes, or comment).
|
||||
Event ReviewEvent `json:"event"`
|
||||
Comments []ReviewComment `json:"comments,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user