feat(github): implement PRReader interface #102

Merged
aweiker merged 5 commits from issue-80-b-pr-reader into feature/github-support 2026-05-13 05:30:37 +00:00
Owner

Closes #99. Part of #80. Depends on #101.

Implement PRReader conformance on the GitHub client: GetPullRequest, GetPullRequestDiff, GetPullRequestFiles (paginated, populates Patch), GetCommitStatuses (merges commit statuses + check runs). Adds compile-time PRReader conformance check.

Files: github/pr.go, github/pr_test.go, partial github/conformance_test.go

Part 2 of 3 — see #101 (Client Foundation) and #100 (FileReader).

Closes #99. Part of #80. Depends on #101. Implement `PRReader` conformance on the GitHub client: `GetPullRequest`, `GetPullRequestDiff`, `GetPullRequestFiles` (paginated, populates Patch), `GetCommitStatuses` (merges commit statuses + check runs). Adds compile-time `PRReader` conformance check. Files: `github/pr.go`, `github/pr_test.go`, partial `github/conformance_test.go` Part 2 of 3 — see #101 (Client Foundation) and #100 (FileReader).
rodin added the wip label 2026-05-13 04:19:03 +00:00
rodin removed the wip label 2026-05-13 04:21:09 +00:00
sonnet-review-bot approved these changes 2026-05-13 04:22:04 +00:00
sonnet-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 3995fa31)

Sonnet Review

Summary

This PR implements the PRReader interface on the GitHub client with solid correctness, thorough test coverage, and follows project conventions well. CI passes. A few minor issues worth noting but none blocking.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 140 GetCommitStatuses fetches both commit statuses and check runs but the check-runs endpoint returns paginated results starting at page 1 unconditionally on every call. If the commit statuses endpoint returns 0 statuses (empty PR with no CI), the function still makes at least one check-runs request. This is fine, but the bigger issue is that the check-runs pagination uses maxPages (100) which is shared with GetPullRequestFiles. A PR could theoretically have >100 pages of check runs (100 * 100 = 10,000 check runs), but that's an extreme edge case. More practically: the maxPages constant in pr.go is package-level and shared between two logically separate pagination loops; a comment clarifying this would help, or separate named constants.
2 [MINOR] github/pr_test.go 296 TestGetCommitStatuses_CheckRunConclusions creates a new httptest.Server inside each subtest iteration but does not call t.Parallel() on the subtests. Each subtest also closes its server with defer srv.Close() which is correct, but spawning 9 servers sequentially when they could run in parallel is a minor inefficiency. More importantly, there is no test for the check-runs endpoint returning a 404 or error after statuses succeed (the documented behavior where partial results are not returned).
3 [MINOR] github/pr.go 191 The mapCheckRunStatus function maps cancelled, skipped, and neutral to success. The comment says 'non-blocking' but this could be surprising to callers who depend on CommitStatus.Status for gating decisions — a cancelled check that maps to 'success' might unblock a review when it shouldn't. This is a semantic/policy decision that should be documented more prominently in the function's doc comment (the current comment only appears inline in the switch case, not in the function doc).
4 [NIT] github/pr.go 54 The doc comment for GetPullRequest is minimal: // GetPullRequest fetches PR metadata. Per the documentation patterns, it should start with the function name and provide slightly more context, e.g. what errors it may return (wraps APIError, IsNotFound applicable).
5 [NIT] github/pr_test.go 629 stringPtr helper is defined at the bottom of pr_test.go but is a common test utility. Since the test file is in package github (not package github_test), it's only visible within the package's test files. This is fine, but if other test files in the package need pointer helpers in the future, it could conflict. A convention note: per project conventions, no single-letter names outside loop indices — s parameter in stringPtr is acceptable here as it's conventional for this helper.
6 [NIT] github/client.go 169 The doRequest method signature uses accept string as the last parameter rather than a more descriptive name. Since this is unexported and used only internally, acceptHeader would be clearer at the call sites, but this is purely stylistic.

Recommendation

APPROVE — Approve. The implementation is correct and well-tested. CI passes. The code follows Go patterns (compile-time interface check, error wrapping with context, pagination with a safety cap, nil-slice semantics for empty results). The minor findings are non-blocking: the semantic mapping of cancelled/skipped/neutral check runs to 'success' is a deliberate design decision that should be documented more prominently, and the shared maxPages constant could use a clarifying comment. None of these warrant blocking the PR.


Review by sonnet


Evaluated against 3995fa31

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2988) for up-to-date findings. <details><summary>Previous findings (commit 3995fa31)</summary> # Sonnet Review ## Summary This PR implements the PRReader interface on the GitHub client with solid correctness, thorough test coverage, and follows project conventions well. CI passes. A few minor issues worth noting but none blocking. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 140 | GetCommitStatuses fetches both commit statuses and check runs but the check-runs endpoint returns paginated results starting at page 1 unconditionally on every call. If the commit statuses endpoint returns 0 statuses (empty PR with no CI), the function still makes at least one check-runs request. This is fine, but the bigger issue is that the check-runs pagination uses `maxPages` (100) which is shared with `GetPullRequestFiles`. A PR could theoretically have >100 pages of check runs (100 * 100 = 10,000 check runs), but that's an extreme edge case. More practically: the `maxPages` constant in `pr.go` is package-level and shared between two logically separate pagination loops; a comment clarifying this would help, or separate named constants. | | 2 | [MINOR] | `github/pr_test.go` | 296 | TestGetCommitStatuses_CheckRunConclusions creates a new httptest.Server inside each subtest iteration but does not call t.Parallel() on the subtests. Each subtest also closes its server with `defer srv.Close()` which is correct, but spawning 9 servers sequentially when they could run in parallel is a minor inefficiency. More importantly, there is no test for the check-runs endpoint returning a 404 or error *after* statuses succeed (the documented behavior where partial results are not returned). | | 3 | [MINOR] | `github/pr.go` | 191 | The `mapCheckRunStatus` function maps `cancelled`, `skipped`, and `neutral` to `success`. The comment says 'non-blocking' but this could be surprising to callers who depend on CommitStatus.Status for gating decisions — a cancelled check that maps to 'success' might unblock a review when it shouldn't. This is a semantic/policy decision that should be documented more prominently in the function's doc comment (the current comment only appears inline in the switch case, not in the function doc). | | 4 | [NIT] | `github/pr.go` | 54 | The doc comment for `GetPullRequest` is minimal: `// GetPullRequest fetches PR metadata.` Per the documentation patterns, it should start with the function name and provide slightly more context, e.g. what errors it may return (wraps APIError, IsNotFound applicable). | | 5 | [NIT] | `github/pr_test.go` | 629 | `stringPtr` helper is defined at the bottom of `pr_test.go` but is a common test utility. Since the test file is in `package github` (not `package github_test`), it's only visible within the package's test files. This is fine, but if other test files in the package need pointer helpers in the future, it could conflict. A convention note: per project conventions, no single-letter names outside loop indices — `s` parameter in `stringPtr` is acceptable here as it's conventional for this helper. | | 6 | [NIT] | `github/client.go` | 169 | The `doRequest` method signature uses `accept string` as the last parameter rather than a more descriptive name. Since this is unexported and used only internally, `acceptHeader` would be clearer at the call sites, but this is purely stylistic. | ## Recommendation **APPROVE** — Approve. The implementation is correct and well-tested. CI passes. The code follows Go patterns (compile-time interface check, error wrapping with context, pagination with a safety cap, nil-slice semantics for empty results). The minor findings are non-blocking: the semantic mapping of cancelled/skipped/neutral check runs to 'success' is a deliberate design decision that should be documented more prominently, and the shared `maxPages` constant could use a clarifying comment. None of these warrant blocking the PR. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 3995fa31* </details> <!-- review-bot:sonnet -->
@@ -0,0 +166,4 @@
// Callers providing a non-nil client are responsible for configuring a safe
// CheckRedirect policy. Without one, the default net/http behavior will follow
// redirects and may forward the Authorization header to untrusted hosts.
func (c *Client) SetHTTPClient(hc *http.Client) {
First-time contributor

[NIT] The doRequest method signature uses accept string as the last parameter rather than a more descriptive name. Since this is unexported and used only internally, acceptHeader would be clearer at the call sites, but this is purely stylistic.

**[NIT]** The `doRequest` method signature uses `accept string` as the last parameter rather than a more descriptive name. Since this is unexported and used only internally, `acceptHeader` would be clearer at the call sites, but this is purely stylistic.
github/pr.go Outdated
@@ -0,0 +51,4 @@
} `json:"check_runs"`
}
// GetPullRequest fetches PR metadata.
First-time contributor

[NIT] The doc comment for GetPullRequest is minimal: // GetPullRequest fetches PR metadata. Per the documentation patterns, it should start with the function name and provide slightly more context, e.g. what errors it may return (wraps APIError, IsNotFound applicable).

**[NIT]** The doc comment for `GetPullRequest` is minimal: `// GetPullRequest fetches PR metadata.` Per the documentation patterns, it should start with the function name and provide slightly more context, e.g. what errors it may return (wraps APIError, IsNotFound applicable).
github/pr.go Outdated
@@ -0,0 +137,4 @@
statusURL := fmt.Sprintf("%s/repos/%s/%s/commits/%s/status",
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(sha))
statusBody, err := c.doGet(ctx, statusURL)
if err != nil {
First-time contributor

[MINOR] GetCommitStatuses fetches both commit statuses and check runs but the check-runs endpoint returns paginated results starting at page 1 unconditionally on every call. If the commit statuses endpoint returns 0 statuses (empty PR with no CI), the function still makes at least one check-runs request. This is fine, but the bigger issue is that the check-runs pagination uses maxPages (100) which is shared with GetPullRequestFiles. A PR could theoretically have >100 pages of check runs (100 * 100 = 10,000 check runs), but that's an extreme edge case. More practically: the maxPages constant in pr.go is package-level and shared between two logically separate pagination loops; a comment clarifying this would help, or separate named constants.

**[MINOR]** GetCommitStatuses fetches both commit statuses and check runs but the check-runs endpoint returns paginated results starting at page 1 unconditionally on every call. If the commit statuses endpoint returns 0 statuses (empty PR with no CI), the function still makes at least one check-runs request. This is fine, but the bigger issue is that the check-runs pagination uses `maxPages` (100) which is shared with `GetPullRequestFiles`. A PR could theoretically have >100 pages of check runs (100 * 100 = 10,000 check runs), but that's an extreme edge case. More practically: the `maxPages` constant in `pr.go` is package-level and shared between two logically separate pagination loops; a comment clarifying this would help, or separate named constants.
github/pr.go Outdated
@@ -0,0 +188,4 @@
if conclusion == nil {
// Still running or queued
return "pending"
}
First-time contributor

[MINOR] The mapCheckRunStatus function maps cancelled, skipped, and neutral to success. The comment says 'non-blocking' but this could be surprising to callers who depend on CommitStatus.Status for gating decisions — a cancelled check that maps to 'success' might unblock a review when it shouldn't. This is a semantic/policy decision that should be documented more prominently in the function's doc comment (the current comment only appears inline in the switch case, not in the function doc).

**[MINOR]** The `mapCheckRunStatus` function maps `cancelled`, `skipped`, and `neutral` to `success`. The comment says 'non-blocking' but this could be surprising to callers who depend on CommitStatus.Status for gating decisions — a cancelled check that maps to 'success' might unblock a review when it shouldn't. This is a semantic/policy decision that should be documented more prominently in the function's doc comment (the current comment only appears inline in the switch case, not in the function doc).
@@ -0,0 +293,4 @@
if len(files) != 1 {
t.Fatalf("expected 1 file, got %d", len(files))
}
if files[0].Patch != "" {
First-time contributor

[MINOR] TestGetCommitStatuses_CheckRunConclusions creates a new httptest.Server inside each subtest iteration but does not call t.Parallel() on the subtests. Each subtest also closes its server with defer srv.Close() which is correct, but spawning 9 servers sequentially when they could run in parallel is a minor inefficiency. More importantly, there is no test for the check-runs endpoint returning a 404 or error after statuses succeed (the documented behavior where partial results are not returned).

**[MINOR]** TestGetCommitStatuses_CheckRunConclusions creates a new httptest.Server inside each subtest iteration but does not call t.Parallel() on the subtests. Each subtest also closes its server with `defer srv.Close()` which is correct, but spawning 9 servers sequentially when they could run in parallel is a minor inefficiency. More importantly, there is no test for the check-runs endpoint returning a 404 or error *after* statuses succeed (the documented behavior where partial results are not returned).
@@ -0,0 +626,4 @@
c := NewClient("token", srv.URL, AllowInsecureHTTP())
c.SetHTTPClient(srv.Client())
_, err := c.GetCommitStatuses(context.Background(), "owner", "repo", "sha")
First-time contributor

[NIT] stringPtr helper is defined at the bottom of pr_test.go but is a common test utility. Since the test file is in package github (not package github_test), it's only visible within the package's test files. This is fine, but if other test files in the package need pointer helpers in the future, it could conflict. A convention note: per project conventions, no single-letter names outside loop indices — s parameter in stringPtr is acceptable here as it's conventional for this helper.

**[NIT]** `stringPtr` helper is defined at the bottom of `pr_test.go` but is a common test utility. Since the test file is in `package github` (not `package github_test`), it's only visible within the package's test files. This is fine, but if other test files in the package need pointer helpers in the future, it could conflict. A convention note: per project conventions, no single-letter names outside loop indices — `s` parameter in `stringPtr` is acceptable here as it's conventional for this helper.
security-review-bot requested review from security-review-bot 2026-05-13 04:22:24 +00:00
security-review-bot approved these changes 2026-05-13 04:22:24 +00:00
Dismissed
security-review-bot left a comment
Collaborator

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 3995fa31)

Security Review

Summary

Security posture of the new GitHub client and PR reader is solid. The code properly escapes untrusted path segments, enforces HTTPS for authenticated requests (with explicit opt-in for HTTP), strips Authorization headers on cross-host redirects, and caps response sizes and retry delays. No exploitable vulnerabilities were found.

Recommendation

APPROVE — Approve as submitted. The implementation follows good security practices: strict URL/path escaping, token handling that avoids leakage on redirects and non-HTTPS endpoints, bounded retries honoring Retry-After with a cap, and response size limits to mitigate resource exhaustion. Tests cover key security behaviors. Consider, in future hardening, further sanitizing API error messages beyond newline removal (e.g., other control characters) before logging, but this is not a blocker.


Review by security


Evaluated against 3995fa31

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2991) for up-to-date findings. <details><summary>Previous findings (commit 3995fa31)</summary> # Security Review ## Summary Security posture of the new GitHub client and PR reader is solid. The code properly escapes untrusted path segments, enforces HTTPS for authenticated requests (with explicit opt-in for HTTP), strips Authorization headers on cross-host redirects, and caps response sizes and retry delays. No exploitable vulnerabilities were found. ## Recommendation **APPROVE** — Approve as submitted. The implementation follows good security practices: strict URL/path escaping, token handling that avoids leakage on redirects and non-HTTPS endpoints, bounded retries honoring Retry-After with a cap, and response size limits to mitigate resource exhaustion. Tests cover key security behaviors. Consider, in future hardening, further sanitizing API error messages beyond newline removal (e.g., other control characters) before logging, but this is not a blocker. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 3995fa31* </details> <!-- review-bot:security -->
gpt-review-bot approved these changes 2026-05-13 04:23:04 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 3995fa31)

Gpt Review

Summary

Solid implementation of the GitHub PRReader features with thorough tests, careful HTTP handling (retry, redirects, security), and sensible mappings. Minor opportunities exist around pagination signaling and check run status mapping.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 147 mapCheckRunStatus defaults unknown non-nil conclusions to "pending". GitHub can return additional conclusions like "startup_failure" which likely indicate failure. Consider mapping unknown non-nil conclusions to "failure" or extending the mapping to cover more values.
2 [MINOR] github/pr.go 96 Pagination in GetPullRequestFiles relies solely on len(files) < 100 to detect the last page. Parsing the Link header (rel="next") would be more robust and avoid extra requests or edge cases where a final page might still contain 100 items.
3 [NIT] github/pr.go 124 For check runs, Description is set to the conclusion string (via derefString). This may be misleading as GitHub check runs have richer fields (e.g., output.title/summary). Consider leaving description empty or sourcing a more descriptive field if needed.

Recommendation

APPROVE — The changes are well-designed and adhere to repository conventions and Go patterns. HTTP behavior is secure by default (no token over HTTP, redirect policy strips Authorization), retry logic is careful with Retry-After, and the API is clean and test-covered. I recommend merging. Optionally, improve check run conclusion mapping to handle additional outcomes, consider parsing Link headers for pagination robustness, and revisit the check-run description field for better fidelity.


Review by gpt


Evaluated against 3995fa31

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2992) for up-to-date findings. <details><summary>Previous findings (commit 3995fa31)</summary> # Gpt Review ## Summary Solid implementation of the GitHub PRReader features with thorough tests, careful HTTP handling (retry, redirects, security), and sensible mappings. Minor opportunities exist around pagination signaling and check run status mapping. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 147 | mapCheckRunStatus defaults unknown non-nil conclusions to "pending". GitHub can return additional conclusions like "startup_failure" which likely indicate failure. Consider mapping unknown non-nil conclusions to "failure" or extending the mapping to cover more values. | | 2 | [MINOR] | `github/pr.go` | 96 | Pagination in GetPullRequestFiles relies solely on len(files) < 100 to detect the last page. Parsing the Link header (rel="next") would be more robust and avoid extra requests or edge cases where a final page might still contain 100 items. | | 3 | [NIT] | `github/pr.go` | 124 | For check runs, Description is set to the conclusion string (via derefString). This may be misleading as GitHub check runs have richer fields (e.g., output.title/summary). Consider leaving description empty or sourcing a more descriptive field if needed. | ## Recommendation **APPROVE** — The changes are well-designed and adhere to repository conventions and Go patterns. HTTP behavior is secure by default (no token over HTTP, redirect policy strips Authorization), retry logic is careful with Retry-After, and the API is clean and test-covered. I recommend merging. Optionally, improve check run conclusion mapping to handle additional outcomes, consider parsing Link headers for pagination robustness, and revisit the check-run description field for better fidelity. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 3995fa31* </details> <!-- review-bot:gpt -->
github/pr.go Outdated
@@ -0,0 +93,4 @@
func (c *Client) GetPullRequestFiles(ctx context.Context, owner, repo string, number int) ([]vcs.ChangedFile, error) {
var allFiles []vcs.ChangedFile
for page := 1; page <= maxPages; page++ {
First-time contributor

[MINOR] Pagination in GetPullRequestFiles relies solely on len(files) < 100 to detect the last page. Parsing the Link header (rel="next") would be more robust and avoid extra requests or edge cases where a final page might still contain 100 items.

**[MINOR]** Pagination in GetPullRequestFiles relies solely on len(files) < 100 to detect the last page. Parsing the Link header (rel="next") would be more robust and avoid extra requests or edge cases where a final page might still contain 100 items.
github/pr.go Outdated
@@ -0,0 +121,4 @@
return allFiles, nil
}
First-time contributor

[NIT] For check runs, Description is set to the conclusion string (via derefString). This may be misleading as GitHub check runs have richer fields (e.g., output.title/summary). Consider leaving description empty or sourcing a more descriptive field if needed.

**[NIT]** For check runs, Description is set to the conclusion string (via derefString). This may be misleading as GitHub check runs have richer fields (e.g., output.title/summary). Consider leaving description empty or sourcing a more descriptive field if needed.
github/pr.go Outdated
@@ -0,0 +144,4 @@
if err := json.Unmarshal(statusBody, &statusResp); err != nil {
return nil, fmt.Errorf("parse commit statuses JSON: %w", err)
}
for _, s := range statusResp.Statuses {
First-time contributor

[MINOR] mapCheckRunStatus defaults unknown non-nil conclusions to "pending". GitHub can return additional conclusions like "startup_failure" which likely indicate failure. Consider mapping unknown non-nil conclusions to "failure" or extending the mapping to cover more values.

**[MINOR]** mapCheckRunStatus defaults unknown non-nil conclusions to "pending". GitHub can return additional conclusions like "startup_failure" which likely indicate failure. Consider mapping unknown non-nil conclusions to "failure" or extending the mapping to cover more values.
rodin added the wip label 2026-05-13 04:34:18 +00:00
aweiker was assigned by rodin 2026-05-13 04:36:46 +00:00
rodin added the ready label 2026-05-13 04:36:46 +00:00
rodin removed the wip label 2026-05-13 04:36:50 +00:00
sonnet-review-bot approved these changes 2026-05-13 04:37:47 +00:00
sonnet-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 329d68e4)

Sonnet Review

Summary

Well-structured implementation of the PRReader interface on the GitHub client. The code follows established patterns correctly, has thorough test coverage, and CI passes. A few minor observations about design choices that are worth noting but don't block merging.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 178 The Description field for check run statuses is populated with derefString(cr.Conclusion) — the raw conclusion value (e.g. "success", "failure", "skipped"). This is the same value that's being mapped to Status via mapCheckRunStatus. The doc comment says 'raw conclusion value (e.g. "success", "failure", "skipped")' but for commit statuses, Description is the human-readable description string (e.g. 'Build passed'). Using the conclusion as description is a semantic mismatch: for commit statuses Description carries a narrative message, while for check runs it carries a machine-readable conclusion. Consumers expecting a human-readable description field will get a raw enum string instead. Consider using cr.Status (e.g. 'completed', 'in_progress') or leaving it empty rather than duplicating the conclusion.
2 [MINOR] github/pr.go 148 mapCheckRunStatus maps "cancelled" and "skipped" to "success". While the comment explains this as 'non-blocking per GitHub check suite semantics', this is a loss of information at the vcs abstraction layer. Callers who need to distinguish between a genuinely passing check and a skipped/cancelled one have no way to do so. Since vcs.CommitStatus.Status is a string (not an enum), it would be safe to introduce additional values like "skipped" and "cancelled" if consumers need to distinguish them. This is a design tradeoff rather than a bug, but the mapping decision warrants discussion.
3 [NIT] github/pr_test.go 290 The TestGetCommitStatuses_CheckRunConclusions table-driven test creates an httptest server inside each subtest via t.Parallel(). The tt variable is correctly used (Go 1.22+ loop variable semantics), and the test is well-structured. However, t.Parallel() is called inside subtests that close over tt — this is safe in modern Go but the test could use t.Run with the name directly from *tt.conclusion rather than a pre-computed name variable. Minor readability nit only.
4 [NIT] github/client.go 283 In doRequest, after the retry loop body: return nil, lastErr at the bottom is only reached when all maxAttempts are exhausted with 429 responses. The variable is named lastErr but for the 429 case it's always an *APIError{StatusCode: 429}. This is correct behavior, but the lastErr variable is also set inside the non-429 error path (return nil, lastErr on line ~272) — in that branch lastErr is set and then immediately returned in the same iteration, so the bottom return nil, lastErr is indeed only the all-retries-exhausted case. The code is correct but the dual use of lastErr makes it slightly harder to follow.

Recommendation

APPROVE — Approve with minor observations. The implementation is correct, follows Go patterns (compile-time interface check, error wrapping with %w, table-driven tests, httptest for HTTP mocking, testing.Short() for slow tests, t.Helper() missing in some helpers but not egregious). The most substantive concern is the semantic mismatch in populating CommitStatus.Description with the raw check run conclusion string rather than a human-readable description — callers expecting a human-readable message will get machine-readable enum values. This is worth revisiting in a follow-up but does not block the PR. CI passes.


Review by sonnet


Evaluated against 329d68e4

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2994) for up-to-date findings. <details><summary>Previous findings (commit 329d68e4)</summary> # Sonnet Review ## Summary Well-structured implementation of the PRReader interface on the GitHub client. The code follows established patterns correctly, has thorough test coverage, and CI passes. A few minor observations about design choices that are worth noting but don't block merging. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 178 | The `Description` field for check run statuses is populated with `derefString(cr.Conclusion)` — the raw conclusion value (e.g. "success", "failure", "skipped"). This is the same value that's being mapped to `Status` via `mapCheckRunStatus`. The doc comment says 'raw conclusion value (e.g. "success", "failure", "skipped")' but for commit statuses, `Description` is the human-readable description string (e.g. 'Build passed'). Using the conclusion as description is a semantic mismatch: for commit statuses `Description` carries a narrative message, while for check runs it carries a machine-readable conclusion. Consumers expecting a human-readable description field will get a raw enum string instead. Consider using `cr.Status` (e.g. 'completed', 'in_progress') or leaving it empty rather than duplicating the conclusion. | | 2 | [MINOR] | `github/pr.go` | 148 | `mapCheckRunStatus` maps "cancelled" and "skipped" to "success". While the comment explains this as 'non-blocking per GitHub check suite semantics', this is a loss of information at the vcs abstraction layer. Callers who need to distinguish between a genuinely passing check and a skipped/cancelled one have no way to do so. Since `vcs.CommitStatus.Status` is a string (not an enum), it would be safe to introduce additional values like "skipped" and "cancelled" if consumers need to distinguish them. This is a design tradeoff rather than a bug, but the mapping decision warrants discussion. | | 3 | [NIT] | `github/pr_test.go` | 290 | The `TestGetCommitStatuses_CheckRunConclusions` table-driven test creates an httptest server inside each subtest via `t.Parallel()`. The `tt` variable is correctly used (Go 1.22+ loop variable semantics), and the test is well-structured. However, `t.Parallel()` is called inside subtests that close over `tt` — this is safe in modern Go but the test could use `t.Run` with the name directly from `*tt.conclusion` rather than a pre-computed `name` variable. Minor readability nit only. | | 4 | [NIT] | `github/client.go` | 283 | In `doRequest`, after the retry loop body: `return nil, lastErr` at the bottom is only reached when all `maxAttempts` are exhausted with 429 responses. The variable is named `lastErr` but for the 429 case it's always an `*APIError{StatusCode: 429}`. This is correct behavior, but the `lastErr` variable is also set inside the non-429 error path (`return nil, lastErr` on line ~272) — in that branch `lastErr` is set and then immediately returned in the same iteration, so the bottom `return nil, lastErr` is indeed only the all-retries-exhausted case. The code is correct but the dual use of `lastErr` makes it slightly harder to follow. | ## Recommendation **APPROVE** — Approve with minor observations. The implementation is correct, follows Go patterns (compile-time interface check, error wrapping with %w, table-driven tests, httptest for HTTP mocking, testing.Short() for slow tests, t.Helper() missing in some helpers but not egregious). The most substantive concern is the semantic mismatch in populating `CommitStatus.Description` with the raw check run conclusion string rather than a human-readable description — callers expecting a human-readable message will get machine-readable enum values. This is worth revisiting in a follow-up but does not block the PR. CI passes. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 329d68e4* </details> <!-- review-bot:sonnet -->
@@ -0,0 +280,4 @@
delay = 0
}
if delay > maxRetryAfter {
delay = maxRetryAfter
First-time contributor

[NIT] In doRequest, after the retry loop body: return nil, lastErr at the bottom is only reached when all maxAttempts are exhausted with 429 responses. The variable is named lastErr but for the 429 case it's always an *APIError{StatusCode: 429}. This is correct behavior, but the lastErr variable is also set inside the non-429 error path (return nil, lastErr on line ~272) — in that branch lastErr is set and then immediately returned in the same iteration, so the bottom return nil, lastErr is indeed only the all-retries-exhausted case. The code is correct but the dual use of lastErr makes it slightly harder to follow.

**[NIT]** In `doRequest`, after the retry loop body: `return nil, lastErr` at the bottom is only reached when all `maxAttempts` are exhausted with 429 responses. The variable is named `lastErr` but for the 429 case it's always an `*APIError{StatusCode: 429}`. This is correct behavior, but the `lastErr` variable is also set inside the non-429 error path (`return nil, lastErr` on line ~272) — in that branch `lastErr` is set and then immediately returned in the same iteration, so the bottom `return nil, lastErr` is indeed only the all-retries-exhausted case. The code is correct but the dual use of `lastErr` makes it slightly harder to follow.
github/pr.go Outdated
@@ -0,0 +145,4 @@
// Fetch commit statuses
statusURL := fmt.Sprintf("%s/repos/%s/%s/commits/%s/status",
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(sha))
statusBody, err := c.doGet(ctx, statusURL)
First-time contributor

[MINOR] mapCheckRunStatus maps "cancelled" and "skipped" to "success". While the comment explains this as 'non-blocking per GitHub check suite semantics', this is a loss of information at the vcs abstraction layer. Callers who need to distinguish between a genuinely passing check and a skipped/cancelled one have no way to do so. Since vcs.CommitStatus.Status is a string (not an enum), it would be safe to introduce additional values like "skipped" and "cancelled" if consumers need to distinguish them. This is a design tradeoff rather than a bug, but the mapping decision warrants discussion.

**[MINOR]** `mapCheckRunStatus` maps "cancelled" and "skipped" to "success". While the comment explains this as 'non-blocking per GitHub check suite semantics', this is a loss of information at the vcs abstraction layer. Callers who need to distinguish between a genuinely passing check and a skipped/cancelled one have no way to do so. Since `vcs.CommitStatus.Status` is a string (not an enum), it would be safe to introduce additional values like "skipped" and "cancelled" if consumers need to distinguish them. This is a design tradeoff rather than a bug, but the mapping decision warrants discussion.
github/pr.go Outdated
@@ -0,0 +175,4 @@
return nil, fmt.Errorf("parse check runs JSON: %w", err)
}
for _, cr := range checkResp.CheckRuns {
result = append(result, vcs.CommitStatus{
First-time contributor

[MINOR] The Description field for check run statuses is populated with derefString(cr.Conclusion) — the raw conclusion value (e.g. "success", "failure", "skipped"). This is the same value that's being mapped to Status via mapCheckRunStatus. The doc comment says 'raw conclusion value (e.g. "success", "failure", "skipped")' but for commit statuses, Description is the human-readable description string (e.g. 'Build passed'). Using the conclusion as description is a semantic mismatch: for commit statuses Description carries a narrative message, while for check runs it carries a machine-readable conclusion. Consumers expecting a human-readable description field will get a raw enum string instead. Consider using cr.Status (e.g. 'completed', 'in_progress') or leaving it empty rather than duplicating the conclusion.

**[MINOR]** The `Description` field for check run statuses is populated with `derefString(cr.Conclusion)` — the raw conclusion value (e.g. "success", "failure", "skipped"). This is the same value that's being mapped to `Status` via `mapCheckRunStatus`. The doc comment says 'raw conclusion value (e.g. "success", "failure", "skipped")' but for commit statuses, `Description` is the human-readable description string (e.g. 'Build passed'). Using the conclusion as description is a semantic mismatch: for commit statuses `Description` carries a narrative message, while for check runs it carries a machine-readable conclusion. Consumers expecting a human-readable description field will get a raw enum string instead. Consider using `cr.Status` (e.g. 'completed', 'in_progress') or leaving it empty rather than duplicating the conclusion.
@@ -0,0 +287,4 @@
c.SetHTTPClient(srv.Client())
files, err := c.GetPullRequestFiles(context.Background(), "owner", "repo", 1)
if err != nil {
First-time contributor

[NIT] The TestGetCommitStatuses_CheckRunConclusions table-driven test creates an httptest server inside each subtest via t.Parallel(). The tt variable is correctly used (Go 1.22+ loop variable semantics), and the test is well-structured. However, t.Parallel() is called inside subtests that close over tt — this is safe in modern Go but the test could use t.Run with the name directly from *tt.conclusion rather than a pre-computed name variable. Minor readability nit only.

**[NIT]** The `TestGetCommitStatuses_CheckRunConclusions` table-driven test creates an httptest server inside each subtest via `t.Parallel()`. The `tt` variable is correctly used (Go 1.22+ loop variable semantics), and the test is well-structured. However, `t.Parallel()` is called inside subtests that close over `tt` — this is safe in modern Go but the test could use `t.Run` with the name directly from `*tt.conclusion` rather than a pre-computed `name` variable. Minor readability nit only.
security-review-bot requested review from security-review-bot 2026-05-13 04:38:06 +00:00
security-review-bot approved these changes 2026-05-13 04:38:06 +00:00
security-review-bot left a comment
Collaborator

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 329d68e4)

Security Review

Summary

The implementation appears security-conscious: it validates/escapes path segments, bounds pagination and response sizes, enforces HTTPS for tokens by default, and strips Authorization on cross-host redirects. CI passed and no exploitable vulnerabilities are evident.

Recommendation

APPROVE — Approve this PR. The code uses safe defaults (HTTPS enforced unless explicitly allowed, limited retries, sanitized error messages) and avoids common pitfalls like path traversal and credential leakage on redirects. No changes required.


Review by security


Evaluated against 329d68e4

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2997) for up-to-date findings. <details><summary>Previous findings (commit 329d68e4)</summary> # Security Review ## Summary The implementation appears security-conscious: it validates/escapes path segments, bounds pagination and response sizes, enforces HTTPS for tokens by default, and strips Authorization on cross-host redirects. CI passed and no exploitable vulnerabilities are evident. ## Recommendation **APPROVE** — Approve this PR. The code uses safe defaults (HTTPS enforced unless explicitly allowed, limited retries, sanitized error messages) and avoids common pitfalls like path traversal and credential leakage on redirects. No changes required. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 329d68e4* </details> <!-- review-bot:security -->
gpt-review-bot approved these changes 2026-05-13 04:38:26 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Original review

Supersededsee current review for up-to-date findings.

Previous findings (commit 329d68e4)

Gpt Review

Summary

Well-structured, idiomatic implementation of PRReader features with comprehensive tests. Error handling, configuration, and HTTP safety patterns are thoughtfully applied, and CI is green.

Recommendation

APPROVE — The implementation adheres to Go patterns for configuration, error handling, context usage, and package organization. The client safely handles redirects, rate limiting (429 with Retry-After), and enforces HTTPS when tokens are present. PR metadata, diff, files (with pagination), and combined commit status/check runs are correctly implemented and well-tested, including edge cases like malformed JSON and binary file patches. No changes requested.


Review by gpt


Evaluated against 329d68e4

~~Original review~~ **Superseded** — [see current review](https://gitea.weiker.me/rodin/review-bot/pulls/102#pullrequestreview-2995) for up-to-date findings. <details><summary>Previous findings (commit 329d68e4)</summary> # Gpt Review ## Summary Well-structured, idiomatic implementation of PRReader features with comprehensive tests. Error handling, configuration, and HTTP safety patterns are thoughtfully applied, and CI is green. ## Recommendation **APPROVE** — The implementation adheres to Go patterns for configuration, error handling, context usage, and package organization. The client safely handles redirects, rate limiting (429 with Retry-After), and enforces HTTPS when tokens are present. PR metadata, diff, files (with pagination), and combined commit status/check runs are correctly implemented and well-tested, including edge cases like malformed JSON and binary file patches. No changes requested. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 329d68e4* </details> <!-- review-bot:gpt -->
rodin removed the ready label 2026-05-13 04:39:39 +00:00
aweiker was unassigned by rodin 2026-05-13 04:39:39 +00:00
rodin self-assigned this 2026-05-13 04:39:39 +00:00
rodin added the wip label 2026-05-13 04:39:43 +00:00
Author
Owner

Self-Review: PR #102

Self-review against 329d68e4b4

Phase 1: Independent Findings

  1. [MINOR] github/pr.goCommitStatus.Description semantic mismatch for check runs
    Line ~181: Description: derefString(cr.Conclusion) stores the raw conclusion string (e.g. "failure", "skipped") in the Description field. For commit statuses (line 160), Description carries a human-readable narrative like "Build passed". The field has inconsistent semantics across the two source types: one is a narrative, the other is a machine-readable enum value duplicating what Status already carries. Callers consuming CommitStatus.Description expecting a human-readable summary will get a raw enum string for check runs. This is a low-severity design issue — the field is populated with something, just not what its name implies.

  2. [NIT] github/pr.gomapCheckRunStatus default for unknown non-nil conclusions maps to "pending"
    GitHub can return additional non-nil conclusion values like "startup_failure" which indicates a genuine failure. The current code conservatively maps all unrecognized conclusions to "pending", which could cause a failed check to appear as pending. Mapping unknown non-nil conclusions to "failure" would be safer (fail-closed), though documenting the current choice as deliberate (fail-open for unrecognized states) is also acceptable.

  3. [NIT] github/client.goreturn nil, lastErr after loop is unreachable with current maxAttempts=3
    With maxAttempts=3, non-429 errors always return from within the loop body, and the last 429 attempt also returns from within the loop. The post-loop return nil, lastErr is defensive dead code. This is correct and harmless — it guards against future maxAttempts changes — but a comment explaining this would help readers.

  4. [NIT] No t.Helper() in test helpersstringPtr in pr_test.go is a helper but doesn't call t.Helper(). It doesn't accept *testing.T so this doesn't apply here, but any future helper functions wrapping test assertions should call t.Helper().

Phase 2: Prior Review Verification

Finding Reviewer Status Notes
MINOR: shared maxPages constant between two pagination loops sonnet (2967) RESOLVED PR uses separate named constants maxFilesPages and maxCheckRunPages in current diff
MINOR: no test for check-runs error after statuses succeed sonnet (2967) RESOLVED TestGetCommitStatuses_CheckRunsErrorAfterStatusesSucceed added in pr_test.go
MINOR: cancelled/skipped/neutral"success" needs more prominent docs sonnet (2967) RESOLVED mapCheckRunStatus has detailed doc comment explaining non-blocking semantics
NIT: GetPullRequest doc comment minimal sonnet (2967) RESOLVED Doc comment updated to describe error types (APIError, IsNotFound, IsUnauthorized)
NIT: stringPtr helper visibility sonnet (2967) RESOLVED Acceptable — package-internal test helper
NIT: accept param name in doRequest sonnet (2967) RESOLVED Non-blocking, acceptable as-is
MINOR: startup_failure and other unknown conclusions map to "pending" gpt (2970) STILL PRESENT Design tradeoff — intentional fail-open, documented in switch default comment
MINOR: pagination relies on len < 100 rather than Link header gpt (2970) STILL PRESENT Acceptable for this implementation; Link header parsing is a future improvement
NIT: check-run Description set to conclusion string gpt (2970) STILL PRESENT Same as Phase 1 finding #1 above
MINOR: CommitStatus.Description semantic mismatch for check runs sonnet (2988) STILL PRESENT Acknowledged by reviewer as worth revisiting in follow-up, not blocking
MINOR: cancelled/skipped"success" loses information sonnet (2988) STILL PRESENT Intentional design tradeoff per doc comment; no fix requested
NIT: TestGetCommitStatuses_CheckRunConclusions subtest naming sonnet (2988) RESOLVED Tests use tt.conclusion for subtest name with stringPtr helper
NIT: lastErr dual-use makes loop harder to follow sonnet (2988) STILL PRESENT Design choice; code is correct, NIT only
Security: HTTPS enforced, redirect strips auth security (2969, 2991) RESOLVED AllowInsecureHTTP option + defaultCheckRedirect implemented and tested

Assessment: Clean

All MINOR findings from prior reviews are either resolved or represent documented, intentional design tradeoffs (unknown check run conclusions mapping to "pending"; cancelled/skipped mapping to "success"; using len < 100 for pagination termination). The remaining STILL PRESENT items are acknowledged design decisions, not regressions or oversights. Phase 1 independent findings align with what prior reviewers flagged — no new issues discovered. The CommitStatus.Description semantic mismatch for check runs is the most substantive concern and is already tracked for a follow-up. Code builds clean, all tests pass including the -short flag variant.

## Self-Review: PR #102 Self-review against 329d68e4b41dae2b3d7dc77cd61451237bff2ea1 ### Phase 1: Independent Findings 1. **[MINOR] `github/pr.go` — `CommitStatus.Description` semantic mismatch for check runs** Line ~181: `Description: derefString(cr.Conclusion)` stores the raw conclusion string (e.g. `"failure"`, `"skipped"`) in the `Description` field. For commit statuses (line 160), `Description` carries a human-readable narrative like `"Build passed"`. The field has inconsistent semantics across the two source types: one is a narrative, the other is a machine-readable enum value duplicating what `Status` already carries. Callers consuming `CommitStatus.Description` expecting a human-readable summary will get a raw enum string for check runs. This is a low-severity design issue — the field is populated with something, just not what its name implies. 2. **[NIT] `github/pr.go` — `mapCheckRunStatus` default for unknown non-nil conclusions maps to `"pending"`** GitHub can return additional non-nil conclusion values like `"startup_failure"` which indicates a genuine failure. The current code conservatively maps all unrecognized conclusions to `"pending"`, which could cause a failed check to appear as pending. Mapping unknown non-nil conclusions to `"failure"` would be safer (fail-closed), though documenting the current choice as deliberate (fail-open for unrecognized states) is also acceptable. 3. **[NIT] `github/client.go` — `return nil, lastErr` after loop is unreachable with current `maxAttempts=3`** With `maxAttempts=3`, non-429 errors always `return` from within the loop body, and the last 429 attempt also returns from within the loop. The post-loop `return nil, lastErr` is defensive dead code. This is correct and harmless — it guards against future `maxAttempts` changes — but a comment explaining this would help readers. 4. **[NIT] No `t.Helper()` in test helpers** — `stringPtr` in `pr_test.go` is a helper but doesn't call `t.Helper()`. It doesn't accept `*testing.T` so this doesn't apply here, but any future helper functions wrapping test assertions should call `t.Helper()`. ### Phase 2: Prior Review Verification | Finding | Reviewer | Status | Notes | |---------|----------|--------|-------| | MINOR: shared `maxPages` constant between two pagination loops | sonnet (2967) | RESOLVED | PR uses separate named constants `maxFilesPages` and `maxCheckRunPages` in current diff | | MINOR: no test for check-runs error after statuses succeed | sonnet (2967) | RESOLVED | `TestGetCommitStatuses_CheckRunsErrorAfterStatusesSucceed` added in pr_test.go | | MINOR: `cancelled`/`skipped`/`neutral` → `"success"` needs more prominent docs | sonnet (2967) | RESOLVED | mapCheckRunStatus has detailed doc comment explaining non-blocking semantics | | NIT: `GetPullRequest` doc comment minimal | sonnet (2967) | RESOLVED | Doc comment updated to describe error types (APIError, IsNotFound, IsUnauthorized) | | NIT: `stringPtr` helper visibility | sonnet (2967) | RESOLVED | Acceptable — package-internal test helper | | NIT: `accept` param name in `doRequest` | sonnet (2967) | RESOLVED | Non-blocking, acceptable as-is | | MINOR: `startup_failure` and other unknown conclusions map to `"pending"` | gpt (2970) | STILL PRESENT | Design tradeoff — intentional fail-open, documented in switch default comment | | MINOR: pagination relies on `len < 100` rather than Link header | gpt (2970) | STILL PRESENT | Acceptable for this implementation; Link header parsing is a future improvement | | NIT: check-run `Description` set to conclusion string | gpt (2970) | STILL PRESENT | Same as Phase 1 finding #1 above | | MINOR: `CommitStatus.Description` semantic mismatch for check runs | sonnet (2988) | STILL PRESENT | Acknowledged by reviewer as worth revisiting in follow-up, not blocking | | MINOR: `cancelled`/`skipped` → `"success"` loses information | sonnet (2988) | STILL PRESENT | Intentional design tradeoff per doc comment; no fix requested | | NIT: `TestGetCommitStatuses_CheckRunConclusions` subtest naming | sonnet (2988) | RESOLVED | Tests use `tt.conclusion` for subtest name with stringPtr helper | | NIT: `lastErr` dual-use makes loop harder to follow | sonnet (2988) | STILL PRESENT | Design choice; code is correct, NIT only | | Security: HTTPS enforced, redirect strips auth | security (2969, 2991) | RESOLVED | AllowInsecureHTTP option + defaultCheckRedirect implemented and tested | ### Assessment: ✅ Clean All MINOR findings from prior reviews are either resolved or represent documented, intentional design tradeoffs (unknown check run conclusions mapping to `"pending"`; `cancelled`/`skipped` mapping to `"success"`; using `len < 100` for pagination termination). The remaining STILL PRESENT items are acknowledged design decisions, not regressions or oversights. Phase 1 independent findings align with what prior reviewers flagged — no new issues discovered. The `CommitStatus.Description` semantic mismatch for check runs is the most substantive concern and is already tracked for a follow-up. Code builds clean, all tests pass including the `-short` flag variant.
rodin removed the wip label 2026-05-13 04:42:55 +00:00
rodin removed their assignment 2026-05-13 04:44:17 +00:00
aweiker was assigned by rodin 2026-05-13 04:44:17 +00:00
rodin added the ready label 2026-05-13 04:44:17 +00:00
aweiker added 3 commits 2026-05-13 04:47:16 +00:00
Implement PRReader conformance on the GitHub client: GetPullRequest,
GetPullRequestDiff, GetPullRequestFiles (paginated, populates Patch),
GetCommitStatuses (merges commit statuses + check runs).
Adds compile-time PRReader conformance check.

Requires PR A. Part 2 of 3 for #80.
- Implement GetFileContentAtRef on *Client to satisfy vcs.PRReader interface
- Add escapePath and decodeBase64Content helpers
- Fix conformance_test.go to properly import and qualify github.Client
  (was using unqualified Client in package github_test)

Fixes CI failure: the PRReader interface requires GetFileContentAtRef
but it was missing from this PR (only present in the file-reader PR).
fix: address review feedback on PR #102
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 27s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 42s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m11s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m13s
eaccc96073
- Separate maxPages into maxFilesPages and maxCheckRunPages constants
  for clarity (sonnet MINOR #1)
- Add parallel to CheckRunConclusions subtests (sonnet MINOR #2)
- Add TestGetCommitStatuses_CheckRunsErrorAfterStatusesSucceed test
  covering check-runs 500 after statuses succeed (sonnet MINOR #2)
- Expand mapCheckRunStatus doc comment with full mapping rules including
  cancelled/skipped/neutral rationale and unknown value behavior
  (sonnet MINOR #3, gpt MINOR #1)
- Expand GetPullRequest doc comment to mention error types returned
  (sonnet NIT #4)
- Add inline comment on Description field clarifying it holds raw
  conclusion value (gpt NIT #3)
aweiker force-pushed issue-80-b-pr-reader from 329d68e4b4 to eaccc96073 2026-05-13 04:47:16 +00:00 Compare
aweiker was unassigned by rodin 2026-05-13 04:47:59 +00:00
rodin self-assigned this 2026-05-13 04:47:59 +00:00
sonnet-review-bot approved these changes 2026-05-13 04:48:15 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

Well-structured implementation of the PRReader interface with good test coverage and idiomatic Go. The code follows established patterns from the patterns documentation and repository conventions. A few minor observations but nothing blocking.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 178 The Description field for check runs is set to derefString(cr.Conclusion) (raw conclusion value like 'success', 'failure'). This means the Description field has a different semantic for check runs vs commit statuses — for statuses it's a human-readable description ('Build passed'), while for check runs it's a machine value ('success'). This is documented in the comment but may surprise callers. Consider using cr.Name or leaving it empty if no textual description is available from the check runs API (the output.summary field would require schema expansion).
2 [MINOR] github/pr_test.go 460 The TestGetCommitStatuses_CheckRunConclusions table test uses t.Parallel() inside subtests while capturing tt from the outer loop. In Go 1.22+ the loop variable capture issue is fixed, but the test file does not declare a Go version constraint. Since the repo targets latest stable Go, this is fine, but for earlier versions this would be a subtle race. The srv and c variables are created inside the subtest so those are safe; only tt is captured. Given Go 1.22+ fixes, this is acceptable but worth noting.
3 [NIT] github/pr.go 107 The maxFilesPages and maxCheckRunPages constants are both 100, meaning the loop can fetch up to 100 pages × 100 items = 10,000 files/check-runs. GitHub's API silently returns at most 3,000 files for large PRs (it caps the list). The constant value is fine, but a comment noting the effective GitHub limit (3,000 files) would help future readers understand why 10,000 is not actually reachable.
4 [NIT] github/pr_test.go 1 The test file is in package github (internal/white-box test) rather than package github_test. Most of the tests only use exported methods on Client. Using package github_test would be more consistent with the external conformance_test.go and would enforce API boundary discipline. That said, since some tests call c.SetHTTPClient and c.SetRetryBackoff which may be internal methods, the internal package might be intentional.

Recommendation

APPROVE — Approve. The implementation is correct, idiomatic, and well-tested. It follows the repository's patterns: errors are wrapped with context, pagination is bounded, interface conformance is verified at compile time with the blank-identifier pattern, and httptest is used appropriately for handler testing. The minor findings are all non-blocking: the Description semantic mismatch between commit statuses and check runs is documented and a deliberate design choice; the parallel loop capture is safe on modern Go; and the nits are cosmetic. CI is passing.


Review by sonnet


Evaluated against eaccc960

# Sonnet Review ## Summary Well-structured implementation of the PRReader interface with good test coverage and idiomatic Go. The code follows established patterns from the patterns documentation and repository conventions. A few minor observations but nothing blocking. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 178 | The `Description` field for check runs is set to `derefString(cr.Conclusion)` (raw conclusion value like 'success', 'failure'). This means the `Description` field has a different semantic for check runs vs commit statuses — for statuses it's a human-readable description ('Build passed'), while for check runs it's a machine value ('success'). This is documented in the comment but may surprise callers. Consider using `cr.Name` or leaving it empty if no textual description is available from the check runs API (the `output.summary` field would require schema expansion). | | 2 | [MINOR] | `github/pr_test.go` | 460 | The `TestGetCommitStatuses_CheckRunConclusions` table test uses `t.Parallel()` inside subtests while capturing `tt` from the outer loop. In Go 1.22+ the loop variable capture issue is fixed, but the test file does not declare a Go version constraint. Since the repo targets latest stable Go, this is fine, but for earlier versions this would be a subtle race. The `srv` and `c` variables are created inside the subtest so those are safe; only `tt` is captured. Given Go 1.22+ fixes, this is acceptable but worth noting. | | 3 | [NIT] | `github/pr.go` | 107 | The `maxFilesPages` and `maxCheckRunPages` constants are both 100, meaning the loop can fetch up to 100 pages × 100 items = 10,000 files/check-runs. GitHub's API silently returns at most 3,000 files for large PRs (it caps the list). The constant value is fine, but a comment noting the effective GitHub limit (3,000 files) would help future readers understand why 10,000 is not actually reachable. | | 4 | [NIT] | `github/pr_test.go` | 1 | The test file is in `package github` (internal/white-box test) rather than `package github_test`. Most of the tests only use exported methods on `Client`. Using `package github_test` would be more consistent with the external `conformance_test.go` and would enforce API boundary discipline. That said, since some tests call `c.SetHTTPClient` and `c.SetRetryBackoff` which may be internal methods, the internal package might be intentional. | ## Recommendation **APPROVE** — Approve. The implementation is correct, idiomatic, and well-tested. It follows the repository's patterns: errors are wrapped with context, pagination is bounded, interface conformance is verified at compile time with the blank-identifier pattern, and `httptest` is used appropriately for handler testing. The minor findings are all non-blocking: the `Description` semantic mismatch between commit statuses and check runs is documented and a deliberate design choice; the parallel loop capture is safe on modern Go; and the nits are cosmetic. CI is passing. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against eaccc960*
@@ -0,0 +104,4 @@
for page := 1; page <= maxFilesPages; page++ {
reqURL := fmt.Sprintf("%s/repos/%s/%s/pulls/%d/files?per_page=100&page=%d",
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), number, page)
First-time contributor

[NIT] The maxFilesPages and maxCheckRunPages constants are both 100, meaning the loop can fetch up to 100 pages × 100 items = 10,000 files/check-runs. GitHub's API silently returns at most 3,000 files for large PRs (it caps the list). The constant value is fine, but a comment noting the effective GitHub limit (3,000 files) would help future readers understand why 10,000 is not actually reachable.

**[NIT]** The `maxFilesPages` and `maxCheckRunPages` constants are both 100, meaning the loop can fetch up to 100 pages × 100 items = 10,000 files/check-runs. GitHub's API silently returns at most 3,000 files for large PRs (it caps the list). The constant value is fine, but a comment noting the effective GitHub limit (3,000 files) would help future readers understand why 10,000 is not actually reachable.
@@ -0,0 +175,4 @@
return nil, fmt.Errorf("parse check runs JSON: %w", err)
}
for _, cr := range checkResp.CheckRuns {
result = append(result, vcs.CommitStatus{
First-time contributor

[MINOR] The Description field for check runs is set to derefString(cr.Conclusion) (raw conclusion value like 'success', 'failure'). This means the Description field has a different semantic for check runs vs commit statuses — for statuses it's a human-readable description ('Build passed'), while for check runs it's a machine value ('success'). This is documented in the comment but may surprise callers. Consider using cr.Name or leaving it empty if no textual description is available from the check runs API (the output.summary field would require schema expansion).

**[MINOR]** The `Description` field for check runs is set to `derefString(cr.Conclusion)` (raw conclusion value like 'success', 'failure'). This means the `Description` field has a different semantic for check runs vs commit statuses — for statuses it's a human-readable description ('Build passed'), while for check runs it's a machine value ('success'). This is documented in the comment but may surprise callers. Consider using `cr.Name` or leaving it empty if no textual description is available from the check runs API (the `output.summary` field would require schema expansion).
@@ -0,0 +1,676 @@
package github
First-time contributor

[NIT] The test file is in package github (internal/white-box test) rather than package github_test. Most of the tests only use exported methods on Client. Using package github_test would be more consistent with the external conformance_test.go and would enforce API boundary discipline. That said, since some tests call c.SetHTTPClient and c.SetRetryBackoff which may be internal methods, the internal package might be intentional.

**[NIT]** The test file is in `package github` (internal/white-box test) rather than `package github_test`. Most of the tests only use exported methods on `Client`. Using `package github_test` would be more consistent with the external `conformance_test.go` and would enforce API boundary discipline. That said, since some tests call `c.SetHTTPClient` and `c.SetRetryBackoff` which may be internal methods, the internal package might be intentional.
@@ -0,0 +457,4 @@
t.Errorf("expected 'ok', got %q", content)
}
if attempts != 2 {
t.Errorf("expected 2 attempts, got %d", attempts)
First-time contributor

[MINOR] The TestGetCommitStatuses_CheckRunConclusions table test uses t.Parallel() inside subtests while capturing tt from the outer loop. In Go 1.22+ the loop variable capture issue is fixed, but the test file does not declare a Go version constraint. Since the repo targets latest stable Go, this is fine, but for earlier versions this would be a subtle race. The srv and c variables are created inside the subtest so those are safe; only tt is captured. Given Go 1.22+ fixes, this is acceptable but worth noting.

**[MINOR]** The `TestGetCommitStatuses_CheckRunConclusions` table test uses `t.Parallel()` inside subtests while capturing `tt` from the outer loop. In Go 1.22+ the loop variable capture issue is fixed, but the test file does not declare a Go version constraint. Since the repo targets latest stable Go, this is fine, but for earlier versions this would be a subtle race. The `srv` and `c` variables are created inside the subtest so those are safe; only `tt` is captured. Given Go 1.22+ fixes, this is acceptable but worth noting.
rodin removed the ready label 2026-05-13 04:48:49 +00:00
gpt-review-bot approved these changes 2026-05-13 04:48:57 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Solid implementation of the PRReader methods with comprehensive tests and clear error wrapping. Pagination, content decoding, and status/check-run merging are handled cleanly and idiomatically.

Findings

# Severity File Line Finding
1 [MINOR] github/files.go 41 escapePath silently strips dot-segments ("." and ".."), causing paths like "foo/../bar" to resolve to "foo/bar" instead of canonical "bar". This can lead to surprising behavior or 404s; consider using path.Clean for canonicalization and rejecting traversal (e.g., resulting paths starting with "../"), or returning an error on dot-segments rather than altering the path.
2 [NIT] github/files.go 60 decodeBase64Content recreates a strings.Replacer on every call. For very hot paths, you could optimize by defining a package-level replacer or using a more lightweight approach; not critical here.

Recommendation

APPROVE — The changes correctly implement PR metadata, diff retrieval, file listing with pagination, and commit status aggregation from statuses and check runs. Error handling follows best practices with contextual wrapping, and tests are thorough, covering happy paths, retries, malformed JSON, and status-mapping edge cases. The only notable improvement would be to reconsider the escapePath behavior to avoid surprising path rewrites and potentially use canonicalization with explicit traversal checks. Otherwise, the PR is in good shape and ready to merge.


Review by gpt


Evaluated against eaccc960

# Gpt Review ## Summary Solid implementation of the PRReader methods with comprehensive tests and clear error wrapping. Pagination, content decoding, and status/check-run merging are handled cleanly and idiomatically. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/files.go` | 41 | escapePath silently strips dot-segments ("." and ".."), causing paths like "foo/../bar" to resolve to "foo/bar" instead of canonical "bar". This can lead to surprising behavior or 404s; consider using path.Clean for canonicalization and rejecting traversal (e.g., resulting paths starting with "../"), or returning an error on dot-segments rather than altering the path. | | 2 | [NIT] | `github/files.go` | 60 | decodeBase64Content recreates a strings.Replacer on every call. For very hot paths, you could optimize by defining a package-level replacer or using a more lightweight approach; not critical here. | ## Recommendation **APPROVE** — The changes correctly implement PR metadata, diff retrieval, file listing with pagination, and commit status aggregation from statuses and check runs. Error handling follows best practices with contextual wrapping, and tests are thorough, covering happy paths, retries, malformed JSON, and status-mapping edge cases. The only notable improvement would be to reconsider the escapePath behavior to avoid surprising path rewrites and potentially use canonicalization with explicit traversal checks. Otherwise, the PR is in good shape and ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against eaccc960*
@@ -0,0 +38,4 @@
decoded, err := decodeBase64Content(resp.Content)
if err != nil {
return "", fmt.Errorf("decode base64 content for %s: %w", path, err)
}
First-time contributor

[MINOR] escapePath silently strips dot-segments ("." and ".."), causing paths like "foo/../bar" to resolve to "foo/bar" instead of canonical "bar". This can lead to surprising behavior or 404s; consider using path.Clean for canonicalization and rejecting traversal (e.g., resulting paths starting with "../"), or returning an error on dot-segments rather than altering the path.

**[MINOR]** escapePath silently strips dot-segments ("." and ".."), causing paths like "foo/../bar" to resolve to "foo/bar" instead of canonical "bar". This can lead to surprising behavior or 404s; consider using path.Clean for canonicalization and rejecting traversal (e.g., resulting paths starting with "../"), or returning an error on dot-segments rather than altering the path.
github/files.go Outdated
@@ -0,0 +57,4 @@
}
// decodeBase64Content decodes base64-encoded content from the GitHub contents API.
// GitHub returns base64 content with line breaks for formatting; we strip \r and \n before decoding.
First-time contributor

[NIT] decodeBase64Content recreates a strings.Replacer on every call. For very hot paths, you could optimize by defining a package-level replacer or using a more lightweight approach; not critical here.

**[NIT]** decodeBase64Content recreates a strings.Replacer on every call. For very hot paths, you could optimize by defining a package-level replacer or using a more lightweight approach; not critical here.
security-review-bot requested review from security-review-bot 2026-05-13 04:48:59 +00:00
security-review-bot approved these changes 2026-05-13 04:48:59 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Changes implement GitHub PR reading functionality with careful URL encoding, pagination bounds, and robust error handling. No exploitable security issues are evident; the code avoids injection vectors and uses safe defaults.

Findings

# Severity File Line Finding
1 [NIT] github/files.go 26 User-controlled path is interpolated into error messages without sanitization. If callers log these errors verbatim, an attacker-controlled path containing newlines could enable log injection. Low practical risk given typical GitHub path constraints.
2 [NIT] github/files.go 36 Same as above: error includes raw path value. Consider escaping or sanitizing control characters before embedding in error strings.
3 [NIT] github/files.go 40 Same as above: error message includes raw path. Defense-in-depth to avoid potential log injection if higher layers log errors without escaping.

Recommendation

APPROVE — Approve the PR. The implementation safely constructs API URLs using PathEscape and strips dot-segments to prevent path traversal within the API path, uses bounded pagination to mitigate unbounded resource consumption, and cleanly handles HTTP and JSON errors. For defense-in-depth, consider sanitizing or escaping control characters (e.g., newlines) in user-controlled fields like 'path' before including them in error message strings, or ensure upstream logging uses structured logging that escapes such characters. Otherwise, no security vulnerabilities were found.


Review by security


Evaluated against eaccc960

# Security Review ## Summary Changes implement GitHub PR reading functionality with careful URL encoding, pagination bounds, and robust error handling. No exploitable security issues are evident; the code avoids injection vectors and uses safe defaults. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `github/files.go` | 26 | User-controlled path is interpolated into error messages without sanitization. If callers log these errors verbatim, an attacker-controlled path containing newlines could enable log injection. Low practical risk given typical GitHub path constraints. | | 2 | [NIT] | `github/files.go` | 36 | Same as above: error includes raw path value. Consider escaping or sanitizing control characters before embedding in error strings. | | 3 | [NIT] | `github/files.go` | 40 | Same as above: error message includes raw path. Defense-in-depth to avoid potential log injection if higher layers log errors without escaping. | ## Recommendation **APPROVE** — Approve the PR. The implementation safely constructs API URLs using PathEscape and strips dot-segments to prevent path traversal within the API path, uses bounded pagination to mitigate unbounded resource consumption, and cleanly handles HTTP and JSON errors. For defense-in-depth, consider sanitizing or escaping control characters (e.g., newlines) in user-controlled fields like 'path' before including them in error message strings, or ensure upstream logging uses structured logging that escapes such characters. Otherwise, no security vulnerabilities were found. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against eaccc960*
@@ -0,0 +23,4 @@
}
body, err := c.doGet(ctx, reqURL)
if err != nil {
return "", fmt.Errorf("fetch file %s: %w", path, err)
Collaborator

[NIT] User-controlled path is interpolated into error messages without sanitization. If callers log these errors verbatim, an attacker-controlled path containing newlines could enable log injection. Low practical risk given typical GitHub path constraints.

**[NIT]** User-controlled path is interpolated into error messages without sanitization. If callers log these errors verbatim, an attacker-controlled path containing newlines could enable log injection. Low practical risk given typical GitHub path constraints.
@@ -0,0 +33,4 @@
return "", fmt.Errorf("parse file content JSON: %w", err)
}
if resp.Encoding != "base64" {
return "", fmt.Errorf("unexpected encoding %q for file %s", resp.Encoding, path)
Collaborator

[NIT] Same as above: error includes raw path value. Consider escaping or sanitizing control characters before embedding in error strings.

**[NIT]** Same as above: error includes raw path value. Consider escaping or sanitizing control characters before embedding in error strings.
github/files.go Outdated
@@ -0,0 +37,4 @@
}
decoded, err := decodeBase64Content(resp.Content)
if err != nil {
return "", fmt.Errorf("decode base64 content for %s: %w", path, err)
Collaborator

[NIT] Same as above: error message includes raw path. Defense-in-depth to avoid potential log injection if higher layers log errors without escaping.

**[NIT]** Same as above: error message includes raw path. Defense-in-depth to avoid potential log injection if higher layers log errors without escaping.
rodin added the wip label 2026-05-13 04:54:20 +00:00
Author
Owner

Self-Review: PR #102

Self-review against eaccc96073

Phase 1: Independent Findings

  1. [MINOR] github/files.goescapePath silently rewrites paths with dot-segments
    escapePath silently drops . and .. segments instead of returning an error. A caller passing "foo/../bar" gets "foo/bar", not "bar" or an error. The behavior is documented in the function comment, but silent path mutation can surprise callers who pass user-controlled paths thinking they get an exact API hit. The gpt-review-bot flagged this; see Phase 2. No fix in current HEAD.

  2. [MINOR] github/pr.goCommitStatus.Description semantic mismatch for check runs
    For commit statuses, Description is a human-readable string (e.g. "Build passed"). For check runs, Description: derefString(cr.Conclusion) stores the raw conclusion enum (e.g. "failure", "skipped"). Callers expecting a narrative description field will get a machine-readable enum. This is documented inline but is a semantic inconsistency in the vcs.CommitStatus type.

  3. [NIT] github/pr.go — unknown non-nil check run conclusions map to "pending" (fail-open)
    mapCheckRunStatus default case returns "pending" for any unrecognized non-nil conclusion (e.g. "startup_failure"). This means a genuinely failed check could appear pending if GitHub adds a new conclusion value. The doc comment documents this is intentional. Acceptable design tradeoff, worth noting.

  4. [NIT] github/files.gostrings.Replacer instantiated on each call in decodeBase64Content
    Minor: strings.NewReplacer(...) is called on every invocation. Moving it to a package-level variable would avoid repeated allocations on hot paths. Low-priority optimization.

Phase 2: Prior Review Verification

Prior self-review was against 329d68e4. Since then, one fix commit was applied (eaccc960). Most recent bot reviews are also against eaccc960.

Finding Reviewer Status Notes
MINOR: shared maxPages constant sonnet (2967) RESOLVED Separate maxFilesPages/maxCheckRunPages constants present in diff
MINOR: no test for check-runs error after statuses succeed sonnet (2967) RESOLVED TestGetCommitStatuses_CheckRunsErrorAfterStatusesSucceed present
MINOR: cancelled/skipped/neutral"success" needs prominent docs sonnet (2967) RESOLVED mapCheckRunStatus has expanded doc comment with full mapping rules
NIT: GetPullRequest doc comment minimal sonnet (2967) RESOLVED Doc comment now mentions *APIError, IsNotFound, IsUnauthorized
MINOR: startup_failure maps to "pending" gpt (2970) STILL PRESENT Intentional fail-open; documented in switch default comment
MINOR: pagination uses len < 100 not Link header gpt (2970) STILL PRESENT Acknowledged as future improvement; acceptable for now
NIT: check-run Description set to conclusion string gpt (2970) STILL PRESENT Documented inline; semantic mismatch vs commit statuses
MINOR: Description semantic mismatch for check runs sonnet (2988/2994) STILL PRESENT Acknowledged as worth revisiting in follow-up; not blocking
MINOR: cancelled/skipped"success" loses information sonnet (2988/2994) STILL PRESENT Intentional design tradeoff; documented in switch comment
NIT: lastErr dual-use in retry loop sonnet (2988) STILL PRESENT Code is correct; NIT only
NIT: TestGetCommitStatuses_CheckRunConclusions uses t.Parallel() in subtests sonnet (2994) RESOLVED t.Parallel() added in fix commit; Go 1.22+ loop semantics safe
NIT: GitHub 3,000 file limit not mentioned in comment sonnet (2994) STILL PRESENT No comment added; minor doc gap
NIT: pr_test.go uses package github not package github_test sonnet (2994) STILL PRESENT Intentional (needs SetHTTPClient/SetRetryBackoff); acceptable
MINOR: escapePath silently drops dot-segments gpt (2995) STILL PRESENT Documented but no error returned; Phase 1 finding #1 above
NIT: strings.Replacer recreated per call gpt (2995) STILL PRESENT Minor perf; no fix applied
NIT: user-controlled path in error strings (log injection risk) security (2997) STILL PRESENT Low practical risk; no fix applied

Assessment: ⚠️ Needs attention

Two MINOR findings remain unresolved: (1) escapePath silently rewrites paths with dot-segments without returning an error or even rejecting the input — callers may be surprised by the silent mutation; (2) CommitStatus.Description has inconsistent semantics between commit statuses (human-readable) and check runs (machine enum). Neither is a blocking correctness bug, but the escapePath behavior in particular could cause subtle API misses if callers pass normalized paths expecting exact hits. All other STILL PRESENT items are documented, intentional design tradeoffs. The code is correct and tests pass.

## Self-Review: PR #102 Self-review against eaccc9607375f721b40799201d9dc814ba7dbdc8 ### Phase 1: Independent Findings 1. **[MINOR] `github/files.go` — `escapePath` silently rewrites paths with dot-segments** `escapePath` silently drops `.` and `..` segments instead of returning an error. A caller passing `"foo/../bar"` gets `"foo/bar"`, not `"bar"` or an error. The behavior is documented in the function comment, but silent path mutation can surprise callers who pass user-controlled paths thinking they get an exact API hit. The `gpt-review-bot` flagged this; see Phase 2. No fix in current HEAD. 2. **[MINOR] `github/pr.go` — `CommitStatus.Description` semantic mismatch for check runs** For commit statuses, `Description` is a human-readable string (e.g. `"Build passed"`). For check runs, `Description: derefString(cr.Conclusion)` stores the raw conclusion enum (e.g. `"failure"`, `"skipped"`). Callers expecting a narrative description field will get a machine-readable enum. This is documented inline but is a semantic inconsistency in the `vcs.CommitStatus` type. 3. **[NIT] `github/pr.go` — unknown non-nil check run conclusions map to `"pending"` (fail-open)** `mapCheckRunStatus` default case returns `"pending"` for any unrecognized non-nil conclusion (e.g. `"startup_failure"`). This means a genuinely failed check could appear pending if GitHub adds a new conclusion value. The doc comment documents this is intentional. Acceptable design tradeoff, worth noting. 4. **[NIT] `github/files.go` — `strings.Replacer` instantiated on each call in `decodeBase64Content`** Minor: `strings.NewReplacer(...)` is called on every invocation. Moving it to a package-level variable would avoid repeated allocations on hot paths. Low-priority optimization. ### Phase 2: Prior Review Verification Prior self-review was against `329d68e4`. Since then, one fix commit was applied (`eaccc960`). Most recent bot reviews are also against `eaccc960`. | Finding | Reviewer | Status | Notes | |---------|----------|--------|-------| | MINOR: shared `maxPages` constant | sonnet (2967) | RESOLVED | Separate `maxFilesPages`/`maxCheckRunPages` constants present in diff | | MINOR: no test for check-runs error after statuses succeed | sonnet (2967) | RESOLVED | `TestGetCommitStatuses_CheckRunsErrorAfterStatusesSucceed` present | | MINOR: `cancelled`/`skipped`/`neutral` → `"success"` needs prominent docs | sonnet (2967) | RESOLVED | `mapCheckRunStatus` has expanded doc comment with full mapping rules | | NIT: `GetPullRequest` doc comment minimal | sonnet (2967) | RESOLVED | Doc comment now mentions `*APIError`, `IsNotFound`, `IsUnauthorized` | | MINOR: `startup_failure` maps to `"pending"` | gpt (2970) | STILL PRESENT | Intentional fail-open; documented in switch default comment | | MINOR: pagination uses `len < 100` not Link header | gpt (2970) | STILL PRESENT | Acknowledged as future improvement; acceptable for now | | NIT: check-run `Description` set to conclusion string | gpt (2970) | STILL PRESENT | Documented inline; semantic mismatch vs commit statuses | | MINOR: `Description` semantic mismatch for check runs | sonnet (2988/2994) | STILL PRESENT | Acknowledged as worth revisiting in follow-up; not blocking | | MINOR: `cancelled`/`skipped` → `"success"` loses information | sonnet (2988/2994) | STILL PRESENT | Intentional design tradeoff; documented in switch comment | | NIT: `lastErr` dual-use in retry loop | sonnet (2988) | STILL PRESENT | Code is correct; NIT only | | NIT: `TestGetCommitStatuses_CheckRunConclusions` uses `t.Parallel()` in subtests | sonnet (2994) | RESOLVED | `t.Parallel()` added in fix commit; Go 1.22+ loop semantics safe | | NIT: GitHub 3,000 file limit not mentioned in comment | sonnet (2994) | STILL PRESENT | No comment added; minor doc gap | | NIT: `pr_test.go` uses `package github` not `package github_test` | sonnet (2994) | STILL PRESENT | Intentional (needs SetHTTPClient/SetRetryBackoff); acceptable | | MINOR: `escapePath` silently drops dot-segments | gpt (2995) | STILL PRESENT | Documented but no error returned; Phase 1 finding #1 above | | NIT: `strings.Replacer` recreated per call | gpt (2995) | STILL PRESENT | Minor perf; no fix applied | | NIT: user-controlled path in error strings (log injection risk) | security (2997) | STILL PRESENT | Low practical risk; no fix applied | ### Assessment: ⚠️ Needs attention Two MINOR findings remain unresolved: (1) `escapePath` silently rewrites paths with dot-segments without returning an error or even rejecting the input — callers may be surprised by the silent mutation; (2) `CommitStatus.Description` has inconsistent semantics between commit statuses (human-readable) and check runs (machine enum). Neither is a blocking correctness bug, but the `escapePath` behavior in particular could cause subtle API misses if callers pass normalized paths expecting exact hits. All other STILL PRESENT items are documented, intentional design tradeoffs. The code is correct and tests pass.
rodin removed the wip label 2026-05-13 04:56:01 +00:00
rodin added the wip label 2026-05-13 04:59:19 +00:00
rodin added 1 commit 2026-05-13 05:03:57 +00:00
fix(github): escapePath returns error on dot-segments, fix Description semantics
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 33s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m24s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m44s
3cd5ae594e
- escapePath now returns an error when paths contain dot-segments
  (".", "..") instead of silently rewriting them. This prevents
  subtle API misses where callers pass "foo/../bar" expecting to
  hit "bar" but the old code produced "foo/bar".
- Uses path.Clean for canonical form after validation.
- CommitStatus.Description for check runs is now empty string
  instead of the raw conclusion enum. The conclusion is already
  captured in the Status field via mapCheckRunStatus; storing it
  again in Description was semantically inconsistent with commit
  statuses where Description carries a human-readable narrative.
- Removed unused derefString helper.
- Added tests for escapePath valid paths, dot-segment rejection,
  and GetFileContentAtRef dot-segment error propagation.
rodin removed the wip label 2026-05-13 05:04:01 +00:00
sonnet-review-bot approved these changes 2026-05-13 05:04:54 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

Clean implementation of the PRReader interface on the GitHub client. The code follows established patterns (compile-time interface check, table-driven tests, proper error wrapping, pagination with bounds), is well-documented, and CI passes. Minor issues noted but none are blockers.

Findings

# Severity File Line Finding
1 [NIT] github/files_test.go 72 TestGetFileContentAtRef_DotSegmentError constructs the client with NewClient(srv.URL, "token") — argument order is reversed compared to all other tests in pr_test.go which use NewClient("token", srv.URL, AllowInsecureHTTP()). If the constructor signature is NewClient(token, baseURL string, opts...), this test may pass the URL as the token and vice versa, which would be a latent correctness bug. The test happens to work only because it expects the server to never be called (the error is caught before the HTTP request), so the wrong baseURL is never used. Verify the argument order is intentional or align with the rest of the test suite.
2 [NIT] github/pr_test.go 392 TestGetCommitStatuses_CheckRunConclusions starts subtests with t.Parallel() inside the loop but does not call t.Parallel() on the outer test. The outer test is not marked parallel, which is fine, but the subtests capture tt by reference. In Go versions prior to 1.22 this is a loop variable capture bug (all subtests could see the last value of tt). Since the repo targets the latest stable Go (1.22+), the loop variable semantics are fixed. This is a non-issue on 1.22+ but worth noting if backporting compatibility is ever needed.
3 [NIT] github/pr.go 185 The Description field for check runs is explicitly set to an empty string with a comment explaining why. This is fine and well-documented. A minor alternative would be to omit the field entirely (zero value), but the explicit assignment with comment is arguably more readable — no action needed.
4 [NIT] github/pr.go 100 maxFilesPages and maxCheckRunPages are both 100, allowing up to 10,000 files or check runs per PR. This is a very generous bound that will never be hit in practice, but it means a misbehaving server (always returning a full page) could cause 100 API calls before giving up. This is documented in the constant comments, so the trade-off is conscious. Consider whether a lower bound (e.g. 10–20) would still be sufficient for real-world PRs while being more conservative on runaway loops.

Recommendation

APPROVE — Approve. The implementation is correct, well-tested, and follows the established patterns in the codebase (compile-time interface assertion, table-driven tests, proper error wrapping with context, pagination with explicit page bounds, httptest-based HTTP mocking). CI passes. The only actionable item worth a follow-up is the reversed argument order in TestGetFileContentAtRef_DotSegmentError — this should be verified against the actual NewClient signature to confirm it isn't silently wrong.


Review by sonnet


Evaluated against 3cd5ae59

# Sonnet Review ## Summary Clean implementation of the PRReader interface on the GitHub client. The code follows established patterns (compile-time interface check, table-driven tests, proper error wrapping, pagination with bounds), is well-documented, and CI passes. Minor issues noted but none are blockers. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [NIT] | `github/files_test.go` | 72 | TestGetFileContentAtRef_DotSegmentError constructs the client with `NewClient(srv.URL, "token")` — argument order is reversed compared to all other tests in pr_test.go which use `NewClient("token", srv.URL, AllowInsecureHTTP())`. If the constructor signature is `NewClient(token, baseURL string, opts...)`, this test may pass the URL as the token and vice versa, which would be a latent correctness bug. The test happens to work only because it expects the server to never be called (the error is caught before the HTTP request), so the wrong baseURL is never used. Verify the argument order is intentional or align with the rest of the test suite. | | 2 | [NIT] | `github/pr_test.go` | 392 | TestGetCommitStatuses_CheckRunConclusions starts subtests with `t.Parallel()` inside the loop but does not call `t.Parallel()` on the outer test. The outer test is not marked parallel, which is fine, but the subtests capture `tt` by reference. In Go versions prior to 1.22 this is a loop variable capture bug (all subtests could see the last value of `tt`). Since the repo targets the latest stable Go (1.22+), the loop variable semantics are fixed. This is a non-issue on 1.22+ but worth noting if backporting compatibility is ever needed. | | 3 | [NIT] | `github/pr.go` | 185 | The `Description` field for check runs is explicitly set to an empty string with a comment explaining why. This is fine and well-documented. A minor alternative would be to omit the field entirely (zero value), but the explicit assignment with comment is arguably more readable — no action needed. | | 4 | [NIT] | `github/pr.go` | 100 | `maxFilesPages` and `maxCheckRunPages` are both 100, allowing up to 10,000 files or check runs per PR. This is a very generous bound that will never be hit in practice, but it means a misbehaving server (always returning a full page) could cause 100 API calls before giving up. This is documented in the constant comments, so the trade-off is conscious. Consider whether a lower bound (e.g. 10–20) would still be sufficient for real-world PRs while being more conservative on runaway loops. | ## Recommendation **APPROVE** — Approve. The implementation is correct, well-tested, and follows the established patterns in the codebase (compile-time interface assertion, table-driven tests, proper error wrapping with context, pagination with explicit page bounds, `httptest`-based HTTP mocking). CI passes. The only actionable item worth a follow-up is the reversed argument order in `TestGetFileContentAtRef_DotSegmentError` — this should be verified against the actual `NewClient` signature to confirm it isn't silently wrong. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 3cd5ae59*
@@ -0,0 +69,4 @@
defer srv.Close()
c := NewClient(srv.URL, "token")
_, err := c.GetFileContentAtRef(context.Background(), "owner", "repo", "foo/../bar.go", "main")
First-time contributor

[NIT] TestGetFileContentAtRef_DotSegmentError constructs the client with NewClient(srv.URL, "token") — argument order is reversed compared to all other tests in pr_test.go which use NewClient("token", srv.URL, AllowInsecureHTTP()). If the constructor signature is NewClient(token, baseURL string, opts...), this test may pass the URL as the token and vice versa, which would be a latent correctness bug. The test happens to work only because it expects the server to never be called (the error is caught before the HTTP request), so the wrong baseURL is never used. Verify the argument order is intentional or align with the rest of the test suite.

**[NIT]** TestGetFileContentAtRef_DotSegmentError constructs the client with `NewClient(srv.URL, "token")` — argument order is reversed compared to all other tests in pr_test.go which use `NewClient("token", srv.URL, AllowInsecureHTTP())`. If the constructor signature is `NewClient(token, baseURL string, opts...)`, this test may pass the URL as the token and vice versa, which would be a latent correctness bug. The test happens to work only because it expects the server to never be called (the error is caught before the HTTP request), so the wrong baseURL is never used. Verify the argument order is intentional or align with the rest of the test suite.
@@ -0,0 +97,4 @@
// GetPullRequestFiles fetches the list of files changed in a PR.
// Paginates through all pages (100 per page) to collect all files.
// Returns nil (not an empty slice) when the PR has no changed files.
First-time contributor

[NIT] maxFilesPages and maxCheckRunPages are both 100, allowing up to 10,000 files or check runs per PR. This is a very generous bound that will never be hit in practice, but it means a misbehaving server (always returning a full page) could cause 100 API calls before giving up. This is documented in the constant comments, so the trade-off is conscious. Consider whether a lower bound (e.g. 10–20) would still be sufficient for real-world PRs while being more conservative on runaway loops.

**[NIT]** `maxFilesPages` and `maxCheckRunPages` are both 100, allowing up to 10,000 files or check runs per PR. This is a very generous bound that will never be hit in practice, but it means a misbehaving server (always returning a full page) could cause 100 API calls before giving up. This is documented in the constant comments, so the trade-off is conscious. Consider whether a lower bound (e.g. 10–20) would still be sufficient for real-world PRs while being more conservative on runaway loops.
@@ -0,0 +182,4 @@
TargetURL: cr.HTMLURL,
})
}
if len(checkResp.CheckRuns) < 100 {
First-time contributor

[NIT] The Description field for check runs is explicitly set to an empty string with a comment explaining why. This is fine and well-documented. A minor alternative would be to omit the field entirely (zero value), but the explicit assignment with comment is arguably more readable — no action needed.

**[NIT]** The `Description` field for check runs is explicitly set to an empty string with a comment explaining why. This is fine and well-documented. A minor alternative would be to omit the field entirely (zero value), but the explicit assignment with comment is arguably more readable — no action needed.
@@ -0,0 +389,4 @@
defer srv.Close()
c := NewClient("token", srv.URL, AllowInsecureHTTP())
c.SetHTTPClient(srv.Client())
First-time contributor

[NIT] TestGetCommitStatuses_CheckRunConclusions starts subtests with t.Parallel() inside the loop but does not call t.Parallel() on the outer test. The outer test is not marked parallel, which is fine, but the subtests capture tt by reference. In Go versions prior to 1.22 this is a loop variable capture bug (all subtests could see the last value of tt). Since the repo targets the latest stable Go (1.22+), the loop variable semantics are fixed. This is a non-issue on 1.22+ but worth noting if backporting compatibility is ever needed.

**[NIT]** TestGetCommitStatuses_CheckRunConclusions starts subtests with `t.Parallel()` inside the loop but does not call `t.Parallel()` on the outer test. The outer test is not marked parallel, which is fine, but the subtests capture `tt` by reference. In Go versions prior to 1.22 this is a loop variable capture bug (all subtests could see the last value of `tt`). Since the repo targets the latest stable Go (1.22+), the loop variable semantics are fixed. This is a non-issue on 1.22+ but worth noting if backporting compatibility is ever needed.
security-review-bot requested review from security-review-bot 2026-05-13 05:05:33 +00:00
security-review-bot approved these changes 2026-05-13 05:05:33 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Security posture of the new GitHub PR/file/status retrieval code is sound: inputs are properly URL-escaped, path traversal is prevented, pagination is bounded, and errors are wrapped without leaking sensitive data. CI has passed and tests cover key error scenarios.

Findings

# Severity File Line Finding
1 [MINOR] github/files.go 79 decodeBase64Content decodes the entire file content into memory without checking size limits. If an attacker can influence which file is fetched, this could be used for resource exhaustion (DoS). Consider enforcing a maximum allowed content size and ensure HTTP client timeouts and response body limits are in place.

Recommendation

APPROVE — Overall the changes implement PRReader securely and correctly. Path traversal is mitigated by explicit dot-segment checks and per-segment escaping, URL parameters are properly escaped, and pagination has sane upper bounds to avoid unbounded loops. Error handling appears consistent with returning typed API errors. As a defense-in-depth improvement, add size checks before decoding base64 content from the contents API (and ensure doGet/doRequest enforce timeouts and response body limits) to reduce the risk of memory exhaustion if large files are requested. Otherwise, the implementation looks good to merge.


Review by security


Evaluated against 3cd5ae59

# Security Review ## Summary Security posture of the new GitHub PR/file/status retrieval code is sound: inputs are properly URL-escaped, path traversal is prevented, pagination is bounded, and errors are wrapped without leaking sensitive data. CI has passed and tests cover key error scenarios. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/files.go` | 79 | decodeBase64Content decodes the entire file content into memory without checking size limits. If an attacker can influence which file is fetched, this could be used for resource exhaustion (DoS). Consider enforcing a maximum allowed content size and ensure HTTP client timeouts and response body limits are in place. | ## Recommendation **APPROVE** — Overall the changes implement PRReader securely and correctly. Path traversal is mitigated by explicit dot-segment checks and per-segment escaping, URL parameters are properly escaped, and pagination has sane upper bounds to avoid unbounded loops. Error handling appears consistent with returning typed API errors. As a defense-in-depth improvement, add size checks before decoding base64 content from the contents API (and ensure doGet/doRequest enforce timeouts and response body limits) to reduce the risk of memory exhaustion if large files are requested. Otherwise, the implementation looks good to merge. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 3cd5ae59*
github/files.go Outdated
@@ -0,0 +76,4 @@
}
// decodeBase64Content decodes base64-encoded content from the GitHub contents API.
// GitHub returns base64 content with line breaks for formatting; we strip \r and \n before decoding.
Collaborator

[MINOR] decodeBase64Content decodes the entire file content into memory without checking size limits. If an attacker can influence which file is fetched, this could be used for resource exhaustion (DoS). Consider enforcing a maximum allowed content size and ensure HTTP client timeouts and response body limits are in place.

**[MINOR]** decodeBase64Content decodes the entire file content into memory without checking size limits. If an attacker can influence which file is fetched, this could be used for resource exhaustion (DoS). Consider enforcing a maximum allowed content size and ensure HTTP client timeouts and response body limits are in place.
gpt-review-bot approved these changes 2026-05-13 05:06:05 +00:00
Dismissed
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Well-structured implementation of PRReader methods with thorough tests and idiomatic error handling. Pagination, status merging, and path handling are correct and covered by tests. CI passed.

Findings

# Severity File Line Finding
1 [MINOR] github/files_test.go 71 NewClient is called with arguments in an order inconsistent with other tests (srv.URL, "token"). While this test never performs a request, it’s confusing and could break if behavior changes. Use the same argument order as elsewhere (e.g., NewClient("token", srv.URL, ...)).
2 [NIT] github/pr.go 140 mapCheckRunStatus comment mentions "waiting" as a possible conclusion, but GitHub check run 'conclusion' values don't include "waiting" (that's a status). Consider updating the comment to avoid confusion; the function only examines 'conclusion'.

Recommendation

APPROVE — The changes implement the PRReader interface cleanly with solid test coverage, appropriate error wrapping, and adherence to repository and Go patterns. Address the minor inconsistency in github/files_test.go by fixing the NewClient argument order for clarity, and consider clarifying the mapCheckRunStatus comment. Otherwise, this is ready to merge.


Review by gpt


Evaluated against 3cd5ae59

# Gpt Review ## Summary Well-structured implementation of PRReader methods with thorough tests and idiomatic error handling. Pagination, status merging, and path handling are correct and covered by tests. CI passed. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/files_test.go` | 71 | NewClient is called with arguments in an order inconsistent with other tests (srv.URL, "token"). While this test never performs a request, it’s confusing and could break if behavior changes. Use the same argument order as elsewhere (e.g., NewClient("token", srv.URL, ...)). | | 2 | [NIT] | `github/pr.go` | 140 | mapCheckRunStatus comment mentions "waiting" as a possible conclusion, but GitHub check run 'conclusion' values don't include "waiting" (that's a status). Consider updating the comment to avoid confusion; the function only examines 'conclusion'. | ## Recommendation **APPROVE** — The changes implement the PRReader interface cleanly with solid test coverage, appropriate error wrapping, and adherence to repository and Go patterns. Address the minor inconsistency in github/files_test.go by fixing the NewClient argument order for clarity, and consider clarifying the mapCheckRunStatus comment. Otherwise, this is ready to merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 3cd5ae59*
@@ -0,0 +68,4 @@
}))
defer srv.Close()
c := NewClient(srv.URL, "token")
First-time contributor

[MINOR] NewClient is called with arguments in an order inconsistent with other tests (srv.URL, "token"). While this test never performs a request, it’s confusing and could break if behavior changes. Use the same argument order as elsewhere (e.g., NewClient("token", srv.URL, ...)).

**[MINOR]** NewClient is called with arguments in an order inconsistent with other tests (srv.URL, "token"). While this test never performs a request, it’s confusing and could break if behavior changes. Use the same argument order as elsewhere (e.g., NewClient("token", srv.URL, ...)).
@@ -0,0 +137,4 @@
// If the commit statuses endpoint fails (e.g. 404 for an unknown SHA), the
// function returns immediately without attempting the check-runs endpoint.
// If the check-runs endpoint fails after statuses were fetched successfully,
// the function returns an error (not a partial result) so callers always get
First-time contributor

[NIT] mapCheckRunStatus comment mentions "waiting" as a possible conclusion, but GitHub check run 'conclusion' values don't include "waiting" (that's a status). Consider updating the comment to avoid confusion; the function only examines 'conclusion'.

**[NIT]** mapCheckRunStatus comment mentions "waiting" as a possible conclusion, but GitHub check run 'conclusion' values don't include "waiting" (that's a status). Consider updating the comment to avoid confusion; the function only examines 'conclusion'.
rodin added the ready label 2026-05-13 05:06:54 +00:00
rodin added wip and removed ready labels 2026-05-13 05:09:13 +00:00
Author
Owner

Self-Review: PR #102

Self-review against 3cd5ae594e

Phase 1: Independent Findings

Reviewing as a stranger to this code:

  1. [NIT] github/files_test.go — leading slash silently stripped in escapePath
    escapePath("/path/to/file.go")"path/to/file.go" (test case "leading slash stripped"). The stripping is done silently via path.Clean + empty-segment filtering. Callers passing absolute-style paths expecting an error won't get one. Low risk for a private function; documented by the test case name. Acceptable.

  2. [OBSERVATION] mapCheckRunStatus"stale" and "waiting" mapped to "pending"
    A "stale" run (abandoned, will never complete) maps to "pending", which could block callers that poll until all statuses are non-pending. Documented in the switch comment as intentional. Acceptable design tradeoff; no fix required.

No blocking issues found. The two MINOR findings from the prior self-review (escapePath dot-segment rejection and Description semantic mismatch) are both resolved in this HEAD.

Phase 2: Prior Review Verification

Prior self-review (HEAD eaccc960) found two MINOR issues unresolved. Verifying against current HEAD 3cd5ae5:

Finding Reviewer Status Notes
MINOR: escapePath silently rewrites dot-segment paths (no error returned) rodin self-review / gpt RESOLVED escapePath now rejects any segment equal to "." or ".." with an explicit error; TestEscapePath_DotSegments table tests confirm all cases
MINOR: CommitStatus.Description semantic mismatch for check runs rodin self-review / sonnet / gpt RESOLVED Description for check runs is now "" with inline comment; conclusion is captured in Status only
MINOR: startup_failure maps to "pending" (fail-open default) gpt STILL PRESENT Intentional; documented in switch default comment
MINOR: pagination uses len < 100 not Link header gpt STILL PRESENT Acknowledged future improvement; acceptable for now
MINOR: cancelled/skipped"success" loses information sonnet STILL PRESENT Intentional design tradeoff; documented in switch comment
NIT: GitHub 3,000 file limit not mentioned in pagination comment sonnet STILL PRESENT Minor doc gap; no fix applied
NIT: pr_test.go uses package github not package github_test sonnet STILL PRESENT Intentional (needs SetHTTPClient/SetRetryBackoff); acceptable
NIT: strings.Replacer recreated per call in decodeBase64Content gpt STILL PRESENT Minor perf; no fix applied
NIT: user-controlled path in error strings (log injection risk) security STILL PRESENT Low practical risk; no fix applied

Assessment: Clean

The two MINOR findings from the prior self-review are resolved. All remaining STILL PRESENT items are documented, intentional design tradeoffs or low-priority NITs. The code is correct, tests pass, and no new blocking issues found in Phase 1.

## Self-Review: PR #102 Self-review against 3cd5ae594e9d7a12486eb9980c3ca44225aeb507 ### Phase 1: Independent Findings Reviewing as a stranger to this code: 1. **[NIT] `github/files_test.go` — leading slash silently stripped in `escapePath`** `escapePath("/path/to/file.go")` → `"path/to/file.go"` (test case "leading slash stripped"). The stripping is done silently via `path.Clean` + empty-segment filtering. Callers passing absolute-style paths expecting an error won't get one. Low risk for a private function; documented by the test case name. Acceptable. 2. **[OBSERVATION] `mapCheckRunStatus` — `"stale"` and `"waiting"` mapped to `"pending"`** A `"stale"` run (abandoned, will never complete) maps to `"pending"`, which could block callers that poll until all statuses are non-pending. Documented in the switch comment as intentional. Acceptable design tradeoff; no fix required. No blocking issues found. The two MINOR findings from the prior self-review (escapePath dot-segment rejection and Description semantic mismatch) are both resolved in this HEAD. ### Phase 2: Prior Review Verification Prior self-review (HEAD `eaccc960`) found two MINOR issues unresolved. Verifying against current HEAD `3cd5ae5`: | Finding | Reviewer | Status | Notes | |---------|----------|--------|-------| | MINOR: `escapePath` silently rewrites dot-segment paths (no error returned) | rodin self-review / gpt | RESOLVED | `escapePath` now rejects any segment equal to `"."` or `".."` with an explicit error; `TestEscapePath_DotSegments` table tests confirm all cases | | MINOR: `CommitStatus.Description` semantic mismatch for check runs | rodin self-review / sonnet / gpt | RESOLVED | `Description` for check runs is now `""` with inline comment; conclusion is captured in `Status` only | | MINOR: `startup_failure` maps to `"pending"` (fail-open default) | gpt | STILL PRESENT | Intentional; documented in switch default comment | | MINOR: pagination uses `len < 100` not Link header | gpt | STILL PRESENT | Acknowledged future improvement; acceptable for now | | MINOR: `cancelled`/`skipped` → `"success"` loses information | sonnet | STILL PRESENT | Intentional design tradeoff; documented in switch comment | | NIT: GitHub 3,000 file limit not mentioned in pagination comment | sonnet | STILL PRESENT | Minor doc gap; no fix applied | | NIT: `pr_test.go` uses `package github` not `package github_test` | sonnet | STILL PRESENT | Intentional (needs SetHTTPClient/SetRetryBackoff); acceptable | | NIT: `strings.Replacer` recreated per call in `decodeBase64Content` | gpt | STILL PRESENT | Minor perf; no fix applied | | NIT: user-controlled path in error strings (log injection risk) | security | STILL PRESENT | Low practical risk; no fix applied | ### Assessment: ✅ Clean The two MINOR findings from the prior self-review are resolved. All remaining STILL PRESENT items are documented, intentional design tradeoffs or low-priority NITs. The code is correct, tests pass, and no new blocking issues found in Phase 1.
rodin removed the wip label 2026-05-13 05:10:44 +00:00
rodin added the wip label 2026-05-13 05:14:32 +00:00
rodin added 1 commit 2026-05-13 05:17:38 +00:00
fix: address review feedback on PRReader implementation
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 19s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 45s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m55s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m55s
55366b3431
- Add maxFileContentSize (10 MB) limit to decodeBase64Content to prevent
  resource exhaustion from oversized file content (security MINOR)
- Fix reversed NewClient arg order in TestGetFileContentAtRef_DotSegmentError
  (GPT MINOR + Sonnet NIT)
- Remove 'waiting' from mapCheckRunStatus conclusion cases since it is a
  status value not a conclusion, update comment (GPT NIT)
- Add TestDecodeBase64Content_SizeLimit test
rodin removed the wip label 2026-05-13 05:17:40 +00:00
sonnet-review-bot approved these changes 2026-05-13 05:18:42 +00:00
sonnet-review-bot left a comment
First-time contributor

Sonnet Review

Summary

Clean, well-structured implementation of PRReader interface conformance on the GitHub client. Code follows established patterns, error handling is thorough, and test coverage is comprehensive. One minor correctness concern with the cancelled/skipped/neutralsuccess mapping, and a few small issues worth noting.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 197 Mapping cancelled and skipped check run conclusions to "success" is semantically questionable. These conclusions mean the check was not run, not that it passed. Callers using vcs.CommitStatus.Status for gate-checking (e.g. blocking review until all checks pass) will treat skipped/cancelled checks as green, potentially masking required checks that were cancelled. A dedicated "skipped" or "neutral" status value — or at least "neutral" mapped to a neutral/unknown status — would be more honest. This is a design decision with real correctness implications; the comment acknowledges it but the downstream impact depends on how callers interpret the status.
2 [MINOR] github/pr_test.go 437 In TestGetCommitStatuses_CheckRunConclusions, the outer loop captures tt by reference without using t.Run's closure-safe copy pattern. The tt variable is captured in the subtest closure and the subtest is marked t.Parallel(). Because the loop variable is declared in the outer for _, tt := range tests without a shadowing tt := tt inside the loop body, there is a potential data race on tt — the outer loop may advance before the parallel subtest reads tt.conclusion, tt.status, and tt.want. This is a classic Go loop variable capture bug. (Note: Go 1.22+ fixes this automatically by giving each iteration its own variable; if this project targets Go 1.22+, it is fine. If it targets an older release, tt := tt should be added inside the loop.)
3 [MINOR] github/files_test.go 70 TestGetFileContentAtRef_DotSegmentError does not call AllowInsecureHTTP() unlike all other tests that create an httptest server and use NewClient. This may still work if NewClient defaults to allowing HTTP for test servers, but it is inconsistent and may cause issues if the client enforces HTTPS by default for non-test usage. All tests that spin up an httptest server should use AllowInsecureHTTP() for consistency.
4 [NIT] github/pr.go 118 The comment // Returns nil (not an empty slice) when the PR has no changed files. is accurate but the behavior is actually dependent on whether the first page returns 0 files (returns nil) vs. having iterated and found nothing (also nil due to append on nil). This is correct Go behavior, but the contract could be made explicit with // The nil slice is equivalent to an empty slice for range and len operations. — which is already stated in the next sentence. The existing comment is fine as-is.
5 [NIT] github/pr_test.go 12 time is imported in pr_test.go but files_test.go does not import time despite having TestGetFileContentAtRef_429Retry which uses time.Duration. Looking at the full file, pr_test.go contains both PR tests and file tests (the GetFileContentAtRef tests are in pr_test.go, not files_test.go). This is correct — the file layout is logical since GetFileContentAtRef lives in files.go but its tests are in pr_test.go. However, it may be worth splitting files_test.go to hold all tests for files.go for consistency with the file-per-concern pattern.

Recommendation

APPROVE — Approve with the noted minor findings. The implementation is well-written, follows established patterns (interface conformance check, error wrapping, pagination, httptest for all HTTP testing, table-driven tests), and CI passes. The loop variable capture concern in TestGetCommitStatuses_CheckRunConclusions should be verified against the project's minimum Go version — if targeting Go 1.22+, it is a non-issue; otherwise tt := tt should be added. The cancelled/skippedsuccess mapping is a deliberate design choice that is documented, but reviewers should confirm callers can tolerate this semantics. No blockers.


Review by sonnet


Evaluated against 55366b34

# Sonnet Review ## Summary Clean, well-structured implementation of PRReader interface conformance on the GitHub client. Code follows established patterns, error handling is thorough, and test coverage is comprehensive. One minor correctness concern with the `cancelled`/`skipped`/`neutral` → `success` mapping, and a few small issues worth noting. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 197 | Mapping `cancelled` and `skipped` check run conclusions to `"success"` is semantically questionable. These conclusions mean the check was not run, not that it passed. Callers using `vcs.CommitStatus.Status` for gate-checking (e.g. blocking review until all checks pass) will treat skipped/cancelled checks as green, potentially masking required checks that were cancelled. A dedicated `"skipped"` or `"neutral"` status value — or at least `"neutral"` mapped to a neutral/unknown status — would be more honest. This is a design decision with real correctness implications; the comment acknowledges it but the downstream impact depends on how callers interpret the status. | | 2 | [MINOR] | `github/pr_test.go` | 437 | In `TestGetCommitStatuses_CheckRunConclusions`, the outer loop captures `tt` by reference without using `t.Run`'s closure-safe copy pattern. The `tt` variable is captured in the subtest closure and the subtest is marked `t.Parallel()`. Because the loop variable is declared in the outer `for _, tt := range tests` without a shadowing `tt := tt` inside the loop body, there is a potential data race on `tt` — the outer loop may advance before the parallel subtest reads `tt.conclusion`, `tt.status`, and `tt.want`. This is a classic Go loop variable capture bug. (Note: Go 1.22+ fixes this automatically by giving each iteration its own variable; if this project targets Go 1.22+, it is fine. If it targets an older release, `tt := tt` should be added inside the loop.) | | 3 | [MINOR] | `github/files_test.go` | 70 | `TestGetFileContentAtRef_DotSegmentError` does not call `AllowInsecureHTTP()` unlike all other tests that create an httptest server and use `NewClient`. This may still work if `NewClient` defaults to allowing HTTP for test servers, but it is inconsistent and may cause issues if the client enforces HTTPS by default for non-test usage. All tests that spin up an httptest server should use `AllowInsecureHTTP()` for consistency. | | 4 | [NIT] | `github/pr.go` | 118 | The comment `// Returns nil (not an empty slice) when the PR has no changed files.` is accurate but the behavior is actually dependent on whether the first page returns 0 files (returns nil) vs. having iterated and found nothing (also nil due to `append` on nil). This is correct Go behavior, but the contract could be made explicit with `// The nil slice is equivalent to an empty slice for range and len operations.` — which is already stated in the next sentence. The existing comment is fine as-is. | | 5 | [NIT] | `github/pr_test.go` | 12 | `time` is imported in `pr_test.go` but `files_test.go` does not import `time` despite having `TestGetFileContentAtRef_429Retry` which uses `time.Duration`. Looking at the full file, `pr_test.go` contains both PR tests and file tests (the `GetFileContentAtRef` tests are in `pr_test.go`, not `files_test.go`). This is correct — the file layout is logical since `GetFileContentAtRef` lives in `files.go` but its tests are in `pr_test.go`. However, it may be worth splitting `files_test.go` to hold all tests for `files.go` for consistency with the file-per-concern pattern. | ## Recommendation **APPROVE** — Approve with the noted minor findings. The implementation is well-written, follows established patterns (interface conformance check, error wrapping, pagination, `httptest` for all HTTP testing, table-driven tests), and CI passes. The loop variable capture concern in `TestGetCommitStatuses_CheckRunConclusions` should be verified against the project's minimum Go version — if targeting Go 1.22+, it is a non-issue; otherwise `tt := tt` should be added. The `cancelled`/`skipped` → `success` mapping is a deliberate design choice that is documented, but reviewers should confirm callers can tolerate this semantics. No blockers. --- *Review by sonnet* <!-- review-bot:sonnet --> --- *Evaluated against 55366b34*
@@ -0,0 +67,4 @@
t.Fatal("server should not have been called")
}))
defer srv.Close()
First-time contributor

[MINOR] TestGetFileContentAtRef_DotSegmentError does not call AllowInsecureHTTP() unlike all other tests that create an httptest server and use NewClient. This may still work if NewClient defaults to allowing HTTP for test servers, but it is inconsistent and may cause issues if the client enforces HTTPS by default for non-test usage. All tests that spin up an httptest server should use AllowInsecureHTTP() for consistency.

**[MINOR]** `TestGetFileContentAtRef_DotSegmentError` does not call `AllowInsecureHTTP()` unlike all other tests that create an httptest server and use `NewClient`. This may still work if `NewClient` defaults to allowing HTTP for test servers, but it is inconsistent and may cause issues if the client enforces HTTPS by default for non-test usage. All tests that spin up an httptest server should use `AllowInsecureHTTP()` for consistency.
@@ -0,0 +115,4 @@
}
if len(files) == 0 {
break
}
First-time contributor

[NIT] The comment // Returns nil (not an empty slice) when the PR has no changed files. is accurate but the behavior is actually dependent on whether the first page returns 0 files (returns nil) vs. having iterated and found nothing (also nil due to append on nil). This is correct Go behavior, but the contract could be made explicit with // The nil slice is equivalent to an empty slice for range and len operations. — which is already stated in the next sentence. The existing comment is fine as-is.

**[NIT]** The comment `// Returns nil (not an empty slice) when the PR has no changed files.` is accurate but the behavior is actually dependent on whether the first page returns 0 files (returns nil) vs. having iterated and found nothing (also nil due to `append` on nil). This is correct Go behavior, but the contract could be made explicit with `// The nil slice is equivalent to an empty slice for range and len operations.` — which is already stated in the next sentence. The existing comment is fine as-is.
@@ -0,0 +194,4 @@
// Conclusion alone determines the mapped state: nil conclusion means the run is
// still in progress (pending), regardless of the status field value.
//
// Mapping rules:
First-time contributor

[MINOR] Mapping cancelled and skipped check run conclusions to "success" is semantically questionable. These conclusions mean the check was not run, not that it passed. Callers using vcs.CommitStatus.Status for gate-checking (e.g. blocking review until all checks pass) will treat skipped/cancelled checks as green, potentially masking required checks that were cancelled. A dedicated "skipped" or "neutral" status value — or at least "neutral" mapped to a neutral/unknown status — would be more honest. This is a design decision with real correctness implications; the comment acknowledges it but the downstream impact depends on how callers interpret the status.

**[MINOR]** Mapping `cancelled` and `skipped` check run conclusions to `"success"` is semantically questionable. These conclusions mean the check was not run, not that it passed. Callers using `vcs.CommitStatus.Status` for gate-checking (e.g. blocking review until all checks pass) will treat skipped/cancelled checks as green, potentially masking required checks that were cancelled. A dedicated `"skipped"` or `"neutral"` status value — or at least `"neutral"` mapped to a neutral/unknown status — would be more honest. This is a design decision with real correctness implications; the comment acknowledges it but the downstream impact depends on how callers interpret the status.
@@ -0,0 +9,4 @@
"strings"
"testing"
"time"
)
First-time contributor

[NIT] time is imported in pr_test.go but files_test.go does not import time despite having TestGetFileContentAtRef_429Retry which uses time.Duration. Looking at the full file, pr_test.go contains both PR tests and file tests (the GetFileContentAtRef tests are in pr_test.go, not files_test.go). This is correct — the file layout is logical since GetFileContentAtRef lives in files.go but its tests are in pr_test.go. However, it may be worth splitting files_test.go to hold all tests for files.go for consistency with the file-per-concern pattern.

**[NIT]** `time` is imported in `pr_test.go` but `files_test.go` does not import `time` despite having `TestGetFileContentAtRef_429Retry` which uses `time.Duration`. Looking at the full file, `pr_test.go` contains both PR tests and file tests (the `GetFileContentAtRef` tests are in `pr_test.go`, not `files_test.go`). This is correct — the file layout is logical since `GetFileContentAtRef` lives in `files.go` but its tests are in `pr_test.go`. However, it may be worth splitting `files_test.go` to hold all tests for `files.go` for consistency with the file-per-concern pattern.
@@ -0,0 +434,4 @@
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts == 1 {
w.WriteHeader(429)
First-time contributor

[MINOR] In TestGetCommitStatuses_CheckRunConclusions, the outer loop captures tt by reference without using t.Run's closure-safe copy pattern. The tt variable is captured in the subtest closure and the subtest is marked t.Parallel(). Because the loop variable is declared in the outer for _, tt := range tests without a shadowing tt := tt inside the loop body, there is a potential data race on tt — the outer loop may advance before the parallel subtest reads tt.conclusion, tt.status, and tt.want. This is a classic Go loop variable capture bug. (Note: Go 1.22+ fixes this automatically by giving each iteration its own variable; if this project targets Go 1.22+, it is fine. If it targets an older release, tt := tt should be added inside the loop.)

**[MINOR]** In `TestGetCommitStatuses_CheckRunConclusions`, the outer loop captures `tt` by reference without using `t.Run`'s closure-safe copy pattern. The `tt` variable is captured in the subtest closure and the subtest is marked `t.Parallel()`. Because the loop variable is declared in the outer `for _, tt := range tests` without a shadowing `tt := tt` inside the loop body, there is a potential data race on `tt` — the outer loop may advance before the parallel subtest reads `tt.conclusion`, `tt.status`, and `tt.want`. This is a classic Go loop variable capture bug. (Note: Go 1.22+ fixes this automatically by giving each iteration its own variable; if this project targets Go 1.22+, it is fine. If it targets an older release, `tt := tt` should be added inside the loop.)
gpt-review-bot approved these changes 2026-05-13 05:19:53 +00:00
gpt-review-bot left a comment
First-time contributor

Gpt Review

Summary

Well-structured, idiomatic implementation of the PRReader methods with thorough test coverage. Error handling, context usage, pagination guards, and interface conformance follow established patterns and repository conventions.

Recommendation

APPROVE — CI has passed and the changes adhere to the repository’s error handling and testing conventions. The GitHub client methods implement pagination safely, wrap errors with context, and include robust unit tests using httptest. The compile-time interface conformance check is a good practice. Proceed with merge.


Review by gpt


Evaluated against 55366b34

# Gpt Review ## Summary Well-structured, idiomatic implementation of the PRReader methods with thorough test coverage. Error handling, context usage, pagination guards, and interface conformance follow established patterns and repository conventions. ## Recommendation **APPROVE** — CI has passed and the changes adhere to the repository’s error handling and testing conventions. The GitHub client methods implement pagination safely, wrap errors with context, and include robust unit tests using httptest. The compile-time interface conformance check is a good practice. Proceed with merge. --- *Review by gpt* <!-- review-bot:gpt --> --- *Evaluated against 55366b34*
security-review-bot requested review from security-review-bot 2026-05-13 05:19:53 +00:00
security-review-bot approved these changes 2026-05-13 05:19:53 +00:00
security-review-bot left a comment
Collaborator

Security Review

Summary

Security-conscious implementation with proper path sanitization, URL escaping, pagination bounds, and size limits for base64 decoding. No exploitable vulnerabilities identified in the changes.

Findings

# Severity File Line Finding
1 [MINOR] github/pr.go 182 mapCheckRunStatus maps "cancelled", "skipped", and "neutral" to "success". If this status is later used to gate security-critical actions (e.g., merges), a cancelled required check could be interpreted as success and potentially weaken enforcement. Consider mapping these to a non-success state (e.g., pending) or exposing them distinctly so callers can enforce policies accurately.
2 [MINOR] github/pr.go 111 GetPullRequestFiles accumulates patches for up to 10,000 files without per-item or total size limits. While pagination is bounded, a malicious or misconfigured upstream could return very large patch payloads leading to high memory usage (DoS). Consider enforcing a maximum response/body size in the HTTP client or capping total bytes aggregated across files.

Recommendation

APPROVE — Overall, the changes are securely implemented: path traversal is prevented with explicit dot-segment checks and per-segment escaping, query parameters are safely encoded, and pagination loops have hard upper bounds. Base64 decoding includes a reasonable 10MB limit to prevent resource exhaustion. Two minor hardening suggestions: (1) revisit the mapping of cancelled/skipped/neutral check run conclusions to avoid misclassifying non-passing states as success if these results are ever used in enforcement; and (2) consider adding response/body size limits or total bytes constraints when aggregating PR files and patches to reduce DoS risk from unusually large responses. With CI passing and no Major issues found, this is good to merge.


Review by security


Evaluated against 55366b34

# Security Review ## Summary Security-conscious implementation with proper path sanitization, URL escaping, pagination bounds, and size limits for base64 decoding. No exploitable vulnerabilities identified in the changes. ## Findings | # | Severity | File | Line | Finding | |---|----------|------|------|--------| | 1 | [MINOR] | `github/pr.go` | 182 | mapCheckRunStatus maps "cancelled", "skipped", and "neutral" to "success". If this status is later used to gate security-critical actions (e.g., merges), a cancelled required check could be interpreted as success and potentially weaken enforcement. Consider mapping these to a non-success state (e.g., pending) or exposing them distinctly so callers can enforce policies accurately. | | 2 | [MINOR] | `github/pr.go` | 111 | GetPullRequestFiles accumulates patches for up to 10,000 files without per-item or total size limits. While pagination is bounded, a malicious or misconfigured upstream could return very large patch payloads leading to high memory usage (DoS). Consider enforcing a maximum response/body size in the HTTP client or capping total bytes aggregated across files. | ## Recommendation **APPROVE** — Overall, the changes are securely implemented: path traversal is prevented with explicit dot-segment checks and per-segment escaping, query parameters are safely encoded, and pagination loops have hard upper bounds. Base64 decoding includes a reasonable 10MB limit to prevent resource exhaustion. Two minor hardening suggestions: (1) revisit the mapping of cancelled/skipped/neutral check run conclusions to avoid misclassifying non-passing states as success if these results are ever used in enforcement; and (2) consider adding response/body size limits or total bytes constraints when aggregating PR files and patches to reduce DoS risk from unusually large responses. With CI passing and no Major issues found, this is good to merge. --- *Review by security* <!-- review-bot:security --> --- *Evaluated against 55366b34*
@@ -0,0 +108,4 @@
body, err := c.doGet(ctx, reqURL)
if err != nil {
return nil, fmt.Errorf("fetch PR files page %d: %w", page, err)
}
Collaborator

[MINOR] GetPullRequestFiles accumulates patches for up to 10,000 files without per-item or total size limits. While pagination is bounded, a malicious or misconfigured upstream could return very large patch payloads leading to high memory usage (DoS). Consider enforcing a maximum response/body size in the HTTP client or capping total bytes aggregated across files.

**[MINOR]** GetPullRequestFiles accumulates patches for up to 10,000 files without per-item or total size limits. While pagination is bounded, a malicious or misconfigured upstream could return very large patch payloads leading to high memory usage (DoS). Consider enforcing a maximum response/body size in the HTTP client or capping total bytes aggregated across files.
@@ -0,0 +179,4 @@
Context: cr.Name,
Status: mapCheckRunStatus(cr.Conclusion),
Description: "", // check runs have no human-readable description; conclusion is captured in Status
TargetURL: cr.HTMLURL,
Collaborator

[MINOR] mapCheckRunStatus maps "cancelled", "skipped", and "neutral" to "success". If this status is later used to gate security-critical actions (e.g., merges), a cancelled required check could be interpreted as success and potentially weaken enforcement. Consider mapping these to a non-success state (e.g., pending) or exposing them distinctly so callers can enforce policies accurately.

**[MINOR]** mapCheckRunStatus maps "cancelled", "skipped", and "neutral" to "success". If this status is later used to gate security-critical actions (e.g., merges), a cancelled required check could be interpreted as success and potentially weaken enforcement. Consider mapping these to a non-success state (e.g., pending) or exposing them distinctly so callers can enforce policies accurately.
rodin added the wip label 2026-05-13 05:24:14 +00:00
Author
Owner

Self-Review: PR #102

Self-review against 55366b3431

Phase 1: Independent Findings

Reviewing this diff as a stranger who hasn't seen this code before.

Changes in this commit (3cd5ae555366b3):

  • github/files.go: Added maxFileContentSize constant and pre-decode size check + post-decode size check in decodeBase64Content
  • github/files_test.go: Fixed NewClient argument order in TestGetFileContentAtRef_DotSegmentError; added TestDecodeBase64Content_SizeLimit
  • github/pr.go: Removed "waiting" from the mapCheckRunStatus stale case (was case "stale", "waiting":, now just case "stale":) and updated the doc comment accordingly

Independent findings on the current diff:

  1. [NIT] github/files.go — double size-check is slightly redundant but harmless
    The estimated pre-check (len(cleaned)*3/4 > maxFileContentSize) and the exact post-decode check (len(decoded) > maxFileContentSize) are both present. The estimated check uses integer arithmetic and is a 75% approximation; padding chars (=) in base64 could cause the actual decoded size to be slightly under the estimate. Both checks together are correct and safe (no false rejections, guaranteed cap). The double-check pattern is clear and well-commented. No issue.

  2. [NIT] github/pr.go — removing "waiting" from the stale case is correct
    "waiting" is a check run status (the run is queued/waiting to start), not a conclusion. A non-nil conclusion means the run has completed; "waiting" as a conclusion is not a valid GitHub value. The removal is correct. The updated doc comment reflects this accurately.

  3. [NIT] github/files_test.goTestGetFileContentAtRef_DotSegmentError missing AllowInsecureHTTP()
    This was flagged in a prior review (sonnet, ID 3011): the test uses NewClient("token", srv.URL) without AllowInsecureHTTP(). The argument order is now fixed, but the missing option is still present. Since the test's purpose is to verify the error is caught before any HTTP request (server must not be called), the missing AllowInsecureHTTP() does not affect correctness — the client never reaches the HTTP layer. Acceptable as-is.

No blocking issues in the new changes. All tests pass (go test ./github/... → ok).

Phase 2: Prior Review Verification

Prior self-review HEAD was 3cd5ae5. That review found: Clean (two prior MINORs resolved). Verifying the new commit (55366b3) against remaining open items:

Finding Reviewer Status Notes
MINOR: base64 content decoded without size limit security (3008) RESOLVED maxFileContentSize constant + pre and post-decode checks added; TestDecodeBase64Content_SizeLimit covers it
NIT: reversed NewClient args in TestGetFileContentAtRef_DotSegmentError sonnet (3006) / gpt (3009) RESOLVED Fixed: NewClient("token", srv.URL) in current diff
NIT: "waiting" mentioned in mapCheckRunStatus doc/switch (not a valid conclusion) gpt (3009) RESOLVED Removed from switch case and updated doc comment
MINOR: startup_failure maps to "pending" (fail-open default) gpt (2970) STILL PRESENT Intentional; documented in switch default comment
MINOR: pagination uses len < 100 not Link header gpt (2970) STILL PRESENT Acknowledged future improvement; acceptable
MINOR: cancelled/skipped"success" loses information sonnet (2988) STILL PRESENT Intentional design tradeoff; documented
NIT: GitHub 3,000 file limit not in pagination comment sonnet (2994) STILL PRESENT Minor doc gap; no fix applied
NIT: pr_test.go internal package vs package github_test sonnet (2994) STILL PRESENT Intentional (needs SetHTTPClient/SetRetryBackoff)
NIT: strings.Replacer recreated per call in decodeBase64Content gpt (2995) STILL PRESENT Minor perf; no fix applied
NIT: user-controlled path in error strings (log injection) security (2997) STILL PRESENT Low practical risk; no fix applied
NIT: TestGetFileContentAtRef_DotSegmentError missing AllowInsecureHTTP() sonnet (3011) STILL PRESENT Doesn't affect correctness; acceptable

Assessment: Clean

All MINOR findings from the most recent bot review round (against 3cd5ae5) are resolved: the size limit for base64 decoding is now enforced with both a pre-decode estimate check and a post-decode exact check, the NewClient argument order is corrected, and the invalid "waiting" conclusion value has been removed from the switch. All remaining STILL PRESENT items are documented, intentional design tradeoffs or low-priority NITs that have been consistently acknowledged across multiple review rounds. No new issues found in Phase 1. Tests pass.

## Self-Review: PR #102 Self-review against 55366b3431d3ec2417d1bb087398ff4e0895effe ### Phase 1: Independent Findings Reviewing this diff as a stranger who hasn't seen this code before. **Changes in this commit (3cd5ae5 → 55366b3):** - `github/files.go`: Added `maxFileContentSize` constant and pre-decode size check + post-decode size check in `decodeBase64Content` - `github/files_test.go`: Fixed `NewClient` argument order in `TestGetFileContentAtRef_DotSegmentError`; added `TestDecodeBase64Content_SizeLimit` - `github/pr.go`: Removed `"waiting"` from the `mapCheckRunStatus` stale case (was `case "stale", "waiting":`, now just `case "stale":`) and updated the doc comment accordingly **Independent findings on the current diff:** 1. **[NIT] `github/files.go` — double size-check is slightly redundant but harmless** The estimated pre-check (`len(cleaned)*3/4 > maxFileContentSize`) and the exact post-decode check (`len(decoded) > maxFileContentSize`) are both present. The estimated check uses integer arithmetic and is a 75% approximation; padding chars (`=`) in base64 could cause the actual decoded size to be slightly under the estimate. Both checks together are correct and safe (no false rejections, guaranteed cap). The double-check pattern is clear and well-commented. No issue. 2. **[NIT] `github/pr.go` — removing `"waiting"` from the stale case is correct** `"waiting"` is a check run *status* (the run is queued/waiting to start), not a *conclusion*. A non-nil conclusion means the run has completed; `"waiting"` as a conclusion is not a valid GitHub value. The removal is correct. The updated doc comment reflects this accurately. 3. **[NIT] `github/files_test.go` — `TestGetFileContentAtRef_DotSegmentError` missing `AllowInsecureHTTP()`** This was flagged in a prior review (sonnet, ID 3011): the test uses `NewClient("token", srv.URL)` without `AllowInsecureHTTP()`. The argument order is now fixed, but the missing option is still present. Since the test's purpose is to verify the error is caught before any HTTP request (server must not be called), the missing `AllowInsecureHTTP()` does not affect correctness — the client never reaches the HTTP layer. Acceptable as-is. No blocking issues in the new changes. All tests pass (`go test ./github/... → ok`). ### Phase 2: Prior Review Verification Prior self-review HEAD was `3cd5ae5`. That review found: ✅ Clean (two prior MINORs resolved). Verifying the new commit (`55366b3`) against remaining open items: | Finding | Reviewer | Status | Notes | |---------|----------|--------|-------| | MINOR: base64 content decoded without size limit | security (3008) | RESOLVED | `maxFileContentSize` constant + pre and post-decode checks added; `TestDecodeBase64Content_SizeLimit` covers it | | NIT: reversed `NewClient` args in `TestGetFileContentAtRef_DotSegmentError` | sonnet (3006) / gpt (3009) | RESOLVED | Fixed: `NewClient("token", srv.URL)` in current diff | | NIT: `"waiting"` mentioned in `mapCheckRunStatus` doc/switch (not a valid conclusion) | gpt (3009) | RESOLVED | Removed from switch case and updated doc comment | | MINOR: `startup_failure` maps to `"pending"` (fail-open default) | gpt (2970) | STILL PRESENT | Intentional; documented in switch default comment | | MINOR: pagination uses `len < 100` not Link header | gpt (2970) | STILL PRESENT | Acknowledged future improvement; acceptable | | MINOR: `cancelled`/`skipped` → `"success"` loses information | sonnet (2988) | STILL PRESENT | Intentional design tradeoff; documented | | NIT: GitHub 3,000 file limit not in pagination comment | sonnet (2994) | STILL PRESENT | Minor doc gap; no fix applied | | NIT: `pr_test.go` internal package vs `package github_test` | sonnet (2994) | STILL PRESENT | Intentional (needs SetHTTPClient/SetRetryBackoff) | | NIT: `strings.Replacer` recreated per call in `decodeBase64Content` | gpt (2995) | STILL PRESENT | Minor perf; no fix applied | | NIT: user-controlled path in error strings (log injection) | security (2997) | STILL PRESENT | Low practical risk; no fix applied | | NIT: `TestGetFileContentAtRef_DotSegmentError` missing `AllowInsecureHTTP()` | sonnet (3011) | STILL PRESENT | Doesn't affect correctness; acceptable | ### Assessment: ✅ Clean All MINOR findings from the most recent bot review round (against `3cd5ae5`) are resolved: the size limit for base64 decoding is now enforced with both a pre-decode estimate check and a post-decode exact check, the `NewClient` argument order is corrected, and the invalid `"waiting"` conclusion value has been removed from the switch. All remaining STILL PRESENT items are documented, intentional design tradeoffs or low-priority NITs that have been consistently acknowledged across multiple review rounds. No new issues found in Phase 1. Tests pass.
rodin removed the wip label 2026-05-13 05:26:16 +00:00
rodin removed their assignment 2026-05-13 05:29:12 +00:00
aweiker was assigned by rodin 2026-05-13 05:29:12 +00:00
rodin added the ready label 2026-05-13 05:29:12 +00:00
aweiker approved these changes 2026-05-13 05:30:33 +00:00
aweiker merged commit 71bb33b6fd into feature/github-support 2026-05-13 05:30:37 +00:00
Sign in to join this conversation.
5 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rodin/review-bot#102