From 43317f6f0947f9d0951b6e584dfb9c218a3d151e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 14:38:34 -0800 Subject: [PATCH 01/20] prelim --- .../organization_repository_settings.py | 6 ++ src/sentry/models/repositorysettings.py | 21 +++++- .../test_organization_repository_settings.py | 73 ++++++++++++++----- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_settings.py b/src/sentry/integrations/api/endpoints/organization_repository_settings.py index cd9077e3756d38..f58972035a9c42 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_settings.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_settings.py @@ -73,6 +73,12 @@ 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 = list(updated_code_review_triggers) + [ + CodeReviewTrigger.ON_COMMAND_PHRASE.value + ] + repositories = list( Repository.objects.filter( id__in=repository_ids, diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 4ae894d084966b..24c5803b379629 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -1,6 +1,7 @@ from __future__ import annotations from enum import StrEnum +from typing import Any from django.contrib.postgres.fields.array import ArrayField from django.db import models @@ -9,6 +10,11 @@ from sentry.db.models import FlexibleForeignKey, Model, region_silo_model, sane_repr +def default_code_review_triggers() -> list[str]: + """Code review triggers always include ON_COMMAND_PHRASE.""" + return [CodeReviewTrigger.ON_COMMAND_PHRASE.value] + + class CodeReviewTrigger(StrEnum): ON_COMMAND_PHRASE = "on_command_phrase" ON_NEW_COMMIT = "on_new_commit" @@ -33,7 +39,7 @@ class RepositorySettings(Model): enabled_code_review = models.BooleanField(default=False) code_review_triggers = ArrayField( models.CharField(max_length=32, choices=CodeReviewTrigger.as_choices()), - default=list, + default=default_code_review_triggers, ) class Meta: @@ -41,3 +47,16 @@ class Meta: db_table = "sentry_repositorysettings" __repr__ = sane_repr("repository_id", "enabled_code_review") + + def save(self, *args: Any, **kwargs: Any) -> None: + # Ensure ON_COMMAND_PHRASE is always a trigger + if ( + self.code_review_triggers is not None + and CodeReviewTrigger.ON_COMMAND_PHRASE.value not in self.code_review_triggers + ): + self.code_review_triggers = list(self.code_review_triggers) + [ + CodeReviewTrigger.ON_COMMAND_PHRASE.value + ] + + super().save(*args, **kwargs) + super().save(*args, **kwargs) 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, + ] From ccd99395a1b3f42807432e405b0c8ed90c42e386 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 14:40:16 -0800 Subject: [PATCH 02/20] thanks cursor --- src/sentry/models/repositorysettings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 24c5803b379629..4eb7ee8de5f1c1 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -59,4 +59,3 @@ def save(self, *args: Any, **kwargs: Any) -> None: ] super().save(*args, **kwargs) - super().save(*args, **kwargs) From 042b1fa5683c465f5d5489bc4b776a9dffa743c9 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 15:17:09 -0800 Subject: [PATCH 03/20] add migration; fix failing tests --- migrations_lockfile.txt | 2 +- ...1015_alter_code_review_triggers_default.py | 39 +++++++++++++++++++ .../test_organization_repository_details.py | 5 ++- tests/sentry/models/test_repository.py | 6 ++- .../overwatch/endpoints/test_overwatch_rpc.py | 5 ++- 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/sentry/migrations/1015_alter_code_review_triggers_default.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 08ddc6c0724b28..e909742e7f966c 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: 1014_add_pkce_to_apigrant +sentry: 1015_alter_code_review_triggers_default social_auth: 0003_social_auth_json_field diff --git a/src/sentry/migrations/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_alter_code_review_triggers_default.py new file mode 100644 index 00000000000000..cc305367ba0e5f --- /dev/null +++ b/src/sentry/migrations/1015_alter_code_review_triggers_default.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.8 on 2025-12-19 23:11 + +import django.contrib.postgres.fields +from django.db import migrations, models + +import sentry.models.repositorysettings +from sentry.new_migrations.migrations import CheckedMigration + + +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", "1014_add_pkce_to_apigrant"), + ] + + operations = [ + migrations.AlterField( + model_name="repositorysettings", + name="code_review_triggers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=32), + default=sentry.models.repositorysettings.default_code_review_triggers, + size=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..b1a3b87a630fa1 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py @@ -68,10 +68,11 @@ def test_get_repository_expand_settings(self) -> None: assert response.data["id"] == str(repo.id) assert response.data["settings"] is not None assert response.data["settings"]["enabledCodeReview"] is True - assert response.data["settings"]["codeReviewTriggers"] == [ + assert set(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: self.login_as(user=self.user) diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index 21038798d7ceba..94db51d7c1e80f 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -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 set(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() diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index f4837c4e50646b..1cf28e56d20995 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -705,7 +705,10 @@ def test_scopes_by_organization(self): auth = self._auth_header_for_get(url, params2, "test-secret") resp2 = self.client.get(url, params2, HTTP_AUTHORIZATION=auth) assert resp2.status_code == 200 - assert resp2.data == {"enabledCodeReview": False, "codeReviewTriggers": []} + assert resp2.data == { + "enabledCodeReview": False, + "codeReviewTriggers": ["on_command_phrase"], + } class TestPreventPrReviewEligibilityEndpoint(APITestCase): From 326e24e9aedc53cfd6aef3d3d3eac7b33a251650 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 15:33:23 -0800 Subject: [PATCH 04/20] fix test --- .../api/endpoints/test_organization_repository_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b1a3b87a630fa1..98e5a8dd0a301f 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_details.py @@ -68,11 +68,11 @@ def test_get_repository_expand_settings(self) -> None: assert response.data["id"] == str(repo.id) assert response.data["settings"] is not None assert response.data["settings"]["enabledCodeReview"] is True - assert set(response.data["settings"]["codeReviewTriggers"]) == { + 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: self.login_as(user=self.user) From f831fcf5b8447f8abe216208b28b614d2e5c2d2c Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 15:48:53 -0800 Subject: [PATCH 05/20] overwatch rpc --- src/sentry/overwatch/endpoints/overwatch_rpc.py | 4 ++-- .../api/endpoints/test_organization_repositories.py | 1 + tests/sentry/overwatch/endpoints/test_overwatch_rpc.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index 4a50d1fd704864..75269facc7a7f1 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -21,7 +21,7 @@ from sentry.models.organization import Organization from sentry.models.organizationcontributors import OrganizationContributors from sentry.models.repository import Repository -from sentry.models.repositorysettings import RepositorySettings +from sentry.models.repositorysettings import CodeReviewTrigger, RepositorySettings from sentry.prevent.models import PreventAIConfiguration from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1 from sentry.silo.base import SiloMode @@ -216,7 +216,7 @@ def get(self, request: Request) -> Response: return Response( { "enabledCodeReview": False, - "codeReviewTriggers": [], + "codeReviewTriggers": [CodeReviewTrigger.ON_COMMAND_PHRASE], } ) 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/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index 1cf28e56d20995..965d5ebec5b172 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -693,7 +693,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) From 507ea1f32a76357f99a69db14c0e1b111c5c411e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 19 Dec 2025 16:16:51 -0800 Subject: [PATCH 06/20] fix even more tests --- src/sentry/overwatch/endpoints/overwatch_rpc.py | 2 +- .../overwatch/endpoints/test_overwatch_rpc.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index 75269facc7a7f1..b90430cdcb1e89 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -216,7 +216,7 @@ def get(self, request: Request) -> Response: return Response( { "enabledCodeReview": False, - "codeReviewTriggers": [CodeReviewTrigger.ON_COMMAND_PHRASE], + "codeReviewTriggers": [CodeReviewTrigger.ON_COMMAND_PHRASE.value], } ) diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index 965d5ebec5b172..91ce42760f68fb 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -533,7 +533,10 @@ def test_returns_defaults_when_no_repo_settings_exist(self): auth = self._auth_header_for_get(url, params, "test-secret") resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 - assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} + assert resp.data == { + "enabledCodeReview": False, + "codeReviewTriggers": ["on_command_phrase"], + } @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -551,7 +554,10 @@ def test_returns_defaults_when_repo_not_found(self): auth = self._auth_header_for_get(url, params, "test-secret") resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 - assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} + assert resp.data == { + "enabledCodeReview": False, + "codeReviewTriggers": ["on_command_phrase"], + } @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -647,7 +653,10 @@ def test_filters_inactive_repositories(self): resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 # Should return defaults since repository is inactive - assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} + assert resp.data == { + "enabledCodeReview": False, + "codeReviewTriggers": ["on_command_phrase"], + } @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", From a98b528b9e32dfc980074030f09b15f869c2be4b Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 11:06:00 -0800 Subject: [PATCH 07/20] backfill --- ...1015_alter_code_review_triggers_default.py | 34 +++++++++++++++++-- src/sentry/models/repositorysettings.py | 21 +++++------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/sentry/migrations/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_alter_code_review_triggers_default.py index cc305367ba0e5f..d333b653dc3124 100644 --- a/src/sentry/migrations/1015_alter_code_review_triggers_default.py +++ b/src/sentry/migrations/1015_alter_code_review_triggers_default.py @@ -2,11 +2,36 @@ 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 -import sentry.models.repositorysettings +from sentry.models.repositorysettings import CodeReviewTrigger 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.""" + RepositorySettings = apps.get_model("sentry", "RepositorySettings") + + settings_to_update = [] + batch_size = 1000 + + for setting in RepositorySettings.objects.all(): + triggers = setting.code_review_triggers or [] + if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in triggers: + setting.code_review_triggers = list(triggers) + [ + CodeReviewTrigger.ON_COMMAND_PHRASE.value + ] + settings_to_update.append(setting) + + if settings_to_update: + RepositorySettings.objects.bulk_update( + settings_to_update, ["code_review_triggers"], batch_size=batch_size + ) + + 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 @@ -32,8 +57,13 @@ class Migration(CheckedMigration): name="code_review_triggers", field=django.contrib.postgres.fields.ArrayField( base_field=models.CharField(max_length=32), - default=sentry.models.repositorysettings.default_code_review_triggers, + default=lambda: [CodeReviewTrigger.ON_COMMAND_PHRASE.value], size=None, ), ), + migrations.RunPython( + backfill_on_command_phrase_trigger, + reverse_code=migrations.RunPython.noop, + hints={"tables": ["sentry_repositorysettings"]}, + ), ] diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 4eb7ee8de5f1c1..c8784e80aa4a28 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -10,11 +10,6 @@ from sentry.db.models import FlexibleForeignKey, Model, region_silo_model, sane_repr -def default_code_review_triggers() -> list[str]: - """Code review triggers always include ON_COMMAND_PHRASE.""" - return [CodeReviewTrigger.ON_COMMAND_PHRASE.value] - - class CodeReviewTrigger(StrEnum): ON_COMMAND_PHRASE = "on_command_phrase" ON_NEW_COMMIT = "on_new_commit" @@ -39,7 +34,7 @@ class RepositorySettings(Model): enabled_code_review = models.BooleanField(default=False) code_review_triggers = ArrayField( models.CharField(max_length=32, choices=CodeReviewTrigger.as_choices()), - default=default_code_review_triggers, + default=lambda: [CodeReviewTrigger.ON_COMMAND_PHRASE.value], ) class Meta: @@ -50,12 +45,12 @@ class Meta: def save(self, *args: Any, **kwargs: Any) -> None: # Ensure ON_COMMAND_PHRASE is always a trigger - if ( - self.code_review_triggers is not None - and CodeReviewTrigger.ON_COMMAND_PHRASE.value not in self.code_review_triggers - ): - self.code_review_triggers = list(self.code_review_triggers) + [ - CodeReviewTrigger.ON_COMMAND_PHRASE.value - ] + if self.code_review_triggers is None: + self.code_review_triggers = [CodeReviewTrigger.ON_COMMAND_PHRASE.value] + else: + triggers = list(self.code_review_triggers) + 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) From 11a8252c83d59ca728abcc1d1adc4b757b196ec1 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 11:13:01 -0800 Subject: [PATCH 08/20] fix --- .../1015_alter_code_review_triggers_default.py | 11 +++++++++-- src/sentry/models/repositorysettings.py | 11 ++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sentry/migrations/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_alter_code_review_triggers_default.py index d333b653dc3124..e8a043fae78196 100644 --- a/src/sentry/migrations/1015_alter_code_review_triggers_default.py +++ b/src/sentry/migrations/1015_alter_code_review_triggers_default.py @@ -18,14 +18,21 @@ def backfill_on_command_phrase_trigger( settings_to_update = [] batch_size = 1000 - for setting in RepositorySettings.objects.all(): - triggers = setting.code_review_triggers or [] + for setting in RepositorySettings.objects.all().iterator(chunk_size=batch_size): + triggers = list(setting.code_review_triggers or []) if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in triggers: setting.code_review_triggers = list(triggers) + [ CodeReviewTrigger.ON_COMMAND_PHRASE.value ] settings_to_update.append(setting) + if len(settings_to_update) >= batch_size: + RepositorySettings.objects.bulk_update( + settings_to_update, ["code_review_triggers"], batch_size=batch_size + ) + settings_to_update = [] + + # Update remaining items if settings_to_update: RepositorySettings.objects.bulk_update( settings_to_update, ["code_review_triggers"], batch_size=batch_size diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index c8784e80aa4a28..91303d0b85d903 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -45,12 +45,9 @@ class Meta: def save(self, *args: Any, **kwargs: Any) -> None: # Ensure ON_COMMAND_PHRASE is always a trigger - if self.code_review_triggers is None: - self.code_review_triggers = [CodeReviewTrigger.ON_COMMAND_PHRASE.value] - else: - triggers = list(self.code_review_triggers) - if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in triggers: - triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) - self.code_review_triggers = triggers + 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) From d70def44587a964adfb90acc53613083805723cc Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 12:01:51 -0800 Subject: [PATCH 09/20] dont modify default in repositorysettings --- .../endpoints/organization_repository_settings.py | 4 +++- .../1015_alter_code_review_triggers_default.py | 12 +----------- src/sentry/models/repositorysettings.py | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_settings.py b/src/sentry/integrations/api/endpoints/organization_repository_settings.py index f58972035a9c42..1ad824c2b77233 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_settings.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_settings.py @@ -99,7 +99,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/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_alter_code_review_triggers_default.py index e8a043fae78196..3651c3c7cc6e90 100644 --- a/src/sentry/migrations/1015_alter_code_review_triggers_default.py +++ b/src/sentry/migrations/1015_alter_code_review_triggers_default.py @@ -1,8 +1,7 @@ # Generated by Django 5.2.8 on 2025-12-19 23:11 -import django.contrib.postgres.fields -from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations import migrations from django.db.migrations.state import StateApps from sentry.models.repositorysettings import CodeReviewTrigger @@ -59,15 +58,6 @@ class Migration(CheckedMigration): ] operations = [ - migrations.AlterField( - model_name="repositorysettings", - name="code_review_triggers", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=32), - default=lambda: [CodeReviewTrigger.ON_COMMAND_PHRASE.value], - size=None, - ), - ), migrations.RunPython( backfill_on_command_phrase_trigger, reverse_code=migrations.RunPython.noop, diff --git a/src/sentry/models/repositorysettings.py b/src/sentry/models/repositorysettings.py index 91303d0b85d903..3a1c5db07591d0 100644 --- a/src/sentry/models/repositorysettings.py +++ b/src/sentry/models/repositorysettings.py @@ -34,7 +34,7 @@ class RepositorySettings(Model): enabled_code_review = models.BooleanField(default=False) code_review_triggers = ArrayField( models.CharField(max_length=32, choices=CodeReviewTrigger.as_choices()), - default=lambda: [CodeReviewTrigger.ON_COMMAND_PHRASE.value], + default=list, ) class Meta: From 77c80af6d98f25183a89aad62ee2bfac61f457d9 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 13:24:48 -0800 Subject: [PATCH 10/20] fix import; more tests --- .../organization_repository_settings.py | 4 +- ...1015_alter_code_review_triggers_default.py | 9 +- tests/sentry/models/test_repository.py | 97 ++++++++++++++++++- 3 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_settings.py b/src/sentry/integrations/api/endpoints/organization_repository_settings.py index 1ad824c2b77233..acf40cdb23363a 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_settings.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_settings.py @@ -75,9 +75,7 @@ def put(self, request: Request, organization: Organization) -> Response: # Ensure ON_COMMAND_PHRASE is always a trigger if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in updated_code_review_triggers: - updated_code_review_triggers = list(updated_code_review_triggers) + [ - CodeReviewTrigger.ON_COMMAND_PHRASE.value - ] + updated_code_review_triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) repositories = list( Repository.objects.filter( diff --git a/src/sentry/migrations/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_alter_code_review_triggers_default.py index 3651c3c7cc6e90..952aa481871370 100644 --- a/src/sentry/migrations/1015_alter_code_review_triggers_default.py +++ b/src/sentry/migrations/1015_alter_code_review_triggers_default.py @@ -1,10 +1,9 @@ # 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 import migrations from django.db.migrations.state import StateApps -from sentry.models.repositorysettings import CodeReviewTrigger from sentry.new_migrations.migrations import CheckedMigration @@ -19,10 +18,8 @@ def backfill_on_command_phrase_trigger( for setting in RepositorySettings.objects.all().iterator(chunk_size=batch_size): triggers = list(setting.code_review_triggers or []) - if CodeReviewTrigger.ON_COMMAND_PHRASE.value not in triggers: - setting.code_review_triggers = list(triggers) + [ - CodeReviewTrigger.ON_COMMAND_PHRASE.value - ] + if "on_command_phrase" not in triggers: + setting.code_review_triggers = triggers + ["on_command_phrase"] settings_to_update.append(setting) if len(settings_to_update) >= batch_size: diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index 94db51d7c1e80f..82852c8f18b268 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 @@ -245,3 +245,98 @@ 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 = RepositorySettings.objects.create( + 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 = RepositorySettings.objects.create( + 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, + } + + 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 = RepositorySettings.objects.create( + 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, + } + + def test_save_enforces_on_command_phrase_on_update(self): + """Test that save() enforces ON_COMMAND_PHRASE when updating an existing instance.""" + org = self.create_organization() + repo = Repository.objects.create( + organization_id=org.id, + name="test-repo", + provider="integrations:github", + ) + + settings = RepositorySettings.objects.create( + 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, + } From 945e00ede3ba6896c52b1f39a1b83c096026c552 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 13:48:16 -0800 Subject: [PATCH 11/20] backfill migration test --- migrations_lockfile.txt | 2 +- ...015_backfill_on_command_phrase_trigger.py} | 0 ...1015_backfill_on_command_phrase_trigger.py | 67 +++++++++++++++++++ tests/sentry/models/test_repository.py | 3 + 4 files changed, 71 insertions(+), 1 deletion(-) rename src/sentry/migrations/{1015_alter_code_review_triggers_default.py => 1015_backfill_on_command_phrase_trigger.py} (100%) create mode 100644 tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index e909742e7f966c..9f32e3ec68e6fc 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_alter_code_review_triggers_default +sentry: 1015_backfill_on_command_phrase_trigger social_auth: 0003_social_auth_json_field diff --git a/src/sentry/migrations/1015_alter_code_review_triggers_default.py b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py similarity index 100% rename from src/sentry/migrations/1015_alter_code_review_triggers_default.py rename to src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py diff --git a/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py new file mode 100644 index 00000000000000..dae67bbf4b3a47 --- /dev/null +++ b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py @@ -0,0 +1,67 @@ +import pytest + +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 = "1014_add_pkce_to_apigrant" + migrate_to = "1015_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.setting_empty = RepositorySettings.objects.create( + repository=self.repo_empty, + enabled_code_review=True, + code_review_triggers=[], + ) + + self.repo_with_triggers = Repository.objects.create( + organization_id=self.org.id, + name="test-repo-2", + provider="integrations:github", + ) + self.setting_with_triggers = RepositorySettings.objects.create( + repository=self.repo_with_triggers, + enabled_code_review=True, + code_review_triggers=["on_new_commit", "on_ready_for_review"], + ) + + self.repo_already_has = Repository.objects.create( + organization_id=self.org.id, + name="test-repo-3", + provider="integrations:github", + ) + self.setting_already_has = RepositorySettings.objects.create( + repository=self.repo_already_has, + enabled_code_review=True, + code_review_triggers=["on_command_phrase", "on_new_commit"], + ) + + def test_backfills_on_command_phrase_trigger(self) -> None: + self.setting_empty.refresh_from_db() + assert set(self.setting_empty.code_review_triggers) == {"on_command_phrase"} + assert len(self.setting_empty.code_review_triggers) == 1 + + self.setting_with_triggers.refresh_from_db() + assert set(self.setting_with_triggers.code_review_triggers) == { + "on_command_phrase", + "on_new_commit", + "on_ready_for_review", + } + assert len(self.setting_with_triggers.code_review_triggers) == 3 + + self.setting_already_has.refresh_from_db() + assert self.setting_already_has.code_review_triggers.count("on_command_phrase") == 1 + assert set(self.setting_already_has.code_review_triggers) == { + "on_command_phrase", + "on_new_commit", + } diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index 82852c8f18b268..22bc78b9a562bf 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -286,6 +286,7 @@ def test_save_adds_on_command_phrase_when_missing(self): 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() @@ -309,6 +310,7 @@ def test_save_does_not_duplicate_on_command_phrase(self): 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): """Test that save() enforces ON_COMMAND_PHRASE when updating an existing instance.""" @@ -340,3 +342,4 @@ def test_save_enforces_on_command_phrase_on_update(self): CodeReviewTrigger.ON_NEW_COMMIT.value, CodeReviewTrigger.ON_READY_FOR_REVIEW.value, } + assert len(settings.code_review_triggers) == 3 From 10d0a0d8dee7e890a4478a2ace47021d56249aba Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 14:00:37 -0800 Subject: [PATCH 12/20] remove overwatch rpc changeS --- .../overwatch/endpoints/overwatch_rpc.py | 4 ++-- tests/sentry/models/test_repository.py | 4 ++-- .../overwatch/endpoints/test_overwatch_rpc.py | 20 ++++--------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/sentry/overwatch/endpoints/overwatch_rpc.py b/src/sentry/overwatch/endpoints/overwatch_rpc.py index e0164660ec8e38..34fdca4b3264f7 100644 --- a/src/sentry/overwatch/endpoints/overwatch_rpc.py +++ b/src/sentry/overwatch/endpoints/overwatch_rpc.py @@ -21,7 +21,7 @@ from sentry.models.organization import Organization from sentry.models.organizationcontributors import OrganizationContributors from sentry.models.repository import Repository -from sentry.models.repositorysettings import CodeReviewTrigger, RepositorySettings +from sentry.models.repositorysettings import RepositorySettings from sentry.prevent.models import PreventAIConfiguration from sentry.prevent.types.config import PREVENT_AI_CONFIG_DEFAULT, PREVENT_AI_CONFIG_DEFAULT_V1 from sentry.silo.base import SiloMode @@ -224,7 +224,7 @@ def get(self, request: Request) -> Response: return Response( { "enabledCodeReview": False, - "codeReviewTriggers": [CodeReviewTrigger.ON_COMMAND_PHRASE.value], + "codeReviewTriggers": [], } ) diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index 22bc78b9a562bf..f245cdacdcebe4 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -129,11 +129,11 @@ def test_settings_created_with_triggers(self): settings = RepositorySettings.objects.get(repository=repo) assert settings.enabled_code_review is True - assert set(settings.code_review_triggers) == { + 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() diff --git a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py index e28836ed039ce9..dfb9bfff7a9ddd 100644 --- a/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py +++ b/tests/sentry/overwatch/endpoints/test_overwatch_rpc.py @@ -533,10 +533,7 @@ def test_returns_defaults_when_no_repo_settings_exist(self): auth = self._auth_header_for_get(url, params, "test-secret") resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 - assert resp.data == { - "enabledCodeReview": False, - "codeReviewTriggers": ["on_command_phrase"], - } + assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -554,10 +551,7 @@ def test_returns_defaults_when_repo_not_found(self): auth = self._auth_header_for_get(url, params, "test-secret") resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 - assert resp.data == { - "enabledCodeReview": False, - "codeReviewTriggers": ["on_command_phrase"], - } + assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -677,10 +671,7 @@ def test_filters_inactive_repositories(self): resp = self.client.get(url, params, HTTP_AUTHORIZATION=auth) assert resp.status_code == 200 # Should return defaults since repository is inactive - assert resp.data == { - "enabledCodeReview": False, - "codeReviewTriggers": ["on_command_phrase"], - } + assert resp.data == {"enabledCodeReview": False, "codeReviewTriggers": []} @patch( "sentry.overwatch.endpoints.overwatch_rpc.settings.OVERWATCH_RPC_SHARED_SECRET", @@ -738,10 +729,7 @@ def test_scopes_by_organization(self): auth = self._auth_header_for_get(url, params2, "test-secret") resp2 = self.client.get(url, params2, HTTP_AUTHORIZATION=auth) assert resp2.status_code == 200 - assert resp2.data == { - "enabledCodeReview": False, - "codeReviewTriggers": ["on_command_phrase"], - } + assert resp2.data == {"enabledCodeReview": False, "codeReviewTriggers": []} class TestPreventPrReviewEligibilityEndpoint(APITestCase): From 79d8a53926785e13b7c01114415e55d37fedd169 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 14:34:24 -0800 Subject: [PATCH 13/20] use factory create --- tests/sentry/models/test_repository.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index f245cdacdcebe4..ba9660f88057b3 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -254,7 +254,7 @@ def test_save_adds_on_command_phrase_when_triggers_empty(self): provider="integrations:github", ) - settings = RepositorySettings.objects.create( + settings = self.create_repository_settings( repository=repo, enabled_code_review=True, code_review_triggers=[], @@ -271,7 +271,7 @@ def test_save_adds_on_command_phrase_when_missing(self): provider="integrations:github", ) - settings = RepositorySettings.objects.create( + settings = self.create_repository_settings( repository=repo, enabled_code_review=True, code_review_triggers=[ @@ -296,7 +296,7 @@ def test_save_does_not_duplicate_on_command_phrase(self): provider="integrations:github", ) - settings = RepositorySettings.objects.create( + settings = self.create_repository_settings( repository=repo, enabled_code_review=True, code_review_triggers=[ @@ -321,7 +321,7 @@ def test_save_enforces_on_command_phrase_on_update(self): provider="integrations:github", ) - settings = RepositorySettings.objects.create( + settings = self.create_repository_settings( repository=repo, enabled_code_review=True, code_review_triggers=[ From 4bd87f04cd4c7ee9f74831e7220b523903cd3119 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 14:38:49 -0800 Subject: [PATCH 14/20] remove useless comment --- tests/sentry/models/test_repository.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sentry/models/test_repository.py b/tests/sentry/models/test_repository.py index ba9660f88057b3..30a4906dd20e0b 100644 --- a/tests/sentry/models/test_repository.py +++ b/tests/sentry/models/test_repository.py @@ -313,7 +313,6 @@ def test_save_does_not_duplicate_on_command_phrase(self): assert len(settings.code_review_triggers) == 2 def test_save_enforces_on_command_phrase_on_update(self): - """Test that save() enforces ON_COMMAND_PHRASE when updating an existing instance.""" org = self.create_organization() repo = Repository.objects.create( organization_id=org.id, From 9c02bc84aaa14dc4a5a4caf7516837761eb4d7a4 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 5 Jan 2026 15:42:26 -0800 Subject: [PATCH 15/20] fix migration test --- ...1015_backfill_on_command_phrase_trigger.py | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py index dae67bbf4b3a47..5d762f9b6d3558 100644 --- a/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py +++ b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py @@ -18,50 +18,56 @@ def setup_initial_state(self) -> None: name="test-repo", provider="integrations:github", ) - self.setting_empty = RepositorySettings.objects.create( - repository=self.repo_empty, - enabled_code_review=True, - code_review_triggers=[], - ) self.repo_with_triggers = Repository.objects.create( organization_id=self.org.id, name="test-repo-2", provider="integrations:github", ) - self.setting_with_triggers = RepositorySettings.objects.create( - repository=self.repo_with_triggers, - enabled_code_review=True, - code_review_triggers=["on_new_commit", "on_ready_for_review"], - ) 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") + + 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=self.repo_already_has, + repository_id=self.repo_already_has.id, enabled_code_review=True, code_review_triggers=["on_command_phrase", "on_new_commit"], ) def test_backfills_on_command_phrase_trigger(self) -> None: - self.setting_empty.refresh_from_db() - assert set(self.setting_empty.code_review_triggers) == {"on_command_phrase"} - assert len(self.setting_empty.code_review_triggers) == 1 + 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 - self.setting_with_triggers.refresh_from_db() - assert set(self.setting_with_triggers.code_review_triggers) == { + 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(self.setting_with_triggers.code_review_triggers) == 3 + assert len(setting_with_triggers.code_review_triggers) == 3 - self.setting_already_has.refresh_from_db() - assert self.setting_already_has.code_review_triggers.count("on_command_phrase") == 1 - assert set(self.setting_already_has.code_review_triggers) == { + 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", } From ec2d0c58f9508b06713ff85021874c1bf4023c7b Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 6 Jan 2026 10:27:19 -0800 Subject: [PATCH 16/20] remove batch size --- .../1015_backfill_on_command_phrase_trigger.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py index 952aa481871370..5cbc7a652e4dd3 100644 --- a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py +++ b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py @@ -14,25 +14,15 @@ def backfill_on_command_phrase_trigger( RepositorySettings = apps.get_model("sentry", "RepositorySettings") settings_to_update = [] - batch_size = 1000 - for setting in RepositorySettings.objects.all().iterator(chunk_size=batch_size): + 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 len(settings_to_update) >= batch_size: - RepositorySettings.objects.bulk_update( - settings_to_update, ["code_review_triggers"], batch_size=batch_size - ) - settings_to_update = [] - - # Update remaining items if settings_to_update: - RepositorySettings.objects.bulk_update( - settings_to_update, ["code_review_triggers"], batch_size=batch_size - ) + RepositorySettings.objects.bulk_update(settings_to_update, ["code_review_triggers"]) class Migration(CheckedMigration): From ba9923f3145f9c5315d7efb6cfa02197e6287027 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 6 Jan 2026 11:38:26 -0800 Subject: [PATCH 17/20] fix failing tests from merge conflicts --- tests/sentry/models/test_repositorysettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: From ee73e39d8c278af3de506380da008052f2f6610e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 6 Jan 2026 16:48:30 -0800 Subject: [PATCH 18/20] ensure the trigger in organization_details --- .../core/endpoints/organization_details.py | 10 +++++++ .../endpoints/test_organization_details.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index a14188f27ba140..e7dcff658a1f80 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.value not in triggers: + triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) + value = triggers + return value + def validate_attachmentsRole(self, value): try: roles.get(value) 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 From 7b601144484cc69a12fb0c14c9cf6dc92fbb2cc5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 6 Jan 2026 17:02:29 -0800 Subject: [PATCH 19/20] migration & tests for organization_details --- .../core/endpoints/organization_details.py | 4 +- ...1015_backfill_on_command_phrase_trigger.py | 18 ++++++++- ...1015_backfill_on_command_phrase_trigger.py | 40 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index e7dcff658a1f80..c0276951b33478 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -406,8 +406,8 @@ 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.value not in triggers: - triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE.value) + if CodeReviewTrigger.ON_COMMAND_PHRASE not in triggers: + triggers.append(CodeReviewTrigger.ON_COMMAND_PHRASE) value = triggers return value diff --git a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py index 5cbc7a652e4dd3..a352577bcde672 100644 --- a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py +++ b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py @@ -10,9 +10,11 @@ def backfill_on_command_phrase_trigger( apps: StateApps, schema_editor: BaseDatabaseSchemaEditor ) -> None: - """Backfill on_command_phrase code review trigger for all existing RepositorySettings.""" + """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(): @@ -24,6 +26,18 @@ def backfill_on_command_phrase_trigger( 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. @@ -48,6 +62,6 @@ class Migration(CheckedMigration): migrations.RunPython( backfill_on_command_phrase_trigger, reverse_code=migrations.RunPython.noop, - hints={"tables": ["sentry_repositorysettings"]}, + hints={"tables": ["sentry_repositorysettings", "sentry_organizationoptions"]}, ), ] diff --git a/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py index 5d762f9b6d3558..65c24001c3cf7d 100644 --- a/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py +++ b/tests/sentry/migrations/test_1015_backfill_on_command_phrase_trigger.py @@ -1,5 +1,6 @@ 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 @@ -33,6 +34,7 @@ def setup_initial_state(self) -> None: 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, @@ -52,7 +54,26 @@ def setup_before_migration(self, apps) -> None: 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 @@ -71,3 +92,22 @@ def test_backfills_on_command_phrase_trigger(self) -> None: "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", + ] From 9e64f0f0a727fea9cd0073d80d2df8564baf445b Mon Sep 17 00:00:00 2001 From: srest2021 Date: Wed, 7 Jan 2026 10:04:59 -0800 Subject: [PATCH 20/20] make post deploy --- .../migrations/1015_backfill_on_command_phrase_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py index a352577bcde672..465ea2ed9e35e0 100644 --- a/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py +++ b/src/sentry/migrations/1015_backfill_on_command_phrase_trigger.py @@ -52,7 +52,7 @@ class Migration(CheckedMigration): # 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 + is_post_deployment = True dependencies = [ ("sentry", "1014_add_pkce_to_apigrant"),