fix: address PR #62 review findings
CI / test (pull_request) Successful in 16s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 27s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m5s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m40s

- Remove duplicate flag.Parse() call
- Fix nil map panic in LoadRemotePersonas error path by assigning
  empty map when LoadRemotePersonas returns an error
- Tighten isNotFoundError to only check HTTP 404 (remove broad
  'not found' substring check to avoid false positives)
- Clean up personaErr variable scope using narrower-scoped err variables
- Add proper doc comment to LoadRemotePersonasFromPath (Go convention)
- Add file count cap (50 files) in LoadRemotePersonasFromPath to
  prevent resource exhaustion from repos with thousands of small files
- Update test expectation for tightened isNotFoundError
This commit is contained in:
Rodin
2026-05-10 20:44:24 -07:00
parent 2f8d047ef2
commit 5fac8bc505
3 changed files with 25 additions and 11 deletions
+10 -8
View File
@@ -79,7 +79,6 @@ func main() {
aicoreAPIURL := flag.String("aicore-api-url", envOrDefault("AICORE_API_URL", ""), "SAP AI Core API URL (for provider=aicore)") aicoreAPIURL := flag.String("aicore-api-url", envOrDefault("AICORE_API_URL", ""), "SAP AI Core API URL (for provider=aicore)")
aicoreResourceGroup := flag.String("aicore-resource-group", envOrDefault("AICORE_RESOURCE_GROUP", "default"), "SAP AI Core resource group (for provider=aicore)") aicoreResourceGroup := flag.String("aicore-resource-group", envOrDefault("AICORE_RESOURCE_GROUP", "default"), "SAP AI Core resource group (for provider=aicore)")
flag.Parse()
flag.Parse() flag.Parse()
if *versionFlag { if *versionFlag {
@@ -119,7 +118,6 @@ func main() {
// Persona loading is deferred until after giteaClient is initialized, // Persona loading is deferred until after giteaClient is initialized,
// so we can try loading from the target repo first. // so we can try loading from the target repo first.
var persona *review.Persona var persona *review.Persona
var personaErr error
// Validate reviewer-name: only safe characters allowed in sentinel // Validate reviewer-name: only safe characters allowed in sentinel
if err := validateReviewerName(*reviewerName); err != nil { if err := validateReviewerName(*reviewerName); err != nil {
@@ -184,6 +182,8 @@ func main() {
remotePersonas, err := review.LoadRemotePersonas(ctx, fetcher, owner, repoName) remotePersonas, err := review.LoadRemotePersonas(ctx, fetcher, owner, repoName)
if err != nil { if err != nil {
slog.Warn("could not load remote personas", "repo", fmt.Sprintf("%s/%s", owner, repoName), "error", err) slog.Warn("could not load remote personas", "repo", fmt.Sprintf("%s/%s", owner, repoName), "error", err)
// Assign empty map so the lookup below doesn't panic
remotePersonas = map[string]*review.Persona{}
} }
if p, ok := remotePersonas[*personaName]; ok { if p, ok := remotePersonas[*personaName]; ok {
@@ -191,9 +191,10 @@ func main() {
slog.Info("loaded persona from target repo", "persona", persona.Name, "display", persona.DisplayName) slog.Info("loaded persona from target repo", "persona", persona.Name, "display", persona.DisplayName)
} else { } else {
// Fall back to built-in persona // Fall back to built-in persona
persona, personaErr = review.LoadBuiltinPersona(*personaName) var err error
if personaErr != nil { persona, err = review.LoadBuiltinPersona(*personaName)
slog.Error("failed to load persona", "persona", *personaName, "error", personaErr) if err != nil {
slog.Error("failed to load persona", "persona", *personaName, "error", err)
os.Exit(1) os.Exit(1)
} }
slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName) slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName)
@@ -204,9 +205,10 @@ func main() {
slog.Error("invalid persona-file path", "error", err) slog.Error("invalid persona-file path", "error", err)
os.Exit(1) os.Exit(1)
} }
persona, personaErr = review.LoadPersona(resolvedPath) var err2 error
if personaErr != nil { persona, err2 = review.LoadPersona(resolvedPath)
slog.Error("failed to load persona file", "file", *personaFile, "error", personaErr) if err2 != nil {
slog.Error("failed to load persona file", "file", *personaFile, "error", err2)
os.Exit(1) os.Exit(1)
} }
slog.Info("loaded persona from file", "file", *personaFile, "persona", persona.Name) slog.Info("loaded persona from file", "file", *personaFile, "persona", persona.Name)
+14 -2
View File
@@ -40,7 +40,9 @@ func LoadRemotePersonas(ctx context.Context, fetcher PersonaFetcher, owner, repo
return LoadRemotePersonasFromPath(ctx, fetcher, owner, repo, DefaultPersonasPath) return LoadRemotePersonasFromPath(ctx, fetcher, owner, repo, DefaultPersonasPath)
} }
// LoadRemotePersonasFromPath is like LoadRemotePersonas but allows specifying a custom path. // LoadRemotePersonasFromPath loads personas from a custom path in a remote repository.
// It behaves the same as LoadRemotePersonas but allows specifying a path other than
// the default .review-bot/personas directory.
func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, owner, repo, path string) (map[string]*Persona, error) { func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, owner, repo, path string) (map[string]*Persona, error) {
entries, err := fetcher.ListContents(ctx, owner, repo, path) entries, err := fetcher.ListContents(ctx, owner, repo, path)
if err != nil { if err != nil {
@@ -52,8 +54,17 @@ func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, own
return nil, fmt.Errorf("list remote personas: %w", err) return nil, fmt.Errorf("list remote personas: %w", err)
} }
// Cap the number of files to process to prevent resource exhaustion
// from repos with thousands of small files.
const maxPersonaFiles = 50
result := make(map[string]*Persona) result := make(map[string]*Persona)
processed := 0
for _, entry := range entries { for _, entry := range entries {
if processed >= maxPersonaFiles {
slog.Warn("persona file limit reached", "limit", maxPersonaFiles, "repo", fmt.Sprintf("%s/%s", owner, repo))
break
}
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
} }
@@ -85,6 +96,7 @@ func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, own
} }
result[persona.Name] = persona result[persona.Name] = persona
processed++
slog.Debug("loaded remote persona", "name", persona.Name, "file", entry.Path) slog.Debug("loaded remote persona", "name", persona.Name, "file", entry.Path)
} }
@@ -148,5 +160,5 @@ func isNotFoundError(err error) bool {
return false return false
} }
errStr := err.Error() errStr := err.Error()
return strings.Contains(errStr, "HTTP 404") || strings.Contains(errStr, "not found") return strings.Contains(errStr, "HTTP 404")
} }
+1 -1
View File
@@ -379,7 +379,7 @@ func TestIsNotFoundError(t *testing.T) {
}{ }{
{"nil error", nil, false}, {"nil error", nil, false},
{"HTTP 404", errors.New("HTTP 404: not found"), true}, {"HTTP 404", errors.New("HTTP 404: not found"), true},
{"not found text", errors.New("path not found"), true}, {"not found text", errors.New("path not found"), false},
{"server error", errors.New("server error"), false}, {"server error", errors.New("server error"), false},
{"HTTP 500", errors.New("HTTP 500: internal error"), false}, {"HTTP 500", errors.New("HTTP 500: internal error"), false},
} }