This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new e7a60cbdd feat(github): add username filtering helper for bot 
exclusion (#8716)
e7a60cbdd is described below

commit e7a60cbddf4d5c93268b497969651172c5076548
Author: AvivGuiser <[email protected]>
AuthorDate: Thu Feb 26 17:22:43 2026 +0200

    feat(github): add username filtering helper for bot exclusion (#8716)
    
    * feat(github): add username filtering helper for bot exclusion
    
    Implements shouldSkipByUsername() function to filter bot accounts
    by username using GITHUB_PR_EXCLUDELIST environment variable.
    
    - Case-insensitive matching
    - Comma-separated list support
    - Whitespace trimming
    - Returns false for empty usernames or empty exclusion list
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    Signed-off-by: AvivGuiser <[email protected]>
    
    * feat(github): filter bot PRs in extractor
    
    Adds username filtering to PR extractor to skip bot-authored PRs
    when GITHUB_PR_EXCLUDELIST is set.
    
    - Checks author username before extraction
    - Logs debug message when PR is skipped
    - Includes unit test for bot filtering
    - Includes e2e test data for bot filtering
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    
    * feat(github): filter bot reviews in extractor
    
    Adds username filtering to review extractor to skip bot reviews
    when GITHUB_PR_EXCLUDELIST is set.
    
    - Checks reviewer username before extraction
    - Logs debug message when review is skipped
    - Includes e2e test for bot filtering
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    
    * feat(github): filter bot PR review comments in extractor
    
    Adds username filtering to PR review comment extractor to skip
    bot comments when GITHUB_PR_EXCLUDELIST is set.
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    
    * feat(github): filter bot issue comments in extractor
    
    Adds username filtering to issue comment extractor to skip
    bot comments when GITHUB_PR_EXCLUDELIST is set.
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    
    * docs(github): add bot filtering documentation
    
    Documents GITHUB_PR_EXCLUDELIST configuration and usage.
    
    Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
    
    * fix(github): fix CI errors in bot filtering PR
    
    - Add Apache license header to README_FILTERING.md
    - Export ResetExcludedUsernamesForTest to fix unused lint error
    - Call ResetExcludedUsernamesForTest in e2e tests to reset sync.Once
      cache before setting GITHUB_PR_EXCLUDELIST env var
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    ---------
    
    Signed-off-by: AvivGuiser <[email protected]>
    Co-authored-by: Claude Sonnet 4.5 <[email protected]>
---
 backend/plugins/github/README_FILTERING.md         | 118 +++++++++++++++++++++
 backend/plugins/github/e2e/pr_review_test.go       |  40 +++++++
 backend/plugins/github/e2e/pr_test.go              |  40 +++++++
 .../_raw_github_api_pr_reviews_bot_filter.csv      |   3 +
 .../_raw_github_api_pull_requests_bot_filter.csv   |   3 +
 backend/plugins/github/tasks/comment_extractor.go  |  29 +++--
 backend/plugins/github/tasks/pr_extractor.go       |   5 +
 .../github/tasks/pr_review_comment_extractor.go    |   6 ++
 .../plugins/github/tasks/pr_review_extractor.go    |   5 +
 backend/plugins/github/tasks/username_filter.go    |  87 +++++++++++++++
 .../plugins/github/tasks/username_filter_test.go   | 101 ++++++++++++++++++
 11 files changed, 430 insertions(+), 7 deletions(-)

diff --git a/backend/plugins/github/README_FILTERING.md 
b/backend/plugins/github/README_FILTERING.md
new file mode 100644
index 000000000..81f9db65a
--- /dev/null
+++ b/backend/plugins/github/README_FILTERING.md
@@ -0,0 +1,118 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# GitHub Plugin - Bot Filtering
+
+## Overview
+
+The GitHub plugin supports filtering bot-generated PRs, reviews, and comments 
from data collection to prevent them from skewing metrics like lead time for 
changes and PR pickup time.
+
+## Configuration
+
+Set the `GITHUB_PR_EXCLUDELIST` environment variable with a comma-separated 
list of bot usernames to exclude:
+
+```bash
+export 
GITHUB_PR_EXCLUDELIST="renovate[bot],dependabot[bot],github-actions[bot]"
+```
+
+## Common Bot Usernames
+
+- `renovate[bot]` - Renovate dependency updates
+- `dependabot[bot]` - GitHub Dependabot
+- `github-actions[bot]` - GitHub Actions automated PRs
+- `sonarcloud[bot]` - SonarCloud code analysis
+- `codecov[bot]` - Codecov coverage reports
+
+## What Gets Filtered
+
+When a username is in the exclusion list, the following entities are NOT 
collected:
+
+1. **Pull Requests** - PRs authored by bots
+2. **PR Reviews** - Reviews submitted by bots
+3. **PR Review Comments** - Comments on PR reviews by bots
+4. **Issue Comments** - Comments on issues by bots
+
+## How It Works
+
+- Filtering happens at the **extraction** layer
+- Raw API responses are still saved (in `_raw_github_api_*` tables)
+- Filtered entities never reach the tool layer tables
+- Metrics queries only see non-bot entities
+
+## Matching Rules
+
+- **Case-insensitive**: `renovate[bot]` matches `Renovate[bot]` and 
`RENOVATE[BOT]`
+- **Exact match**: Must match the full username
+- **Whitespace trimmed**: Extra spaces in the config are ignored
+
+## Examples
+
+### Docker Compose
+
+```yaml
+services:
+  devlake:
+    environment:
+      - GITHUB_PR_EXCLUDELIST=renovate[bot],dependabot[bot]
+```
+
+### Kubernetes
+
+```yaml
+env:
+  - name: GITHUB_PR_EXCLUDELIST
+    value: "renovate[bot],dependabot[bot],github-actions[bot]"
+```
+
+### Local Development
+
+```bash
+# .env file
+GITHUB_PR_EXCLUDELIST=renovate[bot],dependabot[bot]
+```
+
+## Updating the Exclusion List
+
+Changes to `GITHUB_PR_EXCLUDELIST` require a DevLake restart. After updating:
+
+1. Restart DevLake
+2. Trigger re-collection for affected repositories
+3. Previously collected bot data remains in the database
+4. New collections will respect the updated filter
+
+## Verification
+
+Check logs for filtering activity:
+
+```
+DEBUG: Skipping PR #123 from bot user: renovate[bot]
+DEBUG: Skipping review #456 from bot user: dependabot[bot]
+```
+
+## Troubleshooting
+
+**Bot PRs still appearing in metrics:**
+
+1. Verify `GITHUB_PR_EXCLUDELIST` is set correctly
+2. Check DevLake logs for "Skipping" messages
+3. Ensure username matches exactly (case-insensitive)
+4. Restart DevLake after config changes
+5. Re-run collection for the repository
+
+**How to find bot usernames:**
+
+Check GitHub PR/comment authors in the web UI - bot usernames typically end 
with `[bot]`.
diff --git a/backend/plugins/github/e2e/pr_review_test.go 
b/backend/plugins/github/e2e/pr_review_test.go
index 043b8a936..b676497da 100644
--- a/backend/plugins/github/e2e/pr_review_test.go
+++ b/backend/plugins/github/e2e/pr_review_test.go
@@ -18,8 +18,10 @@ limitations under the License.
 package e2e
 
 import (
+       "os"
        "testing"
 
+       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/models/domainlayer/code"
        "github.com/apache/incubator-devlake/helpers/e2ehelper"
        "github.com/apache/incubator-devlake/plugins/github/impl"
@@ -99,3 +101,41 @@ func TestPrReviewDataFlow(t *testing.T) {
                },
        )
 }
+
+func TestPrReviewDataFlowWithBotFiltering(t *testing.T) {
+       var plugin impl.Github
+       dataflowTester := e2ehelper.NewDataFlowTester(t, "github", plugin)
+
+       // Set up bot filtering
+       tasks.ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "renovate[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       taskData := &tasks.GithubTaskData{
+               Options: &tasks.GithubOptions{
+                       ConnectionId: 1,
+                       Name:         "test/repo",
+                       GithubId:     123,
+               },
+       }
+
+       // import raw data table with bot and human reviews
+       
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_github_api_pr_reviews_bot_filter.csv",
 "_raw_github_api_pull_request_reviews")
+
+       // verify review extraction filters bot reviews
+       dataflowTester.FlushTabler(&models.GithubPrReview{})
+       dataflowTester.FlushTabler(&models.GithubReviewer{})
+       dataflowTester.FlushTabler(&models.GithubRepoAccount{})
+       dataflowTester.Subtask(tasks.ExtractApiPullRequestReviewsMeta, taskData)
+
+       // Verify only human review was extracted
+       var reviews []models.GithubPrReview
+       dataflowTester.Dal.All(&reviews, dal.Where("connection_id = ?", 1))
+
+       if len(reviews) != 1 {
+               t.Errorf("Expected 1 review (human), got %d", len(reviews))
+       }
+       if len(reviews) > 0 && reviews[0].GithubId != 5002 {
+               t.Errorf("Expected review #5002 (human), got #%d", 
reviews[0].GithubId)
+       }
+}
diff --git a/backend/plugins/github/e2e/pr_test.go 
b/backend/plugins/github/e2e/pr_test.go
index 1299cd8c7..8a9528045 100644
--- a/backend/plugins/github/e2e/pr_test.go
+++ b/backend/plugins/github/e2e/pr_test.go
@@ -18,8 +18,10 @@ limitations under the License.
 package e2e
 
 import (
+       "os"
        "testing"
 
+       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/models/domainlayer/code"
        "github.com/apache/incubator-devlake/helpers/e2ehelper"
        "github.com/apache/incubator-devlake/plugins/github/impl"
@@ -171,3 +173,41 @@ func TestPrDataFlow(t *testing.T) {
                },
        )
 }
+
+func TestPrDataFlowWithBotFiltering(t *testing.T) {
+       var plugin impl.Github
+       dataflowTester := e2ehelper.NewDataFlowTester(t, "github", plugin)
+
+       // Set up bot filtering
+       tasks.ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "renovate[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       taskData := &tasks.GithubTaskData{
+               Options: &tasks.GithubOptions{
+                       ConnectionId: 1,
+                       Name:         "test/repo",
+                       GithubId:     123,
+                       ScopeConfig:  &models.GithubScopeConfig{},
+               },
+       }
+
+       // import raw data table with bot and human PRs
+       
dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_github_api_pull_requests_bot_filter.csv",
 "_raw_github_api_pull_requests")
+
+       // verify pr extraction filters bot PRs
+       dataflowTester.FlushTabler(&models.GithubPullRequest{})
+       dataflowTester.FlushTabler(&models.GithubRepoAccount{})
+       dataflowTester.Subtask(tasks.ExtractApiPullRequestsMeta, taskData)
+
+       // Verify only human PR was extracted
+       var prs []models.GithubPullRequest
+       dataflowTester.Dal.All(&prs, dal.Where("connection_id = ?", 1))
+
+       if len(prs) != 1 {
+               t.Errorf("Expected 1 PR (human), got %d", len(prs))
+       }
+       if len(prs) > 0 && prs[0].Number != 1000 {
+               t.Errorf("Expected PR #1000 (human), got #%d", prs[0].Number)
+       }
+}
diff --git 
a/backend/plugins/github/e2e/raw_tables/_raw_github_api_pr_reviews_bot_filter.csv
 
b/backend/plugins/github/e2e/raw_tables/_raw_github_api_pr_reviews_bot_filter.csv
new file mode 100644
index 000000000..50dff0b97
--- /dev/null
+++ 
b/backend/plugins/github/e2e/raw_tables/_raw_github_api_pr_reviews_bot_filter.csv
@@ -0,0 +1,3 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""Name"":""test/repo""}","{""id"":5001,""user"":{""login"":""renovate[bot]"",""id"":29139614},""body"":""LGTM"",""state"":""APPROVED"",""commit_id"":""abc123"",""submitted_at"":""2024-01-01T00:00:00Z""}",https://api.github.com/repos/test/repo/pulls/1/reviews,"{""GithubId"":1,""Number"":1}",2024-01-01
 00:00:00
+2,"{""ConnectionId"":1,""Name"":""test/repo""}","{""id"":5002,""user"":{""login"":""human-reviewer"",""id"":12345},""body"":""Looks
 
good"",""state"":""APPROVED"",""commit_id"":""abc123"",""submitted_at"":""2024-01-02T00:00:00Z""}",https://api.github.com/repos/test/repo/pulls/1/reviews,"{""GithubId"":1,""Number"":1}",2024-01-02
 00:00:00
diff --git 
a/backend/plugins/github/e2e/raw_tables/_raw_github_api_pull_requests_bot_filter.csv
 
b/backend/plugins/github/e2e/raw_tables/_raw_github_api_pull_requests_bot_filter.csv
new file mode 100644
index 000000000..5f70488f1
--- /dev/null
+++ 
b/backend/plugins/github/e2e/raw_tables/_raw_github_api_pull_requests_bot_filter.csv
@@ -0,0 +1,3 @@
+id,params,data,url,input,created_at
+1,"{""ConnectionId"":1,""Name"":""test/repo""}","{""id"":999,""number"":999,""state"":""closed"",""title"":""Dependency
 
Update"",""user"":{""login"":""renovate[bot]"",""id"":29139614},""body"":""Updates
 
dependencies"",""created_at"":""2024-01-01T00:00:00Z"",""updated_at"":""2024-01-02T00:00:00Z"",""closed_at"":""2024-01-02T00:00:00Z"",""merged_at"":""2024-01-02T00:00:00Z"",""merge_commit_sha"":""abc123"",""merged"":true,""additions"":10,""deletions"":5,""changed_files"":2,""comments"":0,
 [...]
+2,"{""ConnectionId"":1,""Name"":""test/repo""}","{""id"":1000,""number"":1000,""state"":""open"",""title"":""Feature
 PR"",""user"":{""login"":""human-dev"",""id"":12345},""body"":""Adds 
feature"",""created_at"":""2024-01-03T00:00:00Z"",""updated_at"":""2024-01-03T00:00:00Z"",""closed_at"":null,""merged_at"":null,""merge_commit_sha"":"""",""merged"":false,""additions"":100,""deletions"":20,""changed_files"":5,""comments"":2,""review_comments"":3,""commits"":5,""draft"":false,""labels"":[]
 [...]
diff --git a/backend/plugins/github/tasks/comment_extractor.go 
b/backend/plugins/github/tasks/comment_extractor.go
index 386babe79..8fc52a18b 100644
--- a/backend/plugins/github/tasks/comment_extractor.go
+++ b/backend/plugins/github/tasks/comment_extractor.go
@@ -107,8 +107,19 @@ func ExtractApiComments(taskCtx plugin.SubTaskContext) 
errors.Error {
                                        Type:            "NORMAL",
                                }
                                if apiComment.User != nil {
+                                       // Filter bot comments by username
+                                       if 
shouldSkipByUsername(apiComment.User.Login) {
+                                               
taskCtx.GetLogger().Debug("Skipping PR comment #%d from bot user: %s", 
apiComment.GithubId, apiComment.User.Login)
+                                               return nil, nil
+                                       }
                                        githubPrComment.AuthorUsername = 
apiComment.User.Login
                                        githubPrComment.AuthorUserId = 
apiComment.User.Id
+
+                                       githubAccount, err := 
convertAccount(apiComment.User, data.Options.GithubId, 
data.Options.ConnectionId)
+                                       if err != nil {
+                                               return nil, err
+                                       }
+                                       results = append(results, githubAccount)
                                }
                                results = append(results, githubPrComment)
                        } else {
@@ -121,18 +132,22 @@ func ExtractApiComments(taskCtx plugin.SubTaskContext) 
errors.Error {
                                        GithubUpdatedAt: 
apiComment.GithubUpdatedAt.ToTime(),
                                }
                                if apiComment.User != nil {
+                                       // Filter bot comments by username
+                                       if 
shouldSkipByUsername(apiComment.User.Login) {
+                                               
taskCtx.GetLogger().Debug("Skipping issue comment #%d from bot user: %s", 
apiComment.GithubId, apiComment.User.Login)
+                                               return nil, nil
+                                       }
                                        githubIssueComment.AuthorUsername = 
apiComment.User.Login
                                        githubIssueComment.AuthorUserId = 
apiComment.User.Id
+
+                                       githubAccount, err := 
convertAccount(apiComment.User, data.Options.GithubId, 
data.Options.ConnectionId)
+                                       if err != nil {
+                                               return nil, err
+                                       }
+                                       results = append(results, githubAccount)
                                }
                                results = append(results, githubIssueComment)
                        }
-                       if apiComment.User != nil {
-                               githubAccount, err := 
convertAccount(apiComment.User, data.Options.GithubId, 
data.Options.ConnectionId)
-                               if err != nil {
-                                       return nil, err
-                               }
-                               results = append(results, githubAccount)
-                       }
                        return results, nil
                },
        })
diff --git a/backend/plugins/github/tasks/pr_extractor.go 
b/backend/plugins/github/tasks/pr_extractor.go
index 20f1bed01..ea7858385 100644
--- a/backend/plugins/github/tasks/pr_extractor.go
+++ b/backend/plugins/github/tasks/pr_extractor.go
@@ -131,6 +131,11 @@ func ExtractApiPullRequests(taskCtx plugin.SubTaskContext) 
errors.Error {
                        if rawL.GithubId == 0 {
                                return nil, nil
                        }
+                       // Filter bot PRs by username
+                       if rawL.User != nil && 
shouldSkipByUsername(rawL.User.Login) {
+                               taskCtx.GetLogger().Debug("Skipping PR #%d from 
bot user: %s", rawL.Number, rawL.User.Login)
+                               return nil, nil
+                       }
                        //If this is a pr, ignore
                        githubPr, err := convertGithubPullRequest(rawL, 
data.Options.ConnectionId, data.Options.GithubId)
                        if err != nil {
diff --git a/backend/plugins/github/tasks/pr_review_comment_extractor.go 
b/backend/plugins/github/tasks/pr_review_comment_extractor.go
index 7d93f2344..ed8aa1825 100644
--- a/backend/plugins/github/tasks/pr_review_comment_extractor.go
+++ b/backend/plugins/github/tasks/pr_review_comment_extractor.go
@@ -103,6 +103,12 @@ func ExtractApiPrReviewComments(taskCtx 
plugin.SubTaskContext) errors.Error {
                        }
 
                        if prReviewComment.User != nil {
+                               // Filter bot comments by username
+                               if 
shouldSkipByUsername(prReviewComment.User.Login) {
+                                       taskCtx.GetLogger().Debug("Skipping PR 
review comment #%d from bot user: %s", prReviewComment.GithubId, 
prReviewComment.User.Login)
+                                       return nil, nil
+                               }
+
                                githubPrComment.AuthorUserId = 
prReviewComment.User.Id
                                githubPrComment.AuthorUsername = 
prReviewComment.User.Login
 
diff --git a/backend/plugins/github/tasks/pr_review_extractor.go 
b/backend/plugins/github/tasks/pr_review_extractor.go
index 2fba9a955..2af41b60a 100644
--- a/backend/plugins/github/tasks/pr_review_extractor.go
+++ b/backend/plugins/github/tasks/pr_review_extractor.go
@@ -84,6 +84,11 @@ func ExtractApiPullRequestReviews(taskCtx 
plugin.SubTaskContext) errors.Error {
                        if apiPullRequestReview.State == "PENDING" || 
apiPullRequestReview.User == nil {
                                return nil, nil
                        }
+                       // Filter bot reviews by username
+                       if 
shouldSkipByUsername(apiPullRequestReview.User.Login) {
+                               taskCtx.GetLogger().Debug("Skipping review #%d 
from bot user: %s", apiPullRequestReview.GithubId, 
apiPullRequestReview.User.Login)
+                               return nil, nil
+                       }
                        pull := &SimplePr{}
                        err = errors.Convert(json.Unmarshal(row.Input, pull))
                        if err != nil {
diff --git a/backend/plugins/github/tasks/username_filter.go 
b/backend/plugins/github/tasks/username_filter.go
new file mode 100644
index 000000000..ec13cf6b6
--- /dev/null
+++ b/backend/plugins/github/tasks/username_filter.go
@@ -0,0 +1,87 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "os"
+       "strings"
+       "sync"
+)
+
+var (
+       excludedUsernames     []string
+       excludedUsernamesOnce sync.Once
+       excludedUsernamesMu   sync.RWMutex
+)
+
+// initExcludedUsernames reads and parses the GITHUB_PR_EXCLUDELIST 
environment variable
+func initExcludedUsernames() {
+       excludedUsernamesOnce.Do(func() {
+               loadExcludedUsernames()
+       })
+}
+
+// loadExcludedUsernames parses the environment variable (called by 
initExcludedUsernames or tests)
+func loadExcludedUsernames() {
+       excludedUsernamesMu.Lock()
+       defer excludedUsernamesMu.Unlock()
+
+       envValue := os.Getenv("GITHUB_PR_EXCLUDELIST")
+       if envValue == "" {
+               excludedUsernames = []string{}
+               return
+       }
+
+       usernames := strings.Split(envValue, ",")
+       excludedUsernames = make([]string, 0, len(usernames))
+       for _, username := range usernames {
+               trimmed := strings.TrimSpace(username)
+               if trimmed != "" {
+                       excludedUsernames = append(excludedUsernames, 
strings.ToLower(trimmed))
+               }
+       }
+}
+
+// ResetExcludedUsernamesForTest resets the cache for testing purposes
+func ResetExcludedUsernamesForTest() {
+       excludedUsernamesMu.Lock()
+       defer excludedUsernamesMu.Unlock()
+       excludedUsernames = nil
+       excludedUsernamesOnce = sync.Once{}
+}
+
+// shouldSkipByUsername checks if the given username should be filtered out
+// Returns true if the username matches any entry in the GITHUB_PR_EXCLUDELIST
+func shouldSkipByUsername(username string) bool {
+       initExcludedUsernames()
+
+       if username == "" {
+               return false
+       }
+
+       excludedUsernamesMu.RLock()
+       defer excludedUsernamesMu.RUnlock()
+
+       lowerUsername := strings.ToLower(username)
+       for _, excluded := range excludedUsernames {
+               if lowerUsername == excluded {
+                       return true
+               }
+       }
+       return false
+}
diff --git a/backend/plugins/github/tasks/username_filter_test.go 
b/backend/plugins/github/tasks/username_filter_test.go
new file mode 100644
index 000000000..638aa1b9e
--- /dev/null
+++ b/backend/plugins/github/tasks/username_filter_test.go
@@ -0,0 +1,101 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "os"
+       "testing"
+)
+
+func TestShouldSkipByUsername_EmptyList(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if shouldSkipByUsername("renovate[bot]") {
+               t.Error("Expected false for any username when excludelist is 
empty")
+       }
+}
+
+func TestShouldSkipByUsername_SingleMatch(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "renovate[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if !shouldSkipByUsername("renovate[bot]") {
+               t.Error("Expected true for renovate[bot]")
+       }
+       if shouldSkipByUsername("human-user") {
+               t.Error("Expected false for human-user")
+       }
+}
+
+func TestShouldSkipByUsername_MultipleUsernames(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", 
"renovate[bot],dependabot[bot],github-actions[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if !shouldSkipByUsername("renovate[bot]") {
+               t.Error("Expected true for renovate[bot]")
+       }
+       if !shouldSkipByUsername("dependabot[bot]") {
+               t.Error("Expected true for dependabot[bot]")
+       }
+       if !shouldSkipByUsername("github-actions[bot]") {
+               t.Error("Expected true for github-actions[bot]")
+       }
+       if shouldSkipByUsername("human-user") {
+               t.Error("Expected false for human-user")
+       }
+}
+
+func TestShouldSkipByUsername_CaseInsensitive(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "renovate[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if !shouldSkipByUsername("Renovate[bot]") {
+               t.Error("Expected true for Renovate[bot] (case insensitive)")
+       }
+       if !shouldSkipByUsername("RENOVATE[BOT]") {
+               t.Error("Expected true for RENOVATE[BOT] (case insensitive)")
+       }
+}
+
+func TestShouldSkipByUsername_WhitespaceTrimming(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", " renovate[bot] , dependabot[bot] ")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if !shouldSkipByUsername("renovate[bot]") {
+               t.Error("Expected true for renovate[bot] with whitespace in 
config")
+       }
+       if !shouldSkipByUsername("dependabot[bot]") {
+               t.Error("Expected true for dependabot[bot] with whitespace in 
config")
+       }
+}
+
+func TestShouldSkipByUsername_EmptyUsername(t *testing.T) {
+       ResetExcludedUsernamesForTest()
+       os.Setenv("GITHUB_PR_EXCLUDELIST", "renovate[bot]")
+       defer os.Unsetenv("GITHUB_PR_EXCLUDELIST")
+
+       if shouldSkipByUsername("") {
+               t.Error("Expected false for empty username")
+       }
+}

Reply via email to