diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index 4de82a14e6dcf5..210f06dec57129 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -5,7 +5,7 @@ from collections import defaultdict from collections.abc import Mapping, MutableMapping, Sequence from http import HTTPStatus -from typing import Any, NotRequired, TypedDict +from typing import Any, NotRequired, TypedDict, cast from urllib.parse import urlparse import rest_framework @@ -41,6 +41,7 @@ from sentry.models.groupinbox import GroupInboxRemoveAction, remove_group_from_inbox from sentry.models.grouplink import GroupLink from sentry.models.groupopenperiod import update_group_open_period +from sentry.models.grouprelease import GroupRelease from sentry.models.groupresolution import GroupResolution from sentry.models.groupseen import GroupSeen from sentry.models.groupshare import GroupShare @@ -154,7 +155,31 @@ def get_current_release_version_of_group(group: Group, follows_semver: bool = Fa """ current_release_version = None if follows_semver: - release = greatest_semver_release(group.project) + # Fetch all the release-packages associated with the group. We'll find the largest semver + # version for one of these packages. + release_ids = list( + GroupRelease.objects.filter( + group_id=group.id, + project_id=group.project_id, + ) + .distinct() + .values_list("release_id", flat=True) + ) + + group_packages = cast( + list[str], + list( + Release.objects.filter( + organization_id=group.project.organization_id, + id__in=release_ids, + package__isnull=False, + ) + .distinct() + .values_list("package", flat=True) + ), + ) + + release = greatest_semver_release(group.project, packages=group_packages) if release is not None: current_release_version = release.version else: @@ -537,6 +562,10 @@ def process_group_resolution( # in release resolution_params.update( { + "release": Release.objects.filter( + organization_id=release.organization_id, + version=current_release_version, + ).get(), "type": GroupResolution.Type.in_release, "status": GroupResolution.Status.resolved, } @@ -819,7 +848,11 @@ def get_release_to_resolve_by(project: Project) -> Release | None: follows_semver = follows_semver_versioning_scheme( org_id=project.organization_id, project_id=project.id ) - return greatest_semver_release(project) if follows_semver else most_recent_release(project) + return ( + greatest_semver_release(project, packages=[]) + if follows_semver + else most_recent_release(project) + ) def most_recent_release(project: Project) -> Release | None: @@ -841,14 +874,20 @@ def most_recent_release_matching_commit( ) -def greatest_semver_release(project: Project) -> Release | None: - return get_semver_releases(project).first() +def greatest_semver_release(project: Project, packages: list[str]) -> Release | None: + return get_semver_releases(project, packages).first() -def get_semver_releases(project: Project) -> QuerySet[Release]: +def get_semver_releases(project: Project, packages: list[str]) -> QuerySet[Release]: + query = Release.objects.filter(projects=project, organization_id=project.organization_id) + + # Multiple packages may exist for a single project. If we were able to infer the packages + # associated with an issue we'll include them. + if packages: + query = query.filter(package__in=packages) + return ( - Release.objects.filter(projects=project, organization_id=project.organization_id) - .filter_to_semver() # type: ignore[attr-defined] + query.filter_to_semver() # type: ignore[attr-defined] .annotate_prerelease_column() .order_by(*[f"-{col}" for col in Release.SEMVER_COLS]) ) diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index fe601da410868f..d9236ef9487258 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -3588,6 +3588,66 @@ def test_set_resolved_in_next_release(self) -> None: ) assert activity.data["version"] == "" + def test_set_resolved_in_next_semver_release(self) -> None: + release = Release.objects.create( + organization_id=self.project.organization_id, version="a@1.0.0" + ) + release.add_project(self.project) + + # Smaller than 1.0.0 but more recent. + release2 = Release.objects.create( + organization_id=self.project.organization_id, version="a@0.9.1" + ) + release2.add_project(self.project) + + # Bigger than 1.0.0 and more recent but different package. + release3 = Release.objects.create( + organization_id=self.project.organization_id, version="b@1.0.1" + ) + release3.add_project(self.project) + + # Smaller than 1.0.0, a different package, and associated to the group. This package's + # release should be in contention for largest semver but not selected. + release4 = Release.objects.create( + organization_id=self.project.organization_id, version="c@0.9.9" + ) + release4.add_project(self.project) + + group = self.create_group(status=GroupStatus.UNRESOLVED) + + # Record the release as a group release so the group is scoped to at least one package. + self.create_group_release(self.project, group=group, release=release) + self.create_group_release(self.project, group=group, release=release4) + + self.login_as(user=self.user) + + response = self.get_success_response( + qs_params={"id": group.id}, status="resolved", statusDetails={"inNextRelease": True} + ) + assert response.data["status"] == "resolved" + assert response.data["statusDetails"]["inNextRelease"] + assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id) + assert "activity" in response.data + + group = Group.objects.get(id=group.id) + assert group.status == GroupStatus.RESOLVED + + resolution = GroupResolution.objects.get(group=group) + assert resolution.release == release + assert resolution.type == GroupResolution.Type.in_release + assert resolution.status == GroupResolution.Status.resolved + assert resolution.actor_id == self.user.id + + assert GroupSubscription.objects.filter( + user_id=self.user.id, group=group, is_active=True + ).exists() + + activity = Activity.objects.get( + group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value + ) + assert activity.data["version"] == "" + assert activity.data["current_release_version"] == "a@1.0.0" + def test_set_resolved_in_next_release_legacy(self) -> None: release = Release.objects.create(organization_id=self.project.organization_id, version="a") release.add_project(self.project)