Skip to content

Commit 7fed2b2

Browse files
authored
feat: add configurable account exclusion for auto code review (#343)
- Add review.excluded_accounts configuration option - Support wildcard patterns and exact matching for excluded accounts - Automatically exclude accounts ending with [bot] - Skip auto-review for PRs from excluded authors - Update configuration files with example excluded accounts
1 parent 158d6a7 commit 7fed2b2

File tree

5 files changed

+289
-4
lines changed

5 files changed

+289
-4
lines changed

cmd/server/config.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ server:
55
github:
66
#token: "1234567890"
77
webhook_url: "http://localhost:8888/hook"
8-
8+
99
# GitHub App 配置
1010
# app:
1111
# app_id: 1032243 # GitHub App ID
1212
# private_key_path: "/Users/dir/xxxxx.private-key.pem" # GitHub App Private Key 文件路径
13-
# 或者通过环境变量指定: private_key_env: "GITHUB_APP_PRIVATE_KEY"
14-
# 或者直接配置: private_key: "-----BEGIN RSA PRIVATE KEY-----..."
13+
# 或者通过环境变量指定: private_key_env: "GITHUB_APP_PRIVATE_KEY"
14+
# 或者直接配置: private_key: "-----BEGIN RSA PRIVATE KEY-----..."
1515

1616
workspace:
1717
base_dir: "/Users/jicarl/codeagent"
@@ -26,3 +26,10 @@ claude:
2626
gemini:
2727
container_image: "goplusorg/codeagent:v0.5"
2828
timeout: "30m"
29+
30+
review:
31+
excluded_accounts:
32+
- "dependabot"
33+
- "renovate"
34+
- "github-actions"
35+
- "qiniu-ci"

config.example.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ mention:
4444
- "@qiniu-ai" # another company AI assistant
4545
# Default trigger (used when no specific trigger list is provided)
4646
default_trigger: "@qiniu-ci"
47+
48+
# Review configuration
49+
review:
50+
# List of accounts to exclude from automatic code review
51+
# Supports multiple accounts and patterns
52+
excluded_accounts:
53+
- "dependabot" # GitHub's dependency bot
54+
- "renovate" # Renovate bot
55+
- "github-actions" # GitHub Actions bot
56+
- "*-bot" # Pattern to exclude all accounts ending with -bot (optional)

internal/config/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77
"strconv"
8+
"strings"
89
"time"
910

1011
yaml "gopkg.in/yaml.v3"
@@ -24,6 +25,8 @@ type Config struct {
2425
Commands CommandsConfig `yaml:"commands"`
2526
// AI Mention Configuration
2627
Mention MentionConfig `yaml:"mention"`
28+
// Review Configuration
29+
Review ReviewConfig `yaml:"review"`
2730
}
2831

2932
type GeminiConfig struct {
@@ -82,6 +85,11 @@ type MentionConfig struct {
8285
DefaultTrigger string `yaml:"default_trigger"`
8386
}
8487

88+
type ReviewConfig struct {
89+
// 自动审查的排除账号,支持多个
90+
ExcludedAccounts []string `yaml:"excluded_accounts"`
91+
}
92+
8593
func Load(configPath string) (*Config, error) {
8694
// 首先尝试从文件加载
8795
if _, err := os.Stat(configPath); err == nil {
@@ -181,6 +189,10 @@ func (c *Config) loadFromEnv() {
181189
if mentionTrigger := os.Getenv("MENTION_TRIGGER"); mentionTrigger != "" {
182190
c.Mention.DefaultTrigger = mentionTrigger
183191
}
192+
// Review configuration from environment
193+
if excludedAccounts := os.Getenv("REVIEW_EXCLUDED_ACCOUNTS"); excludedAccounts != "" {
194+
c.Review.ExcludedAccounts = strings.Split(excludedAccounts, ",")
195+
}
184196
}
185197

186198
func loadFromEnv() *Config {
@@ -230,6 +242,9 @@ func loadFromEnv() *Config {
230242
Triggers: []string{getEnvOrDefault("MENTION_TRIGGER", "@qiniu-ci")},
231243
DefaultTrigger: getEnvOrDefault("MENTION_TRIGGER", "@qiniu-ci"),
232244
},
245+
Review: ReviewConfig{
246+
ExcludedAccounts: []string{},
247+
},
233248
CodeProvider: getEnvOrDefault("CODE_PROVIDER", "claude"),
234249
UseDocker: getEnvBoolOrDefault("USE_DOCKER", true),
235250
}

internal/modes/review_handler.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"regexp"
78
"strings"
89
"time"
910

@@ -92,6 +93,40 @@ func (rh *ReviewHandler) canHandlePREvent(ctx context.Context, event *models.Pul
9293
}
9394
}
9495

96+
// isAccountExcluded 检查账号是否应该被排除在自动审查之外
97+
func (rh *ReviewHandler) isAccountExcluded(ctx context.Context, username string) bool {
98+
xl := xlog.NewWith(ctx)
99+
100+
// 1. 检查是否以"[bot]"结尾(GitHub App账号的常见模式)
101+
if strings.HasSuffix(strings.ToLower(username), "[bot]") {
102+
xl.Infof("Account %s excluded: ends with '[bot]'", username)
103+
return true
104+
}
105+
106+
// 2. 检查配置中的排除账号列表
107+
for _, excludedAccount := range rh.config.Review.ExcludedAccounts {
108+
// 支持通配符模式匹配
109+
if strings.Contains(excludedAccount, "*") {
110+
// 将通配符模式转换为正则表达式
111+
pattern := strings.ReplaceAll(excludedAccount, "*", ".*")
112+
matched, err := regexp.MatchString(pattern, username)
113+
if err == nil && matched {
114+
xl.Infof("Account %s excluded: matches pattern '%s'", username, excludedAccount)
115+
return true
116+
}
117+
} else {
118+
// 精确匹配
119+
if strings.EqualFold(username, excludedAccount) {
120+
xl.Infof("Account %s excluded: exact match with '%s'", username, excludedAccount)
121+
return true
122+
}
123+
}
124+
}
125+
126+
xl.Debugf("Account %s not excluded from auto-review", username)
127+
return false
128+
}
129+
95130
// Execute 执行Review模式处理逻辑
96131
func (rh *ReviewHandler) Execute(ctx context.Context, event models.GitHubContext) error {
97132
xl := xlog.NewWith(ctx)
@@ -129,7 +164,14 @@ func (rh *ReviewHandler) handlePREvent(ctx context.Context, event *models.PullRe
129164

130165
switch event.GetEventAction() {
131166
case "opened", "reopened", "synchronize", "ready_for_review":
132-
xl.Infof("Auto-reviewing PR #%d", event.PullRequest.GetNumber())
167+
// 检查PR作者是否应该被排除
168+
prAuthor := event.PullRequest.GetUser().GetLogin()
169+
if rh.isAccountExcluded(ctx, prAuthor) {
170+
xl.Infof("Skipping auto-review for PR #%d: author %s is excluded", event.PullRequest.GetNumber(), prAuthor)
171+
return nil
172+
}
173+
174+
xl.Infof("Auto-reviewing PR #%d by author %s", event.PullRequest.GetNumber(), prAuthor)
133175

134176
// 执行自动代码审查
135177
return rh.processCodeReview(ctx, event, client, nil)

internal/modes/review_handler_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package modes
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/qiniu/codeagent/internal/config"
8+
)
9+
10+
func TestReviewHandler_isAccountExcluded(t *testing.T) {
11+
// 创建测试配置
12+
config := &config.Config{
13+
Review: config.ReviewConfig{
14+
ExcludedAccounts: []string{
15+
"dependabot",
16+
"renovate",
17+
"test-bot",
18+
},
19+
},
20+
}
21+
22+
// 创建ReviewHandler实例
23+
handler := &ReviewHandler{
24+
config: config,
25+
}
26+
27+
ctx := context.Background()
28+
29+
tests := []struct {
30+
name string
31+
username string
32+
expected bool
33+
}{
34+
{
35+
name: "[bot] suffix account should be excluded",
36+
username: "someuser[bot]",
37+
expected: true,
38+
},
39+
{
40+
name: "[BOT] suffix account should be excluded",
41+
username: "someuser[BOT]",
42+
expected: true,
43+
},
44+
{
45+
name: "exact match dependabot should be excluded",
46+
username: "dependabot",
47+
expected: true,
48+
},
49+
{
50+
name: "exact match renovate should be excluded",
51+
username: "renovate",
52+
expected: true,
53+
},
54+
{
55+
name: "exact match test-bot should be excluded",
56+
username: "test-bot",
57+
expected: true,
58+
},
59+
{
60+
name: "my-bot should not be excluded (not in config)",
61+
username: "my-bot",
62+
expected: false,
63+
},
64+
{
65+
name: "normal user should not be excluded",
66+
username: "normaluser",
67+
expected: false,
68+
},
69+
{
70+
name: "user with bot in middle should not be excluded",
71+
username: "mybotuser",
72+
expected: false,
73+
},
74+
{
75+
name: "case insensitive match should work",
76+
username: "DEPENDABOT",
77+
expected: true,
78+
},
79+
{
80+
name: "user with -bot suffix should not be excluded (not [bot])",
81+
username: "someuser-bot",
82+
expected: false,
83+
},
84+
{
85+
name: "user with -BOT suffix should not be excluded (not [BOT])",
86+
username: "someuser-BOT",
87+
expected: false,
88+
},
89+
}
90+
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
result := handler.isAccountExcluded(ctx, tt.username)
94+
if result != tt.expected {
95+
t.Errorf("isAccountExcluded(%s) = %v, want %v", tt.username, result, tt.expected)
96+
}
97+
})
98+
}
99+
}
100+
101+
func TestReviewHandler_isAccountExcluded_EmptyConfig(t *testing.T) {
102+
// 测试空配置的情况
103+
config := &config.Config{
104+
Review: config.ReviewConfig{
105+
ExcludedAccounts: []string{},
106+
},
107+
}
108+
109+
handler := &ReviewHandler{
110+
config: config,
111+
}
112+
113+
ctx := context.Background()
114+
115+
tests := []struct {
116+
name string
117+
username string
118+
expected bool
119+
}{
120+
{
121+
name: "[bot] suffix should still be excluded even with empty config",
122+
username: "someuser[bot]",
123+
expected: true,
124+
},
125+
{
126+
name: "normal user should not be excluded with empty config",
127+
username: "normaluser",
128+
expected: false,
129+
},
130+
{
131+
name: "-bot suffix should not be excluded with empty config (not [bot])",
132+
username: "someuser-bot",
133+
expected: false,
134+
},
135+
}
136+
137+
for _, tt := range tests {
138+
t.Run(tt.name, func(t *testing.T) {
139+
result := handler.isAccountExcluded(ctx, tt.username)
140+
if result != tt.expected {
141+
t.Errorf("isAccountExcluded(%s) = %v, want %v", tt.username, result, tt.expected)
142+
}
143+
})
144+
}
145+
}
146+
147+
func TestReviewHandler_isAccountExcluded_WildcardPatterns(t *testing.T) {
148+
// 测试通配符模式
149+
config := &config.Config{
150+
Review: config.ReviewConfig{
151+
ExcludedAccounts: []string{
152+
"*dependabot*",
153+
"*test*",
154+
"prefix-*",
155+
"*-suffix",
156+
},
157+
},
158+
}
159+
160+
handler := &ReviewHandler{
161+
config: config,
162+
}
163+
164+
ctx := context.Background()
165+
166+
tests := []struct {
167+
name string
168+
username string
169+
expected bool
170+
}{
171+
{
172+
name: "pattern *dependabot* should match dependabot",
173+
username: "dependabot",
174+
expected: true,
175+
},
176+
{
177+
name: "pattern *dependabot* should match my-dependabot-user",
178+
username: "my-dependabot-user",
179+
expected: true,
180+
},
181+
{
182+
name: "pattern *test* should match testuser",
183+
username: "testuser",
184+
expected: true,
185+
},
186+
{
187+
name: "pattern prefix-* should match prefix-anything",
188+
username: "prefix-anything",
189+
expected: true,
190+
},
191+
{
192+
name: "pattern *-suffix should match anything-suffix",
193+
username: "anything-suffix",
194+
expected: true,
195+
},
196+
{
197+
name: "pattern should not match unrelated user",
198+
username: "unrelated",
199+
expected: false,
200+
},
201+
}
202+
203+
for _, tt := range tests {
204+
t.Run(tt.name, func(t *testing.T) {
205+
result := handler.isAccountExcluded(ctx, tt.username)
206+
if result != tt.expected {
207+
t.Errorf("isAccountExcluded(%s) = %v, want %v", tt.username, result, tt.expected)
208+
}
209+
})
210+
}
211+
}

0 commit comments

Comments
 (0)