diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 243789f7c2e878..64251564c8f4fd 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -31,7 +31,7 @@ releases: 0004_cleanup_failed_safe_deletes replays: 0007_organizationmember_replay_access -sentry: 1015_backfill_self_hosted_sentry_app_emails +sentry: 1016_backfill_on_command_phrase_trigger social_auth: 0003_social_auth_json_field diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index a14188f27ba140..c0276951b33478 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -97,6 +97,7 @@ from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmember import OrganizationMember from sentry.models.project import Project +from sentry.models.repositorysettings import CodeReviewTrigger from sentry.organizations.services.organization import organization_service from sentry.organizations.services.organization.model import ( RpcOrganization, @@ -401,6 +402,15 @@ def validate_safeFields(self, value): raise serializers.ValidationError("Empty values are not allowed.") return validate_pii_selectors(value) + def validate_defaultCodeReviewTriggers(self, value): + # Ensure ON_COMMAND_PHRASE is always a default code review trigger + if value is not None: + triggers = list(value) + if CodeReviewTrigger.ON_COMMAND_PHRASE not in triggers: + triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE) + value = triggers + return value + def validate_attachmentsRole(self, value): try: roles.get(value) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_settings.py b/src/sentry/integrations/api/endpoints/organization_repository_settings.py index cd9077e3756d38..acf40cdb23363a 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_settings.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_settings.py @@ -73,6 +73,10 @@ def put(self, request: Request, organization: Organization) -> Response: if updated_code_review_triggers is not None: update_fields.append("code_review_triggers") + # Ensure ON_COMMAND_PHRASE is always a trigger + if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in updated_code_review_triggers: + updated_code_review_triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) + repositories = list( Repository.objects.filter( id__in=repository_ids, @@ -93,7 +97,9 @@ def put(self, request: Request, organization: Organization) -> Response: settings_to_upsert = [] for repo in repositories: - setting = existing_settings.get(repo.id) or RepositorySettings(repository=repo) + setting = existing_settings.get(repo.id) or RepositorySettings( + repository=repo, code_review_triggers=[CodeReviewTrigger.ON_COMMAND_PHRASE.value] + ) if updated_enabled_code_review is not None: setting.enabled_code_review = updated_enabled_code_review diff --git a/src/sentry/migrations/1016_backfill_on_command_phrase_trigger.py b/src/sentry/migrations/1016_backfill_on_command_phrase_trigger.py new file mode 100644 index 00000000000000..b43dd63c7c2201 --- /dev/null +++ b/src/sentry/migrations/1016_backfill_on_command_phrase_trigger.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.8 on 2025-12-19 23:11 + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps + +from sentry.new_migrations.migrations import CheckedMigration + + +def backfill_on_command_phrase_trigger( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + """Backfill on_command_phrase code review trigger for all existing RepositorySettings and Organization defaultCodeReviewTriggers.""" + RepositorySettings = apps.get_model("sentry", "RepositorySettings") + OrganizationOption = apps.get_model("sentry", "OrganizationOption") + + # Backfill RepositorySettings + settings_to_update = [] + + for setting in RepositorySettings.objects.all(): + triggers = list(setting.code_review_triggers or []) + if "on_command_phrase" not in triggers: + setting.code_review_triggers = triggers + ["on_command_phrase"] + settings_to_update.append(setting) + + if settings_to_update: + RepositorySettings.objects.bulk_update(settings_to_update, ["code_review_triggers"]) + + # Backfill Organization defaultCodeReviewTriggers + org_options_to_update = [] + + for org_option in OrganizationOption.objects.filter(key="sentry:default_code_review_triggers"): + triggers = list(org_option.value or []) + if "on_command_phrase" not in triggers: + org_option.value = triggers + ["on_command_phrase"] + org_options_to_update.append(org_option) + + if org_options_to_update: + OrganizationOption.objects.bulk_update(org_options_to_update, ["value"]) + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = True + + dependencies = [ + ("sentry", "1015_backfill_self_hosted_sentry_app_emails"), + ] + + operations = [ + migrations.RunPython( + backfill_on_command_phrase_trigger, + reverse_code=migrations.RunPython.noop, + hints={"tables": ["sentry_repositorysettings", "sentry_organizationoptions"]}, + ), + ] diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 5a80c398cf21ab..4150236f734e97 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from enum import StrEnum +from typing import Any from django.contrib.postgres.fields.array import ArrayField from django.db import models @@ -51,6 +52,15 @@ class Meta: __repr__ = sane_repr("repository_id", "enabled_code_review") + def save(self, *args: Any, **kwargs: Any) -> None: + # Ensure ON_COMMAND_PHRASE is always a trigger + triggers = list(self.code_review_triggers or []) + if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in triggers: + triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) + self.code_review_triggers = triggers + + super().save(*args, **kwargs) + def get_code_review_settings(self) -> CodeReviewSettings: """Return code review settings for this repository.""" triggers = [CodeReviewTrigger(t) for t in self.code_review_triggers] diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index a152d5ad06291e..ef7e19a497be31 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -32,6 +32,7 @@ from sentry.models.organization import Organization, OrganizationStatus from sentry.models.organizationmapping import OrganizationMapping from sentry.models.organizationslugreservation import OrganizationSlugReservation +from sentry.models.repositorysettings import CodeReviewTrigger from sentry.replays.models import OrganizationMemberReplayAccess from sentry.signals import project_created from sentry.silo.safety import unguarded_write @@ -1309,6 +1310,33 @@ def test_default_seer_scanner_automation(self) -> None: self.get_success_response(self.organization.slug, **data) assert self.organization.get_option("sentry:default_seer_scanner_automation") is True + def test_default_code_review_triggers_adds_on_command_phrase(self) -> None: + data: dict[str, list[CodeReviewTrigger]] = {"defaultCodeReviewTriggers": []} + self.get_success_response(self.organization.slug, **data) + assert self.organization.get_option("sentry:default_code_review_triggers") == [ + CodeReviewTrigger.ON_COMMAND_PHRASE + ] + + data = {"defaultCodeReviewTriggers": [CodeReviewTrigger.ON_READY_FOR_REVIEW]} + self.get_success_response(self.organization.slug, **data) + assert self.organization.get_option("sentry:default_code_review_triggers") == [ + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] + + def test_default_code_review_triggers_preserves_on_command_phrase(self) -> None: + data = { + "defaultCodeReviewTriggers": [ + CodeReviewTrigger.ON_COMMAND_PHRASE, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + ] + } + self.get_success_response(self.organization.slug, **data) + assert self.organization.get_option("sentry:default_code_review_triggers") == [ + CodeReviewTrigger.ON_COMMAND_PHRASE, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + ] + def test_enabled_console_platforms_present_in_response(self) -> None: response = self.get_success_response(self.organization.slug) assert "enabledConsolePlatforms" in response.data diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repositories.py b/tests/sentry/integrations/api/endpoints/test_organization_repositories.py index 5b80d153778fbf..c5e38d382d59f6 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repositories.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repositories.py @@ -305,6 +305,7 @@ def test_expand_settings_with_settings(self) -> None: assert response.data[0]["settings"]["codeReviewTriggers"] == [ "on_new_commit", "on_ready_for_review", + "on_command_phrase", ] def test_expand_settings_without_settings(self) -> None: diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py b/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py index 1d618dd9473e6f..98e5a8dd0a301f 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py @@ -71,6 +71,7 @@ def test_get_repository_expand_settings(self) -> None: assert response.data["settings"]["codeReviewTriggers"] == [ "on_new_commit", "on_ready_for_review", + "on_command_phrase", ] def test_get_repository_expand_settings_no_settings_exist(self) -> None: diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py b/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py index 5c0ceb7e04deaa..2231275a5c59e2 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py @@ -27,7 +27,6 @@ def test_bulk_create_settings(self) -> None: "enabledCodeReview": True, "codeReviewTriggers": [ CodeReviewTrigger.ON_NEW_COMMIT, - CodeReviewTrigger.ON_READY_FOR_REVIEW, ], }, format="json", @@ -38,11 +37,16 @@ def test_bulk_create_settings(self) -> None: settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is True - assert settings1.code_review_triggers == ["on_new_commit", "on_ready_for_review"] - + assert settings1.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is True - assert settings2.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + assert settings2.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] for repo_data in response.data: assert "settings" in repo_data @@ -55,7 +59,7 @@ def test_bulk_update_existing_settings(self) -> None: self.create_repository_settings( repository=repo1, enabled_code_review=False, - code_review_triggers=[CodeReviewTrigger.ON_COMMAND_PHRASE], + code_review_triggers=[CodeReviewTrigger.ON_READY_FOR_REVIEW], ) self.create_repository_settings( repository=repo2, @@ -77,11 +81,17 @@ def test_bulk_update_existing_settings(self) -> None: settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is True - assert settings1.code_review_triggers == ["on_new_commit"] + assert settings1.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is True - assert settings2.code_review_triggers == ["on_new_commit"] + assert settings2.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] def test_repository_not_found(self) -> None: response = self.client.put( @@ -168,7 +178,7 @@ def test_partial_bulk_update_enabled_code_review_preserves_triggers(self) -> Non repository=repo1, enabled_code_review=False, code_review_triggers=[ - CodeReviewTrigger.ON_COMMAND_PHRASE, + CodeReviewTrigger.ON_READY_FOR_REVIEW, CodeReviewTrigger.ON_NEW_COMMIT, ], ) @@ -191,11 +201,15 @@ def test_partial_bulk_update_enabled_code_review_preserves_triggers(self) -> Non settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is True - assert settings1.code_review_triggers == ["on_command_phrase", "on_new_commit"] + assert settings1.code_review_triggers == [ + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is True - assert settings2.code_review_triggers == [] + assert settings2.code_review_triggers == [CodeReviewTrigger.ON_COMMAND_PHRASE] def test_partial_bulk_update_code_review_triggers_preserves_enabled(self) -> None: repo1 = Repository.objects.create(name="repo1", organization_id=self.org.id) @@ -204,7 +218,7 @@ def test_partial_bulk_update_code_review_triggers_preserves_enabled(self) -> Non self.create_repository_settings( repository=repo1, enabled_code_review=True, - code_review_triggers=[CodeReviewTrigger.ON_COMMAND_PHRASE], + code_review_triggers=[CodeReviewTrigger.ON_NEW_COMMIT], ) self.create_repository_settings( repository=repo2, @@ -228,13 +242,21 @@ def test_partial_bulk_update_code_review_triggers_preserves_enabled(self) -> Non settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is True - assert settings1.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + assert settings1.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is False - assert settings2.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + assert settings2.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] - def test_partial_bulk_create_enabled_code_review_uses_default(self) -> None: + def test_partial_bulk_create_code_review_triggers_uses_default(self) -> None: repo1 = Repository.objects.create(name="repo1", organization_id=self.org.id) repo2 = Repository.objects.create(name="repo2", organization_id=self.org.id) @@ -251,13 +273,13 @@ def test_partial_bulk_create_enabled_code_review_uses_default(self) -> None: settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is True - assert settings1.code_review_triggers == [] + assert settings1.code_review_triggers == [CodeReviewTrigger.ON_COMMAND_PHRASE] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is True - assert settings2.code_review_triggers == [] + assert settings2.code_review_triggers == [CodeReviewTrigger.ON_COMMAND_PHRASE] - def test_partial_bulk_create_code_review_triggers_uses_default(self) -> None: + def test_partial_bulk_create_enabled_code_review_uses_default(self) -> None: repo1 = Repository.objects.create(name="repo1", organization_id=self.org.id) repo2 = Repository.objects.create(name="repo2", organization_id=self.org.id) @@ -277,11 +299,19 @@ def test_partial_bulk_create_code_review_triggers_uses_default(self) -> None: settings1 = RepositorySettings.objects.get(repository=repo1) assert settings1.enabled_code_review is False - assert settings1.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + assert settings1.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is False - assert settings2.code_review_triggers == ["on_new_commit", "on_ready_for_review"] + assert settings2.code_review_triggers == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_READY_FOR_REVIEW, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] def test_audit_log_created_on_update(self) -> None: repo1 = Repository.objects.create(name="repo1", organization_id=self.org.id) @@ -309,4 +339,7 @@ def test_audit_log_created_on_update(self) -> None: assert audit_log.data["repository_count"] == 2 assert set(audit_log.data["repository_ids"]) == {repo1.id, repo2.id} assert audit_log.data["enabled_code_review"] is True - assert audit_log.data["code_review_triggers"] == [CodeReviewTrigger.ON_NEW_COMMIT] + assert audit_log.data["code_review_triggers"] == [ + CodeReviewTrigger.ON_NEW_COMMIT, + CodeReviewTrigger.ON_COMMAND_PHRASE, + ] diff --git a/tests/sentry/migrations/test_1016_backfill_on_command_phrase_trigger.py b/tests/sentry/migrations/test_1016_backfill_on_command_phrase_trigger.py new file mode 100644 index 00000000000000..b61b28f0e24748 --- /dev/null +++ b/tests/sentry/migrations/test_1016_backfill_on_command_phrase_trigger.py @@ -0,0 +1,113 @@ +import pytest + +from sentry.models.options.organization_option import OrganizationOption +from sentry.models.repository import Repository +from sentry.models.repositorysettings import RepositorySettings +from sentry.testutils.cases import TestMigrations + + +@pytest.mark.migrations +class BackfillOnCommandPhraseTriggerTest(TestMigrations): + migrate_from = "1015_backfill_self_hosted_sentry_app_emails" + migrate_to = "1016_backfill_on_command_phrase_trigger" + + def setup_initial_state(self) -> None: + self.org = self.create_organization() + + self.repo_empty = Repository.objects.create( + organization_id=self.org.id, + name="test-repo", + provider="integrations:github", + ) + + self.repo_with_triggers = Repository.objects.create( + organization_id=self.org.id, + name="test-repo-2", + provider="integrations:github", + ) + + self.repo_already_has = Repository.objects.create( + organization_id=self.org.id, + name="test-repo-3", + provider="integrations:github", + ) + + def setup_before_migration(self, apps) -> None: + RepositorySettings = apps.get_model("sentry", "RepositorySettings") + OrganizationOption = apps.get_model("sentry", "OrganizationOption") + + self.setting_empty = RepositorySettings.objects.create( + repository_id=self.repo_empty.id, + enabled_code_review=True, + code_review_triggers=[], + ) + + self.setting_with_triggers = RepositorySettings.objects.create( + repository_id=self.repo_with_triggers.id, + enabled_code_review=True, + code_review_triggers=["on_new_commit", "on_ready_for_review"], + ) + + self.setting_already_has = RepositorySettings.objects.create( + repository_id=self.repo_already_has.id, + enabled_code_review=True, + code_review_triggers=["on_command_phrase", "on_new_commit"], + ) + + self.org_option_empty = OrganizationOption.objects.create( + organization_id=self.org.id, + key="sentry:default_code_review_triggers", + value=[], + ) + + self.org_option_with_triggers = OrganizationOption.objects.create( + organization_id=self.create_organization().id, + key="sentry:default_code_review_triggers", + value=["on_new_commit", "on_ready_for_review"], + ) + + self.org_option_already_has = OrganizationOption.objects.create( + organization_id=self.create_organization().id, + key="sentry:default_code_review_triggers", + value=["on_command_phrase", "on_new_commit"], + ) + + def test_backfills_on_command_phrase_trigger(self) -> None: + # Test repository settings backfill + setting_empty = RepositorySettings.objects.get(id=self.setting_empty.id) + assert set(setting_empty.code_review_triggers) == {"on_command_phrase"} + assert len(setting_empty.code_review_triggers) == 1 + + setting_with_triggers = RepositorySettings.objects.get(id=self.setting_with_triggers.id) + assert set(setting_with_triggers.code_review_triggers) == { + "on_command_phrase", + "on_new_commit", + "on_ready_for_review", + } + assert len(setting_with_triggers.code_review_triggers) == 3 + + setting_already_has = RepositorySettings.objects.get(id=self.setting_already_has.id) + assert setting_already_has.code_review_triggers.count("on_command_phrase") == 1 + assert set(setting_already_has.code_review_triggers) == { + "on_command_phrase", + "on_new_commit", + } + + # Test organization options backfill + org_option_empty = OrganizationOption.objects.get(id=self.org_option_empty.id) + assert org_option_empty.value == ["on_command_phrase"] + + org_option_with_triggers = OrganizationOption.objects.get( + id=self.org_option_with_triggers.id + ) + assert org_option_with_triggers.value == [ + "on_new_commit", + "on_ready_for_review", + "on_command_phrase", + ] + + org_option_already_has = OrganizationOption.objects.get(id=self.org_option_already_has.id) + assert org_option_already_has.value == [ + "on_command_phrase", + "on_new_commit", + ] diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index 21038798d7ceba..30a4906dd20e0b 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -6,7 +6,7 @@ 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.models.repositorysettings import CodeReviewTrigger, RepositorySettings from sentry.plugins.providers.dummy import DummyRepositoryProvider from sentry.testutils.cases import TestCase from sentry.testutils.helpers.features import with_feature @@ -129,7 +129,11 @@ def test_settings_created_with_triggers(self): 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"] + assert settings.code_review_triggers == [ + "on_new_commit", + "on_ready_for_review", + "on_command_phrase", + ] def test_no_settings_for_unsupported_provider(self): org = self.create_organization() @@ -241,3 +245,100 @@ def test_both_repository_and_settings_saved_atomically(self): # Verify the settings are correct settings = RepositorySettings.objects.get(repository=repo) assert settings.enabled_code_review is True + + def test_save_adds_on_command_phrase_when_triggers_empty(self): + org = self.create_organization() + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = self.create_repository_settings( + repository=repo, + enabled_code_review=True, + code_review_triggers=[], + ) + + settings.refresh_from_db() + assert settings.code_review_triggers == [CodeReviewTrigger.ON_COMMAND_PHRASE.value] + + def test_save_adds_on_command_phrase_when_missing(self): + org = self.create_organization() + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = self.create_repository_settings( + repository=repo, + enabled_code_review=True, + code_review_triggers=[ + CodeReviewTrigger.ON_NEW_COMMIT.value, + CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + ], + ) + + settings.refresh_from_db() + assert set(settings.code_review_triggers) == { + CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, + CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + } + assert len(settings.code_review_triggers) == 3 + + def test_save_does_not_duplicate_on_command_phrase(self): + org = self.create_organization() + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = self.create_repository_settings( + repository=repo, + enabled_code_review=True, + code_review_triggers=[ + CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, + ], + ) + + settings.refresh_from_db() + assert set(settings.code_review_triggers) == { + CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, + } + assert len(settings.code_review_triggers) == 2 + + def test_save_enforces_on_command_phrase_on_update(self): + org = self.create_organization() + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = self.create_repository_settings( + repository=repo, + enabled_code_review=True, + code_review_triggers=[ + CodeReviewTrigger.ON_NEW_COMMIT.value, + CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + ], + ) + + settings.code_review_triggers = [ + CodeReviewTrigger.ON_NEW_COMMIT.value, + CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + ] + settings.save() + + settings.refresh_from_db() + assert set(settings.code_review_triggers) == { + CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, + CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + } + assert len(settings.code_review_triggers) == 3 diff --git a/tests/sentry/models/test_repositorysettings.py b/tests/sentry/models/test_repositorysettings.py index f1bc181ace2fc1..4fcefa9f77520b 100644 --- a/tests/sentry/models/test_repositorysettings.py +++ b/tests/sentry/models/test_repositorysettings.py @@ -31,7 +31,7 @@ def test_get_code_review_settings_with_defaults(self) -> None: settings = repo_settings.get_code_review_settings() assert settings.enabled is False - assert settings.triggers == [] + assert settings.triggers == [CodeReviewTrigger.ON_COMMAND_PHRASE] def test_get_code_review_settings_with_enabled_and_triggers(self) -> None: repo_settings = RepositorySettings.objects.create( @@ -59,7 +59,7 @@ def test_get_code_review_settings_converts_string_triggers_to_enum(self) -> None settings = repo_settings.get_code_review_settings() - assert settings.triggers == [CodeReviewTrigger.ON_NEW_COMMIT] + assert CodeReviewTrigger.ON_NEW_COMMIT in settings.triggers assert isinstance(settings.triggers[0], CodeReviewTrigger) def test_repository_settings_unique_per_repository(self) -> None: diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index bbd6a00c863195..814ee88cf0907b 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -717,7 +717,7 @@ def test_scopes_by_organization(self): assert resp1.status_code == 200 assert resp1.data == { "enabledCodeReview": True, - "codeReviewTriggers": ["on_new_commit"], + "codeReviewTriggers": ["on_new_commit", "on_command_phrase"], } # Request for org2 should return defaults (no settings created)