diff --git a/src/sentry/models/repository.py b/src/sentry/models/repository.py index 9bdf41f3cd9574..d609af39363baa 100644 --- a/src/sentry/models/repository.py +++ b/src/sentry/models/repository.py @@ -3,14 +3,14 @@ from typing import Any from django.contrib.postgres.fields.array import ArrayField -from django.db import models +from django.db import models, router, transaction from django.db.models.signals import pre_delete from django.utils import timezone from sentry.backup.dependencies import NormalizedModelName, get_model_name from sentry.backup.sanitize import SanitizableField, Sanitizer from sentry.backup.scopes import RelocationScope -from sentry.constants import ObjectStatus +from sentry.constants import DEFAULT_CODE_REVIEW_TRIGGERS, ObjectStatus from sentry.db.models import ( BoundedBigIntegerField, BoundedPositiveIntegerField, @@ -24,6 +24,8 @@ rename_on_pending_deletion, reset_pending_deletion_field_names, ) +from sentry.models.options.organization_option import OrganizationOption +from sentry.models.repositorysettings import RepositorySettings from sentry.organizations.services.organization.service import organization_service from sentry.signals import pending_delete from sentry.users.services.user import RpcUser @@ -147,6 +149,40 @@ def sanitize_relocation_json( sanitizer.set_string(json, SanitizableField(model_name, "provider")) json["fields"]["languages"] = "[]" + def save(self, *args: Any, **kwargs: Any) -> None: + is_new = self.pk is None + with transaction.atomic(router.db_for_write(Repository)): + super().save(*args, **kwargs) + if is_new: + self._handle_auto_enable_code_review() + + def _handle_auto_enable_code_review(self) -> None: + """ + When a new repository is created, auto enable code review if applicable. + """ + SUPPORTED_PROVIDERS = {"integrations:github"} + + if self.provider not in SUPPORTED_PROVIDERS: + return + + if OrganizationOption.objects.get_value( + organization=self.organization_id, + key="sentry:auto_enable_code_review", + default=False, + ): + triggers = OrganizationOption.objects.get_value( + organization=self.organization_id, + key="sentry:default_code_review_triggers", + default=DEFAULT_CODE_REVIEW_TRIGGERS, + ) + if not isinstance(triggers, list): + triggers = DEFAULT_CODE_REVIEW_TRIGGERS + + RepositorySettings.objects.get_or_create( + repository_id=self.id, + defaults={"enabled_code_review": True, "code_review_triggers": triggers}, + ) + def on_delete(instance, actor: RpcUser | None = None, **kwargs): """ diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index c1c7f5d6b2c0e2..70dae05805b801 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -1,6 +1,12 @@ +from unittest.mock import patch + +import pytest from django.core import mail +from sentry.constants import DEFAULT_CODE_REVIEW_TRIGGERS +from sentry.models.options.organization_option import OrganizationOption from sentry.models.repository import Repository +from sentry.models.repositorysettings import RepositorySettings from sentry.plugins.providers.dummy import DummyRepositoryProvider from sentry.testutils.cases import TestCase from sentry.testutils.helpers.features import with_feature @@ -64,3 +70,172 @@ def test_generate_delete_fail_email(self) -> None: assert msg.context["repo"] == self.repo assert msg.context["error_message"] == "Test error message" assert msg.context["provider_name"] == "Example" + + +class RepositoryCodeReviewSettingsTest(TestCase): + """Tests for auto-enabling code review settings on repository creation.""" + + def test_no_settings_created_when_auto_enable_disabled(self): + org = self.create_organization() + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + assert not RepositorySettings.objects.filter(repository=repo).exists() + + def test_settings_created_when_auto_enable_enabled(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = RepositorySettings.objects.get(repository=repo) + assert settings.enabled_code_review is True + assert settings.code_review_triggers == DEFAULT_CODE_REVIEW_TRIGGERS + + def test_settings_created_with_triggers(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + OrganizationOption.objects.set_value( + organization=org, + key="sentry:default_code_review_triggers", + value=["on_new_commit", "on_ready_for_review"], + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = RepositorySettings.objects.get(repository=repo) + assert settings.enabled_code_review is True + assert settings.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + + def test_no_settings_for_unsupported_provider(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="unsupported_provider", + ) + + assert not RepositorySettings.objects.filter(repository=repo).exists() + + def test_invalid_triggers_type_defaults_to_empty_list(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + # Set invalid triggers type (string instead of list) + OrganizationOption.objects.set_value( + organization=org, + key="sentry:default_code_review_triggers", + value="invalid_string", + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = RepositorySettings.objects.get(repository=repo) + assert settings.code_review_triggers == DEFAULT_CODE_REVIEW_TRIGGERS + + def test_settings_not_duplicated_on_update(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + repo.name = "updated-repo" + repo.save() + + assert RepositorySettings.objects.filter(repository=repo).count() == 1 + + def test_transaction_rollback_on_auto_enable_failure(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + + initial_repo_count = Repository.objects.filter(organization_id=org.id).count() + + with patch.object( + Repository, + "_handle_auto_enable_code_review", + side_effect=Exception("Test exception"), + ): + with pytest.raises(Exception, match="Test exception"): + Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + # Neither Repository nor RepositorySettings should be saved due to transaction rollback + assert Repository.objects.filter(organization_id=org.id).count() == initial_repo_count + assert not RepositorySettings.objects.filter(repository__organization_id=org.id).exists() + + def test_both_repository_and_settings_saved_atomically(self): + org = self.create_organization() + + OrganizationOption.objects.set_value( + organization=org, + key="sentry:auto_enable_code_review", + value=True, + ) + + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + # Both should exist + assert Repository.objects.filter(id=repo.id).exists() + assert RepositorySettings.objects.filter(repository=repo).exists() + + # Verify the settings are correct + settings = RepositorySettings.objects.get(repository=repo) + assert settings.enabled_code_review is True