-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
🐛 fix[gitlab]: sync assignees by external id #115356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,11 @@ | |
|
|
||
| from sentry import features | ||
| from sentry.integrations.gitlab.types import GitLabIssueAction, GitLabIssueStatus | ||
| from sentry.integrations.mixins.issues import IssueSyncIntegration, ResolveSyncAction | ||
| from sentry.integrations.mixins.issues import ( | ||
| IntegrationSyncTargetNotFound, | ||
| IssueSyncIntegration, | ||
| ResolveSyncAction, | ||
| ) | ||
| from sentry.integrations.models.external_actor import ExternalActor | ||
| from sentry.integrations.models.external_issue import ExternalIssue | ||
| from sentry.integrations.models.integration_external_project import IntegrationExternalProject | ||
|
|
@@ -20,6 +24,11 @@ | |
| logger = logging.getLogger("sentry.integrations.gitlab.issue_sync") | ||
|
|
||
|
|
||
| def _add_lifecycle_extras(lifecycle: Any, extras: Mapping[str, Any]) -> None: | ||
| if lifecycle is not None and hasattr(lifecycle, "add_extras"): | ||
| lifecycle.add_extras(extras) | ||
|
|
||
|
|
||
| class GitlabIssueSyncSpec(IssueSyncIntegration): | ||
| comment_key = "sync_comments" | ||
| outbound_assignee_key = "sync_forward_assignment" | ||
|
|
@@ -70,20 +79,24 @@ def sync_assignee_outbound( | |
| If assign=True, we're assigning the issue. Otherwise, deassign. | ||
| """ | ||
| client = self.get_client() | ||
| lifecycle = kwargs.get("lifecycle") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. temporary shim so i can try to get some more context |
||
|
|
||
| project_id, issue_id = self.split_external_issue_key(external_issue.key) | ||
| log_context: dict[str, Any] = { | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "external_issue_key": external_issue.key, | ||
| "external_issue_id": external_issue.id, | ||
| "project_path": project_id, | ||
| "issue_iid": issue_id, | ||
| "assign": assign, | ||
| "user_id": user.id if user else None, | ||
| } | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
|
|
||
| if not project_id or not issue_id: | ||
| logger.error( | ||
| "assignee-outbound.invalid-key", | ||
| extra={ | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "external_issue_key": external_issue.key, | ||
| "external_issue_id": external_issue.id, | ||
| }, | ||
| ) | ||
| return | ||
| logger.warning("assignee-outbound.invalid-key", extra=log_context) | ||
| raise IntegrationSyncTargetNotFound("Invalid GitLab issue key") | ||
|
|
||
| gitlab_user_id = None | ||
|
|
||
|
|
@@ -98,57 +111,78 @@ def sync_assignee_outbound( | |
| organization=external_issue.organization, | ||
| ).first() | ||
| if not external_actor: | ||
| logger.info( | ||
| "assignee-outbound.external-actor-not-found", | ||
| extra={ | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "user_id": user.id, | ||
| }, | ||
| logger.info("assignee-outbound.external-actor-not-found", extra=log_context) | ||
| raise IntegrationSyncTargetNotFound("No matching GitLab external actor found") | ||
|
|
||
| log_context.update( | ||
| { | ||
| "external_actor_id": external_actor.id, | ||
| "external_actor_name": external_actor.external_name, | ||
| "external_actor_external_id": external_actor.external_id, | ||
| } | ||
| ) | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
|
|
||
| if external_actor.external_id: | ||
| try: | ||
| gitlab_user_id = int(external_actor.external_id) | ||
| except ValueError: | ||
| logger.warning("assignee-outbound.invalid-external-id", extra=log_context) | ||
|
Comment on lines
+126
to
+130
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This case will never execute. We don't set external ID for any of our GitLab mappings, so I'm not sure if this changes anything. |
||
| raise IntegrationSyncTargetNotFound("Invalid GitLab external actor ID") | ||
|
|
||
| log_context.update( | ||
| { | ||
| "assignee_resolution_method": "external_id", | ||
| "resolved_gitlab_user_id": gitlab_user_id, | ||
| } | ||
| ) | ||
| return | ||
| logger.info("assignee-outbound.gitlab-user-found", extra=log_context) | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
|
|
||
| # Strip the @ from the username stored in external_name | ||
| gitlab_username = external_actor.external_name.lstrip("@") | ||
|
|
||
| # Search for the GitLab user by username to get their user ID | ||
| try: | ||
| users = client.search_users(gitlab_username) | ||
| if not users or len(users) == 0: | ||
| logger.warning( | ||
| "assignee-outbound.gitlab-user-not-found", | ||
| extra={ | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "gitlab_username": gitlab_username, | ||
| }, | ||
| ) | ||
| return | ||
|
|
||
| # Take the first matching user (exact username match) | ||
| gitlab_user = users[0] | ||
| gitlab_user_id = gitlab_user["id"] | ||
|
|
||
| logger.info( | ||
| "assignee-outbound.gitlab-user-found", | ||
| extra={ | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "gitlab_username": gitlab_username, | ||
| "gitlab_user_id": gitlab_user_id, | ||
| }, | ||
| if gitlab_user_id is None: | ||
| gitlab_username = external_actor.external_name.lstrip("@") | ||
|
|
||
| # Search for the GitLab user by username to get their user ID | ||
| try: | ||
| users = client.search_users(gitlab_username) or [] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hand tested this API with my GitLab user, and this request should have returned a single user, so I can vouch that this is correct at least. Also, this API only returns a single username when a username is passed as query param, so the previous logic was slightly more correct: https://docs.gitlab.com/api/users/#as-a-regular-user |
||
| except ApiError as e: | ||
| log_context["error"] = str(e) | ||
| logger.warning("assignee-outbound.gitlab-user-search-failed", extra=log_context) | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
| raise IntegrationSyncTargetNotFound("GitLab user search failed") | ||
|
|
||
| usernames = [ | ||
| found_user.get("username") | ||
| for found_user in users[:5] | ||
| if found_user.get("username") | ||
| ] | ||
| log_context.update( | ||
| { | ||
| "assignee_resolution_method": "username_search", | ||
| "gitlab_search_result_count": len(users), | ||
| "gitlab_search_usernames": usernames, | ||
| } | ||
| ) | ||
| except ApiError as e: | ||
| logger.warning( | ||
| "assignee-outbound.gitlab-user-search-failed", | ||
| extra={ | ||
| "provider": self.model.provider, | ||
| "integration_id": external_issue.integration_id, | ||
| "gitlab_username": gitlab_username, | ||
| "error": str(e), | ||
| }, | ||
|
|
||
| gitlab_user = next( | ||
| ( | ||
| found_user | ||
| for found_user in users | ||
| if found_user.get("username", "").lower() == gitlab_username.lower() | ||
| ), | ||
| None, | ||
| ) | ||
| return | ||
| if gitlab_user is None: | ||
| logger.warning("assignee-outbound.gitlab-user-not-found", extra=log_context) | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
| raise IntegrationSyncTargetNotFound("No matching GitLab user found") | ||
|
|
||
| gitlab_user_id = gitlab_user["id"] | ||
| log_context["resolved_gitlab_user_id"] = gitlab_user_id | ||
|
|
||
| logger.info("assignee-outbound.gitlab-user-found", extra=log_context) | ||
| _add_lifecycle_extras(lifecycle, log_context) | ||
|
|
||
| # Update GitLab issue assignees | ||
| try: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
temp should be thrown away