Browse Source

Fix UI for latest deploys in Release page (#65587)

## Description
Fixes:
https://github.com/getsentry/team-core-product-foundations/issues/59

Previously, we weren't filtering the latest deploys by project By using
the ReleaseProjectEnvironment, we can map all the filters correctly.

### Before
![Screenshot 2024-02-21 at 3 01
15 PM](https://github.com/getsentry/sentry/assets/1569818/4bb3cd64-3fc6-43c1-a49e-817ac48c240b)

Before, we weren't filtering by project id, so we could have a release
to a single project and it'd show up on all release pages as a deploy.

### After
![Screenshot 2024-02-21 at 2 59
14 PM](https://github.com/getsentry/sentry/assets/1569818/4c3658f7-96bf-4974-b5ec-e5f322602575)

Now, we look at the ReleaseProjectEnvrinoments to map the org, version,
and project_id to filter per release project view.
Josh Callender 1 year ago
parent
commit
23df7dcfbb

+ 1 - 0
.gitignore

@@ -44,6 +44,7 @@ node_modules
 Gemfile.lock
 .idea/
 .vim/
+*.sw*
 *.iml
 .pytest_cache/
 .vscode/tags

+ 15 - 3
src/sentry/api/endpoints/release_deploys.py

@@ -50,7 +50,7 @@ class ReleaseDeploysEndpoint(OrganizationReleasesBaseEndpoint):
         List a Release's Deploys
         ````````````````````````
 
-        Return a list of deploys for a given release.
+        Returns a list of deploys based on the organization, version, and project.
 
         :pparam string organization_slug: the organization short name
         :pparam string version: the version identifier of the release.
@@ -63,13 +63,25 @@ class ReleaseDeploysEndpoint(OrganizationReleasesBaseEndpoint):
         if not self.has_release_permission(request, organization, release):
             raise ResourceDoesNotExist
 
-        queryset = Deploy.objects.filter(organization_id=organization.id, release=release)
+        release_project_envs = ReleaseProjectEnvironment.objects.select_related("release").filter(
+            release__organization_id=organization.id,
+            release__version=version,
+        )
+
+        projects = self.get_projects(request, organization)
+        project_id = [p.id for p in projects]
+
+        if project_id and project_id != "-1":
+            release_project_envs = release_project_envs.filter(project_id__in=project_id)
+
+        deploy_ids = release_project_envs.values_list("last_deploy_id", flat=True)
+        queryset = Deploy.objects.filter(id__in=deploy_ids)
 
         return self.paginate(
             request=request,
+            paginator_cls=OffsetPaginator,
             queryset=queryset,
             order_by="-date_finished",
-            paginator_cls=OffsetPaginator,
             on_results=lambda x: serialize(x, request.user),
         )
 

+ 9 - 1
static/app/views/releases/detail/index.tsx

@@ -132,7 +132,15 @@ class ReleasesDetail extends DeprecatedAsyncView<Props, State> {
     ];
 
     if (releaseMeta.deployCount > 0) {
-      endpoints.push(['deploys', `${basePath}deploys/`]);
+      endpoints.push([
+        'deploys',
+        `${basePath}deploys/`,
+        {
+          query: {
+            project: location.query.project,
+          },
+        },
+      ]);
     }
 
     // Used to figure out if the release has any health data

+ 20 - 2
tests/apidocs/endpoints/releases/test_deploys.py

@@ -6,13 +6,16 @@ from django.urls import reverse
 from fixtures.apidocs_test_case import APIDocsTestCase
 from sentry.models.deploy import Deploy
 from sentry.models.environment import Environment
+from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
 
 
 class ReleaseDeploysDocs(APIDocsTestCase):
     def setUp(self):
         project = self.create_project(name="foo")
         release = self.create_release(project=project, version="1")
-        Deploy.objects.create(
+        release.add_project(project)
+
+        prod_deploy = Deploy.objects.create(
             environment_id=Environment.objects.create(
                 organization_id=project.organization_id, name="production"
             ).id,
@@ -20,7 +23,8 @@ class ReleaseDeploysDocs(APIDocsTestCase):
             release=release,
             date_finished=datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1),
         )
-        Deploy.objects.create(
+
+        staging_deploy = Deploy.objects.create(
             environment_id=Environment.objects.create(
                 organization_id=project.organization_id, name="staging"
             ).id,
@@ -28,6 +32,20 @@ class ReleaseDeploysDocs(APIDocsTestCase):
             release=release,
         )
 
+        ReleaseProjectEnvironment.objects.create(
+            project=project,
+            release_id=release.id,
+            environment_id=prod_deploy.environment_id,
+            last_deploy_id=prod_deploy.id,
+        )
+
+        ReleaseProjectEnvironment.objects.create(
+            project=project,
+            release_id=release.id,
+            environment_id=staging_deploy.environment_id,
+            last_deploy_id=staging_deploy.id,
+        )
+
         self.url = reverse(
             "sentry-api-0-organization-release-deploys",
             kwargs={"organization_slug": project.organization.slug, "version": release.version},

+ 91 - 8
tests/sentry/api/endpoints/test_release_deploys.py

@@ -18,20 +18,41 @@ class ReleaseDeploysListTest(APITestCase):
             version="1–0",
         )
         release.add_project(project)
-        Deploy.objects.create(
-            environment_id=Environment.objects.create(
-                organization_id=project.organization_id, name="production"
-            ).id,
+        production_env = Environment.objects.create(
+            organization_id=project.organization_id,
+            name="production",
+        )
+
+        prod_deploy = Deploy.objects.create(
+            environment_id=production_env.id,
             organization_id=project.organization_id,
             release=release,
             date_finished=datetime.datetime.utcnow() - datetime.timedelta(days=1),
         )
-        Deploy.objects.create(
-            environment_id=Environment.objects.create(
-                organization_id=project.organization_id, name="staging"
-            ).id,
+
+        staging_env = Environment.objects.create(
             organization_id=project.organization_id,
+            name="staging",
+        )
+
+        staging_deploy = Deploy.objects.create(
+            environment_id=staging_env.id,
+            organization_id=project.organization_id,
+            release=release,
+        )
+
+        ReleaseProjectEnvironment.objects.create(
+            project=project,
+            release=release,
+            environment=production_env,
+            last_deploy_id=prod_deploy.id,
+        )
+
+        ReleaseProjectEnvironment.objects.create(
+            project=project,
             release=release,
+            environment=staging_env,
+            last_deploy_id=staging_deploy.id,
         )
 
         url = reverse(
@@ -47,6 +68,68 @@ class ReleaseDeploysListTest(APITestCase):
         assert response.data[0]["environment"] == "staging"
         assert response.data[1]["environment"] == "production"
 
+    def test_with_project(self):
+        project = self.create_project(name="bar")
+        project2 = self.create_project(name="baz")
+
+        release = Release.objects.create(
+            organization_id=project.organization_id,
+            # test unicode
+            version="1–1",
+        )
+
+        release.add_project(project)
+        release.add_project(project2)
+
+        production_env = Environment.objects.create(
+            organization_id=project.organization_id,
+            name="production",
+        )
+
+        prod_deploy = Deploy.objects.create(
+            environment_id=production_env.id,
+            organization_id=project.organization_id,
+            release=release,
+            date_finished=datetime.datetime.utcnow() - datetime.timedelta(days=1),
+        )
+
+        ReleaseProjectEnvironment.objects.create(
+            project=project,
+            release=release,
+            environment=production_env,
+            last_deploy_id=prod_deploy.id,
+        )
+
+        url = reverse(
+            "sentry-api-0-organization-release-deploys",
+            kwargs={"organization_slug": project.organization.slug, "version": release.version},
+        )
+
+        self.login_as(user=self.user)
+
+        # Test that the first project returns the deploy as expected
+        response_bar = self.client.get(url + f"?project={project.id}")
+        assert response_bar.status_code == 200, response_bar.content
+        assert len(response_bar.data) == 1
+        assert response_bar.data[0]["environment"] == "production"
+
+        # Test that the second project does not return any deploys
+        response_baz = self.client.get(url + f"?project={project2.id}")
+        assert response_baz.status_code == 200, response_baz.content
+        assert len(response_baz.data) == 0
+
+        # Test that not setting the project id returns the deploy
+        response = self.client.get(url)
+        assert response.status_code == 200, response.content
+        assert len(response.data) == 1
+        assert response.data[0]["environment"] == "production"
+
+        # Negative ID set as the project_id is same as not setting it
+        response_negative = self.client.get(url, data={"project": "-1"})
+        assert response_negative.status_code == 200, response_negative.content
+        assert len(response_negative.data) == 1
+        assert response_negative.data[0]["environment"] == "production"
+
 
 class ReleaseDeploysCreateTest(APITestCase):
     def setUp(self):