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")
+ }
+}