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
Complete the GitHub client by implementing Reviewer and Identity, making github.Client a full vcs.Client.
Background
The GitHub client struct and HTTP helpers were created in issue #80. This issue adds the remaining interface methods.
The vcs.Client interface and all types live in vcs/ (created in issue #78). The canonical ReviewRequest.Event values are GitHub's strings: "APPROVE", "REQUEST_CHANGES", "COMMENT" — no translation needed for the GitHub client (it sends them directly to the API).
GitHub uses diff-position natively — ReviewComment.Position maps directly to the position field in the POST body. No translation needed.
ReviewComment.CommitID must be the PR head SHA. Callers are responsible for populating it (see issue #82).
Design note: DismissReview vs DeleteReview
GitHub does not allow deleting a submitted review (APPROVED, REQUEST_CHANGES, COMMENT) — only PENDING reviews can be deleted. Attempting to delete a submitted review returns 422.
review-bot replaces old reviews when reposting. The correct cross-platform primitive is DismissReview:
GitHub: PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals
Gitea adapter (issue #79): calls the underlying DeleteReview (Gitea allows full deletion)
DeleteReview stays on the interface for PENDING-only cleanup. Callers that need to replace a submitted review must use DismissReview.
Work
github/review.go
PostReview — POST /repos/{owner}/{repo}/pulls/{number}/reviews
event values are passed directly — they match GitHub's canonical values
ListReviews — GET /repos/{owner}/{repo}/pulls/{number}/reviews. Map each review to vcs.Review. Canonical State values — GitHub returns native strings that differ from the canonical vcs values; translate in the adapter:
GitHub API state
vcs.Review.State (canonical)
"APPROVED"
"APPROVED"
"CHANGES_REQUESTED"
"REQUEST_CHANGES"
"COMMENTED"
"COMMENT"
"DISMISSED"
"DISMISSED"
anything else
pass through as-is
The Gitea adapter must also translate to these same canonical values. Both adapters must agree so that main.go can compare review.State without knowing the provider.
Only works for PENDING reviews. On 422, return ErrCannotDeleteSubmittedReview (exported sentinel error defined in github/review.go). Callers in main.go do not need to check for this specifically — the supersede flow uses DismissReview, not DeleteReview, for submitted reviews.
DismissReview — PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals
## 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
Complete the GitHub client by implementing `Reviewer` and `Identity`, making `github.Client` a full `vcs.Client`.
## Background
The GitHub client struct and HTTP helpers were created in issue #80. This issue adds the remaining interface methods.
The `vcs.Client` interface and all types live in `vcs/` (created in issue #78). The canonical `ReviewRequest.Event` values are GitHub's strings: `"APPROVE"`, `"REQUEST_CHANGES"`, `"COMMENT"` — no translation needed for the GitHub client (it sends them directly to the API).
GitHub uses diff-position natively — `ReviewComment.Position` maps directly to the `position` field in the POST body. No translation needed.
`ReviewComment.CommitID` must be the PR head SHA. Callers are responsible for populating it (see issue #82).
## Design note: DismissReview vs DeleteReview
GitHub does **not** allow deleting a submitted review (APPROVED, REQUEST_CHANGES, COMMENT) — only PENDING reviews can be deleted. Attempting to delete a submitted review returns 422.
review-bot replaces old reviews when reposting. The correct cross-platform primitive is `DismissReview`:
- **GitHub**: `PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals`
- **Gitea adapter** (issue #79): calls the underlying `DeleteReview` (Gitea allows full deletion)
`DeleteReview` stays on the interface for PENDING-only cleanup. Callers that need to replace a submitted review must use `DismissReview`.
## Work
### `github/review.go`
- `PostReview` — `POST /repos/{owner}/{repo}/pulls/{number}/reviews`
- Body: `{ "commit_id": "...", "body": "...", "event": "APPROVE|REQUEST_CHANGES|COMMENT", "comments": [{"path", "position", "body"}] }`
- `event` values are passed directly — they match GitHub's canonical values
- `ListReviews` — `GET /repos/{owner}/{repo}/pulls/{number}/reviews`. Map each review to `vcs.Review`. **Canonical `State` values** — GitHub returns native strings that differ from the canonical vcs values; translate in the adapter:
| GitHub API `state` | `vcs.Review.State` (canonical) |
|--------------------|-------------------------------|
| `"APPROVED"` | `"APPROVED"` |
| `"CHANGES_REQUESTED"` | `"REQUEST_CHANGES"` |
| `"COMMENTED"` | `"COMMENT"` |
| `"DISMISSED"` | `"DISMISSED"` |
| anything else | pass through as-is |
The Gitea adapter must also translate to these same canonical values. Both adapters must agree so that `main.go` can compare `review.State` without knowing the provider.
- `DeleteReview` — `DELETE /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}`
- Only works for PENDING reviews. On 422, return `ErrCannotDeleteSubmittedReview` (exported sentinel error defined in `github/review.go`). Callers in `main.go` do not need to check for this specifically — the supersede flow uses `DismissReview`, not `DeleteReview`, for submitted reviews.
- `DismissReview` — `PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals`
- Body: `{"message": "<message>", "event": "DISMISS"}`
### `github/identity.go`
- `GetAuthenticatedUser` — `GET /user`, returns `login` field
### Compile-time check
```go
var _ vcs.Client = (*Client)(nil)
```
### Unit tests
- `PostReview`: happy path, 401, 422, malformed response
- `ListReviews`: happy path, 404, 401
- `DeleteReview`: happy path (PENDING), 422 → `ErrCannotDeleteSubmittedReview`
- `DismissReview`: happy path, 404, 401
- `GetAuthenticatedUser`: happy path, 401
## Exit criteria
- `var _ vcs.Client = (*github.Client)(nil)` compiles
- `go test ./github/...` passes for all methods
## Out of scope
- CLI flag wiring (issue #82)
- Gitea adapter (issue #79)
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
Complete the GitHub client by implementing
ReviewerandIdentity, makinggithub.Clienta fullvcs.Client.Background
The GitHub client struct and HTTP helpers were created in issue #80. This issue adds the remaining interface methods.
The
vcs.Clientinterface and all types live invcs/(created in issue #78). The canonicalReviewRequest.Eventvalues are GitHub's strings:"APPROVE","REQUEST_CHANGES","COMMENT"— no translation needed for the GitHub client (it sends them directly to the API).GitHub uses diff-position natively —
ReviewComment.Positionmaps directly to thepositionfield in the POST body. No translation needed.ReviewComment.CommitIDmust be the PR head SHA. Callers are responsible for populating it (see issue #82).Design note: DismissReview vs DeleteReview
GitHub does not allow deleting a submitted review (APPROVED, REQUEST_CHANGES, COMMENT) — only PENDING reviews can be deleted. Attempting to delete a submitted review returns 422.
review-bot replaces old reviews when reposting. The correct cross-platform primitive is
DismissReview:PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissalsDeleteReview(Gitea allows full deletion)DeleteReviewstays on the interface for PENDING-only cleanup. Callers that need to replace a submitted review must useDismissReview.Work
github/review.goPostReview—POST /repos/{owner}/{repo}/pulls/{number}/reviews{ "commit_id": "...", "body": "...", "event": "APPROVE|REQUEST_CHANGES|COMMENT", "comments": [{"path", "position", "body"}] }eventvalues are passed directly — they match GitHub's canonical valuesListReviews—GET /repos/{owner}/{repo}/pulls/{number}/reviews. Map each review tovcs.Review. CanonicalStatevalues — GitHub returns native strings that differ from the canonical vcs values; translate in the adapter:statevcs.Review.State(canonical)"APPROVED""APPROVED""CHANGES_REQUESTED""REQUEST_CHANGES""COMMENTED""COMMENT""DISMISSED""DISMISSED"The Gitea adapter must also translate to these same canonical values. Both adapters must agree so that
main.gocan comparereview.Statewithout knowing the provider.DeleteReview—DELETE /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}ErrCannotDeleteSubmittedReview(exported sentinel error defined ingithub/review.go). Callers inmain.godo not need to check for this specifically — the supersede flow usesDismissReview, notDeleteReview, for submitted reviews.DismissReview—PUT /repos/{owner}/{repo}/pulls/{number}/reviews/{review_id}/dismissals{"message": "<message>", "event": "DISMISS"}github/identity.goGetAuthenticatedUser—GET /user, returnsloginfieldCompile-time check
Unit tests
PostReview: happy path, 401, 422, malformed responseListReviews: happy path, 404, 401DeleteReview: happy path (PENDING), 422 →ErrCannotDeleteSubmittedReviewDismissReview: happy path, 404, 401GetAuthenticatedUser: happy path, 401Exit criteria
var _ vcs.Client = (*github.Client)(nil)compilesgo test ./github/...passes for all methodsOut of scope