Skip to content

Commit 73bbb6f

Browse files
authored
fix(releases): Normalize both release values to equal one another (#101184)
Normalize resolve-in-next-release release values between activity data and group resolution params. Also, fetches the latest semver release scoped to the packages present on the group. Previously when resolving in the next semver release the `release` stored on the group resolution was the most recent time-ordered release. This PR changes that so we're setting the latest semver release as the `release` value. But some projects can have multiple packages. Either old packages no longer in use or concurrent packages with their own ordering characteristics. By filtering by the package we force the group resolution to only consider releases relevant to the problem being solved.
1 parent 7d2afd7 commit 73bbb6f

File tree

3 files changed

+119
-8
lines changed

3 files changed

+119
-8
lines changed

src/sentry/api/helpers/group_index/update.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from sentry.models.project import Project
5050
from sentry.models.release import Release, follows_semver_versioning_scheme
5151
from sentry.notifications.types import SUBSCRIPTION_REASON_MAP, GroupSubscriptionReason
52+
from sentry.releases.use_cases.release import fetch_semver_packages_for_group
5253
from sentry.signals import issue_resolved
5354
from sentry.types.activity import ActivityType
5455
from sentry.types.actor import Actor, ActorType
@@ -154,7 +155,14 @@ def get_current_release_version_of_group(group: Group, follows_semver: bool = Fa
154155
"""
155156
current_release_version = None
156157
if follows_semver:
157-
release = greatest_semver_release(group.project)
158+
# Fetch all the release-packages associated with the group. We'll find the largest semver
159+
# version for one of these packages.
160+
group_packages = fetch_semver_packages_for_group(
161+
organization_id=group.project.organization_id,
162+
project_id=group.project_id,
163+
group_id=group.id,
164+
)
165+
release = greatest_semver_release(group.project, packages=group_packages)
158166
if release is not None:
159167
current_release_version = release.version
160168
else:
@@ -537,6 +545,10 @@ def process_group_resolution(
537545
# in release
538546
resolution_params.update(
539547
{
548+
"release": Release.objects.filter(
549+
organization_id=release.organization_id,
550+
version=current_release_version,
551+
).get(),
540552
"type": GroupResolution.Type.in_release,
541553
"status": GroupResolution.Status.resolved,
542554
}
@@ -819,7 +831,11 @@ def get_release_to_resolve_by(project: Project) -> Release | None:
819831
follows_semver = follows_semver_versioning_scheme(
820832
org_id=project.organization_id, project_id=project.id
821833
)
822-
return greatest_semver_release(project) if follows_semver else most_recent_release(project)
834+
return (
835+
greatest_semver_release(project, packages=[])
836+
if follows_semver
837+
else most_recent_release(project)
838+
)
823839

824840

825841
def most_recent_release(project: Project) -> Release | None:
@@ -841,14 +857,20 @@ def most_recent_release_matching_commit(
841857
)
842858

843859

844-
def greatest_semver_release(project: Project) -> Release | None:
845-
return get_semver_releases(project).first()
860+
def greatest_semver_release(project: Project, packages: list[str]) -> Release | None:
861+
return get_semver_releases(project, packages).first()
862+
846863

864+
def get_semver_releases(project: Project, packages: list[str]) -> QuerySet[Release]:
865+
query = Release.objects.filter(projects=project, organization_id=project.organization_id)
866+
867+
# Multiple packages may exist for a single project. If we were able to infer the packages
868+
# associated with an issue we'll include them.
869+
if packages:
870+
query = query.filter(package__in=packages)
847871

848-
def get_semver_releases(project: Project) -> QuerySet[Release]:
849872
return (
850-
Release.objects.filter(projects=project, organization_id=project.organization_id)
851-
.filter_to_semver() # type: ignore[attr-defined]
873+
query.filter_to_semver() # type: ignore[attr-defined]
852874
.annotate_prerelease_column()
853875
.order_by(*[f"-{col}" for col in Release.SEMVER_COLS])
854876
)

src/sentry/releases/use_cases/release.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import defaultdict
22
from collections.abc import Callable, Iterable, Mapping
33
from datetime import datetime, timezone
4-
from typing import Any
4+
from typing import Any, cast
55

66
import sentry_sdk
77
from django.contrib.auth.models import AnonymousUser
@@ -16,6 +16,7 @@
1616
from sentry.models.commit import Commit
1717
from sentry.models.commitauthor import CommitAuthor
1818
from sentry.models.deploy import Deploy
19+
from sentry.models.grouprelease import GroupRelease
1920
from sentry.models.project import Project
2021
from sentry.models.projectplatform import ProjectPlatform
2122
from sentry.models.release import Release
@@ -435,3 +436,31 @@ def fetch_project_platforms(project_ids: Iterable[int]) -> list[tuple[int, str]]
435436
"project_id", "platform"
436437
)
437438
)
439+
440+
441+
def fetch_semver_packages_for_group(
442+
organization_id: int, project_id: int, group_id: int
443+
) -> list[str]:
444+
"""Fetch a unique list of semver release packages associated with the group."""
445+
release_ids = (
446+
GroupRelease.objects.filter(
447+
group_id=group_id,
448+
project_id=project_id,
449+
)
450+
.distinct()
451+
.values_list("release_id", flat=True)
452+
)
453+
454+
return cast(
455+
list[str],
456+
list(
457+
Release.objects.filter_to_semver()
458+
.filter(
459+
organization_id=organization_id,
460+
id__in=release_ids,
461+
package__isnull=False,
462+
)
463+
.distinct()
464+
.values_list("package", flat=True)
465+
),
466+
)

tests/sentry/issues/endpoints/test_organization_group_index.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3588,6 +3588,66 @@ def test_set_resolved_in_next_release(self) -> None:
35883588
)
35893589
assert activity.data["version"] == ""
35903590

3591+
def test_set_resolved_in_next_semver_release(self) -> None:
3592+
release = Release.objects.create(
3593+
organization_id=self.project.organization_id, version="[email protected]"
3594+
)
3595+
release.add_project(self.project)
3596+
3597+
# Smaller than 1.0.0 but more recent.
3598+
release2 = Release.objects.create(
3599+
organization_id=self.project.organization_id, version="[email protected]"
3600+
)
3601+
release2.add_project(self.project)
3602+
3603+
# Bigger than 1.0.0 and more recent but different package.
3604+
release3 = Release.objects.create(
3605+
organization_id=self.project.organization_id, version="[email protected]"
3606+
)
3607+
release3.add_project(self.project)
3608+
3609+
# Smaller than 1.0.0, a different package, and associated to the group. This package's
3610+
# release should be in contention for largest semver but not selected.
3611+
release4 = Release.objects.create(
3612+
organization_id=self.project.organization_id, version="[email protected]"
3613+
)
3614+
release4.add_project(self.project)
3615+
3616+
group = self.create_group(status=GroupStatus.UNRESOLVED)
3617+
3618+
# Record the release as a group release so the group is scoped to at least one package.
3619+
self.create_group_release(self.project, group=group, release=release)
3620+
self.create_group_release(self.project, group=group, release=release4)
3621+
3622+
self.login_as(user=self.user)
3623+
3624+
response = self.get_success_response(
3625+
qs_params={"id": group.id}, status="resolved", statusDetails={"inNextRelease": True}
3626+
)
3627+
assert response.data["status"] == "resolved"
3628+
assert response.data["statusDetails"]["inNextRelease"]
3629+
assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
3630+
assert "activity" in response.data
3631+
3632+
group = Group.objects.get(id=group.id)
3633+
assert group.status == GroupStatus.RESOLVED
3634+
3635+
resolution = GroupResolution.objects.get(group=group)
3636+
assert resolution.release == release
3637+
assert resolution.type == GroupResolution.Type.in_release
3638+
assert resolution.status == GroupResolution.Status.resolved
3639+
assert resolution.actor_id == self.user.id
3640+
3641+
assert GroupSubscription.objects.filter(
3642+
user_id=self.user.id, group=group, is_active=True
3643+
).exists()
3644+
3645+
activity = Activity.objects.get(
3646+
group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
3647+
)
3648+
assert activity.data["version"] == ""
3649+
assert activity.data["current_release_version"] == "[email protected]"
3650+
35913651
def test_set_resolved_in_next_release_legacy(self) -> None:
35923652
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
35933653
release.add_project(self.project)

0 commit comments

Comments
 (0)