diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index 04b82ee04ec9ee1240385c7ac338846e01bf3065..e5e08d2971fa351bc18a2b86b467306d4e769297 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -9,8 +9,8 @@
 #     updated_before: DateTime
 #     finished_after: DateTime
 #     finished_before: DateTime
-#     environment: String
-#     status: String (see Deployment.statuses)
+#     environment: String (name) or Integer (ID)
+#     status: String or Array<String> (see Deployment.statuses)
 #     order_by: String (see ALLOWED_SORT_VALUES constant)
 #     sort: String (asc | desc)
 class DeploymentsFinder
@@ -33,6 +33,7 @@ class DeploymentsFinder
 
   def initialize(params = {})
     @params = params
+    @params[:status] = Array(@params[:status]).map(&:to_s) if @params[:status]
 
     validate!
   end
@@ -68,16 +69,25 @@ def validate!
       raise error if raise_for_inefficient_updated_at_query?
     end
 
-    if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?)
-      raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired'
+    if filter_by_finished_at? && !order_by_finished_at?
+      raise InefficientQueryError, '`finished_at` filter requires `finished_at` sort.'
+    end
+
+    if order_by_finished_at? && !(filter_by_finished_at? || filter_by_finished_statuses?)
+      raise InefficientQueryError,
+        '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.'
     end
 
     if filter_by_finished_at? && !filter_by_successful_deployment?
       raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.'
     end
 
-    if params[:environment].present? && !params[:project].present?
-      raise InefficientQueryError, '`environment` filter must be combined with `project` scope.'
+    if filter_by_environment_name? && !params[:project].present?
+      raise InefficientQueryError, '`environment` name filter must be combined with `project` scope.'
+    end
+
+    if filter_by_finished_statuses? && filter_by_upcoming_statuses?
+      raise InefficientQueryError, 'finished statuses and upcoming statuses must be separately queried.'
     end
   end
 
@@ -86,6 +96,8 @@ def init_collection
       params[:project].deployments
     elsif params[:group].present?
       ::Deployment.for_projects(params[:group].all_projects)
+    elsif filter_by_environment_id?
+      ::Deployment.for_environment(params[:environment])
     else
       ::Deployment.none
     end
@@ -112,7 +124,7 @@ def by_finished_at(items)
   end
 
   def by_environment(items)
-    if params[:project].present? && params[:environment].present?
+    if params[:project].present? && filter_by_environment_name?
       items.for_environment_name(params[:project], params[:environment])
     else
       items
@@ -122,7 +134,7 @@ def by_environment(items)
   def by_status(items)
     return items unless params[:status].present?
 
-    unless Deployment.statuses.key?(params[:status])
+    unless Deployment.statuses.keys.intersection(params[:status]) == params[:status]
       raise ArgumentError, "The deployment status #{params[:status]} is invalid"
     end
 
@@ -165,7 +177,23 @@ def filter_by_finished_at?
   end
 
   def filter_by_successful_deployment?
-    params[:status].to_s == 'success'
+    params[:status].present? && params[:status].count == 1 && params[:status].first.to_s == 'success'
+  end
+
+  def filter_by_finished_statuses?
+    params[:status].present? && Deployment::FINISHED_STATUSES.map(&:to_s).intersection(params[:status]).any?
+  end
+
+  def filter_by_upcoming_statuses?
+    params[:status].present? && Deployment::UPCOMING_STATUSES.map(&:to_s).intersection(params[:status]).any?
+  end
+
+  def filter_by_environment_name?
+    params[:environment].present? && params[:environment].is_a?(String)
+  end
+
+  def filter_by_environment_id?
+    params[:environment].present? && params[:environment].is_a?(Integer)
   end
 
   def order_by_updated_at?
diff --git a/app/graphql/resolvers/deployments_resolver.rb b/app/graphql/resolvers/deployments_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..341d23c2ccb34a03ce96affb95707bea5d6a411d
--- /dev/null
+++ b/app/graphql/resolvers/deployments_resolver.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Resolvers
+  class DeploymentsResolver < BaseResolver
+    argument :statuses, [Types::DeploymentStatusEnum],
+             description: 'Statuses of the deployments.',
+             required: false,
+             as: :status
+
+    argument :order_by, Types::DeploymentsOrderByInputType,
+             description: 'Order by a specified field.',
+             required: false
+
+    type Types::DeploymentType, null: true
+
+    alias_method :environment, :object
+
+    def resolve(**args)
+      return unless environment.present? && environment.is_a?(::Environment)
+
+      args = transform_args_for_finder(**args)
+
+      # GraphQL BatchLoader shouldn't be used here because pagination query will be inefficient
+      # that fetches thousands of rows before limiting and offsetting.
+      DeploymentsFinder.new(environment: environment.id, **args).execute
+    end
+
+    private
+
+    def transform_args_for_finder(**args)
+      if (order_by = args.delete(:order_by))
+        order_by = order_by.to_h.map { |k, v| { order_by: k.to_s, sort: v } }.first
+        args.merge!(order_by)
+      end
+
+      args
+    end
+  end
+end
diff --git a/app/graphql/types/deployment_status_enum.rb b/app/graphql/types/deployment_status_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7ef69d3f1c131c0cae7ad685d79ae35db9aab8b0
--- /dev/null
+++ b/app/graphql/types/deployment_status_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+  class DeploymentStatusEnum < BaseEnum
+    graphql_name 'DeploymentStatus'
+    description 'All deployment statuses.'
+
+    ::Deployment.statuses.each_key do |status|
+      value status.upcase,
+            description: "A deployment that is #{status.tr('_', ' ')}.",
+            value: status
+    end
+  end
+end
diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..863699a9cc5d436ca6d51752faaf7b96d86a9276
--- /dev/null
+++ b/app/graphql/types/deployment_type.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Types
+  class DeploymentType < BaseObject
+    graphql_name 'Deployment'
+    description 'The deployment of an environment'
+
+    present_using Deployments::DeploymentPresenter
+
+    authorize :read_deployment
+
+    field :id,
+      GraphQL::Types::ID,
+      description: 'Global ID of the deployment.'
+
+    field :iid,
+      GraphQL::Types::ID,
+      description: 'Project-level internal ID of the deployment.'
+
+    field :ref,
+      GraphQL::Types::String,
+      description: 'Git-Ref that the deployment ran on.'
+
+    field :tag,
+      GraphQL::Types::Boolean,
+      description: 'True or false if the deployment ran on a Git-tag.'
+
+    field :sha,
+      GraphQL::Types::String,
+      description: 'Git-SHA that the deployment ran on.'
+
+    field :created_at,
+      Types::TimeType,
+      description: 'When the deployment record was created.'
+
+    field :updated_at,
+      Types::TimeType,
+      description: 'When the deployment record was updated.'
+
+    field :finished_at,
+      Types::TimeType,
+      description: 'When the deployment finished.'
+
+    field :status,
+      Types::DeploymentStatusEnum,
+      description: 'Status of the deployment.'
+  end
+end
diff --git a/app/graphql/types/deployments_order_by_input_type.rb b/app/graphql/types/deployments_order_by_input_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a87fef9fe8ad0a028565a6fca82132c1643a009a
--- /dev/null
+++ b/app/graphql/types/deployments_order_by_input_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+  class DeploymentsOrderByInputType < BaseInputObject
+    graphql_name 'DeploymentsOrderByInput'
+    description 'Values for ordering deployments by a specific field'
+
+    argument :created_at,
+             Types::SortDirectionEnum,
+             required: false,
+             description: 'Order by Created time.'
+
+    argument :finished_at,
+             Types::SortDirectionEnum,
+             required: false,
+             description: 'Order by Finished time.'
+
+    def prepare
+      raise GraphQL::ExecutionError, 'orderBy parameter must contain one key-value pair.' unless to_h.size == 1
+
+      super
+    end
+  end
+end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 2a7076cc3c9a9aadcfbc2a1d2f4fdac4b0382be7..994be0e5f5aeed90275b61a4ff0ab0673cb41e7e 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -29,5 +29,14 @@ class EnvironmentType < BaseObject
           Types::AlertManagement::AlertType,
           null: true,
           description: 'Most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
+
+    # Setting high complexity for preventing users from querying deployments for multiple environments,
+    # which could result in N+1 issue.
+    field :deployments,
+          Types::DeploymentType.connection_type,
+          null: true,
+          description: 'Deployments of the environment.',
+          resolver: Resolvers::DeploymentsResolver,
+          complexity: 150
   end
 end
diff --git a/app/graphql/types/sort_direction_enum.rb b/app/graphql/types/sort_direction_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28dba1abfb6ce5b9dfabfbb607d9ac846e46ccea
--- /dev/null
+++ b/app/graphql/types/sort_direction_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+  class SortDirectionEnum < BaseEnum
+    graphql_name 'SortDirectionEnum'
+    description 'Values for sort direction'
+
+    value 'ASC', 'Ascending order.', value: 'asc'
+    value 'DESC', 'Descending order.', value: 'desc'
+  end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index a3213a59beda95c29444be3838de352e32ea1830..dfc9f3f8fcad37245d9b694d1eb7e5b5ac3efd87 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -61,6 +61,7 @@ class Deployment < ApplicationRecord
 
   VISIBLE_STATUSES = %i[running success failed canceled blocked].freeze
   FINISHED_STATUSES = %i[success failed canceled].freeze
+  UPCOMING_STATUSES = %i[created blocked running].freeze
 
   state_machine :status, initial: :created do
     event :run do
diff --git a/app/presenters/deployments/deployment_presenter.rb b/app/presenters/deployments/deployment_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..97d4accc59c45bd627f9694b4cd2a929ade67c4a
--- /dev/null
+++ b/app/presenters/deployments/deployment_presenter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Deployments
+  class DeploymentPresenter < Gitlab::View::Presenter::Delegated
+    presents ::Deployment, as: :deployment
+  end
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a0c228a69448bbcf786b728a4f1cf1b6271e8342..1e5472f43311e3692a8838090303e5abd1f3b636 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6959,6 +6959,29 @@ The edge type for [`DependencyProxyManifest`](#dependencyproxymanifest).
 | <a id="dependencyproxymanifestedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="dependencyproxymanifestedgenode"></a>`node` | [`DependencyProxyManifest`](#dependencyproxymanifest) | The item at the end of the edge. |
 
+#### `DeploymentConnection`
+
+The connection type for [`Deployment`](#deployment).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentconnectionedges"></a>`edges` | [`[DeploymentEdge]`](#deploymentedge) | A list of edges. |
+| <a id="deploymentconnectionnodes"></a>`nodes` | [`[Deployment]`](#deployment) | A list of nodes. |
+| <a id="deploymentconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `DeploymentEdge`
+
+The edge type for [`Deployment`](#deployment).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="deploymentedgenode"></a>`node` | [`Deployment`](#deployment) | The item at the end of the edge. |
+
 #### `DesignAtVersionConnection`
 
 The connection type for [`DesignAtVersion`](#designatversion).
@@ -10950,6 +10973,24 @@ Group-level Dependency Proxy settings.
 | ---- | ---- | ----------- |
 | <a id="dependencyproxysettingenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the dependency proxy is enabled for the group. |
 
+### `Deployment`
+
+The deployment of an environment.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <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="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="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
+
 ### `Design`
 
 A single design.
@@ -11381,6 +11422,23 @@ Describes where code is deployed for a project.
 
 #### Fields with arguments
 
+##### `Environment.deployments`
+
+Deployments of the environment.
+
+Returns [`DeploymentConnection`](#deploymentconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="environmentdeploymentsorderby"></a>`orderBy` | [`DeploymentsOrderByInput`](#deploymentsorderbyinput) | Order by a specified field. |
+| <a id="environmentdeploymentsstatuses"></a>`statuses` | [`[DeploymentStatus!]`](#deploymentstatus) | Statuses of the deployments. |
+
 ##### `Environment.metricsDashboard`
 
 Metrics dashboard schema for the environment.
@@ -19579,6 +19637,20 @@ Weight of the data visualization palette.
 | <a id="dependencyproxymanifeststatuspending_destruction"></a>`PENDING_DESTRUCTION` | Dependency proxy manifest has a status of pending_destruction. |
 | <a id="dependencyproxymanifeststatusprocessing"></a>`PROCESSING` | Dependency proxy manifest has a status of processing. |
 
+### `DeploymentStatus`
+
+All deployment statuses.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="deploymentstatusblocked"></a>`BLOCKED` | A deployment that is blocked. |
+| <a id="deploymentstatuscanceled"></a>`CANCELED` | A deployment that is canceled. |
+| <a id="deploymentstatuscreated"></a>`CREATED` | A deployment that is created. |
+| <a id="deploymentstatusfailed"></a>`FAILED` | A deployment that is failed. |
+| <a id="deploymentstatusrunning"></a>`RUNNING` | A deployment that is running. |
+| <a id="deploymentstatusskipped"></a>`SKIPPED` | A deployment that is skipped. |
+| <a id="deploymentstatussuccess"></a>`SUCCESS` | A deployment that is success. |
+
 ### `DeploymentTier`
 
 All environment deployment tiers.
@@ -20595,6 +20667,15 @@ Common sort values.
 | <a id="sortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
 | <a id="sortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
 
+### `SortDirectionEnum`
+
+Values for sort direction.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="sortdirectionenumasc"></a>`ASC` | Ascending order. |
+| <a id="sortdirectionenumdesc"></a>`DESC` | Descending order. |
+
 ### `TestCaseStatus`
 
 | Value | Description |
@@ -22349,6 +22430,17 @@ Input type for DastSiteProfile authentication.
 | <a id="dastsiteprofileauthinputusername"></a>`username` | [`String`](#string) | Username to authenticate with on the target. |
 | <a id="dastsiteprofileauthinputusernamefield"></a>`usernameField` | [`String`](#string) | Name of username field at the sign-in HTML form. |
 
+### `DeploymentsOrderByInput`
+
+Values for ordering deployments by a specific field.
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentsorderbyinputcreatedat"></a>`createdAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Created time. |
+| <a id="deploymentsorderbyinputfinishedat"></a>`finishedAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Finished time. |
+
 ### `DiffImagePositionInput`
 
 #### Arguments
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 51c293bcfd1fab49bafe7f2a23290c5a3d5b52d1..efb739c3d2fb100a87db2a43d81cb3f3b03acba0 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -32,7 +32,17 @@
       it 'raises an error' do
         expect { subject }.to raise_error(
           described_class::InefficientQueryError,
-          '`finished_at` filter and `finished_at` sorting must be paired')
+          '`finished_at` filter requires `finished_at` sort.')
+      end
+    end
+
+    context 'when running status filter and finished_at sorting' do
+      let(:params) { { status: :running, order_by: :finished_at } }
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(
+          described_class::InefficientQueryError,
+          '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.')
       end
     end
 
@@ -52,7 +62,17 @@
       it 'raises an error' do
         expect { subject }.to raise_error(
           described_class::InefficientQueryError,
-          '`environment` filter must be combined with `project` scope.')
+          '`environment` name filter must be combined with `project` scope.')
+      end
+    end
+
+    context 'when status filter with mixed finished and upcoming statuses' do
+      let(:params) { { status: [:success, :running] } }
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(
+          described_class::InefficientQueryError,
+          'finished statuses and upcoming statuses must be separately queried.')
       end
     end
   end
@@ -103,6 +123,24 @@
           end
         end
 
+        context 'when the environment ID is specified' do
+          let!(:environment1) { create(:environment, project: project) }
+          let!(:environment2) { create(:environment, project: project) }
+          let!(:deployment1) do
+            create(:deployment, project: project, environment: environment1)
+          end
+
+          let!(:deployment2) do
+            create(:deployment, project: project, environment: environment2)
+          end
+
+          let(:params) { { environment: environment1.id } }
+
+          it 'returns deployments for the given environment' do
+            is_expected.to match_array([deployment1])
+          end
+        end
+
         context 'when the deployment status is specified' do
           let!(:deployment1) { create(:deployment, :success, project: project) }
           let!(:deployment2) { create(:deployment, :failed, project: project) }
diff --git a/spec/graphql/resolvers/deployments_resolver_spec.rb b/spec/graphql/resolvers/deployments_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4e5564aad0bdb6f91b3424cd23d4d3641fe205a7
--- /dev/null
+++ b/spec/graphql/resolvers/deployments_resolver_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DeploymentsResolver do
+  include GraphqlHelpers
+
+  let_it_be(:project) { create(:project, :repository, :private) }
+  let_it_be(:environment) { create(:environment, project: project) }
+  let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+  let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+  let(:current_user) { developer }
+
+  describe '#resolve' do
+    it 'finds the deployment' do
+      expect(resolve_deployments).to contain_exactly(deployment)
+    end
+
+    it 'finds the deployment when status matches' do
+      expect(resolve_deployments(statuses: [:created])).to contain_exactly(deployment)
+    end
+
+    it 'does not find the deployment when status does not match' do
+      expect(resolve_deployments(statuses: [:success])).to be_empty
+    end
+
+    it 'transforms order_by for finder' do
+      expect(DeploymentsFinder)
+        .to receive(:new)
+        .with(environment: environment.id, status: ['success'], order_by: 'finished_at', sort: 'asc')
+        .and_call_original
+
+      resolve_deployments(statuses: [:success], order_by: { finished_at: :asc })
+    end
+  end
+
+  def resolve_deployments(args = {}, context = { current_user: current_user })
+    resolve(described_class, obj: environment, args: args, ctx: context)
+  end
+end
diff --git a/spec/graphql/types/deployment_type_spec.rb b/spec/graphql/types/deployment_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..21e445c24b2b1971482582e6f1d6e8b1103953d2
--- /dev/null
+++ b/spec/graphql/types/deployment_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Deployment'] do
+  specify { expect(described_class.graphql_name).to eq('Deployment') }
+
+  it 'has the expected fields' do
+    expected_fields = %w[
+      id iid ref tag sha created_at updated_at finished_at status
+    ]
+
+    expect(described_class).to have_graphql_fields(*expected_fields)
+  end
+
+  specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
+end
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 3671d35e8a5a92ed28f7b8790c8d58431ab728a9..16859394978e22b60b38d38f3fb79d3a5c0bb0d9 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -7,7 +7,7 @@
 
   it 'has the expected fields' do
     expected_fields = %w[
-      name id state metrics_dashboard latest_opened_most_severe_alert path
+      name id state metrics_dashboard latest_opened_most_severe_alert path deployments
     ]
 
     expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fbfd9c5d0ac648dabd04fc6af08587efb31c5391
--- /dev/null
+++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments Deployments query' do
+  include GraphqlHelpers
+
+  let_it_be(:project) { create(:project, :private, :repository) }
+  let_it_be(:environment) { create(:environment, project: project) }
+  let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+  let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+
+  let(:user) { developer }
+
+  subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+  context 'when there are deployments in the environment' do
+    let_it_be(:finished_deployment_old) do
+      create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago)
+    end
+
+    let_it_be(:finished_deployment_new) do
+      create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago)
+    end
+
+    let_it_be(:upcoming_deployment_old) do
+      create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago)
+    end
+
+    let_it_be(:upcoming_deployment_new) do
+      create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago)
+    end
+
+    let_it_be(:other_environment) { create(:environment, project: project) }
+    let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) }
+
+    let(:query) do
+      %(
+        query {
+          project(fullPath: "#{project.full_path}") {
+            environment(name: "#{environment.name}") {
+              deployments {
+                nodes {
+                  id
+                  iid
+                  ref
+                  tag
+                  sha
+                  createdAt
+                  updatedAt
+                  finishedAt
+                  status
+                }
+              }
+            }
+          }
+        }
+      )
+    end
+
+    it 'returns all deployments of the environment' do
+      deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+      expect(deployments.count).to eq(4)
+    end
+
+    context 'when query last deployment' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployment' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(1)
+        expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+      end
+    end
+
+    context 'when query latest upcoming deployment' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployment' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(1)
+        expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+      end
+    end
+
+    context 'when query finished deployments in descending order' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(2)
+        expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+        expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s)
+      end
+    end
+
+    context 'when query finished deployments in ascending order' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(2)
+        expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s)
+        expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s)
+      end
+    end
+
+    context 'when query upcoming deployments in descending order' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(2)
+        expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+        expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+      end
+    end
+
+    context 'when query upcoming deployments in ascending order' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returns deployments' do
+        deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+        expect(deployments.count).to eq(2)
+        expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+        expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+      end
+    end
+
+    context 'when query last deployments of multiple environments' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environments {
+                nodes {
+                  name
+                  deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+                    nodes {
+                      iid
+                    }
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'returnes an error for preventing N+1 queries' do
+        expect(subject['errors'][0]['message']).to include('exceeds max complexity')
+      end
+    end
+
+    context 'when query finished and upcoming deployments together' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [CREATED SUCCESS]) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'raises an error' do
+        expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError)
+      end
+    end
+
+    context 'when multiple orderBy input are specified' do
+      let(:query) do
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) {
+                  nodes {
+                    iid
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      it 'raises an error' do
+        expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.')
+      end
+    end
+
+    context 'when user is guest' do
+      let(:user) { guest }
+
+      it 'returns nothing' do
+        expect(subject['data']['project']['environment']).to be_nil
+      end
+    end
+
+    describe 'sorting and pagination' do
+      let(:data_path) { [:project, :environment, :deployments] }
+      let(:current_user) { user }
+
+      def pagination_query(params)
+        %(
+          query {
+            project(fullPath: "#{project.full_path}") {
+              environment(name: "#{environment.name}") {
+                deployments(statuses: [SUCCESS], #{params}) {
+                  nodes {
+                    iid
+                  }
+                  pageInfo {
+                    startCursor
+                    endCursor
+                    hasNextPage
+                    hasPreviousPage
+                  }
+                }
+              }
+            }
+          }
+        )
+      end
+
+      def pagination_results_data(nodes)
+        nodes.map { |deployment| deployment['iid'].to_i }
+      end
+
+      context 'when sorting by finished_at in ascending order' do
+        it_behaves_like 'sorted paginated query' do
+          let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) }
+          let(:first_param) { 2 }
+          let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] }
+        end
+      end
+
+      context 'when sorting by finished_at in descending order' do
+        it_behaves_like 'sorted paginated query' do
+          let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) }
+          let(:first_param) { 2 }
+          let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] }
+        end
+      end
+    end
+  end
+end