Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions src/sentry/integrations/gitlab/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from urllib.parse import urlparse

from django import forms
from django.db import transaction
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase
from django.urls import reverse
Expand Down Expand Up @@ -355,30 +356,34 @@ def update_organization_config(self, data: MutableMapping[str, Any]) -> None:

data["sync_status_forward"] = bool(project_mappings)

IntegrationExternalProject.objects.filter(
organization_integration_id=self.org_integration.id
).delete()

for project_path, statuses in project_mappings.items():
# Validate status values
valid_statuses = {GitLabIssueStatus.OPENED.value, GitLabIssueStatus.CLOSED.value}
if statuses["on_resolve"] not in valid_statuses:
raise IntegrationError(
f"Invalid resolve status: {statuses['on_resolve']}. Must be 'opened' or 'closed'."
)
if statuses["on_unresolve"] not in valid_statuses:
raise IntegrationError(
f"Invalid unresolve status: {statuses['on_unresolve']}. Must be 'opened' or 'closed'."
with transaction.atomic():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atomic transaction targets wrong database connection

High Severity

The transaction.atomic() block defaults to the default database, but IntegrationExternalProject is a @control_silo_model routed to the control database. This mismatch means the delete and create operations are not atomic. If a create fails, the preceding delete won't roll back, leading to data loss. The router import is also missing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ece1db2. Configure here.

IntegrationExternalProject.objects.filter(
organization_integration_id=self.org_integration.id
).delete()

for project_path, statuses in project_mappings.items():
# Validate status values
valid_statuses = {
GitLabIssueStatus.OPENED.value,
GitLabIssueStatus.CLOSED.value,
}
if statuses["on_resolve"] not in valid_statuses:
raise IntegrationError(
f"Invalid resolve status: {statuses['on_resolve']}. Must be 'opened' or 'closed'."
)
if statuses["on_unresolve"] not in valid_statuses:
raise IntegrationError(
f"Invalid unresolve status: {statuses['on_unresolve']}. Must be 'opened' or 'closed'."
)

IntegrationExternalProject.objects.create(
organization_integration_id=self.org_integration.id,
external_id=project_path,
name=project_path,
resolved_status=statuses["on_resolve"],
unresolved_status=statuses["on_unresolve"],
)

IntegrationExternalProject.objects.create(
organization_integration_id=self.org_integration.id,
external_id=project_path,
name=project_path,
resolved_status=statuses["on_resolve"],
unresolved_status=statuses["on_unresolve"],
)

# Check webhook version BEFORE updating config to determine if migration is needed
current_webhook_version = config.get(GITLAB_WEBHOOK_VERSION_KEY, 0)

Expand Down
Loading