From 15c76231c6ed88030ebf08b686a3c8191d7f36c9 Mon Sep 17 00:00:00 2001
From: Shinya Maeda <shinya@gitlab.com>
Date: Thu, 25 Aug 2022 12:29:45 +0000
Subject: [PATCH] Extend deployments graphql query for index page

This commit extends the deployments graphql query
for index page.

Changelog: added
---
 app/finders/deployments_finder.rb             |   1 +
 app/graphql/types/ci/job_type.rb              |   6 +
 app/graphql/types/deployment_type.rb          |  15 ++
 app/models/deployment.rb                      |   4 +
 doc/api/graphql/reference/index.md            |   7 +
 spec/graphql/types/ci/job_type_spec.rb        |  15 ++
 .../types/deployment_details_type_spec.rb     |   2 +-
 spec/graphql/types/deployment_type_spec.rb    |   2 +-
 spec/models/deployment_spec.rb                |  16 ++
 .../environments/deployments_query_spec.rb    | 141 ++++++++++++++++++
 10 files changed, 207 insertions(+), 2 deletions(-)

diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index e5e08d2971fa3..5b2139cb941bf 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -211,6 +211,7 @@ def preload_associations(scope)
       environment: [],
       deployable: {
         job_artifacts: [],
+        user: [],
         pipeline: {
           project: {
             route: [],
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 4ea9a016e74e4..ab6103d946943 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -92,6 +92,8 @@ class JobType < BaseObject
                                              description: 'Indicates the job is stuck.'
       field :triggered, GraphQL::Types::Boolean, null: true,
                                                  description: 'Whether the job was triggered.'
+      field :web_path, GraphQL::Types::String, null: true,
+                                               description: 'Web path of the job.'
 
       def kind
         return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class)
@@ -181,6 +183,10 @@ def ref_path
         ::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_name)
       end
 
+      def web_path
+        ::Gitlab::Routing.url_helpers.project_job_path(object.project, object)
+      end
+
       def coverage
         object&.coverage
       end
diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb
index 1b61001fc10a4..70a3a4cb57495 100644
--- a/app/graphql/types/deployment_type.rb
+++ b/app/graphql/types/deployment_type.rb
@@ -50,5 +50,20 @@ class DeploymentType < BaseObject
     field :status,
       Types::DeploymentStatusEnum,
       description: 'Status of the deployment.'
+
+    field :commit,
+      Types::CommitType,
+      description: 'Commit details of the deployment.',
+      calls_gitaly: true
+
+    field :job,
+      Types::Ci::JobType,
+      description: 'Pipeline job of the deployment.',
+      method: :build
+
+    field :triggerer,
+      Types::UserType,
+      description: 'User who executed the deployment.',
+      method: :deployed_by
   end
 end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 325754f001abc..a463448c2065d 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -222,6 +222,10 @@ def self.builds(limit = 1000)
     Ci::Build.where(id: deployable_ids)
   end
 
+  def build
+    deployable if deployable.is_a?(::Ci::Build)
+  end
+
   class << self
     ##
     # FastDestroyAll concerns
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 68afb17f4859c..5336c1ce4cf8a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -10199,6 +10199,7 @@ CI/CD variables for a GitLab instance.
 | <a id="cijobtags"></a>`tags` | [`[String!]`](#string) | Tags for the current job. |
 | <a id="cijobtriggered"></a>`triggered` | [`Boolean`](#boolean) | Whether the job was triggered. |
 | <a id="cijobuserpermissions"></a>`userPermissions` | [`JobPermissions!`](#jobpermissions) | Permissions for the current user on the resource. |
+| <a id="cijobwebpath"></a>`webPath` | [`String`](#string) | Web path of the job. |
 
 ### `CiJobArtifact`
 
@@ -11019,14 +11020,17 @@ The deployment of an environment.
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="deploymentcommit"></a>`commit` | [`Commit`](#commit) | Commit details of the deployment. |
 | <a id="deploymentcreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
 | <a id="deploymentfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
 | <a id="deploymentid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
 | <a id="deploymentiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
+| <a id="deploymentjob"></a>`job` | [`CiJob`](#cijob) | Pipeline job of the deployment. |
 | <a id="deploymentref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
 | <a id="deploymentsha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
 | <a id="deploymentstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
 | <a id="deploymenttag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
+| <a id="deploymenttriggerer"></a>`triggerer` | [`UserCore`](#usercore) | User who executed the deployment. |
 | <a id="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
 
 ### `DeploymentDetails`
@@ -11037,14 +11041,17 @@ The details of the deployment.
 
 | Name | Type | Description |
 | ---- | ---- | ----------- |
+| <a id="deploymentdetailscommit"></a>`commit` | [`Commit`](#commit) | Commit details of the deployment. |
 | <a id="deploymentdetailscreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
 | <a id="deploymentdetailsfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
 | <a id="deploymentdetailsid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
 | <a id="deploymentdetailsiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
+| <a id="deploymentdetailsjob"></a>`job` | [`CiJob`](#cijob) | Pipeline job of the deployment. |
 | <a id="deploymentdetailsref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
 | <a id="deploymentdetailssha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
 | <a id="deploymentdetailsstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
 | <a id="deploymentdetailstag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
+| <a id="deploymentdetailstriggerer"></a>`triggerer` | [`UserCore`](#usercore) | User who executed the deployment. |
 | <a id="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
 
 ### `Design`
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index bc9e64282bcf9..b3dee082d1fcc 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -3,6 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe Types::Ci::JobType do
+  include GraphqlHelpers
+
   specify { expect(described_class.graphql_name).to eq('CiJob') }
   specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Job) }
 
@@ -45,8 +47,21 @@
       tags
       triggered
       userPermissions
+      webPath
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields)
   end
+
+  describe '#web_path' do
+    subject { resolve_field(:web_path, build, current_user: user, object_type: described_class) }
+
+    let(:project) { create(:project) }
+    let(:user) { create(:user) }
+    let(:build) { create(:ci_build, project: project, user: user) }
+
+    it 'returns the web path of the job' do
+      is_expected.to eq("/#{project.full_path}/-/jobs/#{build.id}")
+    end
+  end
 end
diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb
index 58756798ffb63..4cbc4165f702c 100644
--- a/spec/graphql/types/deployment_details_type_spec.rb
+++ b/spec/graphql/types/deployment_details_type_spec.rb
@@ -7,7 +7,7 @@
 
   it 'has the expected fields' do
     expected_fields = %w[
-      id iid ref tag sha created_at updated_at finished_at status
+      id iid ref tag sha created_at updated_at finished_at status commit job triggerer
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/deployment_type_spec.rb b/spec/graphql/types/deployment_type_spec.rb
index 21e445c24b2b1..bf4be0523c64e 100644
--- a/spec/graphql/types/deployment_type_spec.rb
+++ b/spec/graphql/types/deployment_type_spec.rb
@@ -7,7 +7,7 @@
 
   it 'has the expected fields' do
     expected_fields = %w[
-      id iid ref tag sha created_at updated_at finished_at status
+      id iid ref tag sha created_at updated_at finished_at status commit job triggerer
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index c5ce18739aa65..2fdca7114eba6 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -897,6 +897,22 @@ def subject_method(environment)
     end
   end
 
+  describe '#build' do
+    let!(:deployment) { create(:deployment) }
+
+    subject { deployment.build }
+
+    it 'retrieves build for the deployment' do
+      is_expected.to eq(deployment.deployable)
+    end
+
+    it 'returns nil when the associated build is not found' do
+      deployment.update!(deployable_id: nil, deployable_type: nil)
+
+      is_expected.to be_nil
+    end
+  end
+
   describe '#previous_deployment' do
     using RSpec::Parameterized::TableSyntax
 
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb
index fbfd9c5d0ac64..95cc83e9463c9 100644
--- a/spec/requests/api/graphql/environments/deployments_query_spec.rb
+++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb
@@ -295,6 +295,147 @@
       end
     end
 
+    shared_examples_for 'avoids N+1 database queries' do
+      it 'does not increase the query count' do
+        create_deployments
+
+        baseline = ActiveRecord::QueryRecorder.new do
+          run_with_clean_state(query, context: { current_user: user })
+        end
+
+        create_deployments
+
+        multi = ActiveRecord::QueryRecorder.new do
+          run_with_clean_state(query, context: { current_user: user })
+        end
+
+        expect(multi).not_to exceed_query_limit(baseline)
+      end
+
+      def create_deployments
+        create_list(:deployment, 3, environment: environment, project: project).each do |deployment|
+          deployment.user = create(:user).tap { |u| project.add_developer(u) }
+          deployment.deployable =
+            create(:ci_build, project: project, environment: environment.name, deployment: deployment,
+                              user: deployment.user)
+
+          deployment.save!
+        end
+      end
+    end
+
+    context 'when requesting commits of deployments' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments {
+                  nodes {
+                    iid
+                    commit {
+                      author {
+                        avatarUrl
+                        name
+                        webPath
+                      }
+                      fullTitle
+                      webPath
+                      sha
+                    }
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it_behaves_like 'avoids N+1 database queries'
+
+      it 'returns commits of deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        deployments.each do |deployment|
+          deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+          expect(deployment_in_record.sha).to eq(deployment['commit']['sha'])
+        end
+      end
+    end
+
+    context 'when requesting triggerers of deployments' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments {
+                  nodes {
+                    iid
+                    triggerer {
+                      id
+                      avatarUrl
+                      name
+                      webPath
+                    }
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it_behaves_like 'avoids N+1 database queries'
+
+      it 'returns triggerers of deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        deployments.each do |deployment|
+          deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+          expect(deployment_in_record.deployed_by.name).to eq(deployment['triggerer']['name'])
+        end
+      end
+    end
+
+    context 'when requesting jobs of deployments' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments {
+                  nodes {
+                    iid
+                    job {
+                      id
+                      status
+                      name
+                      webPath
+                    }
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it_behaves_like 'avoids N+1 database queries'
+
+      it 'returns jobs of deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        deployments.each do |deployment|
+          deployment_in_record = project.deployments.find_by_iid(deployment['iid'])
+
+          expect(deployment_in_record.build.to_global_id.to_s).to eq(deployment['job']['id'])
+        end
+      end
+    end
+
     describe 'sorting and pagination' do
       let(:data_path) { [:project, :environment, :deployments] }
       let(:current_user) { user }
-- 
GitLab