diff --git a/app/graphql/resolvers/deployment_resolver.rb b/app/graphql/resolvers/deployment_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..7d9ce0f023c041887199477504a773520e462a96 --- /dev/null +++ b/app/graphql/resolvers/deployment_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + class DeploymentResolver < BaseResolver + argument :iid, + GraphQL::Types::ID, + required: true, + description: 'Project-level internal ID of the Deployment.' + + type Types::DeploymentType, null: true + + alias_method :project, :object + + def resolve(iid:) + return unless project.present? && project.is_a?(::Project) + + Deployment.for_iid(project, iid) + end + end +end diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..c1ea436bf3d27f5f08a557d26d36b2a7e051821c --- /dev/null +++ b/app/graphql/types/deployment_details_type.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Types + class DeploymentDetailsType < DeploymentType + graphql_name 'DeploymentDetails' + description 'The details of the deployment' + authorize :read_deployment + end +end diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb index 863699a9cc5d436ca6d51752faaf7b96d86a9276..1b61001fc10a41142b1c87737dcce09d4de0ed3f 100644 --- a/app/graphql/types/deployment_type.rb +++ b/app/graphql/types/deployment_type.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true module Types + # If you're considering to add a new field in DeploymentType, please follow this guideline: + # - If the field is preloadable in batch, define it in DeploymentType. + # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that + # fetching the specific field for multiple deployments doesn't cause N+1 query problem. + # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType. + # This type can be only fetched for a single deployment, so you don't need to take care of the preloading. class DeploymentType < BaseObject graphql_name 'Deployment' description 'The deployment of an environment' diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index ecc6c9d78119ba55887edd93c43f6adbf78e4e2a..436fec4b8efce6c79163183991a616e03393c960 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -179,6 +179,12 @@ class ProjectType < BaseObject description: 'A single environment of the project.', resolver: Resolvers::EnvironmentsResolver.single + field :deployment, + Types::DeploymentDetailsType, + null: true, + description: 'Details of the deployment of the project.', + resolver: Resolvers::DeploymentResolver.single + field :issue, Types::IssueType, null: true, diff --git a/app/models/deployment.rb b/app/models/deployment.rb index dfc9f3f8fcad37245d9b694d1eb7e5b5ac3efd87..325754f001abc54552b27b8546cdc8527b991e58 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -36,6 +36,7 @@ class Deployment < ApplicationRecord delegate :name, to: :environment, prefix: true delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true + scope :for_iid, -> (project, iid) { where(project: project, iid: iid) } scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment_name, -> (project, name) do where('deployments.environment_id = (?)', diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1e5472f43311e3692a8838090303e5abd1f3b636..2a6607839bb82a5e7093be82281a6b8b747533c6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10991,6 +10991,24 @@ The deployment of an environment. | <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. | +### `DeploymentDetails` + +The details of the deployment. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <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="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="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. | + ### `Design` A single design. @@ -15871,6 +15889,18 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectdastsitevalidationsnormalizedtargeturls"></a>`normalizedTargetUrls` | [`[String!]`](#string) | Normalized URL of the target to be scanned. | | <a id="projectdastsitevalidationsstatus"></a>`status` | [`DastSiteValidationStatusEnum`](#dastsitevalidationstatusenum) | Status of the site validation. | +##### `Project.deployment` + +Details of the deployment of the project. + +Returns [`DeploymentDetails`](#deploymentdetails). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectdeploymentiid"></a>`iid` | [`ID!`](#id) | Project-level internal ID of the Deployment. | + ##### `Project.environment` A single environment of the project. diff --git a/spec/graphql/resolvers/deployment_resolver_spec.rb b/spec/graphql/resolvers/deployment_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9231edefddccbf1f82ff78b0c8b8a90350102e37 --- /dev/null +++ b/spec/graphql/resolvers/deployment_resolver_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::DeploymentResolver 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(iid: deployment.iid)).to contain_exactly(deployment) + end + + it 'does not find the deployment if the IID does not match' do + expect(resolve_deployments(iid: non_existing_record_id)).to be_empty + end + end + + def resolve_deployments(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, args: args, ctx: context) + end +end diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..58756798ffb635f84b0bdbf85a6deeef5aaa9846 --- /dev/null +++ b/spec/graphql/types/deployment_details_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DeploymentDetails'] do + specify { expect(described_class.graphql_name).to eq('DeploymentDetails') } + + 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/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 0a4ee73f3d32ac0dfcb5a7d5925ed4cc450735a8..c5ce18739aa65e7998e02ab31ceb68e6992fb83f 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -74,6 +74,27 @@ end end + describe '.for_iid' do + subject { described_class.for_iid(project, iid) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:deployment) { create(:deployment, project: project) } + + let(:iid) { deployment.iid } + + it 'finds the deployment' do + is_expected.to contain_exactly(deployment) + end + + context 'when iid does not match' do + let(:iid) { non_existing_record_id } + + it 'does not find the deployment' do + is_expected.to be_empty + end + end + end + describe '.for_environment_name' do subject { described_class.for_environment_name(project, environment_name) } diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5ef7bcafbfd9d9aac01aa307d49f2f6f6618e9a --- /dev/null +++ b/spec/requests/api/graphql/project/deployment_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Deployment query' do + let_it_be(:project) { create(:project, :private, :repository) } + 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_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, environment: environment, project: project) } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + deployment(iid: #{deployment.iid}) { + id + iid + ref + tag + sha + createdAt + updatedAt + finishedAt + status + } + } + } + ) + end + + it 'returns the deployment of the project' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data['iid']).to eq(deployment.iid.to_s) + end + + context 'when user is guest' do + let(:user) { guest } + + it 'returns nothing' do + deployment_data = subject.dig('data', 'project', 'deployment') + + expect(deployment_data).to be_nil + end + end +end