Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
76 changes: 64 additions & 12 deletions src/sentry/integrations/services/repository/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,37 @@ def disable_repositories_for_integration(
self, *, organization_id: int, integration_id: int, provider: str
) -> None:
with transaction.atomic(router.db_for_write(Repository)):
Repository.objects.filter(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
).update(status=ObjectStatus.DISABLED)
repos = list(
Repository.objects.filter(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
).values_list("id", "external_id", "provider")
)
repo_ids = [repo_id for repo_id, _, _ in repos]

if repo_ids:
Repository.objects.filter(id__in=repo_ids).update(status=ObjectStatus.DISABLED)

try:
organization = Organization.objects.get_from_cache(id=organization_id)
if features.has("organizations:seer-project-settings-dual-write", organization):
SeerProjectRepository.objects.filter(repository_id__in=repo_ids).delete()
except Organization.DoesNotExist:
pass

repos_to_clean = [
{"repo_external_id": external_id, "repo_provider": repo_provider}
for _, external_id, repo_provider in repos
if external_id and repo_provider
]
if repos_to_clean:
bulk_cleanup_seer_repository_preferences.apply_async(
kwargs={
"organization_id": organization_id,
"repos": repos_to_clean,
}
)

def disable_repositories_by_external_ids(
self,
Expand All @@ -147,13 +173,39 @@ def disable_repositories_by_external_ids(
external_ids: list[str],
) -> None:
with transaction.atomic(router.db_for_write(Repository)):
Repository.objects.filter(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
external_id__in=external_ids,
status=ObjectStatus.ACTIVE,
).update(status=ObjectStatus.DISABLED)
repos = list(
Repository.objects.filter(
organization_id=organization_id,
integration_id=integration_id,
provider=provider,
external_id__in=external_ids,
status=ObjectStatus.ACTIVE,
).values_list("id", "external_id", "provider")
)
repo_ids = [repo_id for repo_id, _, _ in repos]

if repo_ids:
Repository.objects.filter(id__in=repo_ids).update(status=ObjectStatus.DISABLED)

try:
organization = Organization.objects.get_from_cache(id=organization_id)
if features.has("organizations:seer-project-settings-dual-write", organization):
SeerProjectRepository.objects.filter(repository_id__in=repo_ids).delete()
except Organization.DoesNotExist:
pass

repos_to_clean = [
{"repo_external_id": external_id, "repo_provider": repo_provider}
for _, external_id, repo_provider in repos
if external_id and repo_provider
]
if repos_to_clean:
bulk_cleanup_seer_repository_preferences.apply_async(
kwargs={
"organization_id": organization_id,
"repos": repos_to_clean,
}
)

def disassociate_organization_integration(
self,
Expand Down
3 changes: 2 additions & 1 deletion tests/sentry/integrations/github/test_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,8 @@ def test_end_to_end_repos_added(self) -> None:
assert repos[0].provider == "integrations:github"
assert repos[1].name == "getsentry/snuba"

def test_end_to_end_repos_removed(self) -> None:
@patch("sentry.integrations.services.repository.impl.bulk_cleanup_seer_repository_preferences")
def test_end_to_end_repos_removed(self, mock_seer_cleanup: MagicMock) -> None:
"""Full end-to-end: webhook URL → handler → task → Repository disabled."""
future_expires = datetime.now().replace(microsecond=0) + timedelta(minutes=5)
integration = self.create_integration(
Expand Down
91 changes: 91 additions & 0 deletions tests/sentry/integrations/services/repository/test_impl.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from unittest.mock import MagicMock, patch

from sentry.constants import ObjectStatus
from sentry.integrations.services.repository.service import repository_service
from sentry.models.repository import Repository
from sentry.seer.models.project_repository import SeerProjectRepository
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.silo import cell_silo_test


Expand Down Expand Up @@ -160,3 +164,90 @@ def test_empty_external_ids_is_noop(self) -> None:

repo.refresh_from_db()
assert repo.status == ObjectStatus.ACTIVE

@with_feature("organizations:seer-project-settings-dual-write")
@patch("sentry.integrations.services.repository.impl.bulk_cleanup_seer_repository_preferences")
def test_cleans_up_seer_preferences(self, mock_cleanup: MagicMock) -> None:
project = self.create_project(organization=self.organization)
repo = Repository.objects.create(
organization_id=self.organization.id,
name="getsentry/sentry",
external_id="100",
provider=self.provider,
integration_id=self.integration.id,
status=ObjectStatus.ACTIVE,
)
SeerProjectRepository.objects.create(project=project, repository_id=repo.id)

repository_service.disable_repositories_by_external_ids(
organization_id=self.organization.id,
integration_id=self.integration.id,
provider=self.provider,
external_ids=["100"],
)

assert not SeerProjectRepository.objects.filter(repository_id=repo.id).exists()
mock_cleanup.apply_async.assert_called_once_with(
kwargs={
"organization_id": self.organization.id,
"repos": [{"repo_external_id": "100", "repo_provider": self.provider}],
}
)


@cell_silo_test
class DisableRepositoriesForIntegrationTest(TestCase):
def setUp(self) -> None:
self.integration = self.create_integration(
organization=self.organization,
external_id="1",
provider="github",
)
self.provider = "integrations:github"

def test_disables_matching_active_repos(self) -> None:
repo = Repository.objects.create(
organization_id=self.organization.id,
name="getsentry/sentry",
external_id="100",
provider=self.provider,
integration_id=self.integration.id,
status=ObjectStatus.ACTIVE,
)

repository_service.disable_repositories_for_integration(
organization_id=self.organization.id,
integration_id=self.integration.id,
provider=self.provider,
)

repo.refresh_from_db()
assert repo.status == ObjectStatus.DISABLED

@with_feature("organizations:seer-project-settings-dual-write")
@patch("sentry.integrations.services.repository.impl.bulk_cleanup_seer_repository_preferences")
def test_cleans_up_seer_preferences(self, mock_cleanup: MagicMock) -> None:
project = self.create_project(organization=self.organization)
repo = Repository.objects.create(
organization_id=self.organization.id,
name="getsentry/sentry",
external_id="100",
provider=self.provider,
integration_id=self.integration.id,
status=ObjectStatus.ACTIVE,
)
SeerProjectRepository.objects.create(project=project, repository_id=repo.id)

repository_service.disable_repositories_for_integration(
organization_id=self.organization.id,
integration_id=self.integration.id,
provider=self.provider,
)

assert not SeerProjectRepository.objects.filter(repository_id=repo.id).exists()
mock_cleanup.apply_async.assert_called_once_with(
kwargs={
"organization_id": self.organization.id,
"repos": [{"repo_external_id": "100", "repo_provider": self.provider}],
}
)
Loading