This issue is self-contained. For any design questions, the canonical reference is: docs/DESIGN-vcs-abstraction.md
(Parts 2, 3, and 4 are most relevant to this issue)
Base branch:feature/github-support — open all PRs against this branch.
Goal
Implement PRReader and FileReader for the GitHub client, with a configurable base URL to support both github.com and GitHub Enterprise (github.concur.com).
Background
review-bot needs to post reviews on GitHub and GitHub Enterprise PRs. The vcs.Client interface and all canonical types live in vcs/ (created in issue #78).
The GitHub client uses diff-position natively — ReviewComment.Position maps directly to the GitHub API position field with no translation needed.
Auth: Authorization: Bearer <token>
Base URL: configurable; empty string defaults to https://api.github.com. For GitHub Enterprise (github.concur.com), the base URL is https://github.concur.com/api/v3.
Work
github/client.go
funcNewClient(token,baseURLstring)*Client// empty baseURL defaults to https://api.github.com
HTTP helpers: set Authorization: Bearer <token>, Accept, Content-Type headers. Handle 429 with Retry-After.
github/pr.go
GetPullRequest — GET /repos/{owner}/{repo}/pulls/{number}
GetPullRequestDiff — same URL (GET /repos/{owner}/{repo}/pulls/{number}) with Accept: application/vnd.github.diff set per-request (overrides the default application/vnd.github+json). The response body is raw unified diff text — do not JSON-unmarshal it.
GetPullRequestFiles — GET /repos/{owner}/{repo}/pulls/{number}/files. Paginate through all pages (default 30/page, use ?per_page=100&page=N). For each file object, populate vcs.ChangedFile{Filename, Status, Patch} where Patch is the patch field from the response (the per-file unified diff hunk). Patch may be absent for binary files — set to "" in that case.
GetFileContentAtRef — GET /repos/{owner}/{repo}/contents/{path}?ref={ref}, base64-decode the content field. An empty ref omits the query parameter (uses default branch).
GetCommitStatuses — fetches both commit statuses and check runs, merges them into []vcs.CommitStatus:
Commit statuses:GET /repos/{owner}/{repo}/commits/{sha}/status returns a JSON envelope {state, statuses: [...]} — unmarshal the wrapper object and extract the statuses array. Map each entry: Context→context, Status→state, Description→description, TargetURL→target_url.
Check runs:GET /repos/{owner}/{repo}/commits/{sha}/check-runs (paginated). For each check run, map to vcs.CommitStatus{Context: name, Status: <mapped>, Description: conclusion, TargetURL: html_url}. Map check run conclusion to vcs.CommitStatus.Status as follows:
## Architecture Reference
> This issue is self-contained. For any design questions, the canonical reference is:
> [`docs/DESIGN-vcs-abstraction.md`](https://gitea.weiker.me/rodin/review-bot/src/branch/feature/github-support/docs/DESIGN-vcs-abstraction.md)
> (Parts 2, 3, and 4 are most relevant to this issue)
**Base branch:** `feature/github-support` — open all PRs against this branch.
---
## Goal
Implement `PRReader` and `FileReader` for the GitHub client, with a configurable base URL to support both `github.com` and GitHub Enterprise (`github.concur.com`).
## Background
review-bot needs to post reviews on GitHub and GitHub Enterprise PRs. The `vcs.Client` interface and all canonical types live in `vcs/` (created in issue #78).
The GitHub client uses diff-position natively — `ReviewComment.Position` maps directly to the GitHub API `position` field with no translation needed.
Auth: `Authorization: Bearer <token>`
Base URL: configurable; empty string defaults to `https://api.github.com`. For GitHub Enterprise (`github.concur.com`), the base URL is `https://github.concur.com/api/v3`.
## Work
### `github/client.go`
```go
func NewClient(token, baseURL string) *Client
// empty baseURL defaults to https://api.github.com
```
HTTP helpers: set `Authorization: Bearer <token>`, `Accept`, `Content-Type` headers. Handle 429 with `Retry-After`.
### `github/pr.go`
- `GetPullRequest` — `GET /repos/{owner}/{repo}/pulls/{number}`
- `GetPullRequestDiff` — same URL (`GET /repos/{owner}/{repo}/pulls/{number}`) with `Accept: application/vnd.github.diff` set **per-request** (overrides the default `application/vnd.github+json`). The response body is raw unified diff text — do not JSON-unmarshal it.
- `GetPullRequestFiles` — `GET /repos/{owner}/{repo}/pulls/{number}/files`. Paginate through all pages (default 30/page, use `?per_page=100&page=N`). For each file object, populate `vcs.ChangedFile{Filename, Status, Patch}` where `Patch` is the `patch` field from the response (the per-file unified diff hunk). `Patch` may be absent for binary files — set to `""` in that case.
- `GetFileContentAtRef` — `GET /repos/{owner}/{repo}/contents/{path}?ref={ref}`, base64-decode the `content` field. An empty `ref` omits the query parameter (uses default branch).
- `GetCommitStatuses` — fetches both commit statuses and check runs, merges them into `[]vcs.CommitStatus`:
**Commit statuses:** `GET /repos/{owner}/{repo}/commits/{sha}/status` returns a JSON envelope `{state, statuses: [...]}` — unmarshal the wrapper object and extract the `statuses` array. Map each entry: `Context→context`, `Status→state`, `Description→description`, `TargetURL→target_url`.
**Check runs:** `GET /repos/{owner}/{repo}/commits/{sha}/check-runs` (paginated). For each check run, map to `vcs.CommitStatus{Context: name, Status: <mapped>, Description: conclusion, TargetURL: html_url}`. Map check run `conclusion` to `vcs.CommitStatus.Status` as follows:
- `"success"` → `"success"`
- `"failure"`, `"action_required"`, `"timed_out"` → `"failure"`
- `"cancelled"`, `"skipped"`, `"neutral"` → `"success"` (non-blocking)
- `nil` / `"in_progress"` / `"queued"` → `"pending"`
Return the merged slice. Deduplication is not required (different context names).
### `github/files.go`
- `GetFileContent` — delegates to `GetFileContentAtRef(ctx, owner, repo, path, "")` (empty ref = default branch)
- `ListContents` — `GET /repos/{owner}/{repo}/contents/{path}` (no ref), returns array when path is a directory
### Unit tests
For each method: happy path, 404, 401, 429 (with retry), malformed response.
## Compile-time check
```go
var _ vcs.PRReader = (*Client)(nil)
var _ vcs.FileReader = (*Client)(nil)
```
(Full `vcs.Client` check happens after #81 adds Reviewer + Identity.)
## Exit criteria
- `go test ./github/...` passes for all PRReader + FileReader methods
- `NewClient(token, "")` uses `https://api.github.com`
- `NewClient(token, "https://github.concur.com/api/v3")` targets GHE correctly
- `GetFileContent` delegates to `GetFileContentAtRef` with empty ref
- `GetPullRequestFiles` returns all files for PRs with >30 changed files (pagination tested)
- `GetPullRequestFiles` populates `Patch` from the GitHub response `patch` field
- `GetCommitStatuses` returns results from both commit statuses and check-runs endpoints
## Out of scope
- Reviewer + Identity methods (issue #81)
- CLI flag wiring (issue #82)
- Gitea adapter (issue #79)
Update from codebase audit (issue #82): Two additional methods needed in the GitHub client:
GetFileContentAtRef(ctx, owner, repo, path, ref string) (string, error) — same as GetFileContent but with an explicit ref. GitHub: GET /repos/{owner}/{repo}/contents/{path}?ref={ref} + base64 decode (same endpoint as GetFileContent, just with the ref param populated).
GetCommitStatuses(ctx, owner, repo, sha string) ([]CommitStatus, error) — GitHub: GET /repos/{owner}/{repo}/commits/{sha}/status. Map the statuses array to []vcs.CommitStatus.
Add unit tests for both.
**Update from codebase audit (issue #82):** Two additional methods needed in the GitHub client:
1. `GetFileContentAtRef(ctx, owner, repo, path, ref string) (string, error)` — same as `GetFileContent` but with an explicit ref. GitHub: `GET /repos/{owner}/{repo}/contents/{path}?ref={ref}` + base64 decode (same endpoint as `GetFileContent`, just with the ref param populated).
2. `GetCommitStatuses(ctx, owner, repo, sha string) ([]CommitStatus, error)` — GitHub: `GET /repos/{owner}/{repo}/commits/{sha}/status`. Map the `statuses` array to `[]vcs.CommitStatus`.
Add unit tests for both.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Architecture Reference
Base branch:
feature/github-support— open all PRs against this branch.Goal
Implement
PRReaderandFileReaderfor the GitHub client, with a configurable base URL to support bothgithub.comand GitHub Enterprise (github.concur.com).Background
review-bot needs to post reviews on GitHub and GitHub Enterprise PRs. The
vcs.Clientinterface and all canonical types live invcs/(created in issue #78).The GitHub client uses diff-position natively —
ReviewComment.Positionmaps directly to the GitHub APIpositionfield with no translation needed.Auth:
Authorization: Bearer <token>Base URL: configurable; empty string defaults to
https://api.github.com. For GitHub Enterprise (github.concur.com), the base URL ishttps://github.concur.com/api/v3.Work
github/client.goHTTP helpers: set
Authorization: Bearer <token>,Accept,Content-Typeheaders. Handle 429 withRetry-After.github/pr.goGetPullRequest—GET /repos/{owner}/{repo}/pulls/{number}GetPullRequestDiff— same URL (GET /repos/{owner}/{repo}/pulls/{number}) withAccept: application/vnd.github.diffset per-request (overrides the defaultapplication/vnd.github+json). The response body is raw unified diff text — do not JSON-unmarshal it.GetPullRequestFiles—GET /repos/{owner}/{repo}/pulls/{number}/files. Paginate through all pages (default 30/page, use?per_page=100&page=N). For each file object, populatevcs.ChangedFile{Filename, Status, Patch}wherePatchis thepatchfield from the response (the per-file unified diff hunk).Patchmay be absent for binary files — set to""in that case.GetFileContentAtRef—GET /repos/{owner}/{repo}/contents/{path}?ref={ref}, base64-decode thecontentfield. An emptyrefomits the query parameter (uses default branch).GetCommitStatuses— fetches both commit statuses and check runs, merges them into[]vcs.CommitStatus:Commit statuses:
GET /repos/{owner}/{repo}/commits/{sha}/statusreturns a JSON envelope{state, statuses: [...]}— unmarshal the wrapper object and extract thestatusesarray. Map each entry:Context→context,Status→state,Description→description,TargetURL→target_url.Check runs:
GET /repos/{owner}/{repo}/commits/{sha}/check-runs(paginated). For each check run, map tovcs.CommitStatus{Context: name, Status: <mapped>, Description: conclusion, TargetURL: html_url}. Map check runconclusiontovcs.CommitStatus.Statusas follows:"success"→"success""failure","action_required","timed_out"→"failure""cancelled","skipped","neutral"→"success"(non-blocking)nil/"in_progress"/"queued"→"pending"Return the merged slice. Deduplication is not required (different context names).
github/files.goGetFileContent— delegates toGetFileContentAtRef(ctx, owner, repo, path, "")(empty ref = default branch)ListContents—GET /repos/{owner}/{repo}/contents/{path}(no ref), returns array when path is a directoryUnit tests
For each method: happy path, 404, 401, 429 (with retry), malformed response.
Compile-time check
(Full
vcs.Clientcheck happens after #81 adds Reviewer + Identity.)Exit criteria
go test ./github/...passes for all PRReader + FileReader methodsNewClient(token, "")useshttps://api.github.comNewClient(token, "https://github.concur.com/api/v3")targets GHE correctlyGetFileContentdelegates toGetFileContentAtRefwith empty refGetPullRequestFilesreturns all files for PRs with >30 changed files (pagination tested)GetPullRequestFilespopulatesPatchfrom the GitHub responsepatchfieldGetCommitStatusesreturns results from both commit statuses and check-runs endpointsOut of scope
Update from codebase audit (issue #82): Two additional methods needed in the GitHub client:
GetFileContentAtRef(ctx, owner, repo, path, ref string) (string, error)— same asGetFileContentbut with an explicit ref. GitHub:GET /repos/{owner}/{repo}/contents/{path}?ref={ref}+ base64 decode (same endpoint asGetFileContent, just with the ref param populated).GetCommitStatuses(ctx, owner, repo, sha string) ([]CommitStatus, error)— GitHub:GET /repos/{owner}/{repo}/commits/{sha}/status. Map thestatusesarray to[]vcs.CommitStatus.Add unit tests for both.
Split into sub-issues for focused review:
vcs/types.go,github/client.go,github/client_test.go) → PR feat(github): implement GitHub API client foundation (#101)github/pr.go,github/pr_test.go) → PR feat(github): implement PRReader interface (#102)github/files.go,github/files_test.go,github/conformance_test.go) → PR feat(github): implement FileReader interface (#103)Closing in favour of the sub-issues.