-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix(releases): Normalize both release values to equal one another #101184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b1aa352
ada33b9
51b2c66
c9a1533
3b71bc5
fbb12f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ | |
from sentry.models.project import Project | ||
from sentry.models.release import Release, follows_semver_versioning_scheme | ||
from sentry.notifications.types import SUBSCRIPTION_REASON_MAP, GroupSubscriptionReason | ||
from sentry.releases.use_cases.release import fetch_semver_packages_for_group | ||
from sentry.signals import issue_resolved | ||
from sentry.types.activity import ActivityType | ||
from sentry.types.actor import Actor, ActorType | ||
|
@@ -154,7 +155,14 @@ 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. | ||
group_packages = fetch_semver_packages_for_group( | ||
organization_id=group.project.organization_id, | ||
project_id=group.project_id, | ||
group_id=group.id, | ||
) | ||
release = greatest_semver_release(group.project, packages=group_packages) | ||
if release is not None: | ||
current_release_version = release.version | ||
else: | ||
|
@@ -537,6 +545,10 @@ def process_group_resolution( | |
# in release | ||
resolution_params.update( | ||
{ | ||
"release": Release.objects.filter( | ||
organization_id=release.organization_id, | ||
version=current_release_version, | ||
).get(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PR looks good to me. One small question about the extra query we have here to get the release that matches current_release_version--can/should we remove the need for this? Disadvantages are a little bit of extra latency and edge cases where get() raises Release.DoesNotExist. We could do an easy fix in the semver path by returning The non-semver path also queries the release and returns only the version (ugh), but the logic is buried fairly deep and uses caching so I understand if we don't want to change anything in this path. But it would be cleaner and save us a query if we change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. Its not ideal. Definitely something we could address in a follow-up. For this PR, we have enough to worry about so we'll limit the blast radius. On performance, yes its another query but its indexed so it will be fast. These things do add up so its best to not add queries too cavalierly. My long term goal is to refactor this code path entirely. The code, as is, is incomprehensible. An intermediate step might be the change you suggested or it might be something deeper. It just depends on the performance we measure in production and if its causing issues or not. If its not causing issues then we might focus our efforts on more substantial improvements. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. Was gonna ask how I can follow up on measuring perf in prod but I think your DD walkthrough is coming up anyway! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sentry's tracing product would be better! You can filter to the PUT request and find this span in there. You can see how the system performs in aggregate and determine if you need to make a perf change or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Unhandled Exception When Fetching Nonexistent ReleaseDuring issue resolution in semver projects, the code fetches a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True but since the current_release_version has already been shown to exist in the db the only way this could happen is if the release was deleted (which we don't allow). Technically this could raise but I'm not sure what I'd do with the exception anyway. Raising seems to be appropriate for now. |
||
"type": GroupResolution.Type.in_release, | ||
"status": GroupResolution.Status.resolved, | ||
} | ||
|
@@ -819,7 +831,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 +857,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, 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) | ||
|
||
def get_semver_releases(project: Project) -> QuerySet[Release]: | ||
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]) | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
from collections import defaultdict | ||
from collections.abc import Callable, Iterable, Mapping | ||
from datetime import datetime, timezone | ||
from typing import Any | ||
from typing import Any, cast | ||
|
||
import sentry_sdk | ||
from django.contrib.auth.models import AnonymousUser | ||
|
@@ -16,6 +16,7 @@ | |
from sentry.models.commit import Commit | ||
from sentry.models.commitauthor import CommitAuthor | ||
from sentry.models.deploy import Deploy | ||
from sentry.models.grouprelease import GroupRelease | ||
from sentry.models.project import Project | ||
from sentry.models.projectplatform import ProjectPlatform | ||
from sentry.models.release import Release | ||
|
@@ -435,3 +436,31 @@ def fetch_project_platforms(project_ids: Iterable[int]) -> list[tuple[int, str]] | |
"project_id", "platform" | ||
) | ||
) | ||
|
||
|
||
def fetch_semver_packages_for_group( | ||
organization_id: int, project_id: int, group_id: int | ||
) -> list[str]: | ||
"""Fetch a unique list of semver release packages associated with the group.""" | ||
release_ids = ( | ||
GroupRelease.objects.filter( | ||
group_id=group_id, | ||
project_id=project_id, | ||
) | ||
.distinct() | ||
.values_list("release_id", flat=True) | ||
cmanallen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
Comment on lines
+445
to
+452
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High severity vulnerability may affect your project—review required: ℹ️ Why this mattersAffected versions of django are vulnerable to Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). SQL injection in Django's ORM column aliases: when using QuerySet.annotate(), QuerySet.alias(), QuerySet.aggregate(), or QuerySet.extra() with dictionary expansion (**kwargs), the dictionary keys are used unescaped as SQL column aliases. On MySQL and MariaDB backends, an attacker who can influence those keys (for example, by passing a crafted dict of annotations) can inject arbitrary SQL into the generated query. To resolve this comment:
💬 Ignore this findingTo ignore this, reply with:
You can view more details on this finding in the Semgrep AppSec Platform here. |
||
|
||
return cast( | ||
list[str], | ||
list( | ||
Release.objects.filter_to_semver() | ||
.filter( | ||
organization_id=organization_id, | ||
id__in=release_ids, | ||
package__isnull=False, | ||
) | ||
.distinct() | ||
.values_list("package", flat=True) | ||
), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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="[email protected]" | ||
) | ||
release.add_project(self.project) | ||
|
||
# Smaller than 1.0.0 but more recent. | ||
release2 = Release.objects.create( | ||
organization_id=self.project.organization_id, version="[email protected]" | ||
) | ||
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="[email protected]" | ||
) | ||
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="[email protected]" | ||
) | ||
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"] == "[email protected]" | ||
|
||
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) | ||
|
Uh oh!
There was an error while loading. Please reload this page.