Skip to content

Commit 43c6e3c

Browse files
authored
feat(issues): adds backend for "resolve in upcoming release" (#70990)
this pr adds the backend needed to support "resolve in upcoming release" to the ProjectGroupIndex endpoint. this differs from "resolve in next release" because it no longer does the check for if there is an immediate next release to the current release of the issue. It will still create the `GroupResolution` object which will eventually be cleared in `clear_expired_resolutions` but this will now be the only way for it to be resolved, only when a new release is created.
1 parent 0ccac58 commit 43c6e3c

File tree

6 files changed

+146
-4
lines changed

6 files changed

+146
-4
lines changed

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ def update_groups(
225225
)
226226
if user_options:
227227
self_assign_issue = user_options[0].value
228-
229228
if search_fn and not group_ids:
230229
try:
231230
cursor_result, _ = search_fn(
@@ -300,6 +299,34 @@ def update_groups(
300299
res_type = GroupResolution.Type.in_next_release
301300
res_type_str = "in_next_release"
302301
res_status = GroupResolution.Status.pending
302+
elif status_details.get("inUpcomingRelease"):
303+
if len(projects) > 1:
304+
return Response(
305+
{"detail": "Cannot set resolved in upcoming release for multiple projects."},
306+
status=400,
307+
)
308+
release = (
309+
status_details.get("inUpcomingRelease")
310+
or Release.objects.filter(
311+
projects=projects[0], organization_id=projects[0].organization_id
312+
)
313+
.extra(select={"sort": "COALESCE(date_released, date_added)"})
314+
.order_by("-sort")[0]
315+
)
316+
activity_type = ActivityType.SET_RESOLVED_IN_RELEASE.value
317+
activity_data = {"version": ""}
318+
319+
serialized_user = user_service.serialize_many(
320+
filter=dict(user_ids=[user.id]), as_user=user
321+
)
322+
new_status_details = {
323+
"inUpcomingRelease": True,
324+
}
325+
if serialized_user:
326+
new_status_details["actor"] = serialized_user[0]
327+
res_type = GroupResolution.Type.in_upcoming_release
328+
res_type_str = "in_upcoming_release"
329+
res_status = GroupResolution.Status.pending
303330
elif status_details.get("inRelease"):
304331
# TODO(jess): We could update validation to check if release
305332
# applies to multiple projects, but I think we agreed to punt
@@ -606,6 +633,7 @@ def update_groups(
606633
if res_type in (
607634
GroupResolution.Type.in_next_release,
608635
GroupResolution.Type.in_release,
636+
GroupResolution.Type.in_upcoming_release,
609637
):
610638
result["activity"] = serialize(
611639
Activity.objects.get_activities_for_group(

src/sentry/api/helpers/group_index/validators/status_details.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from rest_framework import serializers
22

3+
from sentry import features
34
from sentry.models.release import Release
45

56
from . import InCommitValidator
67

78

89
class StatusDetailsValidator(serializers.Serializer):
910
inNextRelease = serializers.BooleanField()
11+
inUpcomingRelease = serializers.BooleanField()
1012
inRelease = serializers.CharField()
1113
inCommit = InCommitValidator(required=False)
1214
ignoreDuration = serializers.IntegerField()
@@ -54,3 +56,19 @@ def validate_inNextRelease(self, value: bool) -> "Release":
5456
raise serializers.ValidationError(
5557
"No release data present in the system to form a basis for 'Next Release'"
5658
)
59+
60+
def validate_inUpcomingRelease(self, value: bool) -> "Release":
61+
project = self.context["project"]
62+
63+
if not features.has("organizations:resolve-in-upcoming-release", project.organization):
64+
raise serializers.ValidationError(
65+
"Your organization does not have access to this feature."
66+
)
67+
try:
68+
return (
69+
Release.objects.filter(projects=project, organization_id=project.organization_id)
70+
.extra(select={"sort": "COALESCE(date_released, date_added)"})
71+
.order_by("-sort")[0]
72+
)
73+
except IndexError:
74+
raise serializers.ValidationError("No release data present in the system.")

src/sentry/models/groupresolution.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class GroupResolution(Model):
3030
class Type:
3131
in_release = 0
3232
in_next_release = 1
33+
in_upcoming_release = 2
3334

3435
class Status:
3536
pending = 0

src/sentry/tasks/clear_expired_resolutions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def clear_expired_resolutions(release_id):
2020
the system that any pending resolutions older than the given release can now
2121
be safely transitioned to resolved.
2222
23-
This is currently only used for ``in_next_release`` resolutions.
23+
This is currently only used for ``in_next_release`` and ``in_upcoming_release`` resolutions.
2424
"""
2525
try:
2626
release = Release.objects.get(id=release_id)
@@ -29,7 +29,9 @@ def clear_expired_resolutions(release_id):
2929

3030
resolution_list = list(
3131
GroupResolution.objects.filter(
32-
Q(type=GroupResolution.Type.in_next_release) | Q(type__isnull=True),
32+
Q(type=GroupResolution.Type.in_next_release)
33+
| Q(type__isnull=True)
34+
| Q(type=GroupResolution.Type.in_upcoming_release),
3335
release__projects__in=[p.id for p in release.projects.all()],
3436
release__date_added__lt=release.date_added,
3537
status=GroupResolution.Status.pending,

tests/sentry/issues/endpoints/test_organization_group_index.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5085,6 +5085,22 @@ def test_update_priority_no_change(self) -> None:
50855085
group=group2, type=ActivityType.SET_PRIORITY.value, user_id=self.user.id
50865086
).exists()
50875087

5088+
def test_resolved_in_upcoming_release_multiple_projects(self) -> None:
5089+
project_2 = self.create_project(slug="foo")
5090+
group1 = self.create_group(status=GroupStatus.UNRESOLVED)
5091+
group2 = self.create_group(status=GroupStatus.UNRESOLVED, project=project_2)
5092+
5093+
self.login_as(user=self.user)
5094+
response = self.get_response(
5095+
qs_params={
5096+
"id": [group1.id, group2.id],
5097+
"statd": "resolved",
5098+
"statusDetails": {"inUpcomingRelease": True},
5099+
}
5100+
)
5101+
5102+
assert response.status_code == 400
5103+
50885104

50895105
class GroupDeleteTest(APITestCase, SnubaTestCase):
50905106
endpoint = "sentry-api-0-organization-group-index"

tests/snuba/api/endpoints/test_project_group_index.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from sentry.models.release import Release
3030
from sentry.silo.base import SiloMode
3131
from sentry.testutils.cases import APITestCase, SnubaTestCase
32-
from sentry.testutils.helpers import parse_link_header
32+
from sentry.testutils.helpers import parse_link_header, with_feature
3333
from sentry.testutils.helpers.datetime import before_now, iso_format
3434
from sentry.testutils.silo import assume_test_silo_mode
3535
from sentry.types.activity import ActivityType
@@ -791,6 +791,83 @@ def test_set_resolved_in_next_release_legacy(self):
791791
)
792792
assert activity.data["version"] == ""
793793

794+
@with_feature("organizations:resolve-in-upcoming-release")
795+
def test_set_resolved_in_upcoming_release(self):
796+
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
797+
release.add_project(self.project)
798+
799+
group = self.create_group(status=GroupStatus.UNRESOLVED)
800+
801+
self.login_as(user=self.user)
802+
803+
url = f"{self.path}?id={group.id}"
804+
response = self.client.put(
805+
url,
806+
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
807+
format="json",
808+
)
809+
assert response.status_code == 200
810+
assert response.data["status"] == "resolved"
811+
assert response.data["statusDetails"]["inUpcomingRelease"]
812+
assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
813+
assert "activity" in response.data
814+
815+
group = Group.objects.get(id=group.id)
816+
assert group.status == GroupStatus.RESOLVED
817+
818+
resolution = GroupResolution.objects.get(group=group)
819+
assert resolution.release == release
820+
assert resolution.type == GroupResolution.Type.in_upcoming_release
821+
assert resolution.status == GroupResolution.Status.pending
822+
assert resolution.actor_id == self.user.id
823+
824+
assert GroupSubscription.objects.filter(
825+
user_id=self.user.id, group=group, is_active=True
826+
).exists()
827+
828+
activity = Activity.objects.get(
829+
group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
830+
)
831+
assert activity.data["version"] == ""
832+
833+
def test_upcoming_release_flag_validation(self):
834+
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
835+
release.add_project(self.project)
836+
837+
group = self.create_group(status=GroupStatus.UNRESOLVED)
838+
839+
self.login_as(user=self.user)
840+
841+
url = f"{self.path}?id={group.id}"
842+
response = self.client.put(
843+
url,
844+
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
845+
format="json",
846+
)
847+
assert response.status_code == 400
848+
assert (
849+
response.data["statusDetails"]["inUpcomingRelease"][0]
850+
== "Your organization does not have access to this feature."
851+
)
852+
853+
@with_feature("organizations:resolve-in-upcoming-release")
854+
def test_upcoming_release_release_validation(self):
855+
group = self.create_group(status=GroupStatus.UNRESOLVED)
856+
857+
self.login_as(user=self.user)
858+
859+
url = f"{self.path}?id={group.id}"
860+
response = self.client.put(
861+
url,
862+
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
863+
format="json",
864+
)
865+
assert response.status_code == 400
866+
assert (
867+
response.data["statusDetails"]["inUpcomingRelease"][0]
868+
== "No release data present in the system."
869+
)
870+
794871
def test_set_resolved_in_explicit_commit_unreleased(self):
795872
repo = self.create_repo(project=self.project, name=self.project.name)
796873
commit = self.create_commit(project=self.project, repo=repo)

0 commit comments

Comments
 (0)