From 0fe9af79f634a9c34e9a8880e02accce7ce5eae7 Mon Sep 17 00:00:00 2001 From: Bhanu Chander Vallabaneni Date: Thu, 13 Aug 2026 07:57:05 -0400 Subject: [PATCH] fix(org): match user and account emails case-insensitively ConnectUserAccountsExact keyed its lookup map on the raw email address, so the comparison was case-sensitive. A user imported from users.csv as Tyrone.Cummings@corp.com was not linked to an account recorded as tyrone.cummings@corp.com, and the account was left unlinked with no warning: the subtask still reported success. That matters because the two sides come from different systems. A git config, a provider profile and a hand-maintained CSV routinely disagree on the capitalisation of the same address, and email addresses are treated case-insensitively everywhere else. Once an account is unlinked, everything attributing through it stops counting towards that user, and the scan is restricted to accounts not already in user_accounts, so a later run does not revisit it. Folds case on both sides of the email comparison. Name matching is left exactly as it was, since names are not case-insensitive in the same way and loosening them could create links that are simply wrong. The org e2e fixtures gain account a11, whose email differs from user U006's only in case, with a name and login that deliberately match nothing. It fails before this change (10 rows against 11 expected) and passes after. --- backend/plugins/org/e2e/raw_tables/accounts.csv | 1 + backend/plugins/org/e2e/snapshot_tables/user_accounts.csv | 1 + backend/plugins/org/tasks/user_account.go | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/plugins/org/e2e/raw_tables/accounts.csv b/backend/plugins/org/e2e/raw_tables/accounts.csv index 6bbdb2a8f98..9b1f09eb84e 100644 --- a/backend/plugins/org/e2e/raw_tables/accounts.csv +++ b/backend/plugins/org/e2e/raw_tables/accounts.csv @@ -1,6 +1,7 @@ "id","created_at","updated_at","_raw_data_params","_raw_data_table","_raw_data_id","_raw_data_remark","email","avatar_url","full_name","user_name","organization","created_date","status" "a1","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e1","","n1","n1","","2022-07-10 14:27:43.813",0 "a10","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e15","","pq","n10","","2022-07-10 14:27:43.813",0 +"a11","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","E6","","zz","zz","","2022-07-10 14:27:43.813",0 "a2","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e1","","n2","n2","","2022-07-10 14:27:43.813",0 "a3","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e2","","xyz","n3","","2022-07-10 14:27:43.813",0 "a4","2022-07-10 14:27:43.813","2022-07-10 14:27:43.813","","",0,"","e4","","n4","n4","","2022-07-10 14:27:43.813",0 diff --git a/backend/plugins/org/e2e/snapshot_tables/user_accounts.csv b/backend/plugins/org/e2e/snapshot_tables/user_accounts.csv index 4e89a081177..1e66cceb992 100644 --- a/backend/plugins/org/e2e/snapshot_tables/user_accounts.csv +++ b/backend/plugins/org/e2e/snapshot_tables/user_accounts.csv @@ -1,6 +1,7 @@ account_id,user_id,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark a1,U111,,,, a10,U010,,,0, +a11,U006,,,0, a2,U112,,,, a3,U113,,,, a4,U004,,,0, diff --git a/backend/plugins/org/tasks/user_account.go b/backend/plugins/org/tasks/user_account.go index 45f187e3b8c..30cd37b76a7 100644 --- a/backend/plugins/org/tasks/user_account.go +++ b/backend/plugins/org/tasks/user_account.go @@ -24,6 +24,7 @@ import ( "github.com/apache/incubator-devlake/core/plugin" "github.com/apache/incubator-devlake/helpers/pluginhelper/api" "reflect" + "strings" ) var ConnectUserAccountsExactMeta = plugin.SubTaskMeta{ @@ -42,11 +43,14 @@ func ConnectUserAccountsExact(taskCtx plugin.SubTaskContext) errors.Error { if err != nil { return err } + // Email addresses are compared case-insensitively: a corporate git config and a provider + // profile routinely record the same address with different capitalisation, and matching them + // exactly silently drops links that should be made. emails := make(map[string]string) names := make(map[string]string) for _, user := range users { if user.Email != "" { - emails[user.Email] = user.Id + emails[strings.ToLower(user.Email)] = user.Id } if user.Name != "" { names[user.Name] = user.Id @@ -76,7 +80,7 @@ func ConnectUserAccountsExact(taskCtx plugin.SubTaskContext) errors.Error { Convert: func(inputRow interface{}) ([]interface{}, errors.Error) { account := inputRow.(*crossdomain.Account) - if userId, ok := emails[account.Email]; account.Email != "" && ok { + if userId, ok := emails[strings.ToLower(account.Email)]; account.Email != "" && ok { return []interface{}{ &crossdomain.UserAccount{ UserId: userId,