Skip to content

Commit 40b9255

Browse files
joanagmaiaclaude
andcommitted
fix: link email-only maintainers on incremental runs (DE-980)
Two related bugs caused email-only maintainers (e.g. Joshua Crofts in the linux MAINTAINERS file) to never get linked to memberIdentities after the first run for a repo: - compare_and_update_maintainers keyed both current and new maintainers by github_username. Extractions where the AI returns "unknown" (~3959 entries in the kernel file) collapse into a single dict slot, silently dropping the rest. Now keyed by (identityId, role), matching the unique index on maintainersInternal. - get_maintainers_for_repo filtered current maintainers to platform='github' AND type='username', excluding maintainers linked via email. The comparison set was therefore incomplete. Filter relaxed to only deletedAt IS NULL. Identity resolution is also unified: a single _resolve_identity helper falls back from github username to email when the username is missing or "unknown", so the email path is exercised on both first and incremental runs. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent 867c8ac commit 40b9255

2 files changed

Lines changed: 67 additions & 84 deletions

File tree

services/apps/git_integration/src/crowdgit/database/crud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ async def get_maintainers_for_repo(repo_id: str):
409409
SELECT mi.role, mi."originalRole", mi."repoUrl", mi."repoId", mi."identityId", mem.value as github_username
410410
FROM "maintainersInternal" mi
411411
JOIN "memberIdentities" mem ON mi."identityId" = mem.id
412-
WHERE mi."repoId" = $1 AND mem.platform = 'github' AND mem.type = 'username' and mem.verified = True AND mem."deletedAt" is null
412+
WHERE mi."repoId" = $1 AND mem."deletedAt" is null
413413
"""
414414
return await query(
415415
maintainers_sql_query,

services/apps/git_integration/src/crowdgit/services/maintainer/maintainer_service.py

Lines changed: 66 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -153,43 +153,51 @@ def make_role(self, title: str):
153153
)
154154
return slugify(title)
155155

156+
async def _resolve_identity(
157+
self, github_username: str | None, email: str | None
158+
) -> str | None:
159+
# Fall back from github username to email so a wrong/missing username
160+
# does not prevent an email match.
161+
if github_username and github_username != "unknown":
162+
identity_id = await find_github_identity(github_username)
163+
if identity_id:
164+
return identity_id
165+
if email and email != "unknown":
166+
return await find_maintainer_identity_by_email(email)
167+
return None
168+
169+
async def _resolve_maintainers(
170+
self, maintainers: list[MaintainerInfoItem]
171+
) -> list[tuple[MaintainerInfoItem, str]]:
172+
semaphore = asyncio.Semaphore(3)
173+
174+
async def resolve(m: MaintainerInfoItem) -> tuple[MaintainerInfoItem, str | None]:
175+
async with semaphore:
176+
identity_id = await self._resolve_identity(m.github_username, m.email)
177+
return m, identity_id
178+
179+
results = await asyncio.gather(*[resolve(m) for m in maintainers])
180+
181+
resolved: list[tuple[MaintainerInfoItem, str]] = []
182+
for m, identity_id in results:
183+
if identity_id is None:
184+
self.logger.warning(f"Identity not found for maintainer: {m}")
185+
continue
186+
resolved.append((m, identity_id))
187+
return resolved
188+
156189
async def insert_new_maintainers(
157190
self, repo_url: str, repo_id: str, maintainers: list[MaintainerInfoItem]
158191
):
159-
async def process_maintainer(maintainer: MaintainerInfoItem):
160-
self.logger.info(f"Processing maintainer: {maintainer.github_username}")
192+
resolved = await self._resolve_maintainers(maintainers)
193+
for maintainer, identity_id in resolved:
161194
role = maintainer.normalized_title
162195
original_role = self.make_role(maintainer.title)
163-
# Find the identity in the database
164-
github_username = maintainer.github_username
165-
email = maintainer.email
166-
167-
if github_username == "unknown" and email == "unknown":
168-
self.logger.warning("username & email with value 'unknown' aborting")
169-
return
170-
identity_id = (
171-
await find_github_identity(github_username)
172-
if github_username != "unknown"
173-
else await find_maintainer_identity_by_email(email)
174-
)
175-
self.logger.debug(
176-
f"Found identity_id for {github_username}: {identity_id} (type: {type(identity_id)})"
196+
await upsert_maintainer(repo_id, identity_id, repo_url, role, original_role)
197+
self.logger.info(
198+
f"Successfully upserted maintainer {maintainer.github_username} "
199+
f"with identity_id {identity_id}"
177200
)
178-
if identity_id:
179-
await upsert_maintainer(repo_id, identity_id, repo_url, role, original_role)
180-
self.logger.info(
181-
f"Successfully upserted maintainer {github_username} with identity_id {identity_id}"
182-
)
183-
else:
184-
self.logger.warning(f"Identity not found for GitHub user: {maintainer}")
185-
186-
semaphore = asyncio.Semaphore(3)
187-
188-
async def process_with_semaphore(maintainer: MaintainerInfoItem):
189-
async with semaphore:
190-
await process_maintainer(maintainer)
191-
192-
await asyncio.gather(*[process_with_semaphore(maintainer) for maintainer in maintainers])
193201

194202
async def compare_and_update_maintainers(
195203
self,
@@ -200,63 +208,38 @@ async def compare_and_update_maintainers(
200208
):
201209
self.logger.info(f"Comparing and updating maintainers for repo: {repo_id}")
202210
current_maintainers = await get_maintainers_for_repo(repo_id)
203-
current_maintainers_dict = {m["github_username"]: m for m in current_maintainers}
204-
new_maintainers_dict = {m.github_username: m for m in maintainers}
205211

206-
for github_username, maintainer in new_maintainers_dict.items():
207-
role = maintainer.normalized_title
208-
original_role = self.make_role(maintainer.title)
209-
if github_username == "unknown" and maintainer.email in ("unknown", None):
210-
self.logger.warning(
211-
f"Skipping unknown github_username & email with title {maintainer.title}"
212-
)
212+
# Key by (identityId, role) — the natural unique tuple for a linked maintainer.
213+
# Keying by github_username collapses every "unknown" extraction into one slot,
214+
# which silently drops most email-only maintainers (e.g. linux MAINTAINERS).
215+
current_by_key: dict[tuple[str, str], dict] = {
216+
(m["identityId"], m["role"]): m for m in current_maintainers
217+
}
218+
219+
resolved = await self._resolve_maintainers(maintainers)
220+
new_by_key: dict[tuple[str, str], MaintainerInfoItem] = {
221+
(identity_id, m.normalized_title): m for m, identity_id in resolved
222+
}
223+
224+
for (identity_id, role), maintainer in new_by_key.items():
225+
if (identity_id, role) in current_by_key:
213226
continue
214-
elif github_username not in current_maintainers_dict:
215-
# New maintainer
216-
identity_id = (
217-
await find_github_identity(github_username)
218-
if github_username != "unknown"
219-
else await find_maintainer_identity_by_email(maintainer.email)
220-
)
221-
self.logger.info(f"Found new maintainer {github_username} to be inserted")
222-
if identity_id:
223-
await upsert_maintainer(
224-
repo_id, identity_id, repo_url, role, original_role, start_date=change_date
225-
)
226-
self.logger.info(
227-
f"Successfully inserted new maintainer {github_username} with identity_id {identity_id}"
228-
)
229-
else:
230-
# will happen for new users if their identity isn't created yet but should be fixed on the next iteration
231-
self.logger.warning(f"Identity not found for username: {github_username}")
232-
else:
233-
# Existing maintainer
234-
current_maintainer = current_maintainers_dict[github_username]
235-
if current_maintainer["role"] != role:
236-
# Role has changed: we update maintainer
237-
self.logger.info(
238-
f"Role changed from {current_maintainer['role']} to {role} for maintainer {current_maintainer['identityId']}"
239-
)
240-
await upsert_maintainer(
241-
repo_id,
242-
current_maintainer["identityId"],
243-
repo_url,
244-
role,
245-
original_role,
246-
change_date,
247-
)
227+
original_role = self.make_role(maintainer.title)
228+
await upsert_maintainer(
229+
repo_id, identity_id, repo_url, role, original_role, start_date=change_date
230+
)
231+
self.logger.info(
232+
f"Inserted new maintainer {maintainer.github_username} "
233+
f"with identity_id {identity_id} role {role}"
234+
)
248235

249-
for github_username, current_maintainer in current_maintainers_dict.items():
250-
if github_username not in new_maintainers_dict:
236+
for identity_id, role in current_by_key:
237+
if (identity_id, role) not in new_by_key:
251238
self.logger.info(
252-
f"Maintainer {github_username} with identity {current_maintainer['identityId']} no longer exists, updating its endDate..."
253-
)
254-
await set_maintainer_end_date(
255-
repo_id,
256-
current_maintainer["identityId"],
257-
current_maintainer["role"],
258-
change_date,
239+
f"Maintainer with identity {identity_id} role {role} no longer exists, "
240+
f"updating its endDate..."
259241
)
242+
await set_maintainer_end_date(repo_id, identity_id, role, change_date)
260243

261244
async def save_maintainers(
262245
self,

0 commit comments

Comments
 (0)