From 131f5b0d427287a98f6e0f047ee21f074d004935 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 14:20:24 -0700 Subject: [PATCH 1/5] ref(repositories): Remove feature flag branching for RepositoryProjectPathConfig reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the `organizations:project-repository-fk-reads` flag checks from all RepositoryProjectPathConfig read paths. Always use `project_repository` FK for queries and attribute access. The flag registration is left in place — it will be removed in a follow-up PR that also drops the old model columns. Switched paths: - Code mappings list/validate/create API - Code mapping serializer - Code mapping details/delete endpoints - Code mapping codeowners endpoint - Stacktrace link resolution - Source context fetching - Commit context / suspect commits - Code owners auto-sync - Project code owners serializer - get_sorted_code_mapping_configs - Autopilot missing SDK integration detector - Project transfer_to cleanup --- .../endpoints/project_stacktrace_coverage.py | 4 +- .../serializers/models/projectcodeowners.py | 11 +--- .../tasks/missing_sdk_integration.py | 32 ++++-------- .../organization_code_mapping_codeowners.py | 16 ++---- .../organization_code_mapping_details.py | 13 ++--- .../endpoints/organization_code_mappings.py | 45 +++++++---------- .../organization_code_mappings_bulk.py | 4 +- .../models/repository_project_path_config.py | 22 +++----- .../integrations/utils/commit_context.py | 9 +--- .../integrations/utils/source_context.py | 16 ++---- .../integrations/utils/stacktrace_link.py | 15 ++---- .../auto_source_code_config/code_mapping.py | 30 ++++------- .../endpoints/project_stacktrace_link.py | 5 +- .../project_stacktrace_source_context.py | 4 +- src/sentry/issues/endpoints/serializers.py | 10 ++-- src/sentry/models/project.py | 8 +-- .../tasks/codeowners/code_owners_auto_sync.py | 35 ++++--------- .../test_organization_derive_code_mappings.py | 4 +- tests/sentry/deletions/test_repository.py | 2 - .../test_organization_code_mapping_details.py | 2 - .../test_organization_code_mappings_bulk.py | 19 ++++--- .../perforce/test_code_mapping.py | 20 -------- .../perforce/test_stacktrace_link.py | 16 ------ .../test_process_event.py | 13 ++--- .../test_1092_backfill_projectrepository.py | 50 ++++++++++++------- tests/sentry/models/test_project.py | 9 ++-- tests/sentry/seer/endpoints/test_seer_rpc.py | 2 - tests/sentry/tasks/test_repository.py | 2 - 28 files changed, 139 insertions(+), 279 deletions(-) diff --git a/src/sentry/api/endpoints/project_stacktrace_coverage.py b/src/sentry/api/endpoints/project_stacktrace_coverage.py index 764583bc20e336..0f3d785623a105 100644 --- a/src/sentry/api/endpoints/project_stacktrace_coverage.py +++ b/src/sentry/api/endpoints/project_stacktrace_coverage.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -53,8 +52,7 @@ def get(self, request: Request, project: Project) -> Response: if not configs: return Response({"detail": "No code mappings found for this project"}, status=400) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = get_stacktrace_config(configs, ctx, use_project_repository_fk=use_fk) + result = get_stacktrace_config(configs, ctx) error = result["error"] serialized_config = None diff --git a/src/sentry/api/serializers/models/projectcodeowners.py b/src/sentry/api/serializers/models/projectcodeowners.py index c2a6bc0f5dfc33..5631e3bc3790bb 100644 --- a/src/sentry/api/serializers/models/projectcodeowners.py +++ b/src/sentry/api/serializers/models/projectcodeowners.py @@ -1,7 +1,6 @@ import logging from typing import Any -from sentry import features from sentry.api.serializers import Serializer, register, serialize from sentry.integrations.api.serializers.models.repository_project_path_config import ( RepositoryProjectPathConfigSerializer, @@ -24,12 +23,6 @@ def __init__( def get_attrs(self, item_list, user, **kwargs): attrs = {} - use_fk = False - if item_list: - use_fk = features.has( - "organizations:project-repository-fk-reads", - item_list[0].project.organization, - ) integrations = { i.id: i for i in integration_service.get_integrations( @@ -38,9 +31,7 @@ def get_attrs(self, item_list, user, **kwargs): } for item in item_list: code_mapping = item.repository_project_path_config - repository = ( - code_mapping.project_repository.repository if use_fk else code_mapping.repository - ) + repository = code_mapping.project_repository.repository integration = integrations[item.repository_project_path_config.integration_id] install = integration.get_installation( diff --git a/src/sentry/autopilot/tasks/missing_sdk_integration.py b/src/sentry/autopilot/tasks/missing_sdk_integration.py index f5f99c70f3cd08..c164c3d55af665 100644 --- a/src/sentry/autopilot/tasks/missing_sdk_integration.py +++ b/src/sentry/autopilot/tasks/missing_sdk_integration.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from sentry import features, options +from sentry import options from sentry.autopilot.tasks.common import AutopilotDetectorName, create_instrumentation_issue from sentry.constants import INTEGRATION_ID_TO_PLATFORM_DATA, ObjectStatus from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig @@ -88,28 +88,16 @@ def run_missing_sdk_integration_detector() -> None: continue # Get repo configs for this project - if features.has("organizations:project-repository-fk-reads", project.organization): - repo_names = ( - RepositoryProjectPathConfig.objects.filter( - project_repository__project=project, - project_repository__repository__status=ObjectStatus.ACTIVE, - project_repository__repository__provider="integrations:github", - ) - .order_by("project_repository__repository__name") - .distinct("project_repository__repository__name") - .values_list("project_repository__repository__name", flat=True) - ) - else: - repo_names = ( - RepositoryProjectPathConfig.objects.filter( - project=project, - repository__status=ObjectStatus.ACTIVE, - repository__provider="integrations:github", - ) - .order_by("repository__name") - .distinct("repository__name") - .values_list("repository__name", flat=True) + repo_names = ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=project, + project_repository__repository__status=ObjectStatus.ACTIVE, + project_repository__repository__provider="integrations:github", ) + .order_by("project_repository__repository__name") + .distinct("project_repository__repository__name") + .values_list("project_repository__repository__name", flat=True) + ) for repo_name in repo_names: run_missing_sdk_integration_detector_for_project_task.apply_async( args=( diff --git a/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py b/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py index b6f14db63f1405..b239590e885c29 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mapping_codeowners.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -16,7 +15,7 @@ from sentry.shared_integrations.exceptions import ApiError -def get_codeowner_contents(config, use_project_repository_fk: bool = False): +def get_codeowner_contents(config): if not config.organization_integration_id: raise NotFound(detail="No associated integration") @@ -25,12 +24,8 @@ def get_codeowner_contents(config, use_project_repository_fk: bool = False): ) if not integration: return None - if use_project_repository_fk: - org_id = config.project_repository.project.organization_id - repository = config.project_repository.repository - else: - org_id = config.project.organization_id - repository = config.repository + org_id = config.project_repository.project.organization_id + repository = config.project_repository.repository install = integration.get_installation(organization_id=org_id) if isinstance(install, RepositoryIntegration): return install.get_codeowner_file(repository, ref=config.default_branch) @@ -52,8 +47,6 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar try: kwargs["config"] = RepositoryProjectPathConfig.objects.select_related( - "project", - "repository", "project_repository__project", "project_repository__repository", ).get( @@ -67,8 +60,7 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar def get(self, request: Request, config_id, organization, config) -> Response: try: - use_fk = features.has("organizations:project-repository-fk-reads", organization) - codeowner_contents = get_codeowner_contents(config, use_project_repository_fk=use_fk) + codeowner_contents = get_codeowner_contents(config) except ApiError as e: return self.respond({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py b/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py index 27b91b277f32ca..67770f09cfdc96 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mapping_details.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -40,7 +39,7 @@ def convert_args(self, request: Request, organization_id_or_slug, config_id, *ar ) try: kwargs["config"] = RepositoryProjectPathConfig.objects.select_related( - "project", "project_repository__project" + "project_repository__project" ).get( id=config_id, organization_integration_id__in=[oi.id for oi in ois], @@ -69,10 +68,7 @@ def put(self, request: Request, config_id, organization, config, new_project) -> :param string default_branch: :auth: required """ - if features.has("organizations:project-repository-fk-reads", organization): - project = config.project_repository.project - else: - project = config.project + project = config.project_repository.project if not request.access.has_projects_access([project, new_project]): return self.respond(status=status.HTTP_403_FORBIDDEN) @@ -109,10 +105,7 @@ def delete(self, request: Request, config_id, organization, config) -> Response: :auth: required """ - if features.has("organizations:project-repository-fk-reads", organization): - project = config.project_repository.project - else: - project = config.project + project = config.project_repository.project if not request.access.has_project_access(project): return self.respond(status=status.HTTP_403_FORBIDDEN) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings.py b/src/sentry/integrations/api/endpoints/organization_code_mappings.py index c521b39ef13b2d..445b5768b178c8 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings.py @@ -7,7 +7,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -72,18 +71,11 @@ def organization(self): return self.context["organization"] def validate(self, attrs): - if features.has("organizations:project-repository-fk-reads", self.organization): - query = RepositoryProjectPathConfig.objects.filter( - project_repository__project_id=attrs.get("project_id"), - stack_root=attrs.get("stack_root"), - source_root=attrs.get("source_root"), - ) - else: - query = RepositoryProjectPathConfig.objects.filter( - project_id=attrs.get("project_id"), - stack_root=attrs.get("stack_root"), - source_root=attrs.get("source_root"), - ) + query = RepositoryProjectPathConfig.objects.filter( + project_repository__project_id=attrs.get("project_id"), + stack_root=attrs.get("stack_root"), + source_root=attrs.get("source_root"), + ) if self.instance: query = query.exclude(id=self.instance.id) if query.exists(): @@ -149,8 +141,12 @@ def update(self, instance, validated_data): validated_data.pop("id") if self.instance: with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)): - project_id = validated_data.get("project_id", self.instance.project_id) - repository_id = validated_data.get("repository_id", self.instance.repository_id) + project_id = validated_data.get( + "project_id", self.instance.project_repository.project_id + ) + repository_id = validated_data.get( + "repository_id", self.instance.project_repository.repository_id + ) project_repo, _ = ProjectRepository.objects.get_or_create( project_id=project_id, repository_id=repository_id, @@ -211,19 +207,12 @@ def get(self, request: Request, organization: Organization) -> Response: projects = self.get_projects( request, organization, include_all_accessible=not has_explicit_projects ) - if features.has("organizations:project-repository-fk-reads", organization): - queryset = RepositoryProjectPathConfig.objects.filter( - project_repository__project__in=projects - ).select_related( - "project", - "repository", - "project_repository__project", - "project_repository__repository", - ) - else: - queryset = RepositoryProjectPathConfig.objects.filter( - project__in=projects - ).select_related("project", "repository") + queryset = RepositoryProjectPathConfig.objects.filter( + project_repository__project__in=projects + ).select_related( + "project_repository__project", + "project_repository__repository", + ) if integration_id: # get_organization_integration will raise a 404 if no org_integration is found diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py index 9fc4b361bed1d7..6ace8b30743f67 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py @@ -267,7 +267,6 @@ def post(self, request: Request, organization: Organization) -> Response: ) defaults = { - "repository": repo, "organization_integration_id": org_integration.id, "organization_id": organization.id, "integration_id": repo.integration_id, @@ -281,7 +280,7 @@ def post(self, request: Request, organization: Organization) -> Response: with transaction.atomic(using=router.db_for_write(RepositoryProjectPathConfig)): try: config = RepositoryProjectPathConfig.objects.select_for_update().get( - project=project, + project_repository__project=project, stack_root=mapping["stack_root"], source_root=mapping["source_root"], ) @@ -291,6 +290,7 @@ def post(self, request: Request, organization: Organization) -> Response: except RepositoryProjectPathConfig.DoesNotExist: config = RepositoryProjectPathConfig( project=project, + repository=repo, stack_root=mapping["stack_root"], source_root=mapping["source_root"], **defaults, diff --git a/src/sentry/integrations/api/serializers/models/repository_project_path_config.py b/src/sentry/integrations/api/serializers/models/repository_project_path_config.py index 39bc9cbcc6adf7..2a7a0d9cfb667f 100644 --- a/src/sentry/integrations/api/serializers/models/repository_project_path_config.py +++ b/src/sentry/integrations/api/serializers/models/repository_project_path_config.py @@ -1,6 +1,5 @@ from django.db.models import prefetch_related_objects -from sentry import features from sentry.api.serializers import Serializer, register from sentry.integrations.api.serializers.models.integration import serialize_provider from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig @@ -13,16 +12,11 @@ def get_attrs(self, item_list, user, **kwargs): if not item_list: return {} - organization = item_list[0].project.organization - use_fk = features.has("organizations:project-repository-fk-reads", organization) - if use_fk: - prefetch_related_objects( - item_list, "project_repository__project", "project_repository__repository" - ) - else: - prefetch_related_objects(item_list, "project", "repository") + prefetch_related_objects( + item_list, "project_repository__project", "project_repository__repository" + ) - return {item: {"use_project_repository_fk": use_fk} for item in item_list} + return {item: {} for item in item_list} def serialize(self, obj, attrs, user, **kwargs): integration = None @@ -35,12 +29,8 @@ def serialize(self, obj, attrs, user, **kwargs): serialized_provider = serialize_provider(provider) if provider else None integration_id = str(integration.id) if integration else None - if attrs.get("use_project_repository_fk"): - project = obj.project_repository.project - repository = obj.project_repository.repository - else: - project = obj.project - repository = obj.repository + project = obj.project_repository.project + repository = obj.project_repository.repository return { "id": str(obj.id), diff --git a/src/sentry/integrations/utils/commit_context.py b/src/sentry/integrations/utils/commit_context.py index 7003c23924d6d3..5fac2157995abf 100644 --- a/src/sentry/integrations/utils/commit_context.py +++ b/src/sentry/integrations/utils/commit_context.py @@ -8,7 +8,7 @@ from django.utils.datastructures import OrderedSet -from sentry import analytics, features, options +from sentry import analytics, options from sentry.analytics.events.integration_commit_context_all_frames import ( IntegrationsFailedToFetchCommitContextAllFrames, IntegrationsSuccessfullyFetchedCommitContextAllFrames, @@ -199,7 +199,6 @@ def _generate_integration_to_files_mapping( this function is used to separate files into each integration so that we can later call get_commit_context_all_frames on each integration. """ - use_fk = features.has("organizations:project-repository-fk-reads", organization) integration_to_files_mapping: dict[int, list[SourceLineInfo]] = {} num_successfully_mapped_frames = 0 @@ -258,11 +257,7 @@ def _generate_integration_to_files_mapping( lineno=frame.lineno, path=src_path, ref=code_mapping.default_branch or "master", - repo=( - code_mapping.project_repository.repository - if use_fk - else code_mapping.repository - ), + repo=code_mapping.project_repository.repository, code_mapping=code_mapping, ) ) diff --git a/src/sentry/integrations/utils/source_context.py b/src/sentry/integrations/utils/source_context.py index 1761b20e891b71..d27306cdad4225 100644 --- a/src/sentry/integrations/utils/source_context.py +++ b/src/sentry/integrations/utils/source_context.py @@ -61,7 +61,6 @@ def _format_context( def _resolve_integration( config: RepositoryProjectPathConfig, - use_project_repository_fk: bool = False, ) -> tuple[RpcIntegration, RepositoryIntegration] | None: """Resolve the integration and installation for a code mapping config.""" integration = integration_service.get_integration( @@ -71,11 +70,7 @@ def _resolve_integration( if not integration: return None - org_id = ( - config.project_repository.project.organization_id - if use_project_repository_fk - else config.project.organization_id - ) + org_id = config.project_repository.project.organization_id install = integration.get_installation(organization_id=org_id) if not isinstance(install, RepositoryIntegration): return None @@ -159,7 +154,6 @@ def fetch_source_context_from_scm( configs: Sequence[RepositoryProjectPathConfig], ctx: StacktraceLinkContext, context_lines: int = LINES_OF_CONTEXT, - use_project_repository_fk: bool = False, ) -> SourceContextResult: """ Fetch source context lines from an SCM integration for a stack trace frame. @@ -204,9 +198,7 @@ def fetch_source_context_from_scm( org_integration_id = config.organization_integration_id if org_integration_id not in resolved_integrations: - resolved_integrations[org_integration_id] = _resolve_integration( - config, use_project_repository_fk=use_project_repository_fk - ) + resolved_integrations[org_integration_id] = _resolve_integration(config) resolved = resolved_integrations[org_integration_id] if resolved is None: @@ -216,9 +208,7 @@ def fetch_source_context_from_scm( ref = ctx.get("commit_id") or str(config.default_branch or "") - repository = ( - config.project_repository.repository if use_project_repository_fk else config.repository - ) + repository = config.project_repository.repository file_content, fetch_error = _fetch_file_from_scm( install, integration.id, repository, src_path, ref ) diff --git a/src/sentry/integrations/utils/stacktrace_link.py b/src/sentry/integrations/utils/stacktrace_link.py index 0357d711e7cbe3..17d320a834c1eb 100644 --- a/src/sentry/integrations/utils/stacktrace_link.py +++ b/src/sentry/integrations/utils/stacktrace_link.py @@ -32,16 +32,11 @@ def get_link( config: RepositoryProjectPathConfig, src_path: str, version: str | None = None, - use_project_repository_fk: bool = False, ) -> RepositoryLinkOutcome: result: RepositoryLinkOutcome = {} - if use_project_repository_fk: - project = config.project_repository.project - repository = config.project_repository.repository - else: - project = config.project - repository = config.repository + project = config.project_repository.project + repository = config.project_repository.repository integration = integration_service.get_integration( organization_integration_id=config.organization_integration_id, status=ObjectStatus.ACTIVE @@ -94,7 +89,6 @@ class StacktraceLinkOutcome(TypedDict): def get_stacktrace_config( configs: Sequence[RepositoryProjectPathConfig], ctx: StacktraceLinkContext, - use_project_repository_fk: bool = False, ) -> StacktraceLinkOutcome: result: StacktraceLinkOutcome = { "source_url": None, @@ -119,13 +113,10 @@ def get_stacktrace_config( config, src_path, ctx["commit_id"], - use_project_repository_fk=use_project_repository_fk, ) result["iteration_count"] += 1 - repository = ( - config.project_repository.repository if use_project_repository_fk else config.repository - ) + repository = config.project_repository.repository result["current_config"] = { "config": config, "outcome": outcome, diff --git a/src/sentry/issues/auto_source_code_config/code_mapping.py b/src/sentry/issues/auto_source_code_config/code_mapping.py index f636db3f60531d..6a92220548bd8e 100644 --- a/src/sentry/issues/auto_source_code_config/code_mapping.py +++ b/src/sentry/issues/auto_source_code_config/code_mapping.py @@ -7,7 +7,6 @@ from django.db import router, transaction -from sentry import features from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.integrations.source_code_management.repo_trees import ( RepoAndBranch, @@ -410,27 +409,18 @@ def get_sorted_code_mapping_configs(project: Project) -> list[RepositoryProjectP # sure that we are still ordering by `id` because we want to make sure # the ordering is deterministic # codepath mappings must have an associated integration for stacktrace linking. - if features.has("organizations:project-repository-fk-reads", project.organization): - configs = ( - RepositoryProjectPathConfig.objects.filter( - project_repository__project=project, - organization_integration_id__isnull=False, - ) - .select_related( - "project_repository", - "project_repository__project", - "project_repository__repository", - ) - .order_by("id") + configs = ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=project, + organization_integration_id__isnull=False, ) - else: - configs = ( - RepositoryProjectPathConfig.objects.filter( - project=project, organization_integration_id__isnull=False - ) - .select_related("repository") - .order_by("id") + .select_related( + "project_repository", + "project_repository__project", + "project_repository__repository", ) + .order_by("id") + ) sorted_configs: list[RepositoryProjectPathConfig] = [] diff --git a/src/sentry/issues/endpoints/project_stacktrace_link.py b/src/sentry/issues/endpoints/project_stacktrace_link.py index 978d3802930b2e..b9b91680f4ca1b 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_link.py +++ b/src/sentry/issues/endpoints/project_stacktrace_link.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from sentry_sdk import Scope -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -150,8 +150,7 @@ def get(self, request: Request, project: Project) -> Response: scope = Scope.get_isolation_scope() set_top_tags(scope, project, ctx, len(configs) > 0) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = get_stacktrace_config(configs, ctx, use_project_repository_fk=use_fk) + result = get_stacktrace_config(configs, ctx) error = result["error"] src_path = result["src_path"] # Post-processing before exiting scope context diff --git a/src/sentry/issues/endpoints/project_stacktrace_source_context.py b/src/sentry/issues/endpoints/project_stacktrace_source_context.py index aadd4460e381c0..395cb9d4d43c0a 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_source_context.py +++ b/src/sentry/issues/endpoints/project_stacktrace_source_context.py @@ -5,7 +5,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -61,8 +60,7 @@ def get(self, request: Request, project: Project) -> Response: } ) - use_fk = features.has("organizations:project-repository-fk-reads", project.organization) - result = fetch_source_context_from_scm(configs, ctx, use_project_repository_fk=use_fk) + result = fetch_source_context_from_scm(configs, ctx) return Response( { diff --git a/src/sentry/issues/endpoints/serializers.py b/src/sentry/issues/endpoints/serializers.py index c2d34cf91c0846..0a367e6c331b40 100644 --- a/src/sentry/issues/endpoints/serializers.py +++ b/src/sentry/issues/endpoints/serializers.py @@ -8,7 +8,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from sentry import analytics, features +from sentry import analytics from sentry.analytics.events.codeowners_max_length_exceeded import CodeOwnersMaxLengthExceeded from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer from sentry.api.validators.project_codeowners import build_codeowners_associations @@ -97,11 +97,9 @@ def validate_code_mapping_id(self, code_mapping_id: int) -> RepositoryProjectPat project = self.context["project"] try: - if features.has("organizations:project-repository-fk-reads", project.organization): - return RepositoryProjectPathConfig.objects.get( - id=code_mapping_id, project_repository__project=project - ) - return RepositoryProjectPathConfig.objects.get(id=code_mapping_id, project=project) + return RepositoryProjectPathConfig.objects.get( + id=code_mapping_id, project_repository__project=project + ) except RepositoryProjectPathConfig.DoesNotExist: raise serializers.ValidationError("This code mapping does not exist.") diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index 44d122a5f1c024..20082b4ff89ca5 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -16,7 +16,6 @@ from django.utils.translation import gettext_lazy as _ from bitfield import TypedClassBitField -from sentry import features from sentry.backup.dependencies import ImportKind, PrimaryKeyMap from sentry.backup.helpers import ImportFlags from sentry.backup.scopes import ImportScope, RelocationScope @@ -738,12 +737,7 @@ def transfer_to(self, organization: Organization) -> None: # Delete issue ownership objects to prevent them from being stuck on the old org ProjectCodeOwners.objects.filter(project_id=self.id).delete() - if features.has("organizations:project-repository-fk-reads", organization): - RepositoryProjectPathConfig.objects.filter( - project_repository__project_id=self.id - ).delete() - else: - RepositoryProjectPathConfig.objects.filter(project_id=self.id).delete() + RepositoryProjectPathConfig.objects.filter(project_repository__project_id=self.id).delete() ProjectRepository.objects.filter(project_id=self.id).delete() for external_issues in chunked( diff --git a/src/sentry/tasks/codeowners/code_owners_auto_sync.py b/src/sentry/tasks/codeowners/code_owners_auto_sync.py index 0c431e6ac9abaa..6c239b06aa8c56 100644 --- a/src/sentry/tasks/codeowners/code_owners_auto_sync.py +++ b/src/sentry/tasks/codeowners/code_owners_auto_sync.py @@ -5,7 +5,6 @@ from rest_framework.exceptions import NotFound from taskbroker_client.retry import Retry -from sentry import features from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.commit import Commit from sentry.models.organization import Organization @@ -36,24 +35,16 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: commit = Commit.objects.get(id=commit_id) - org = Organization.objects.get(id=commit.organization_id) - use_fk = features.has("organizations:project-repository-fk-reads", org) - if use_fk: - base_qs = RepositoryProjectPathConfig.objects.filter( + code_mappings = list( + RepositoryProjectPathConfig.objects.filter( project_repository__repository_id=commit.repository_id, project_repository__project__organization_id=commit.organization_id, - ).select_related("project", "project_repository__project") - else: - base_qs = RepositoryProjectPathConfig.objects.filter( - repository_id=commit.repository_id, - project__organization_id=commit.organization_id, - ).select_related("project", "project_repository__project") - code_mappings = list( - base_qs.annotate( + ) + .annotate( # By default, we don't create a ProjectOwnership record (bc we treat as a negative cache) when we create ProjectCodeOwners records. ownership_exists=Exists( ProjectOwnership.objects.filter( - project=OuterRef("project"), + project=OuterRef("project_repository__project"), ) ) ) @@ -64,9 +55,9 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: When( ownership_exists=True, then=Subquery( - ProjectOwnership.objects.filter(project=OuterRef("project")).values_list( - "codeowners_auto_sync", flat=True - )[:1], + ProjectOwnership.objects.filter( + project=OuterRef("project_repository__project") + ).values_list("codeowners_auto_sync", flat=True)[:1], ), ), default=True, @@ -80,13 +71,12 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: ) ) .filter(codeowners_auto_sync=True, has_codeowners=True) + .select_related("project_repository__project") ) for code_mapping in code_mappings: try: - codeowner_contents = get_codeowner_contents( - code_mapping, use_project_repository_fk=use_fk - ) + codeowner_contents = get_codeowner_contents(code_mapping) except (NotImplementedError, NotFound): logger.warning( "code_owners_auto_sync.fetch_error", @@ -100,10 +90,7 @@ def code_owners_auto_sync(commit_id: int, **kwargs: Any) -> None: "code_owners_auto_sync.fetch_failed", extra={"commit_id": commit_id, "code_mapping_id": code_mapping.id}, ) - if use_fk: - project = code_mapping.project_repository.project - else: - project = code_mapping.project + project = code_mapping.project_repository.project AutoSyncNotification(project).send() return diff --git a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py index 487a889e9c5747..4d4dcc1e7f68d8 100644 --- a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py +++ b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py @@ -373,11 +373,9 @@ def test_post_existing_code_mapping(self, mock_get_repos: MagicMock) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( - project=self.project, stack_root="/stack/root", source_root="/source/root/wrong", default_branch="master", - repository=self.repo, organization_integration_id=self.organization_integration.id, organization_id=self.organization_integration.organization_id, integration_id=self.organization_integration.integration_id, @@ -396,7 +394,7 @@ def test_post_existing_code_mapping(self, mock_get_repos: MagicMock) -> None: # Both mappings should coexist: the original and the newly derived one mappings = RepositoryProjectPathConfig.objects.filter( - project=self.project, stack_root="/stack/root" + project_repository__project=self.project, stack_root="/stack/root" ) assert mappings.count() == 2 assert set(mappings.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/deletions/test_repository.py b/tests/sentry/deletions/test_repository.py index 51b20179f8ce46..49f4a4ea8f05db 100644 --- a/tests/sentry/deletions/test_repository.py +++ b/tests/sentry/deletions/test_repository.py @@ -126,8 +126,6 @@ def test_codeowners(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) path_config = RepositoryProjectPathConfig.objects.create( - project=project, - repository=repo, stack_root="", source_root="src/packages/store", default_branch="main", diff --git a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py index 70aa75d92b264d..ed8dc00b2165e4 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py @@ -44,8 +44,6 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.config = RepositoryProjectPathConfig.objects.create( - repository_id=self.repo.id, - project_id=self.project.id, organization_integration_id=self.org_integration.id, integration_id=self.org_integration.integration_id, organization_id=self.org_integration.organization_id, diff --git a/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py b/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py index f7b3f5c5bebc95..e32659db4dd665 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_code_mappings_bulk.py @@ -60,11 +60,11 @@ def test_create_single_mapping(self) -> None: assert response.data["mappings"][0]["status"] == "created" config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert config.source_root == "modules/maps/src/main/java/com/example/maps" assert config.default_branch == "main" - assert config.repository == self.repo1 + assert config.project_repository.repository == self.repo1 assert config.organization_id == self.organization.id assert config.automatically_generated is False assert config.project_repository is not None @@ -94,7 +94,12 @@ def test_create_multiple_mappings(self) -> None: assert response.status_code == 200, response.content assert response.data["created"] == 3 assert response.data["updated"] == 0 - assert RepositoryProjectPathConfig.objects.filter(project=self.project1).count() == 3 + assert ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project=self.project1 + ).count() + == 3 + ) def test_update_existing_mapping(self) -> None: self.create_code_mapping( @@ -120,7 +125,7 @@ def test_update_existing_mapping(self) -> None: assert response.data["updated"] == 1 config = RepositoryProjectPathConfig.objects.get( - project=self.project1, + project_repository__project=self.project1, stack_root="com/example/maps", source_root="modules/maps/src/main/java/com/example/maps", ) @@ -291,7 +296,7 @@ def test_missing_default_branch_inferred_from_integration(self) -> None: response = self.client.post(self.url, data=payload, format="json") assert response.status_code == 200, response.content config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/a" + project_repository__project=self.project1, stack_root="com/example/a" ) assert config.default_branch == "develop" @@ -389,7 +394,7 @@ def test_skip_post_save_does_not_leak_to_fetched_instances(self) -> None: carry the suppressed flag, so normal post_save signals fire for them.""" self.make_post() config = RepositoryProjectPathConfig.objects.get( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert config._skip_post_save is False @@ -483,7 +488,7 @@ def test_same_stack_root_different_source_roots_creates_both(self) -> None: assert response.data["updated"] == 0 configs = RepositoryProjectPathConfig.objects.filter( - project=self.project1, stack_root="com/example/maps" + project_repository__project=self.project1, stack_root="com/example/maps" ) assert configs.count() == 2 assert set(configs.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/integrations/perforce/test_code_mapping.py b/tests/sentry/integrations/perforce/test_code_mapping.py index 84ffc5b4e48246..640a9887a87d3f 100644 --- a/tests/sentry/integrations/perforce/test_code_mapping.py +++ b/tests/sentry/integrations/perforce/test_code_mapping.py @@ -55,9 +55,7 @@ def test_code_mapping_depot_root_to_slash(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -97,9 +95,7 @@ def test_code_mapping_with_symbolic_revision_syntax(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -142,9 +138,7 @@ def test_code_mapping_multiple_depots(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) depot_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=depot_repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -159,9 +153,7 @@ def test_code_mapping_multiple_depots(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=myproject_repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="myproject/", @@ -210,9 +202,7 @@ def test_code_mapping_no_match_different_depot(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -249,9 +239,7 @@ def test_code_mapping_abs_path_fallback(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -285,9 +273,7 @@ def test_code_mapping_nested_depot_paths(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/game/project/", @@ -328,9 +314,7 @@ def test_code_mapping_preserves_windows_backslash_conversion(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -386,9 +370,7 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=self.repo, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, stack_root="depot/", @@ -485,9 +467,7 @@ def test_full_flow_with_web_viewer(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping_web = RepositoryProjectPathConfig.objects.create( - project=project_web, organization_id=self.organization.id, - repository=repo_web, organization_integration_id=org_integration_web.id, integration_id=integration_with_web.id, stack_root="depot/", diff --git a/tests/sentry/integrations/perforce/test_stacktrace_link.py b/tests/sentry/integrations/perforce/test_stacktrace_link.py index 7ce134b2c13929..bcc3bfb03bd0b3 100644 --- a/tests/sentry/integrations/perforce/test_stacktrace_link.py +++ b/tests/sentry/integrations/perforce/test_stacktrace_link.py @@ -39,9 +39,7 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=self.repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", @@ -155,9 +153,7 @@ def test_get_stacktrace_config_multiple_code_mappings(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=myproject_repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="myproject/", @@ -220,9 +216,7 @@ def test_get_stacktrace_config_with_web_viewer(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping_web = RepositoryProjectPathConfig.objects.create( - project=project_web, organization_id=self.organization.id, - repository=repo_web, organization_integration_id=org_integration.id, integration_id=integration_with_web.id, stack_root="depot/", @@ -296,9 +290,7 @@ def test_get_stacktrace_config_iteration_count(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) other_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=other_repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="other/", @@ -347,9 +339,7 @@ def test_get_stacktrace_config_stops_on_first_match(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( - project=project2, organization_id=self.organization.id, - repository=myproject_repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", # Same stack_root as depot mapping (but different project) @@ -428,9 +418,7 @@ def test_stacktrace_link_empty_stack_root(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="", @@ -474,9 +462,7 @@ def test_stacktrace_link_with_special_characters_in_path(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", @@ -521,9 +507,7 @@ def test_stacktrace_link_deeply_nested_path(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=self.project, organization_id=self.organization.id, - repository=repo, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, stack_root="depot/", diff --git a/tests/sentry/issues/auto_source_code_config/test_process_event.py b/tests/sentry/issues/auto_source_code_config/test_process_event.py index 02454eb175dd22..62b1159390241a 100644 --- a/tests/sentry/issues/auto_source_code_config/test_process_event.py +++ b/tests/sentry/issues/auto_source_code_config/test_process_event.py @@ -106,11 +106,9 @@ def create_repo_and_code_mapping( defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( - project_id=self.project.id, stack_root=stack_root, source_root=source_root, default_branch=default_branch, - repository=repository, organization_integration_id=organization_integration.id, integration_id=organization_integration.integration_id, organization_id=organization_integration.organization_id, @@ -184,17 +182,20 @@ def _process_and_assert_configuration_changes( ) for expected_cm in expected_new_code_mappings: code_mapping = current_code_mappings.get( - project_id=self.project.id, + project_repository__project_id=self.project.id, stack_root=expected_cm["stack_root"], source_root=expected_cm["source_root"], ) assert code_mapping is not None - assert code_mapping.repository.name == expected_cm["repo_name"] + assert ( + code_mapping.project_repository.repository.name + == expected_cm["repo_name"] + ) assert code_mapping.project_repository is not None assert code_mapping.project_repository.project_id == self.project.id assert ( code_mapping.project_repository.repository_id - == code_mapping.repository_id + == code_mapping.project_repository.repository_id ) else: assert current_code_mappings.count() == starting_code_mappings_count @@ -970,7 +971,7 @@ def test_prevent_creating_duplicate_rules(self) -> None: ) # Both mappings should coexist: the manual one and the auto-created one mappings = RepositoryProjectPathConfig.objects.filter( - project=self.project, stack_root="foo/bar/" + project_repository__project=self.project, stack_root="foo/bar/" ) assert mappings.count() == 2 assert set(mappings.values_list("source_root", flat=True)) == { diff --git a/tests/sentry/migrations/test_1092_backfill_projectrepository.py b/tests/sentry/migrations/test_1092_backfill_projectrepository.py index e132d040d337a1..4bba01b94e6055 100644 --- a/tests/sentry/migrations/test_1092_backfill_projectrepository.py +++ b/tests/sentry/migrations/test_1092_backfill_projectrepository.py @@ -53,49 +53,62 @@ def setup_before_migration(self, apps): ) # Case 1: Auto-generated code mapping only → AUTO_EVENT - RepositoryProjectPathConfig.objects.create( + pr_a = ProjectRepository.objects.create( project=self.proj, repository=self.repo_a, + source=ProjectRepositorySource.AUTO_EVENT, + ) + RepositoryProjectPathConfig.objects.create( organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, stack_root="src/", source_root="src/", automatically_generated=True, + project_repository=pr_a, ) # Case 2: Manual code mapping only → MANUAL - RepositoryProjectPathConfig.objects.create( + pr_b = ProjectRepository.objects.create( project=self.proj, repository=self.repo_b, + source=ProjectRepositorySource.MANUAL, + ) + RepositoryProjectPathConfig.objects.create( organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, stack_root="lib/", source_root="lib/", automatically_generated=False, + project_repository=pr_b, ) # Case 3: Seer preference only → SEER_PREFERENCE - SeerProjectRepository.objects.create( - project=self.proj, repository=self.repo_c, branch_name="main" + pr_c = ProjectRepository.objects.create( + project=self.proj, + repository=self.repo_c, + source=ProjectRepositorySource.SEER_PREFERENCE, ) + SeerProjectRepository.objects.create(project_repository=pr_c, branch_name="main") # Case 4: Both manual code mapping AND Seer preference for same # (project, repo) → SEER_PREFERENCE wins (higher priority). - RepositoryProjectPathConfig.objects.create( + pr_d = ProjectRepository.objects.create( project=self.proj, repository=self.repo_d, + source=ProjectRepositorySource.SEER_PREFERENCE, + ) + RepositoryProjectPathConfig.objects.create( organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, stack_root="app/", source_root="app/", automatically_generated=False, + project_repository=pr_d, ) - SeerProjectRepository.objects.create( - project=self.proj, repository=self.repo_d, branch_name="develop" - ) + SeerProjectRepository.objects.create(project_repository=pr_d, branch_name="develop") # Case 5: Dual-write already created a ProjectRepository row. # The migration should not duplicate it, and should still backfill @@ -106,14 +119,13 @@ def setup_before_migration(self, apps): source=ProjectRepositorySource.MANUAL, ) RepositoryProjectPathConfig.objects.create( - project=self.proj, - repository=self.repo_e, organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, stack_root="pkg/", source_root="pkg/", automatically_generated=True, + project_repository=self.existing_pr, ) def test(self) -> None: @@ -143,7 +155,7 @@ def get_pr(repo): # All RepositoryProjectPathConfig rows have project_repository_id set assert ( RepositoryProjectPathConfig.objects.filter( - project=self.proj, project_repository_id__isnull=True + project_repository__project=self.proj, project_repository_id__isnull=True ).count() == 0 ) @@ -151,18 +163,20 @@ def get_pr(repo): # All SeerProjectRepository rows have project_repository_id set assert ( SeerProjectRepository.objects.filter( - project=self.proj, project_repository_id__isnull=True + project_repository__project=self.proj, project_repository_id__isnull=True ).count() == 0 ) # FK consistency: each row's project_repository points to the right pair - for config in RepositoryProjectPathConfig.objects.filter(project=self.proj): + for config in RepositoryProjectPathConfig.objects.filter( + project_repository__project=self.proj + ): pr = ProjectRepository.objects.get(id=config.project_repository_id) - assert pr.project_id == config.project_id - assert pr.repository_id == config.repository_id + assert pr.project_id == config.project_repository.project_id + assert pr.repository_id == config.project_repository.repository_id - for spr in SeerProjectRepository.objects.filter(project=self.proj): + for spr in SeerProjectRepository.objects.filter(project_repository__project=self.proj): pr = ProjectRepository.objects.get(id=spr.project_repository_id) - assert pr.project_id == spr.project_id - assert pr.repository_id == spr.repository_id + assert pr.project_id == spr.project_repository.project_id + assert pr.repository_id == spr.project_repository.repository_id diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index ff1fb70f4e2cfd..a8d8daf2e56991 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -253,8 +253,6 @@ def test_delete_on_transfer_repository_project_path_configs(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) repository_project_path_config = RepositoryProjectPathConfig.objects.create( - repository=repository, - project=project, organization_integration_id=org_integration.id, organization_id=from_org.id, integration_id=integration.id, @@ -275,7 +273,12 @@ def test_delete_on_transfer_repository_project_path_configs(self) -> None: assert RepositoryProjectPathConfig.objects.filter(organization_id=from_org.id).count() == 0 assert RepositoryProjectPathConfig.objects.filter(organization_id=to_org.id).count() == 0 - assert RepositoryProjectPathConfig.objects.filter(project_id=project.id).count() == 0 + assert ( + RepositoryProjectPathConfig.objects.filter( + project_repository__project_id=project.id + ).count() + == 0 + ) assert ProjectCodeOwners.objects.filter(project_id=project.id).count() == 0 diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 3586340d3456df..5ee70fbad72fad 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1097,8 +1097,6 @@ def test_has_repo_code_mappings_with_mappings(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( - repository=repo, - project=project, organization_integration_id=org_integration.id, integration_id=org_integration.integration_id, organization_id=self.organization.id, diff --git a/tests/sentry/tasks/test_repository.py b/tests/sentry/tasks/test_repository.py index f44b2140e934c4..07ca045e635121 100644 --- a/tests/sentry/tasks/test_repository.py +++ b/tests/sentry/tasks/test_repository.py @@ -48,8 +48,6 @@ def test_deletes_child_relations(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( - project=project, - repository=repo, stack_root="", source_root="src/packages/store", default_branch="main", From f0a643a6b5af9811cf9682cc8eaaed935c669686 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 15:26:33 -0700 Subject: [PATCH 2/5] restore writes --- .../test_organization_derive_code_mappings.py | 2 ++ tests/sentry/deletions/test_repository.py | 2 ++ .../test_organization_code_mapping_details.py | 2 ++ .../perforce/test_code_mapping.py | 20 +++++++++++++++++ .../perforce/test_stacktrace_link.py | 16 ++++++++++++++ .../test_process_event.py | 2 ++ .../test_1092_backfill_projectrepository.py | 22 +++++++++++++++++-- tests/sentry/models/test_project.py | 2 ++ tests/sentry/seer/endpoints/test_seer_rpc.py | 2 ++ tests/sentry/tasks/test_repository.py | 2 ++ 10 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py index 4d4dcc1e7f68d8..6745cff679a860 100644 --- a/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py +++ b/tests/sentry/api/endpoints/issues/test_organization_derive_code_mappings.py @@ -373,6 +373,8 @@ def test_post_existing_code_mapping(self, mock_get_repos: MagicMock) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=self.repo, stack_root="/stack/root", source_root="/source/root/wrong", default_branch="master", diff --git a/tests/sentry/deletions/test_repository.py b/tests/sentry/deletions/test_repository.py index 49f4a4ea8f05db..51b20179f8ce46 100644 --- a/tests/sentry/deletions/test_repository.py +++ b/tests/sentry/deletions/test_repository.py @@ -126,6 +126,8 @@ def test_codeowners(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) path_config = RepositoryProjectPathConfig.objects.create( + project=project, + repository=repo, stack_root="", source_root="src/packages/store", default_branch="main", diff --git a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py index ed8dc00b2165e4..e97c4328e5675c 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_code_mapping_details.py @@ -44,6 +44,8 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.config = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=self.repo, organization_integration_id=self.org_integration.id, integration_id=self.org_integration.integration_id, organization_id=self.org_integration.organization_id, diff --git a/tests/sentry/integrations/perforce/test_code_mapping.py b/tests/sentry/integrations/perforce/test_code_mapping.py index 640a9887a87d3f..1a612cad082e2e 100644 --- a/tests/sentry/integrations/perforce/test_code_mapping.py +++ b/tests/sentry/integrations/perforce/test_code_mapping.py @@ -55,6 +55,8 @@ def test_code_mapping_depot_root_to_slash(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -95,6 +97,8 @@ def test_code_mapping_with_symbolic_revision_syntax(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -138,6 +142,8 @@ def test_code_mapping_multiple_depots(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) depot_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=depot_repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -153,6 +159,8 @@ def test_code_mapping_multiple_depots(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=myproject_repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -202,6 +210,8 @@ def test_code_mapping_no_match_different_depot(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -239,6 +249,8 @@ def test_code_mapping_abs_path_fallback(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -273,6 +285,8 @@ def test_code_mapping_nested_depot_paths(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -314,6 +328,8 @@ def test_code_mapping_preserves_windows_backslash_conversion(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -370,6 +386,8 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=self.repo, organization_id=self.organization.id, organization_integration_id=self.org_integration.id, integration_id=self.integration.id, @@ -467,6 +485,8 @@ def test_full_flow_with_web_viewer(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping_web = RepositoryProjectPathConfig.objects.create( + project=project_web, + repository=repo_web, organization_id=self.organization.id, organization_integration_id=org_integration_web.id, integration_id=integration_with_web.id, diff --git a/tests/sentry/integrations/perforce/test_stacktrace_link.py b/tests/sentry/integrations/perforce/test_stacktrace_link.py index bcc3bfb03bd0b3..5d92d92c41241e 100644 --- a/tests/sentry/integrations/perforce/test_stacktrace_link.py +++ b/tests/sentry/integrations/perforce/test_stacktrace_link.py @@ -39,6 +39,8 @@ def setUp(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) self.code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=self.repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -153,6 +155,8 @@ def test_get_stacktrace_config_multiple_code_mappings(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=myproject_repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -216,6 +220,8 @@ def test_get_stacktrace_config_with_web_viewer(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping_web = RepositoryProjectPathConfig.objects.create( + project=project_web, + repository=repo_web, organization_id=self.organization.id, organization_integration_id=org_integration.id, integration_id=integration_with_web.id, @@ -290,6 +296,8 @@ def test_get_stacktrace_config_iteration_count(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) other_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=other_repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -339,6 +347,8 @@ def test_get_stacktrace_config_stops_on_first_match(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) myproject_mapping = RepositoryProjectPathConfig.objects.create( + project=project2, + repository=myproject_repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -418,6 +428,8 @@ def test_stacktrace_link_empty_stack_root(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -462,6 +474,8 @@ def test_stacktrace_link_with_special_characters_in_path(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, @@ -507,6 +521,8 @@ def test_stacktrace_link_deeply_nested_path(self, mock_check_file): defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repo, organization_id=self.organization.id, organization_integration_id=self.integration.organizationintegration_set.first().id, integration_id=self.integration.id, diff --git a/tests/sentry/issues/auto_source_code_config/test_process_event.py b/tests/sentry/issues/auto_source_code_config/test_process_event.py index 62b1159390241a..647284f5c99855 100644 --- a/tests/sentry/issues/auto_source_code_config/test_process_event.py +++ b/tests/sentry/issues/auto_source_code_config/test_process_event.py @@ -106,6 +106,8 @@ def create_repo_and_code_mapping( defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( + project=self.project, + repository=repository, stack_root=stack_root, source_root=source_root, default_branch=default_branch, diff --git a/tests/sentry/migrations/test_1092_backfill_projectrepository.py b/tests/sentry/migrations/test_1092_backfill_projectrepository.py index 4bba01b94e6055..e5293b0b396d8d 100644 --- a/tests/sentry/migrations/test_1092_backfill_projectrepository.py +++ b/tests/sentry/migrations/test_1092_backfill_projectrepository.py @@ -59,6 +59,8 @@ def setup_before_migration(self, apps): source=ProjectRepositorySource.AUTO_EVENT, ) RepositoryProjectPathConfig.objects.create( + project=self.proj, + repository=self.repo_a, organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, @@ -75,6 +77,8 @@ def setup_before_migration(self, apps): source=ProjectRepositorySource.MANUAL, ) RepositoryProjectPathConfig.objects.create( + project=self.proj, + repository=self.repo_b, organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, @@ -90,7 +94,12 @@ def setup_before_migration(self, apps): repository=self.repo_c, source=ProjectRepositorySource.SEER_PREFERENCE, ) - SeerProjectRepository.objects.create(project_repository=pr_c, branch_name="main") + SeerProjectRepository.objects.create( + project=self.proj, + repository=self.repo_c, + project_repository=pr_c, + branch_name="main", + ) # Case 4: Both manual code mapping AND Seer preference for same # (project, repo) → SEER_PREFERENCE wins (higher priority). @@ -100,6 +109,8 @@ def setup_before_migration(self, apps): source=ProjectRepositorySource.SEER_PREFERENCE, ) RepositoryProjectPathConfig.objects.create( + project=self.proj, + repository=self.repo_d, organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, @@ -108,7 +119,12 @@ def setup_before_migration(self, apps): automatically_generated=False, project_repository=pr_d, ) - SeerProjectRepository.objects.create(project_repository=pr_d, branch_name="develop") + SeerProjectRepository.objects.create( + project=self.proj, + repository=self.repo_d, + project_repository=pr_d, + branch_name="develop", + ) # Case 5: Dual-write already created a ProjectRepository row. # The migration should not duplicate it, and should still backfill @@ -119,6 +135,8 @@ def setup_before_migration(self, apps): source=ProjectRepositorySource.MANUAL, ) RepositoryProjectPathConfig.objects.create( + project=self.proj, + repository=self.repo_e, organization_integration_id=self.oi.id, organization_id=self.org.id, integration_id=self.integration.id, diff --git a/tests/sentry/models/test_project.py b/tests/sentry/models/test_project.py index a8d8daf2e56991..a07697a78ba709 100644 --- a/tests/sentry/models/test_project.py +++ b/tests/sentry/models/test_project.py @@ -253,6 +253,8 @@ def test_delete_on_transfer_repository_project_path_configs(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) repository_project_path_config = RepositoryProjectPathConfig.objects.create( + project=project, + repository=repository, organization_integration_id=org_integration.id, organization_id=from_org.id, integration_id=integration.id, diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 5ee70fbad72fad..2c0c97468cd337 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1097,6 +1097,8 @@ def test_has_repo_code_mappings_with_mappings(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) RepositoryProjectPathConfig.objects.create( + project=project, + repository=repo, organization_integration_id=org_integration.id, integration_id=org_integration.integration_id, organization_id=self.organization.id, diff --git a/tests/sentry/tasks/test_repository.py b/tests/sentry/tasks/test_repository.py index 07ca045e635121..f44b2140e934c4 100644 --- a/tests/sentry/tasks/test_repository.py +++ b/tests/sentry/tasks/test_repository.py @@ -48,6 +48,8 @@ def test_deletes_child_relations(self) -> None: defaults={"source": ProjectRepositorySource.MANUAL}, ) code_mapping = RepositoryProjectPathConfig.objects.create( + project=project, + repository=repo, stack_root="", source_root="src/packages/store", default_branch="main", From 27be940b8c0c1403330c1d13b3064d9b8d57c5c1 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 16:02:10 -0700 Subject: [PATCH 3/5] fix --- .../api/endpoints/organization_code_mappings_bulk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py index 6ace8b30743f67..4c5f2946dbb6ab 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py @@ -267,6 +267,7 @@ def post(self, request: Request, organization: Organization) -> Response: ) defaults = { + "repository": repo, "organization_integration_id": org_integration.id, "organization_id": organization.id, "integration_id": repo.integration_id, From 90bbe5f00c82c07f0bc5eb7d213a2ae926270249 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Thu, 14 May 2026 16:22:30 -0700 Subject: [PATCH 4/5] fix --- .../api/endpoints/organization_code_mappings_bulk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py index 4c5f2946dbb6ab..edaf9854a4be9f 100644 --- a/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py +++ b/src/sentry/integrations/api/endpoints/organization_code_mappings_bulk.py @@ -267,6 +267,7 @@ def post(self, request: Request, organization: Organization) -> Response: ) defaults = { + "project": project, "repository": repo, "organization_integration_id": org_integration.id, "organization_id": organization.id, @@ -290,8 +291,6 @@ def post(self, request: Request, organization: Organization) -> Response: created = False except RepositoryProjectPathConfig.DoesNotExist: config = RepositoryProjectPathConfig( - project=project, - repository=repo, stack_root=mapping["stack_root"], source_root=mapping["source_root"], **defaults, From 925b5ee6424063581b4337939598d124796a06ff Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Fri, 15 May 2026 11:02:01 -0700 Subject: [PATCH 5/5] askfhjasjkhfjakhd --- src/sentry/incidents/serializers/alert_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/incidents/serializers/alert_rule.py b/src/sentry/incidents/serializers/alert_rule.py index 790c35a502d40e..20b65449ce9cbc 100644 --- a/src/sentry/incidents/serializers/alert_rule.py +++ b/src/sentry/incidents/serializers/alert_rule.py @@ -24,6 +24,7 @@ from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer from sentry.api.serializers.rest_framework.environment import EnvironmentField from sentry.api.serializers.rest_framework.project import ProjectField +from sentry.api.utils import to_valid_int_id_list from sentry.incidents.logic import ( CRITICAL_TRIGGER_LABEL, WARNING_TRIGGER_LABEL, @@ -43,7 +44,6 @@ from sentry.snuba.dataset import Dataset from sentry.snuba.models import QuerySubscription, SnubaQueryEventType from sentry.snuba.snuba_query_validator import SnubaQueryValidator -from sentry.workflow_engine.endpoints.utils.ids import to_valid_int_id_list from sentry.workflow_engine.migration_helpers.alert_rule import ( dual_delete_migrated_alert_rule_trigger, dual_update_alert_rule,