Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sentry/deletions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def load_defaults(manager: DeletionTaskManager) -> None:
manager.register(models.ReleaseHeadCommit, BulkModelDeletionTask)
manager.register(models.ReleaseProject, BulkModelDeletionTask)
manager.register(models.ReleaseProjectEnvironment, BulkModelDeletionTask)
manager.register(models.ProjectRepository, defaults.ProjectRepositoryDeletionTask)
manager.register(models.Repository, defaults.RepositoryDeletionTask)
manager.register(models.Rule, defaults.RuleDeletionTask)
manager.register(models.SavedSearch, BulkModelDeletionTask)
Expand Down
1 change: 1 addition & 0 deletions src/sentry/deletions/defaults/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .platform_external_issue import * # noqa: F401,F403
from .preprod_artifact import * # noqa: F401,F403
from .project import * # noqa: F401,F403
from .projectrepository import * # noqa: F401,F403
from .pullrequest import * # noqa: F401,F403
from .querysubscription import * # noqa: F401,F403
from .release import * # noqa: F401,F403
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/deletions/defaults/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def get_child_relations(self, instance: Project) -> list[BaseRelation]:
from sentry.models.projectbookmark import ProjectBookmark
from sentry.models.projectcodeowners import ProjectCodeOwners
from sentry.models.projectkey import ProjectKey
from sentry.models.projectrepository import ProjectRepository
from sentry.models.projectteam import ProjectTeam
from sentry.models.promptsactivity import PromptsActivity
from sentry.models.release_threshold import ReleaseThreshold
Expand Down Expand Up @@ -75,6 +76,7 @@ def get_child_relations(self, instance: Project) -> list[BaseRelation]:
ProjectCodeOwners,
ReplayRecordingSegment,
RepositoryProjectPathConfig,
ProjectRepository,
ServiceHookProject,
ServiceHook,
UserReport,
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/deletions/defaults/projectrepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sentry.deletions.base import BaseRelation, ModelDeletionTask, ModelRelation
from sentry.integrations.models.repository_project_path_config import (
RepositoryProjectPathConfig,
)
from sentry.models.projectrepository import ProjectRepository
from sentry.seer.models.project_repository import SeerProjectRepository


class ProjectRepositoryDeletionTask(ModelDeletionTask[ProjectRepository]):
def get_child_relations(self, instance: ProjectRepository) -> list[BaseRelation]:
return [
ModelRelation(RepositoryProjectPathConfig, {"project_repository_id": instance.id}),
ModelRelation(SeerProjectRepository, {"project_repository_id": instance.id}),
]
9 changes: 7 additions & 2 deletions src/sentry/deletions/defaults/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ def should_proceed(self, instance: Repository) -> bool:
return instance.status in {ObjectStatus.PENDING_DELETION, ObjectStatus.DELETION_IN_PROGRESS}

def get_child_relations(self, instance: Repository) -> list[BaseRelation]:
from sentry.models.projectrepository import ProjectRepository
from sentry.seer.models.project_repository import SeerProjectRepository

return _get_repository_child_relations(instance) + [
# We only delete SeerProjectRepository when the repo is actually deleted,
# but not when it's hidden/disabled (repository_cascade_delete_on_hide).
# Only delete ProjectRepository and SeerProjectRepository when the
# repo is actually deleted, not when it's hidden/disabled
# (repository_cascade_delete_on_hide). Seer preferences and
# project-repo links should survive a hide so they're restored
# if the repo is re-enabled.
ModelRelation(ProjectRepository, {"repository_id": instance.id}),
ModelRelation(SeerProjectRepository, {"repository_id": instance.id}),
]

Expand Down
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:preprod-snapshots", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Enables PR page
manager.add("organizations:pr-page", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Use ProjectRepository FK for code mapping and Seer repo queries
manager.add("organizations:project-repository-fk-reads", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enables the playstation ingestion in relay
manager.add("organizations:relay-playstation-ingestion", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
# Enables new error processing pipeline in Relay.
Expand Down
24 changes: 20 additions & 4 deletions src/sentry/integrations/services/repository/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db import IntegrityError, router, transaction
from django.utils import timezone

from sentry import features
from sentry.api.serializers import serialize
from sentry.constants import ObjectStatus
from sentry.db.postgres.transactions import enforce_constraints
Expand All @@ -17,7 +18,9 @@
from sentry.models.code_review_event import CodeReviewEvent
from sentry.models.commit import Commit
from sentry.models.options.project_option import ProjectOption
from sentry.models.organization import Organization
from sentry.models.projectcodeowners import ProjectCodeOwners
from sentry.models.projectrepository import ProjectRepository
from sentry.models.pullrequest import PullRequest
from sentry.models.repository import Repository
from sentry.seer.models.project_repository import SeerProjectRepository
Expand Down Expand Up @@ -241,10 +244,17 @@ def disassociate_organization_integration(
Repository.objects.filter(id__in=repo_ids).update(integration_id=None)

# Delete Seer preferences for this repository
SeerProjectRepository.objects.filter(
repository_id__in=repo_ids,
project__organization_id=organization_id,
).delete()
org = Organization.objects.get(id=organization_id)
Comment thread
wedamija marked this conversation as resolved.
if features.has("organizations:project-repository-fk-reads", org):
SeerProjectRepository.objects.filter(
project_repository__repository_id__in=repo_ids,
project_repository__project__organization_id=organization_id,
).delete()
else:
SeerProjectRepository.objects.filter(
repository_id__in=repo_ids,
project__organization_id=organization_id,
).delete()

# Delete Code Owners with a Code Mapping using the OrganizationIntegration
ProjectCodeOwners.objects.filter(
Expand All @@ -257,6 +267,12 @@ def disassociate_organization_integration(
RepositoryProjectPathConfig.objects.filter(
organization_integration_id=organization_integration_id
).delete()
# Delete project-repo links for the disconnected repos
if repo_ids:
ProjectRepository.objects.filter(
repository_id__in=repo_ids,
project__organization_id=organization_id,
).delete()

# Clear automation_handoff project options that reference this integration.
affected_project_ids = ProjectOption.objects.filter(
Expand Down
4 changes: 4 additions & 0 deletions src/sentry/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ def transfer_to(self, organization: Organization) -> None:
ProjectCodeOwners.objects.filter(project_id=self.id).delete()
RepositoryProjectPathConfig.objects.filter(project_id=self.id).delete()

from sentry.models.projectrepository import ProjectRepository

ProjectRepository.objects.filter(project_id=self.id).delete()

for external_issues in chunked(
RangeQuerySetWrapper(
ExternalIssue.objects.filter(organization_id=old_org_id, id__in=linked_groups),
Expand Down
95 changes: 74 additions & 21 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,18 @@
list(Project.objects.select_for_update().filter(id__in=project_ids).order_by("id"))

# Only delete SeerProjectRepository for active repos.
SeerProjectRepository.objects.filter(
project_id__in=project_ids, repository__status=ObjectStatus.ACTIVE
).delete()
if features.has(
"organizations:project-repository-fk-reads",
project_preferences[0][0].organization,
):
SeerProjectRepository.objects.filter(
project_repository__project_id__in=project_ids,
project_repository__repository__status=ObjectStatus.ACTIVE,
).delete()
Comment thread
wedamija marked this conversation as resolved.
else:
SeerProjectRepository.objects.filter(
project_id__in=project_ids, repository__status=ObjectStatus.ACTIVE
).delete()

all_repo_ids = {
repo_def.repository_id
Expand Down Expand Up @@ -534,9 +543,13 @@
# Create branch overrides using the created project repos.
overrides_to_create: list[SeerProjectRepositoryBranchOverride] = []
for seer_project_repo in created_project_repos:
for override in overrides_by_key.get(
(seer_project_repo.project_id, seer_project_repo.repository_id), []
):
pr = seer_project_repo.project_repository
key = (
(pr.project_id, pr.repository_id)
if pr is not None
else (seer_project_repo.project_id, seer_project_repo.repository_id)
)
for override in overrides_by_key.get(key, []):
overrides_to_create.append(
SeerProjectRepositoryBranchOverride(
seer_project_repository=seer_project_repo,
Expand Down Expand Up @@ -595,11 +608,16 @@

def build_repo_definition_from_project_repo(
seer_project_repo: SeerProjectRepository,
use_project_repository_fk: bool = False,
) -> SeerRepoDefinition | None:
"""Build a SeerRepoDefinition from a SeerProjectRepository with its joined Repository.

Returns None if Repository name is invalid."""
repo = seer_project_repo.repository
if use_project_repository_fk:
pr = seer_project_repo.project_repository
repo = pr.repository if pr is not None else seer_project_repo.repository
else:
repo = seer_project_repo.repository
repo_name_sections = repo.name.split("/")
if len(repo_name_sections) < 2:
sentry_sdk.capture_exception(ValueError(f"Invalid repository name format: {repo.name}"))
Expand Down Expand Up @@ -647,17 +665,33 @@

def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference:
"""Read a single project's Seer preferences from Sentry DB."""
seer_project_repo_qs = (
SeerProjectRepository.objects.filter(
project=project, repository__status=ObjectStatus.ACTIVE
use_fk = features.has("organizations:project-repository-fk-reads", project.organization)
if use_fk:
seer_project_repo_qs = (
SeerProjectRepository.objects.filter(
project_repository__project=project,
project_repository__repository__status=ObjectStatus.ACTIVE,
)
.select_related("project_repository", "project_repository__repository")
Comment thread
cursor[bot] marked this conversation as resolved.
.prefetch_related("branch_overrides")
)
else:
seer_project_repo_qs = (
SeerProjectRepository.objects.filter(
project=project, repository__status=ObjectStatus.ACTIVE
)
.select_related("repository", "project_repository", "project_repository__repository")
.prefetch_related("branch_overrides")
)
.select_related("repository")
.prefetch_related("branch_overrides")
)
repo_definitions = [
repo_def
for project_repo in seer_project_repo_qs
if (repo_def := build_repo_definition_from_project_repo(project_repo)) is not None
if (
repo_def := build_repo_definition_from_project_repo(
project_repo, use_project_repository_fk=use_fk
)
)
is not None
]

return SeerProjectPreference(
Expand All @@ -679,17 +713,29 @@

projects = list(Project.objects.filter(id__in=project_ids, organization_id=organization_id))

org = Organization.objects.get(id=organization_id)

Check warning on line 716 in src/sentry/seer/autofix/utils.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

Organization.objects.get without DoesNotExist handling raises on deleted/stale org

`Organization.objects.get(id=organization_id)` will raise `Organization.DoesNotExist` if the org is missing or deleted, breaking all callers of `bulk_read_preferences_from_sentry_db` (RPC `bulk_get_project_preferences`, autofix tasks, night-shift cron, endpoints). The previous code never required loading the Organization. Use `Organization.objects.filter(id=organization_id).first()` and default `use_fk` to False if missing, or fetch via the first project's `organization` (already loaded).
Comment thread
sentry-warden[bot] marked this conversation as resolved.
repo_definitions_by_project: defaultdict[int, list[SeerRepoDefinition]] = defaultdict(list)
for project_repo in (
SeerProjectRepository.objects.filter(
use_fk = features.has("organizations:project-repository-fk-reads", org)
if use_fk:
seer_repo_qs = SeerProjectRepository.objects.filter(
project_repository__project_id__in=project_ids,
project_repository__repository__status=ObjectStatus.ACTIVE,
).select_related("project_repository", "project_repository__repository")
Comment thread
sentry[bot] marked this conversation as resolved.
else:
seer_repo_qs = SeerProjectRepository.objects.filter(
project_id__in=project_ids, repository__status=ObjectStatus.ACTIVE
).select_related("repository", "project_repository", "project_repository__repository")
for seer_repo in seer_repo_qs.prefetch_related("branch_overrides"):
repo_def = build_repo_definition_from_project_repo(
seer_repo, use_project_repository_fk=use_fk
)
.select_related("repository")
.prefetch_related("branch_overrides")
):
repo_def = build_repo_definition_from_project_repo(project_repo)
if repo_def is not None:
repo_definitions_by_project[project_repo.project_id].append(repo_def)
if use_fk:
pr = seer_repo.project_repository
pid = pr.project_id if pr is not None else seer_repo.project_id
else:
pid = seer_repo.project_id
repo_definitions_by_project[pid].append(repo_def)

# get_value_bulk_id returns None for missing options, unlike project.get_option
# which automatically falls back to the registered well-known key default.
Expand Down Expand Up @@ -798,6 +844,13 @@

def has_project_connected_repos(organization: Organization, project: Project) -> bool:
"""Check if a project has connected repositories for Seer automation."""
if features.has("organizations:project-repository-fk-reads", organization):
return SeerProjectRepository.objects.filter(
project_repository__project=project,
project_repository__project__organization_id=organization.id,
project_repository__project__status=ObjectStatus.ACTIVE,
project_repository__repository__status=ObjectStatus.ACTIVE,
).exists()
return SeerProjectRepository.objects.filter(
project=project,
project__organization_id=organization.id,
Expand Down
7 changes: 7 additions & 0 deletions src/sentry/seer/code_review/contributor_seats.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ def _is_autofix_enabled_for_repo(organization: Organization, repository_id: int)
this repository, ie, if any project has this repository configured
in Seer preferences.
"""
if features.has("organizations:project-repository-fk-reads", organization):
return SeerProjectRepository.objects.filter(
project_repository__repository_id=repository_id,
project_repository__project__organization_id=organization.id,
project_repository__project__status=ObjectStatus.ACTIVE,
project_repository__repository__status=ObjectStatus.ACTIVE,
).exists()
return SeerProjectRepository.objects.filter(
repository_id=repository_id,
project__organization_id=organization.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def is_autofix_enabled(organization: Organization) -> bool:
Check if autofix/RCA is enabled for any active project in the organization,
ie, if any project has repositories configured in Seer preferences.
"""
if features.has("organizations:project-repository-fk-reads", organization):
return SeerProjectRepository.objects.filter(
project_repository__project__organization_id=organization.id,
project_repository__project__status=ObjectStatus.ACTIVE,
project_repository__repository__status=ObjectStatus.ACTIVE,
).exists()
return SeerProjectRepository.objects.filter(
project__organization_id=organization.id,
project__status=ObjectStatus.ACTIVE,
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/tasks/seer/night_shift/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,13 @@ def schedule_night_shift(
seer_org_ids: set[int] = set()
for spr in RangeQuerySetWrapper[SeerProjectRepository](
SeerProjectRepository.objects.filter(project__status=ObjectStatus.ACTIVE).select_related(
"project"
"project", "project_repository__project"
),
step=1000,
):
seer_org_ids.add(spr.project.organization_id)
pr = spr.project_repository
project = pr.project if pr is not None else spr.project
seer_org_ids.add(project.organization_id)

logger.info(
"night_shift.schedule_org_ids_collected",
Expand Down
29 changes: 29 additions & 0 deletions tests/sentry/seer/autofix/test_autofix_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,21 @@ def test_returns_true_when_at_least_one_active_repo(self):

assert has_project_connected_repos(self.organization, self.project) is True

def test_returns_true_via_project_repository_fk(self):
repo = self.create_repo(
project=self.project,
provider="integrations:github",
external_id="789",
name="owner/fk-repo",
)
pr = ProjectRepository.objects.create(project=self.project, repository=repo)
SeerProjectRepository.objects.create(
project=self.project, repository=repo, project_repository=pr
)

with self.feature("organizations:project-repository-fk-reads"):
assert has_project_connected_repos(self.organization, self.project) is True


class TestDeduplicateRepositories(TestCase):
def test_keys_by_provider_and_external_id(self) -> None:
Expand Down Expand Up @@ -1135,6 +1150,20 @@ def test_project_with_repos_only(self):
assert result.automated_run_stopping_point == "code_changes"
assert result.automation_handoff is None

def test_reads_via_project_repository_fk(self):
pr = ProjectRepository.objects.create(project=self.project, repository=self.repo)
SeerProjectRepository.objects.create(
project=self.project,
repository=self.repo,
project_repository=pr,
branch_name="main",
)

with self.feature("organizations:project-repository-fk-reads"):
result = read_preference_from_sentry_db(self.project)
assert len(result.repositories) == 1
assert result.repositories[0].branch_name == "main"

def test_autofix_automation_tuning_default(self):
SeerProjectRepository.objects.create(
project=self.project, repository=self.repo, branch_name="main"
Expand Down
10 changes: 10 additions & 0 deletions tests/sentry/seer/code_review/test_contributor_seats.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
OrganizationContributors,
)
from sentry.models.project import Project
from sentry.models.projectrepository import ProjectRepository
from sentry.seer.code_review.contributor_seats import (
_is_autofix_enabled_for_repo,
should_increment_contributor_seat,
Expand Down Expand Up @@ -68,6 +69,15 @@ def test_repo_is_inactive(self) -> None:

assert _is_autofix_enabled_for_repo(self.organization, self.repo.id) is False

def test_returns_true_via_project_repository_fk(self) -> None:
pr = ProjectRepository.objects.create(project=self.project, repository=self.repo)
SeerProjectRepository.objects.create(
project=self.project, repository=self.repo, project_repository=pr
)

with self.feature("organizations:project-repository-fk-reads"):
assert _is_autofix_enabled_for_repo(self.organization, self.repo.id) is True


class ShouldIncrementContributorSeatTest(TestCase):
def setUp(self) -> None:
Expand Down
Loading