From 9fec2e3bc876a51bf88c1f047c4ae8e48d26b2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:25:12 +0200 Subject: [PATCH 01/11] fix: when organization has no valid identities lets just skip it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/service/member.service.ts | 26 +++++++++++-------- .../src/service/organization.service.ts | 13 +++------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/member.service.ts b/services/apps/data_sink_worker/src/service/member.service.ts index 74af51d9b0..186049cde3 100644 --- a/services/apps/data_sink_worker/src/service/member.service.ts +++ b/services/apps/data_sink_worker/src/service/member.service.ts @@ -479,7 +479,7 @@ export default class MemberService extends LoggerBase { const key = orgCacheKey(org) let orgIdPromise: Promise if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key) + orgIdPromise = orgPromiseCache.get(key)! } else { orgIdPromise = logExecutionTimeV2( () => orgService.findOrCreate(platform, integrationId, org), @@ -492,10 +492,12 @@ export default class MemberService extends LoggerBase { } } const orgId = await orgIdPromise - organizations.push({ - id: orgId, - source: org.source, - }) + if (orgId) { + organizations.push({ + id: orgId, + source: org.source, + }) + } } } @@ -713,7 +715,7 @@ export default class MemberService extends LoggerBase { const key = orgCacheKey(org) let orgIdPromise: Promise if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key) + orgIdPromise = orgPromiseCache.get(key)! } else { orgIdPromise = logExecutionTimeV2( () => orgService.findOrCreate(platform, integrationId, org), @@ -726,10 +728,12 @@ export default class MemberService extends LoggerBase { } } const orgId = await orgIdPromise - organizations.push({ - id: orgId, - source: data.source, - }) + if (orgId) { + organizations.push({ + id: orgId, + source: data.source, + }) + } } } @@ -841,7 +845,7 @@ export default class MemberService extends LoggerBase { const key = orgCacheKey(org) let orgIdPromise: Promise if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key) + orgIdPromise = orgPromiseCache.get(key)! } else { orgIdPromise = orgService.findOrCreate( OrganizationAttributeSource.EMAIL, diff --git a/services/apps/data_sink_worker/src/service/organization.service.ts b/services/apps/data_sink_worker/src/service/organization.service.ts index 393c8bd0bc..95d279cf2f 100644 --- a/services/apps/data_sink_worker/src/service/organization.service.ts +++ b/services/apps/data_sink_worker/src/service/organization.service.ts @@ -25,18 +25,11 @@ export class OrganizationService extends LoggerBase { source: string, integrationId: string, data: IOrganization, - ): Promise { - const id = await this.store.transactionally(async (txStore) => { + ): Promise { + return this.store.transactionally(async (txStore) => { const qe = dbStoreQx(txStore) - const id = await findOrCreateOrganization(qe, source, data, integrationId, true) - return id + return findOrCreateOrganization(qe, source, data, integrationId, true) }) - - if (!id) { - throw new Error('Organization not found or created!') - } - - return id } public async addToMember( From dae55354542a97f2940a608fb8fc83cc77e9b6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:26:42 +0200 Subject: [PATCH 02/11] fix: when activity only has a verified email identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/service/activity.service.ts | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index 849335367b..3a6f359edb 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -334,18 +334,28 @@ export default class ActivityService extends LoggerBase { if (identities.length === 1) { activity.username = identities[0].value } else if (identities.length === 0) { - this.log.error( - { platform, activity }, - `Activity's member does not have an identity for the platform!`, + // Fall back to same-platform email identity — handles old gerrit records where + // only a type:email identity was stored (before parse-member.ts gained the + // email-as-username fallback). + const emailFallback = activity.member.identities.find( + (i) => i.platform === platform && i.type === MemberIdentityType.EMAIL && i.value, ) - results.set(resultId, { - success: false, - err: new UnrepeatableError( - `Activity's member does not have an identity for the platform: ${platform}!`, - ), - }) - - continue + if (emailFallback) { + activity.username = emailFallback.value + } else { + this.log.error( + { platform, activity }, + `Activity's member does not have an identity for the platform!`, + ) + results.set(resultId, { + success: false, + err: new UnrepeatableError( + `Activity's member does not have an identity for the platform: ${platform}!`, + ), + }) + + continue + } } else { this.log.error( { platform, activity }, From 8720f5cde63ed5c8b799ae6a5ab1ddbb46c627be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:30:35 +0200 Subject: [PATCH 03/11] fix: an issue where identity would have an empty value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/service/activity.service.ts | 16 ++-- .../services/commit/commit_service.py | 84 +++++++++++-------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index 3a6f359edb..fc7614121d 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -343,17 +343,13 @@ export default class ActivityService extends LoggerBase { if (emailFallback) { activity.username = emailFallback.value } else { - this.log.error( - { platform, activity }, - `Activity's member does not have an identity for the platform!`, + // No usable identity at all (e.g. git commit with empty author email). + // Nothing to attribute — skip silently rather than error. + this.log.warn( + { platform, resultId }, + `Activity's member has no usable identity for the platform, skipping.`, ) - results.set(resultId, { - success: false, - err: new UnrepeatableError( - `Activity's member does not have an identity for the platform: ${platform}!`, - ), - }) - + results.set(resultId, { success: true }) continue } } else { diff --git a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py index 005aa4b6a5..4625a7e8cd 100644 --- a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py +++ b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py @@ -538,47 +538,23 @@ def create_activities_from_commit( committer_name = commit["committer_name"] committer_email = commit["committer_email"] - # Create author activity - author = { - "username": author_email, - "displayName": author_name, - "emails": [author_email], - } - activity = self.create_activity( - remote=remote, - commit=commit, - activity_type="authored-commit", - member=author, - source_id=commit_hash, - segment_id=segment_id, - re_onboarding_count=re_onboarding_count, - ) - activity_db, activity_kafka = self.prepare_activity_for_db_and_queue( - activity, segment_id, integration_id - ) - activities_db.append(activity_db) - activities_queue.append(activity_kafka) - - # Only create committer activity if author and committer are different - if author_name != committer_name or author_email != committer_email: - # IMPORTANT: hash_input has a typo in "commited" instead of "committed" - # however fixing it requires recalculating sourceId/parentSourceId for ALL git activities in db - # so far the typo doesn't have any major effect, since the activity type "committed-commit" is correct - hash_input = f"{commit_hash}commited-commit{committer_email}" - committer_source_id = hashlib.sha1(hash_input.encode("utf-8")).hexdigest() - - committer = { - "username": committer_email, - "displayName": committer_name, - "emails": [committer_email], + # Create author activity — skip if email is empty (no identity to attach to) + if not author_email: + self.logger.warning( + f"Skipping authored-commit for {commit_hash} — empty author email" + ) + else: + author = { + "username": author_email, + "displayName": author_name, + "emails": [author_email], } activity = self.create_activity( remote=remote, commit=commit, - activity_type="committed-commit", - member=committer, - source_id=committer_source_id, - source_parent_id=commit_hash, + activity_type="authored-commit", + member=author, + source_id=commit_hash, segment_id=segment_id, re_onboarding_count=re_onboarding_count, ) @@ -588,6 +564,40 @@ def create_activities_from_commit( activities_db.append(activity_db) activities_queue.append(activity_kafka) + # Only create committer activity if author and committer are different + if author_name != committer_name or author_email != committer_email: + if not committer_email: + self.logger.warning( + f"Skipping committed-commit for {commit_hash} — empty committer email" + ) + else: + # IMPORTANT: hash_input has a typo in "commited" instead of "committed" + # however fixing it requires recalculating sourceId/parentSourceId for ALL git activities in db + # so far the typo doesn't have any major effect, since the activity type "committed-commit" is correct + hash_input = f"{commit_hash}commited-commit{committer_email}" + committer_source_id = hashlib.sha1(hash_input.encode("utf-8")).hexdigest() + + committer = { + "username": committer_email, + "displayName": committer_name, + "emails": [committer_email], + } + activity = self.create_activity( + remote=remote, + commit=commit, + activity_type="committed-commit", + member=committer, + source_id=committer_source_id, + source_parent_id=commit_hash, + segment_id=segment_id, + re_onboarding_count=re_onboarding_count, + ) + activity_db, activity_kafka = self.prepare_activity_for_db_and_queue( + activity, segment_id, integration_id + ) + activities_db.append(activity_db) + activities_queue.append(activity_kafka) + # Process extracted activities from commit message extracted_activities = self.extract_activities(commit["message"]) for extracted_activity in extracted_activities: From 78e4a41c20dd45054549ab8d93967fcb8106119d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:36:56 +0200 Subject: [PATCH 04/11] fix: issue where email was not a part of commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/crowdgit/services/commit/commit_service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py index 4625a7e8cd..7c6817b1c0 100644 --- a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py +++ b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py @@ -603,6 +603,12 @@ def create_activities_from_commit( for extracted_activity in extracted_activities: activity_type, member_data = list(extracted_activity.items())[0] + if not member_data.get("email"): + self.logger.warning( + f"Skipping {activity_type} for {commit_hash} — empty email in commit trailer" + ) + continue + # Convert activity type to lowercase and add "-commit" suffix # This matches the legacy behavior: "signed-off-by" -> "signed-off-commit" activity_type = activity_type.lower().replace("-by", "") + "-commit" From 9d2873f01b2bb350bec80f6db54c3ebfd965acd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:43:18 +0200 Subject: [PATCH 05/11] fix: linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/crowdgit/services/commit/commit_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py index 7c6817b1c0..c9c4780c3a 100644 --- a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py +++ b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py @@ -540,9 +540,7 @@ def create_activities_from_commit( # Create author activity — skip if email is empty (no identity to attach to) if not author_email: - self.logger.warning( - f"Skipping authored-commit for {commit_hash} — empty author email" - ) + self.logger.warning(f"Skipping authored-commit for {commit_hash} — empty author email") else: author = { "username": author_email, From 5574e6162d05317350dd32720fc11a8252e5c021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 14:48:22 +0200 Subject: [PATCH 06/11] fix: comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/service/activity.service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index fc7614121d..1eb594c872 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -335,8 +335,8 @@ export default class ActivityService extends LoggerBase { activity.username = identities[0].value } else if (identities.length === 0) { // Fall back to same-platform email identity — handles old gerrit records where - // only a type:email identity was stored (before parse-member.ts gained the - // email-as-username fallback). + // only a type:email identity was stored (before the gerrit integration + // gained the email-as-username fallback). const emailFallback = activity.member.identities.find( (i) => i.platform === platform && i.type === MemberIdentityType.EMAIL && i.value, ) @@ -465,7 +465,13 @@ export default class ActivityService extends LoggerBase { if (!success) { resultMap.set(resultId, { success: false, err }) } else { - relevantPayloads.push(single(payloads, (a) => a.resultId === resultId)) + const payload = single(payloads, (a) => a.resultId === resultId) + if (!payload.activity.username) { + // prepareMemberData found no usable identity — mark as processed and skip. + resultMap.set(resultId, { success: true }) + } else { + relevantPayloads.push(payload) + } } } From f51551ca8b95cfa7ab70c66a6b5221527ec50d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 15:00:32 +0200 Subject: [PATCH 07/11] fix: linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/service/member.service.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/member.service.ts b/services/apps/data_sink_worker/src/service/member.service.ts index 186049cde3..8bdcd558fd 100644 --- a/services/apps/data_sink_worker/src/service/member.service.ts +++ b/services/apps/data_sink_worker/src/service/member.service.ts @@ -477,9 +477,10 @@ export default class MemberService extends LoggerBase { } const key = orgCacheKey(org) + const cachedOrgPromise = key ? orgPromiseCache?.get(key) : undefined let orgIdPromise: Promise - if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key)! + if (cachedOrgPromise) { + orgIdPromise = cachedOrgPromise } else { orgIdPromise = logExecutionTimeV2( () => orgService.findOrCreate(platform, integrationId, org), @@ -713,9 +714,10 @@ export default class MemberService extends LoggerBase { this.log.trace({ memberId: id }, 'Finding or creating organization!') const key = orgCacheKey(org) + const cachedOrgPromise = key ? orgPromiseCache?.get(key) : undefined let orgIdPromise: Promise - if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key)! + if (cachedOrgPromise) { + orgIdPromise = cachedOrgPromise } else { orgIdPromise = logExecutionTimeV2( () => orgService.findOrCreate(platform, integrationId, org), @@ -843,9 +845,10 @@ export default class MemberService extends LoggerBase { ], } const key = orgCacheKey(org) + const cachedOrgPromise = key ? orgPromiseCache?.get(key) : undefined let orgIdPromise: Promise - if (key && orgPromiseCache?.has(key)) { - orgIdPromise = orgPromiseCache.get(key)! + if (cachedOrgPromise) { + orgIdPromise = cachedOrgPromise } else { orgIdPromise = orgService.findOrCreate( OrganizationAttributeSource.EMAIL, From c77ba3c580578b99f5df29e49b6a0b44cc7dc8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 15:13:05 +0200 Subject: [PATCH 08/11] fix: handle whitespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../data_sink_worker/src/service/activity.service.ts | 2 +- .../src/crowdgit/services/commit/commit_service.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index 1eb594c872..aeacf784de 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -466,7 +466,7 @@ export default class ActivityService extends LoggerBase { resultMap.set(resultId, { success: false, err }) } else { const payload = single(payloads, (a) => a.resultId === resultId) - if (!payload.activity.username) { + if (!payload.activity.username?.trim()) { // prepareMemberData found no usable identity — mark as processed and skip. resultMap.set(resultId, { success: true }) } else { diff --git a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py index c9c4780c3a..013167d13d 100644 --- a/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py +++ b/services/apps/git_integration/src/crowdgit/services/commit/commit_service.py @@ -539,6 +539,7 @@ def create_activities_from_commit( committer_email = commit["committer_email"] # Create author activity — skip if email is empty (no identity to attach to) + author_email = author_email.strip() if author_email else "" if not author_email: self.logger.warning(f"Skipping authored-commit for {commit_hash} — empty author email") else: @@ -563,6 +564,7 @@ def create_activities_from_commit( activities_queue.append(activity_kafka) # Only create committer activity if author and committer are different + committer_email = committer_email.strip() if committer_email else "" if author_name != committer_name or author_email != committer_email: if not committer_email: self.logger.warning( @@ -601,7 +603,8 @@ def create_activities_from_commit( for extracted_activity in extracted_activities: activity_type, member_data = list(extracted_activity.items())[0] - if not member_data.get("email"): + trailer_email = (member_data.get("email") or "").strip() + if not trailer_email: self.logger.warning( f"Skipping {activity_type} for {commit_hash} — empty email in commit trailer" ) @@ -613,12 +616,12 @@ def create_activities_from_commit( member = { "displayName": member_data["name"], - "emails": [member_data["email"]], + "emails": [trailer_email], } # Generate unique source ID for extracted activity source_id = hashlib.sha1( - (commit_hash + activity_type + member_data["email"]).encode("utf-8") + (commit_hash + activity_type + trailer_email).encode("utf-8") ).hexdigest() activity = self.create_activity( remote=remote, From 5a125467f85a4a3dd4296b97af1ae15865655494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 15:25:08 +0200 Subject: [PATCH 09/11] fix: also add the email identity as username identity if no username identity is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../apps/data_sink_worker/src/service/activity.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index aeacf784de..ad9bf31740 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -342,6 +342,12 @@ export default class ActivityService extends LoggerBase { ) if (emailFallback) { activity.username = emailFallback.value + activity.member.identities.push({ + platform, + type: MemberIdentityType.USERNAME, + value: emailFallback.value, + verified: emailFallback.verified, + }) } else { // No usable identity at all (e.g. git commit with empty author email). // Nothing to attribute — skip silently rather than error. From 468aeb0ac032b9f4f30d495330cd856773aef3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 15:53:12 +0200 Subject: [PATCH 10/11] fix: also use the same source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- services/apps/data_sink_worker/src/service/activity.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index ad9bf31740..da8b639042 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -347,6 +347,7 @@ export default class ActivityService extends LoggerBase { type: MemberIdentityType.USERNAME, value: emailFallback.value, verified: emailFallback.verified, + source: emailFallback.source, }) } else { // No usable identity at all (e.g. git commit with empty author email). From 5ac35cc5d3e51ea4cc600b75fbec913b5ff7be0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uro=C5=A1=20Marolt?= Date: Wed, 6 May 2026 16:17:56 +0200 Subject: [PATCH 11/11] fix: if there is no verified email or username we throw an unrepeatable error for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Uroš Marolt --- .../src/service/activity.service.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/services/apps/data_sink_worker/src/service/activity.service.ts b/services/apps/data_sink_worker/src/service/activity.service.ts index da8b639042..d20a5b7309 100644 --- a/services/apps/data_sink_worker/src/service/activity.service.ts +++ b/services/apps/data_sink_worker/src/service/activity.service.ts @@ -340,15 +340,24 @@ export default class ActivityService extends LoggerBase { const emailFallback = activity.member.identities.find( (i) => i.platform === platform && i.type === MemberIdentityType.EMAIL && i.value, ) - if (emailFallback) { + if (emailFallback && emailFallback.verified) { activity.username = emailFallback.value activity.member.identities.push({ platform, type: MemberIdentityType.USERNAME, value: emailFallback.value, - verified: emailFallback.verified, + verified: true, source: emailFallback.source, }) + } else if (emailFallback) { + // Email identity exists but is unverified — cannot safely use as username key. + results.set(resultId, { + success: false, + err: new UnrepeatableError( + `Activity's member has no verified username or email identity for platform: ${platform}!`, + ), + }) + continue } else { // No usable identity at all (e.g. git commit with empty author email). // Nothing to attribute — skip silently rather than error.