diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index c26e1a5fff143a..1fe6fd448dd8e2 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_remove_on_command_phrase_trigger social_auth: 0003_social_auth_json_field diff --git a/src/sentry/apidocs/examples/organization_examples.py b/src/sentry/apidocs/examples/organization_examples.py index b274a069fd0cab..d9c7c23532b425 100644 --- a/src/sentry/apidocs/examples/organization_examples.py +++ b/src/sentry/apidocs/examples/organization_examples.py @@ -297,7 +297,6 @@ class OrganizationExamples: "defaultAutofixAutomationTuning": AutofixAutomationTuningSettings.OFF, "autoEnableCodeReview": True, "defaultCodeReviewTriggers": [ - "on_command_phrase", "on_ready_for_review", "on_new_commit", ], diff --git a/src/sentry/constants.py b/src/sentry/constants.py index 311eb6d840cf6e..b78bb4285f87e0 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -725,7 +725,6 @@ class InsightModules(Enum): AUTO_ENABLE_CODE_REVIEW = False # Seer Org level default for code review triggers DEFAULT_CODE_REVIEW_TRIGGERS: list[str] = [ - "on_command_phrase", "on_ready_for_review", "on_new_commit", ] diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index c0e31d74c65bd8..f32ac237878f0b 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -369,9 +369,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) defaultCodeReviewTriggers = serializers.ListField( - child=serializers.ChoiceField( - choices=["on_command_phrase", "on_ready_for_review", "on_new_commit"] - ), + child=serializers.ChoiceField(choices=["on_ready_for_review", "on_new_commit"]), required=False, allow_empty=True, help_text="The default code review triggers for new repositories.", diff --git a/src/sentry/migrations/1016_remove_on_command_phrase_trigger.py b/src/sentry/migrations/1016_remove_on_command_phrase_trigger.py new file mode 100644 index 00000000000000..269cb5b1146a70 --- /dev/null +++ b/src/sentry/migrations/1016_remove_on_command_phrase_trigger.py @@ -0,0 +1,76 @@ +# Generated by Django 5.2.8 on 2026-01-08 18:18 + +import django.contrib.postgres.fields +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps + +from sentry.new_migrations.migrations import CheckedMigration + + +def remove_on_command_phrase_from_repository_settings( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + """Remove 'on_command_phrase' from RepositorySettings.code_review_triggers.""" + RepositorySettings = apps.get_model("sentry", "RepositorySettings") + + all_settings = list(RepositorySettings.objects.all()) + + updated_settings = [] + for settings in all_settings: + if ( + settings.code_review_triggers + and isinstance(settings.code_review_triggers, list) + and "on_command_phrase" in settings.code_review_triggers + ): + settings.code_review_triggers = [ + trigger + for trigger in settings.code_review_triggers + if trigger != "on_command_phrase" + ] + updated_settings.append(settings) + + if updated_settings: + RepositorySettings.objects.bulk_update(updated_settings, fields=["code_review_triggers"]) + + +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 = False + + dependencies = [ + ("sentry", "1015_backfill_self_hosted_sentry_app_emails"), + ] + + operations = [ + migrations.RunPython( + remove_on_command_phrase_from_repository_settings, + reverse_code=migrations.RunPython.noop, + hints={"tables": ["sentry_repositorysettings"]}, + ), + migrations.AlterField( + model_name="repositorysettings", + name="code_review_triggers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("on_new_commit", "on_new_commit"), + ("on_ready_for_review", "on_ready_for_review"), + ], + max_length=32, + ), + default=list, + ), + ), + ] diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 5a80c398cf21ab..2a084031579d47 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -11,7 +11,6 @@ class CodeReviewTrigger(StrEnum): - ON_COMMAND_PHRASE = "on_command_phrase" ON_NEW_COMMIT = "on_new_commit" ON_READY_FOR_REVIEW = "on_ready_for_review" diff --git a/src/sentry/seer/code_review/utils.py b/src/sentry/seer/code_review/utils.py index d6194232c1b9fb..e1b159d94a1419 100644 --- a/src/sentry/seer/code_review/utils.py +++ b/src/sentry/seer/code_review/utils.py @@ -11,7 +11,6 @@ from sentry.integrations.services.integration.model import RpcIntegration from sentry.models.organization import Organization from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger from sentry.net.http import connection_from_url from sentry.seer.signed_seer_api import make_signed_seer_api_request @@ -27,6 +26,19 @@ class ClientError(Exception): pass +class SeerCodeReviewTrigger(StrEnum): + """ + Internal code review trigger type used for Seer flows. + + This includes all user-configurable CodeReviewTrigger values, plus on_command_phrase, + which is always enabled and cannot be turned off by users. + """ + + ON_COMMAND_PHRASE = "on_command_phrase" + ON_NEW_COMMIT = "on_new_commit" + ON_READY_FOR_REVIEW = "on_ready_for_review" + + # These values need to match the value defined in the Seer API. class SeerEndpoint(StrEnum): # https://github.com/getsentry/seer/blob/main/src/seer/routes/automation_request.py#L57 @@ -194,7 +206,7 @@ def transform_webhook_to_codegen_request( organization: Organization, repo: Repository, target_commit_sha: str, - trigger: CodeReviewTrigger, + trigger: SeerCodeReviewTrigger, ) -> dict[str, Any] | None: """ Transform a GitHub webhook payload into CodecovTaskRequest format for Seer. diff --git a/src/sentry/seer/code_review/webhooks/issue_comment.py b/src/sentry/seer/code_review/webhooks/issue_comment.py index f082168308f1fe..dce1e6e19813fc 100644 --- a/src/sentry/seer/code_review/webhooks/issue_comment.py +++ b/src/sentry/seer/code_review/webhooks/issue_comment.py @@ -14,7 +14,6 @@ from sentry.integrations.services.integration import RpcIntegration from sentry.models.organization import Organization from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger from ..metrics import ( CodeReviewErrorType, @@ -24,7 +23,7 @@ record_webhook_handler_error, record_webhook_received, ) -from ..utils import _get_target_commit_sha +from ..utils import SeerCodeReviewTrigger, _get_target_commit_sha from .config import get_direct_to_seer_gh_orgs logger = logging.getLogger(__name__) @@ -153,6 +152,6 @@ def handle_issue_comment_event( organization=organization, repo=repo, target_commit_sha=target_commit_sha, - trigger=CodeReviewTrigger.ON_COMMAND_PHRASE, + trigger=SeerCodeReviewTrigger.ON_COMMAND_PHRASE, ) record_webhook_enqueued(github_event, github_event_action) diff --git a/src/sentry/seer/code_review/webhooks/pull_request.py b/src/sentry/seer/code_review/webhooks/pull_request.py index 8ac9fcee62da41..63787d6a096768 100644 --- a/src/sentry/seer/code_review/webhooks/pull_request.py +++ b/src/sentry/seer/code_review/webhooks/pull_request.py @@ -14,7 +14,6 @@ from sentry.integrations.services.integration import RpcIntegration from sentry.models.organization import Organization from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger from ..metrics import ( CodeReviewErrorType, @@ -24,7 +23,7 @@ record_webhook_handler_error, record_webhook_received, ) -from ..utils import _get_target_commit_sha +from ..utils import SeerCodeReviewTrigger, _get_target_commit_sha from .config import get_direct_to_seer_gh_orgs logger = logging.getLogger(__name__) @@ -74,12 +73,12 @@ class PullRequestAction(enum.StrEnum): } -def _get_trigger_for_action(action: PullRequestAction) -> CodeReviewTrigger: +def _get_trigger_for_action(action: PullRequestAction) -> SeerCodeReviewTrigger: match action: case PullRequestAction.OPENED | PullRequestAction.READY_FOR_REVIEW: - return CodeReviewTrigger.ON_READY_FOR_REVIEW + return SeerCodeReviewTrigger.ON_READY_FOR_REVIEW case PullRequestAction.SYNCHRONIZE: - return CodeReviewTrigger.ON_NEW_COMMIT + return SeerCodeReviewTrigger.ON_NEW_COMMIT case _: raise ValueError(f"Unsupported pull request action: {action}") diff --git a/src/sentry/seer/code_review/webhooks/task.py b/src/sentry/seer/code_review/webhooks/task.py index 18bb34711dbf94..92fed2d9d319d0 100644 --- a/src/sentry/seer/code_review/webhooks/task.py +++ b/src/sentry/seer/code_review/webhooks/task.py @@ -11,7 +11,6 @@ from sentry.integrations.github.webhook_types import GithubWebhookType from sentry.models.organization import Organization from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger from sentry.seer.code_review.utils import ( get_webhook_option_key, transform_webhook_to_codegen_request, @@ -24,7 +23,7 @@ from sentry.utils import metrics from ..metrics import WebhookFilteredReason, record_webhook_filtered -from ..utils import get_seer_endpoint_for_event, make_seer_request +from ..utils import SeerCodeReviewTrigger, get_seer_endpoint_for_event, make_seer_request from .config import get_direct_to_seer_gh_orgs logger = logging.getLogger(__name__) @@ -44,7 +43,7 @@ def schedule_task( organization: Organization, repo: Repository, target_commit_sha: str, - trigger: CodeReviewTrigger, + trigger: SeerCodeReviewTrigger, ) -> None: """Transform and forward a webhook event to Seer for processing.""" from .task import process_github_webhook_event 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..ba1141c5637ca5 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_settings.py @@ -55,7 +55,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, @@ -168,7 +168,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,7 +191,7 @@ 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 == ["on_ready_for_review", "on_new_commit"] settings2 = RepositorySettings.objects.get(repository=repo2) assert settings2.enabled_code_review is True @@ -204,7 +204,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, diff --git a/tests/sentry/migrations/test_1016_remove_on_command_phrase_trigger.py b/tests/sentry/migrations/test_1016_remove_on_command_phrase_trigger.py new file mode 100644 index 00000000000000..d7db84110b764c --- /dev/null +++ b/tests/sentry/migrations/test_1016_remove_on_command_phrase_trigger.py @@ -0,0 +1,43 @@ +from sentry.models.repositorysettings import RepositorySettings +from sentry.testutils.cases import TestMigrations + + +class RemoveOnCommandPhraseTriggerTest(TestMigrations): + migrate_from = "1015_backfill_self_hosted_sentry_app_emails" + migrate_to = "1016_remove_on_command_phrase_trigger" + + def setup_initial_state(self) -> None: + org = self.create_organization() + self.project1 = self.create_project(organization=org) + self.repo1 = self.create_repo(project=self.project1, name="org/repo1") + self.repo_settings1 = RepositorySettings.objects.create( + repository=self.repo1, + enabled_code_review=True, + code_review_triggers=["on_command_phrase", "on_ready_for_review", "on_new_commit"], + ) + + self.project2 = self.create_project(organization=org) + self.repo2 = self.create_repo(project=self.project2, name="org/repo2") + self.repo_settings2 = RepositorySettings.objects.create( + repository=self.repo2, + enabled_code_review=True, + code_review_triggers=["on_ready_for_review", "on_new_commit"], + ) + + self.project3 = self.create_project(organization=org) + self.repo3 = self.create_repo(project=self.project3, name="org/repo3") + self.repo_settings3 = RepositorySettings.objects.create( + repository=self.repo3, enabled_code_review=True + ) + + def test(self) -> None: + repo_settings = RepositorySettings.objects.get(id=self.repo_settings1.id) + assert repo_settings.code_review_triggers == ["on_ready_for_review", "on_new_commit"] + assert "on_command_phrase" not in repo_settings.code_review_triggers + + repo_settings = RepositorySettings.objects.get(id=self.repo_settings2.id) + assert repo_settings.code_review_triggers == ["on_ready_for_review", "on_new_commit"] + assert "on_command_phrase" not in repo_settings.code_review_triggers + + repo_settings = RepositorySettings.objects.get(id=self.repo_settings3.id) + assert repo_settings.code_review_triggers == [] diff --git a/tests/sentry/models/test_repositorysettings.py b/tests/sentry/models/test_repositorysettings.py index f1bc181ace2fc1..a9af49f0a21af4 100644 --- a/tests/sentry/models/test_repositorysettings.py +++ b/tests/sentry/models/test_repositorysettings.py @@ -11,7 +11,7 @@ class TestCodeReviewSettings(TestCase): def test_initialization(self) -> None: - triggers = [CodeReviewTrigger.ON_COMMAND_PHRASE, CodeReviewTrigger.ON_NEW_COMMIT] + triggers = [CodeReviewTrigger.ON_READY_FOR_REVIEW, CodeReviewTrigger.ON_NEW_COMMIT] settings = CodeReviewSettings(enabled=True, triggers=triggers) assert settings.enabled is True @@ -38,7 +38,7 @@ def test_get_code_review_settings_with_enabled_and_triggers(self) -> None: repository=self.repo, enabled_code_review=True, code_review_triggers=[ - CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, CodeReviewTrigger.ON_READY_FOR_REVIEW.value, ], ) @@ -47,7 +47,7 @@ def test_get_code_review_settings_with_enabled_and_triggers(self) -> None: assert settings.enabled is True assert len(settings.triggers) == 2 - assert CodeReviewTrigger.ON_COMMAND_PHRASE in settings.triggers + assert CodeReviewTrigger.ON_NEW_COMMIT in settings.triggers assert CodeReviewTrigger.ON_READY_FOR_REVIEW in settings.triggers def test_get_code_review_settings_converts_string_triggers_to_enum(self) -> None: diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index bbd6a00c863195..4cf7e6d48c0e60 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -620,7 +620,7 @@ def test_returns_repo_settings_when_exist(self): RepositorySettings.objects.create( repository=repo, enabled_code_review=True, - code_review_triggers=["on_command_phrase", "on_ready_for_review"], + code_review_triggers=["on_new_commit", "on_ready_for_review"], ) url = reverse("sentry-api-0-code-review-repo-settings") @@ -634,7 +634,7 @@ def test_returns_repo_settings_when_exist(self): assert resp.status_code == 200 assert resp.data == { "enabledCodeReview": True, - "codeReviewTriggers": ["on_command_phrase", "on_ready_for_review"], + "codeReviewTriggers": ["on_new_commit", "on_ready_for_review"], } @patch( @@ -658,7 +658,7 @@ def test_filters_inactive_repositories(self): RepositorySettings.objects.create( repository=repo, enabled_code_review=True, - code_review_triggers=["on_command_phrase"], + code_review_triggers=["on_new_commit"], ) url = reverse("sentry-api-0-code-review-repo-settings") @@ -864,7 +864,7 @@ def test_returns_false_when_code_review_enabled_but_no_contributor_exists(self): RepositorySettings.objects.create( repository=repo, enabled_code_review=True, - code_review_triggers=["on_command_phrase"], + code_review_triggers=["on_new_commit"], ) url = reverse("sentry-api-0-prevent-pr-review-eligibility") @@ -905,7 +905,7 @@ def test_returns_false_when_quota_check_fails(self, mock_check_quota): RepositorySettings.objects.create( repository=repo, enabled_code_review=True, - code_review_triggers=["on_command_phrase"], + code_review_triggers=["on_new_commit"], ) OrganizationContributors.objects.create( @@ -954,7 +954,7 @@ def test_returns_true_when_code_review_enabled_and_quota_available(self, mock_ch RepositorySettings.objects.create( repository=repo, enabled_code_review=True, - code_review_triggers=["on_command_phrase"], + code_review_triggers=["on_new_commit"], ) contributor = OrganizationContributors.objects.create( @@ -1021,7 +1021,7 @@ def test_returns_true_when_any_org_is_eligible(self, mock_check_quota): RepositorySettings.objects.create( repository=repo2, enabled_code_review=True, - code_review_triggers=["on_command_phrase"], + code_review_triggers=["on_new_commit"], ) OrganizationContributors.objects.create( organization=org2, diff --git a/tests/sentry/seer/code_review/test_preflight.py b/tests/sentry/seer/code_review/test_preflight.py index 020645dbc47591..336408d94896eb 100644 --- a/tests/sentry/seer/code_review/test_preflight.py +++ b/tests/sentry/seer/code_review/test_preflight.py @@ -181,7 +181,7 @@ def test_returns_repo_settings_when_allowed(self, mock_check_quota: MagicMock) - repository=self.repo, enabled_code_review=True, code_review_triggers=[ - CodeReviewTrigger.ON_COMMAND_PHRASE.value, + CodeReviewTrigger.ON_NEW_COMMIT.value, CodeReviewTrigger.ON_READY_FOR_REVIEW.value, ], ) @@ -198,7 +198,7 @@ def test_returns_repo_settings_when_allowed(self, mock_check_quota: MagicMock) - assert result.allowed is True assert result.settings is not None assert result.settings.enabled is True - assert CodeReviewTrigger.ON_COMMAND_PHRASE in result.settings.triggers + assert CodeReviewTrigger.ON_NEW_COMMIT in result.settings.triggers assert CodeReviewTrigger.ON_READY_FOR_REVIEW in result.settings.triggers @patch("sentry.seer.code_review.billing.quotas.backend.check_seer_quota") diff --git a/tests/sentry/seer/code_review/test_utils.py b/tests/sentry/seer/code_review/test_utils.py index 0f04b894e274f1..de499ba8e7dcaf 100644 --- a/tests/sentry/seer/code_review/test_utils.py +++ b/tests/sentry/seer/code_review/test_utils.py @@ -6,8 +6,8 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger from sentry.seer.code_review.utils import ( + SeerCodeReviewTrigger, _get_target_commit_sha, _get_trigger_metadata, transform_webhook_to_codegen_request, @@ -179,7 +179,7 @@ def test_pull_request_event( organization, repo, "abc123sha", - CodeReviewTrigger.ON_READY_FOR_REVIEW, + SeerCodeReviewTrigger.ON_READY_FOR_REVIEW, ) expected_repo = { @@ -203,7 +203,7 @@ def test_pull_request_event( } assert result["data"]["config"] == { "features": {"bug_prediction": True}, - "trigger": CodeReviewTrigger.ON_READY_FOR_REVIEW.value, + "trigger": SeerCodeReviewTrigger.ON_READY_FOR_REVIEW.value, } | {k: v for k, v in result["data"]["config"].items() if k not in ("features", "trigger")} def test_issue_comment_on_pr( @@ -229,14 +229,14 @@ def test_issue_comment_on_pr( organization, repo, "def456sha", - CodeReviewTrigger.ON_COMMAND_PHRASE, + SeerCodeReviewTrigger.ON_NEW_COMMIT, ) assert isinstance(result, dict) data = result["data"] config = data["config"] assert data["pr_id"] == 42 - assert config["trigger"] == CodeReviewTrigger.ON_COMMAND_PHRASE.value + assert config["trigger"] == SeerCodeReviewTrigger.ON_NEW_COMMIT.value assert config["trigger_comment_id"] == 12345 assert config["trigger_user"] == "commenter" assert config["trigger_comment_type"] == "issue_comment" @@ -256,7 +256,7 @@ def test_issue_comment_on_regular_issue_returns_none( organization, repo, "somesha", - CodeReviewTrigger.ON_COMMAND_PHRASE, + SeerCodeReviewTrigger.ON_NEW_COMMIT, ) assert result is None @@ -280,5 +280,5 @@ def test_invalid_repo_name_format_raises( organization, bad_repo, "sha123", - CodeReviewTrigger.ON_READY_FOR_REVIEW, + SeerCodeReviewTrigger.ON_READY_FOR_REVIEW, )