fix: detect shared-token misconfiguration and warn
CI / test (pull_request) Successful in 14s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 24s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SONNET_REVIEW_TOKEN) (pull_request) Successful in 42s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 55s

When two review-bot roles share the same Gitea user token (misconfiguration),
log a WARNING identifying which sibling is sharing. The bot continues normally
with its own honest verdict — no escalation, no deadlock. Operators see the
warning in CI logs and can fix the token setup.

Addresses Aaron's review feedback on #28: graceful degradation when someone
doesn't follow the separate-token deployment instructions.
This commit is contained in:
Rodin
2026-05-02 07:11:57 -07:00
parent b78d9972ac
commit fd179b891b
2 changed files with 107 additions and 0 deletions
+42
View File
@@ -266,6 +266,9 @@ func main() {
if err != nil { if err != nil {
log.Printf("Warning: could not list existing reviews: %v", err) log.Printf("Warning: could not list existing reviews: %v", err)
} else { } else {
// Detect shared-token misconfiguration: warn if sibling sentinel exists from same user
warnSharedToken(existingReviews, sentinel)
existing := findOwnReview(existingReviews, sentinel) existing := findOwnReview(existingReviews, sentinel)
if existing != nil { if existing != nil {
@@ -487,6 +490,45 @@ func reviewUnchanged(reviews []gitea.Review, newBody, newEvent, sentinel string)
return false return false
} }
// warnSharedToken logs a warning if another review-bot role posted under the
// same Gitea user. This detects misconfiguration where two roles share a token
// instead of having separate Gitea accounts. The bot continues normally (posts
// its own honest verdict) but warns operators to fix the token setup.
func warnSharedToken(reviews []gitea.Review, ownSentinel string) {
ownLogin := ""
for _, r := range reviews {
if strings.Contains(r.Body, ownSentinel) {
ownLogin = r.User.Login
break
}
}
if ownLogin == "" {
return
}
for _, r := range reviews {
if r.User.Login == ownLogin && strings.Contains(r.Body, "<!-- review-bot:") && !strings.Contains(r.Body, ownSentinel) {
log.Printf("WARNING: shared token detected — another review-bot role (%s) is using the same Gitea user %q. Each role should have its own token/user for proper multi-reviewer blocking.", extractSentinelName(r.Body), ownLogin)
return
}
}
}
// extractSentinelName pulls the reviewer name from a sentinel comment.
func extractSentinelName(body string) string {
const prefix = "<!-- review-bot:"
const suffix = " -->"
idx := strings.Index(body, prefix)
if idx < 0 {
return "unknown"
}
rest := body[idx+len(prefix):]
end := strings.Index(rest, suffix)
if end < 0 {
return "unknown"
}
return rest[:end]
}
// findOwnReview locates a review matching the given sentinel in its body. // findOwnReview locates a review matching the given sentinel in its body.
func findOwnReview(reviews []gitea.Review, sentinel string) *gitea.Review { func findOwnReview(reviews []gitea.Review, sentinel string) *gitea.Review {
for i := range reviews { for i := range reviews {
+65
View File
@@ -188,3 +188,68 @@ func TestFindOwnReview(t *testing.T) {
}) })
} }
} }
func TestWarnSharedToken(t *testing.T) {
// warnSharedToken should not panic and should handle edge cases gracefully.
// It only logs — we verify it doesn't crash.
tests := []struct {
name string
reviews []gitea.Review
sentinel string
}{
{
name: "no reviews",
reviews: nil,
sentinel: "<!-- review-bot:sonnet -->",
},
{
name: "no own review yet",
reviews: []gitea.Review{
{ID: 1, User: struct{ Login string `json:"login"` }{Login: "other"}, Body: "<!-- review-bot:gpt --> body"},
},
sentinel: "<!-- review-bot:sonnet -->",
},
{
name: "separate users - no warning",
reviews: []gitea.Review{
{ID: 1, User: struct{ Login string `json:"login"` }{Login: "sonnet-review-bot"}, Body: "<!-- review-bot:sonnet --> body"},
{ID: 2, User: struct{ Login string `json:"login"` }{Login: "security-review-bot"}, Body: "<!-- review-bot:security --> body"},
},
sentinel: "<!-- review-bot:sonnet -->",
},
{
name: "shared token detected",
reviews: []gitea.Review{
{ID: 1, User: struct{ Login string `json:"login"` }{Login: "sonnet-review-bot"}, Body: "<!-- review-bot:sonnet --> body"},
{ID: 2, User: struct{ Login string `json:"login"` }{Login: "sonnet-review-bot"}, Body: "<!-- review-bot:security --> body"},
},
sentinel: "<!-- review-bot:sonnet -->",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Should not panic
warnSharedToken(tc.reviews, tc.sentinel)
})
}
}
func TestExtractSentinelName(t *testing.T) {
tests := []struct {
body string
want string
}{
{"<!-- review-bot:sonnet --> rest", "sonnet"},
{"<!-- review-bot:security --> rest", "security"},
{"no sentinel here", "unknown"},
{"<!-- review-bot:gpt-review --> rest", "gpt-review"},
}
for _, tc := range tests {
got := extractSentinelName(tc.body)
if got != tc.want {
t.Errorf("extractSentinelName(%q) = %q, want %q", tc.body, got, tc.want)
}
}
}