diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb index 6d895cc81cf1d3a8a44391f8426ff1d587989c8e..c49633275fb26d6719e5a37511b3eb8d8ef450cf 100644 --- a/app/graphql/types/deployment_type.rb +++ b/app/graphql/types/deployment_type.rb @@ -54,8 +54,7 @@ class DeploymentType < BaseObject field :job, Types::Ci::JobType, - description: 'Pipeline job of the deployment.', - method: :build + description: 'Pipeline job of the deployment.' field :triggerer, Types::UserType, diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 895ff937abeed335dc0a15b5ec4a9de582301f4b..74072c362bd617957685f85ebc4bc3832560405c 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -3,7 +3,6 @@ module Ci class Bridge < Ci::Processable include Ci::Contextable - include Ci::Metadatable include Ci::Deployable include Importable include AfterCommitQueue diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1ce852d4f71c32e2f553aedab0c29670b7598c1d..d85c7a5ccefa9ccb9ba0c7288b3c36c667d13227 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,7 +3,6 @@ module Ci class Build < Ci::Processable prepend Ci::BulkInsertableTags - include Ci::Metadatable include Ci::Contextable include Ci::Deployable include TokenAuthenticatable @@ -158,16 +157,9 @@ class Build < Ci::Processable .includes(:metadata, :job_artifacts_metadata) end - scope :with_project_and_metadata, -> do - if Feature.enabled?(:non_public_artifacts, type: :development) - joins(:metadata).includes(:metadata).preload(:project) - end - end - scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) } scope :with_pipeline_locked_artifacts, -> { joins(:pipeline).where('pipeline.locked': Ci::Pipeline.lockeds[:artifacts_locked]) } scope :last_month, -> { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } scope :ref_protected, -> { where(protected: true) } scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where("#{quoted_table_name}.id = #{Ci::BuildTraceChunk.quoted_table_name}.build_id").select(1)) } @@ -387,10 +379,6 @@ def detailed_status(current_user) .fabricate! end - def other_manual_actions - pipeline.manual_actions.reject { |action| action.name == name } - end - def other_scheduled_actions pipeline.scheduled_actions.reject { |action| action.name == name } end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e2f017b1d14afba89d3d4e4d721aa8eb4f2ff670..a0e5eefafa60bb8b9bc536b6a6fde6f24b06320f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -100,7 +100,7 @@ class Pipeline < Ci::ApplicationRecord has_many :downloadable_artifacts, -> do not_expired.or(where_exists(Ci::Pipeline.artifacts_locked.where("#{Ci::Pipeline.quoted_table_name}.id = #{Ci::Build.quoted_table_name}.commit_id"))).downloadable.with_job end, through: :latest_builds, source: :job_artifacts - has_many :latest_successful_builds, -> { latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :latest_successful_jobs, -> { latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable' has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline @@ -115,7 +115,7 @@ class Pipeline < Ci::ApplicationRecord has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus', inverse_of: :pipeline - has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline + has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Processable', inverse_of: :pipeline has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: :auto_canceled_by_id, diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index 4c421f066f97f9f470d0bfec860e80a77722df0a..7ad1a727a0ef8731af5fc18bc8802c62bd7e3b05 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -6,6 +6,7 @@ module Ci class Processable < ::CommitStatus include Gitlab::Utils::StrongMemoize include FromUnion + include Ci::Metadatable extend ::Gitlab::Utils::Override has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable @@ -16,6 +17,7 @@ class Processable < ::CommitStatus accepts_nested_attributes_for :needs scope :preload_needs, -> { preload(:needs) } + scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } scope :with_needs, -> (names = nil) do needs = Ci::BuildNeed.scoped_build.select(1) @@ -138,6 +140,10 @@ def action? raise NotImplementedError end + def other_manual_actions + pipeline.manual_actions.reject { |action| action.name == name } + end + def when read_attribute(:when) || 'on_success' end diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb index 1c6b82d6ea71236b36b384914d254c893c21229c..b785e39523dcd486f9d5877a043f6492df9da6d4 100644 --- a/app/models/concerns/ci/metadatable.rb +++ b/app/models/concerns/ci/metadatable.rb @@ -24,6 +24,12 @@ module Metadatable delegate :id_tokens, to: :metadata, allow_nil: true before_validation :ensure_metadata, on: :create + + scope :with_project_and_metadata, -> do + if Feature.enabled?(:non_public_artifacts, type: :development) + joins(:metadata).includes(:metadata).preload(:project) + end + end end def has_exposed_artifacts? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 2cef727edf0e600aa80a469af7e1eb18fd498e36..38bf84da073023db8fbc2832df2b7e94f0b139c0 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -185,23 +185,23 @@ def self.last_for_environment(environment) # - deploy job B => production environment # In this case, `last_deployment_group` returns both deployments. # - # NOTE: Preload environment.last_deployment and pipeline.latest_successful_builds prior to avoid N+1. + # NOTE: Preload environment.last_deployment and pipeline.latest_successful_jobs prior to avoid N+1. def self.last_deployment_group_for_environment(env) - return self.none unless env.last_deployment_pipeline&.latest_successful_builds&.present? + return self.none unless env.last_deployment_pipeline&.latest_successful_jobs&.present? BatchLoader.for(env).batch(default_value: self.none) do |environments, loader| - latest_successful_build_ids = [] + latest_successful_job_ids = [] environments_hash = {} environments.each do |environment| environments_hash[environment.id] = environment # Refer comment note above, if not preloaded this can lead to N+1. - latest_successful_build_ids << environment.last_deployment_pipeline.latest_successful_builds.map(&:id) + latest_successful_job_ids << environment.last_deployment_pipeline.latest_successful_jobs.map(&:id) end Deployment - .where(deployable_type: 'CommitStatus', deployable_id: latest_successful_build_ids.flatten) + .where(deployable_type: 'CommitStatus', deployable_id: latest_successful_job_ids.flatten) .preload(last_deployment_group_associations) .group_by { |deployment| deployment.environment_id } .each do |env_id, deployment_group| @@ -218,14 +218,14 @@ def self.find_successful_deployment!(iid) # Fetching any unbounded or large intermediate dataset could lead to loading too many IDs into memory. # See: https://docs.gitlab.com/ee/development/database/multiple_databases.html#use-disable_joins-for-has_one-or-has_many-through-relations # For safety we default limit to fetch not more than 1000 records. - def self.builds(limit = 1000) + def self.jobs(limit = 1000) deployable_ids = where.not(deployable_id: nil).limit(limit).pluck(:deployable_id) - Ci::Build.where(id: deployable_ids) + Ci::Processable.where(id: deployable_ids) end - def build - deployable if deployable.is_a?(::Ci::Build) + def job + deployable if deployable.is_a?(::Ci::Processable) end class << self @@ -290,8 +290,8 @@ def scheduled_actions @scheduled_actions ||= deployable.try(:other_scheduled_actions) end - def playable_build - strong_memoize(:playable_build) do + def playable_job + strong_memoize(:playable_job) do deployable.try(:playable?) ? deployable : nil end end @@ -356,8 +356,8 @@ def formatted_deployment_time end def deployed_by - # We use deployable's user if available because Ci::PlayBuildService - # does not update the deployment's user, just the one for the deployable. + # We use deployable's user if available because Ci::PlayBuildService and Ci::PlayBridgeService + # do not update the deployment's user, just the one for the deployable. # TODO: use deployment's user once https://gitlab.com/gitlab-org/gitlab-foss/issues/66442 # is completed. deployable&.user || user @@ -403,20 +403,20 @@ def update_status(status) false end - def sync_status_with(build) - build_status = build.status + def sync_status_with(job) + job_status = job.status - if ::Feature.enabled?(:track_manual_deployments, build.project) - build_status = 'blocked' if build_status == 'manual' # rubocop:disable Style/SoleNestedConditional + if ::Feature.enabled?(:track_manual_deployments, job.project) + job_status = 'blocked' if job_status == 'manual' # rubocop:disable Style/SoleNestedConditional end - return false unless ::Deployment.statuses.include?(build_status) - return false if build_status == self.status + return false unless ::Deployment.statuses.include?(job_status) + return false if job_status == self.status - update_status!(build_status) + update_status!(job_status) rescue StandardError => e Gitlab::ErrorTracking.track_exception( - StatusSyncError.new(e.message), deployment_id: self.id, build_id: build.id) + StatusSyncError.new(e.message), deployment_id: self.id, job_id: job.id) false end diff --git a/app/models/environment.rb b/app/models/environment.rb index 241b454f5cefa39286f6e69a33a7733f5e268c42..da8513a52fdbcbae02f7893b5e5b60ccf0725635 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -331,9 +331,9 @@ def stop_actions_available? end def cancel_deployment_jobs! - active_deployments.builds.each do |build| - Gitlab::OptimisticLocking.retry_lock(build, name: 'environment_cancel_deployment_jobs') do |build| - build.cancel! if build&.cancelable? + active_deployments.jobs.each do |job| + Gitlab::OptimisticLocking.retry_lock(job, name: 'environment_cancel_deployment_jobs') do |job| + job.cancel! if job&.cancelable? end rescue StandardError => e Gitlab::ErrorTracking.track_exception(e, environment_id: id, deployment_id: deployment.id) @@ -355,8 +355,12 @@ def stop_with_actions!(current_user) Gitlab::OptimisticLocking.retry_lock( stop_action, name: 'environment_stop_with_actions' - ) do |build| - actions << build.play(current_user) + ) do |job| + actions << job.play(current_user) + rescue StateMachines::InvalidTransition + # Ci::PlayBuildService rescues an error of StateMachines::InvalidTransition and fall back to retry. However, + # Ci::PlayBridgeService doesn't rescue it, so we're ignoring the error if it's not playable. + # We should fix this inconsistency in https://gitlab.com/gitlab-org/gitlab/-/issues/420855. end end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index f63a1bf094afbf0dd450eef266f9a9089ad3f8ac..7cd913d057e8995e9da601439dac23c9fbdd1f35 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -41,8 +41,8 @@ class DeploymentEntity < Grape::Entity expose :commit, using: CommitEntity, if: -> (*) { include_details? } expose :manual_actions, using: Ci::JobEntity, if: -> (*) { include_details? && can_create_deployment? } expose :scheduled_actions, using: Ci::JobEntity, if: -> (*) { include_details? && can_create_deployment? } - expose :playable_build, if: -> (deployment) { include_details? && can_create_deployment? && deployment.playable_build } do |deployment, options| - Ci::JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path])) + expose :playable_job, as: :playable_build, if: -> (deployment) { include_details? && can_create_deployment? && deployment.playable_job } do |deployment, options| + Ci::JobEntity.represent(deployment.playable_job, options.merge(only: [:play_path, :retry_path])) end expose :cluster do |deployment, options| diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 0a3bf4c2a7bcf491aa4deeeebc7682be92f33d86..b1f731cdd4de38e381e7162c360096a741a9cd5c 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -4,7 +4,7 @@ class EnvironmentEntity < Grape::Entity include RequestAwareEntity UNNECESSARY_ENTRIES_FOR_UPCOMING_DEPLOYMENT = - %i[manual_actions scheduled_actions playable_build cluster].freeze + %i[manual_actions scheduled_actions playable_job cluster].freeze expose :id diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index d7820dff6ef1b1b565fe3d426ac235103df2ff35..8f3aeea2eedc55ed08b99cace982ceb295df8043 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -94,7 +94,7 @@ def deployment_associations pipeline: { manual_actions: [:metadata, :deployment], scheduled_actions: [:metadata], - latest_successful_builds: [] + latest_successful_jobs: [] }, project: project_associations } diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb index 8865c030d94b9733ffb69e4a0d36a0a7bf1efa1b..62dc323616eb57abc2b744d28aff9ce54c08920b 100644 --- a/app/serializers/environment_status_entity.rb +++ b/app/serializers/environment_status_entity.rb @@ -38,7 +38,7 @@ class EnvironmentStatusEntity < Grape::Entity end expose :deployment, as: :details do |es, options| - DeploymentEntity.represent(es.deployment, options.merge(project: es.project, only: [:playable_build])) + DeploymentEntity.represent(es.deployment, options.merge(project: es.project, only: [:playable_job])) end expose :environment_available do |es| diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb deleted file mode 100644 index 15384fb0db1e15b08d381e1c4a6994bc424c2775..0000000000000000000000000000000000000000 --- a/app/services/deployments/older_deployments_drop_service.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Deployments - class OlderDeploymentsDropService - attr_reader :deployment - - def initialize(deployment_id) - @deployment = Deployment.find_by_id(deployment_id) - end - - def execute - return unless @deployment&.running? - - older_deployments_builds.each do |build| - next if build.manual? - - Gitlab::OptimisticLocking.retry_lock(build, name: 'older_deployments_drop') do |build| - build.drop(:forward_deployment_failure) - end - rescue StandardError => e - Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, build_id: build.id) - end - end - - private - - def older_deployments_builds - @deployment - .environment - .active_deployments - .older_than(@deployment) - .builds - end - end -end diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index c5c0634994d3c101ceba905733f00f9b434d3e9c..1f953ba0c2fc300d82cbb483eaaae983a06c7395 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -49,6 +49,10 @@ status { 'created' } end + trait :running do + status { 'running' } + end + trait :started do started_at { '2013-10-29 09:51:28 CET' } end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 952f3bb6ad8129fbe373b11f7e49e0daa2bc4be0..641cc13adb74665446eb1c4283c5735d94d6ee88 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -326,7 +326,7 @@ ci_pipelines: - security_findings - daily_build_group_report_results - latest_builds -- latest_successful_builds +- latest_successful_jobs - daily_report_results - latest_builds_report_results - messages diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9ffa4263648b5d1905b443dcb3a9b2995d8b8b35..54c0c08614413f629803c463363c2ba97447e482 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -105,18 +105,6 @@ let(:job) { build } end - describe '.manual_actions' do - let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } - let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } - let!(:manual_action) { create(:ci_build, :manual, pipeline: pipeline) } - - subject { described_class.manual_actions } - - it { is_expected.to include(manual_action) } - it { is_expected.to include(manual_but_succeeded) } - it { is_expected.not_to include(manual_but_created) } - end - describe '.ref_protected' do subject { described_class.ref_protected } @@ -2008,29 +1996,6 @@ end end - describe '#other_manual_actions' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - - subject { build.other_manual_actions } - - before do - project.add_developer(user) - end - - it 'returns other actions' do - is_expected.to contain_exactly(other_build) - end - - context 'when build is retried' do - let!(:new_build) { Ci::RetryJobService.new(project, user).execute(build)[:job] } - - it 'does not return any of them' do - is_expected.not_to include(build, new_build) - end - end - end - describe '#other_scheduled_actions' do let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 1e3ce8bc6560465ea1593d55c51ae8e36377ded9..1de19faf063e7d16f5cefbc4f15af42df2a6e1f0 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -110,14 +110,16 @@ end end - describe '#latest_successful_builds' do - it 'has a one to many relationship with its latest successful builds' do + describe '#latest_successful_jobs' do + it 'has a one to many relationship with its latest successful jobs' do _old_build = create(:ci_build, :retried, pipeline: pipeline) _expired_build = create(:ci_build, :expired, pipeline: pipeline) - _failed_builds = create_list(:ci_build, 2, :failed, pipeline: pipeline) - successful_builds = create_list(:ci_build, 2, :success, pipeline: pipeline) + _failed_jobs = [create(:ci_build, :failed, pipeline: pipeline), + create(:ci_bridge, :failed, pipeline: pipeline)] + successful_jobs = [create(:ci_build, :success, pipeline: pipeline), + create(:ci_bridge, :success, pipeline: pipeline)] - expect(pipeline.latest_successful_builds).to contain_exactly(successful_builds.first, successful_builds.second) + expect(pipeline.latest_successful_jobs).to contain_exactly(successful_jobs.first, successful_jobs.second) end end diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index e1c449e18ace308ebf6ad34e8d053ce10e1bf168..0c7a13e415a59bff5927dcd9b368b4fae0eaf5cc 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be_with_refind(:pipeline) { create(:ci_pipeline, project: project) } describe 'delegations' do subject { described_class.new } @@ -503,4 +503,61 @@ end end end + + describe '.manual_actions' do + shared_examples_for 'manual actions for a job' do + let!(:manual_but_created) { create(factory_type, :manual, status: :created, pipeline: pipeline) } + let!(:manual_but_succeeded) { create(factory_type, :manual, status: :success, pipeline: pipeline) } + let!(:manual_action) { create(factory_type, :manual, pipeline: pipeline) } + + subject { described_class.manual_actions } + + it { is_expected.to include(manual_action) } + it { is_expected.to include(manual_but_succeeded) } + it { is_expected.not_to include(manual_but_created) } + end + + it_behaves_like 'manual actions for a job' do + let(:factory_type) { :ci_build } + end + + it_behaves_like 'manual actions for a job' do + let(:factory_type) { :ci_bridge } + end + end + + describe '#other_manual_actions' do + let_it_be(:user) { create(:user) } + + before_all do + project.add_developer(user) + end + + shared_examples_for 'other manual actions for a job' do + let(:job) { create(factory_type, :manual, pipeline: pipeline, project: project) } + let!(:other_job) { create(factory_type, :manual, pipeline: pipeline, project: project, name: 'other action') } + + subject { job.other_manual_actions } + + it 'returns other actions' do + is_expected.to contain_exactly(other_job) + end + + context 'when job is retried' do + let!(:new_job) { Ci::RetryJobService.new(project, user).execute(job)[:job] } + + it 'does not return any of them' do + is_expected.not_to include(job, new_job) + end + end + end + + it_behaves_like 'other manual actions for a job' do + let(:factory_type) { :ci_build } + end + + it_behaves_like 'other manual actions for a job' do + let(:factory_type) { :ci_bridge } + end + end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index cd768244b04c459bbef71d6a40bc5cabd83c0480..60c5035c03eb02142e6c92c201bec9b9edda546c 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -648,163 +648,177 @@ def subject_method(environment) let!(:project) { create(:project, :repository) } let!(:environment) { create(:environment, project: project) } - context 'when there are no deployments and builds' do - it do - expect(subject_method(environment)).to eq(described_class.none) + shared_examples_for 'find last deployment group for environment' do + context 'when there are no deployments and jobs' do + it do + expect(subject_method(environment)).to eq(described_class.none) + end end - end - context 'when there are no successful builds' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:ci_build) { create(:ci_build, :running, project: project, pipeline: pipeline) } + context 'when there are no successful jobs' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:job) { create(factory_type, :created, project: project, pipeline: pipeline) } - before do - create(:deployment, :success, environment: environment, project: project, deployable: ci_build) - end + before do + create(:deployment, :created, environment: environment, project: project, deployable: job) + end - it do - expect(subject_method(environment)).to eq(described_class.none) + it do + expect(subject_method(environment)).to eq(described_class.none) + end end - end - context 'when there are deployments for multiple pipelines' do - let(:pipeline_a) { create(:ci_pipeline, project: project) } - let(:pipeline_b) { create(:ci_pipeline, project: project) } - let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } - let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } - let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } - let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } + context 'when there are deployments for multiple pipelines' do + let(:pipeline_a) { create(:ci_pipeline, project: project) } + let(:pipeline_b) { create(:ci_pipeline, project: project) } + let(:job_a) { create(factory_type, :success, project: project, pipeline: pipeline_a) } + let(:job_b) { create(factory_type, :failed, project: project, pipeline: pipeline_b) } + let(:job_c) { create(factory_type, :success, project: project, pipeline: pipeline_a) } + let(:job_d) { create(factory_type, :failed, project: project, pipeline: pipeline_a) } + + # Successful deployments for pipeline_a + let!(:deployment_a) do + create(:deployment, :success, project: project, environment: environment, deployable: job_a) + end - # Successful deployments for pipeline_a - let!(:deployment_a) do - create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) - end + let!(:deployment_b) do + create(:deployment, :success, project: project, environment: environment, deployable: job_c) + end - let!(:deployment_b) do - create(:deployment, :success, project: project, environment: environment, deployable: ci_build_c) - end + before do + # Failed deployment for pipeline_a + create(:deployment, :failed, project: project, environment: environment, deployable: job_d) - before do - # Failed deployment for pipeline_a - create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d) + # Failed deployment for pipeline_b + create(:deployment, :failed, project: project, environment: environment, deployable: job_b) + end - # Failed deployment for pipeline_b - create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) + it 'returns the successful deployment jobs for the last deployment pipeline' do + expect(subject_method(environment).pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) + end end - it 'returns the successful deployment jobs for the last deployment pipeline' do - expect(subject_method(environment).pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) - end - end + context 'when there are many environments' do + let(:environment_b) { create(:environment, project: project) } + + let(:pipeline_a) { create(:ci_pipeline, project: project) } + let(:pipeline_b) { create(:ci_pipeline, project: project) } + let(:pipeline_c) { create(:ci_pipeline, project: project) } + let(:pipeline_d) { create(:ci_pipeline, project: project) } + + # Builds for first environment: 'environment' with pipeline_a and pipeline_b + let(:job_a) { create(factory_type, :success, project: project, pipeline: pipeline_a) } + let(:job_b) { create(factory_type, :failed, project: project, pipeline: pipeline_b) } + let(:job_c) { create(factory_type, :success, project: project, pipeline: pipeline_a) } + let(:job_d) { create(factory_type, :failed, project: project, pipeline: pipeline_a) } + let!(:stop_env_a) do + create(factory_type, :manual, project: project, pipeline: pipeline_a, name: 'stop_env_a') + end - context 'when there are many environments' do - let(:environment_b) { create(:environment, project: project) } + # Builds for second environment: 'environment_b' with pipeline_c and pipeline_d + let(:job_e) { create(factory_type, :success, project: project, pipeline: pipeline_c) } + let(:job_f) { create(factory_type, :failed, project: project, pipeline: pipeline_d) } + let(:job_g) { create(factory_type, :success, project: project, pipeline: pipeline_c) } + let(:job_h) { create(factory_type, :failed, project: project, pipeline: pipeline_c) } + let!(:stop_env_b) do + create(factory_type, :manual, project: project, pipeline: pipeline_c, name: 'stop_env_b') + end - let(:pipeline_a) { create(:ci_pipeline, project: project) } - let(:pipeline_b) { create(:ci_pipeline, project: project) } - let(:pipeline_c) { create(:ci_pipeline, project: project) } - let(:pipeline_d) { create(:ci_pipeline, project: project) } + # Successful deployments for 'environment' from pipeline_a + let!(:deployment_a) do + create(:deployment, :success, project: project, environment: environment, deployable: job_a) + end - # Builds for first environment: 'environment' with pipeline_a and pipeline_b - let(:ci_build_a) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } - let(:ci_build_b) { create(:ci_build, :failed, project: project, pipeline: pipeline_b) } - let(:ci_build_c) { create(:ci_build, :success, project: project, pipeline: pipeline_a) } - let(:ci_build_d) { create(:ci_build, :failed, project: project, pipeline: pipeline_a) } - let!(:stop_env_a) { create(:ci_build, :manual, project: project, pipeline: pipeline_a, name: 'stop_env_a') } + let!(:deployment_b) do + create(:deployment, :success, + project: project, environment: environment, deployable: job_c, on_stop: 'stop_env_a') + end - # Builds for second environment: 'environment_b' with pipeline_c and pipeline_d - let(:ci_build_e) { create(:ci_build, :success, project: project, pipeline: pipeline_c) } - let(:ci_build_f) { create(:ci_build, :failed, project: project, pipeline: pipeline_d) } - let(:ci_build_g) { create(:ci_build, :success, project: project, pipeline: pipeline_c) } - let(:ci_build_h) { create(:ci_build, :failed, project: project, pipeline: pipeline_c) } - let!(:stop_env_b) { create(:ci_build, :manual, project: project, pipeline: pipeline_c, name: 'stop_env_b') } + # Successful deployments for 'environment_b' from pipeline_c + let!(:deployment_c) do + create(:deployment, :success, project: project, environment: environment_b, deployable: job_e) + end - # Successful deployments for 'environment' from pipeline_a - let!(:deployment_a) do - create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a) - end + let!(:deployment_d) do + create(:deployment, :success, + project: project, environment: environment_b, deployable: job_g, on_stop: 'stop_env_b') + end - let!(:deployment_b) do - create(:deployment, :success, - project: project, environment: environment, deployable: ci_build_c, on_stop: 'stop_env_a') - end + before do + # Failed deployment for 'environment' from pipeline_a and pipeline_b + create(:deployment, :failed, project: project, environment: environment, deployable: job_d) + create(:deployment, :failed, project: project, environment: environment, deployable: job_b) - # Successful deployments for 'environment_b' from pipeline_c - let!(:deployment_c) do - create(:deployment, :success, project: project, environment: environment_b, deployable: ci_build_e) - end + # Failed deployment for 'environment_b' from pipeline_c and pipeline_d + create(:deployment, :failed, project: project, environment: environment_b, deployable: job_h) + create(:deployment, :failed, project: project, environment: environment_b, deployable: job_f) + end - let!(:deployment_d) do - create(:deployment, :success, - project: project, environment: environment_b, deployable: ci_build_g, on_stop: 'stop_env_b') - end + it 'batch loads for environments' do + environments = [environment, environment_b] - before do - # Failed deployment for 'environment' from pipeline_a and pipeline_b - create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_d) - create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b) + # Loads Batch loader + environments.each do |env| + subject_method(env) + end - # Failed deployment for 'environment_b' from pipeline_c and pipeline_d - create(:deployment, :failed, project: project, environment: environment_b, deployable: ci_build_h) - create(:deployment, :failed, project: project, environment: environment_b, deployable: ci_build_f) - end + expect(subject_method(environments.first).pluck(:id)) + .to contain_exactly(deployment_a.id, deployment_b.id) - it 'batch loads for environments' do - environments = [environment, environment_b] + expect { subject_method(environments.second).pluck(:id) }.not_to exceed_query_limit(0) - # Loads Batch loader - environments.each do |env| - subject_method(env) - end + expect(subject_method(environments.second).pluck(:id)) + .to contain_exactly(deployment_c.id, deployment_d.id) - expect(subject_method(environments.first).pluck(:id)) - .to contain_exactly(deployment_a.id, deployment_b.id) + expect(subject_method(environments.first).map(&:stop_action).compact) + .to contain_exactly(stop_env_a) - expect { subject_method(environments.second).pluck(:id) }.not_to exceed_query_limit(0) + expect { subject_method(environments.second).map(&:stop_action) } + .not_to exceed_query_limit(0) - expect(subject_method(environments.second).pluck(:id)) - .to contain_exactly(deployment_c.id, deployment_d.id) + expect(subject_method(environments.second).map(&:stop_action).compact) + .to contain_exactly(stop_env_b) + end + end - expect(subject_method(environments.first).map(&:stop_action).compact) - .to contain_exactly(stop_env_a) + context 'When last deployment for environment is a retried job' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:environment_b) { create(:environment, project: project) } - expect { subject_method(environments.second).map(&:stop_action) } - .not_to exceed_query_limit(0) + let(:job_a) do + create(factory_type, :success, project: project, pipeline: pipeline, environment: environment.name) + end - expect(subject_method(environments.second).map(&:stop_action).compact) - .to contain_exactly(stop_env_b) - end - end + let(:job_b) do + create(factory_type, :success, project: project, pipeline: pipeline, environment: environment_b.name) + end - context 'When last deployment for environment is a retried build' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:environment_b) { create(:environment, project: project) } + let!(:deployment_a) do + create(:deployment, :success, project: project, environment: environment, deployable: job_a) + end - let(:build_a) do - create(:ci_build, :success, project: project, pipeline: pipeline, environment: environment.name) - end + let!(:deployment_b) do + create(:deployment, :success, project: project, environment: environment_b, deployable: job_b) + end - let(:build_b) do - create(:ci_build, :success, project: project, pipeline: pipeline, environment: environment_b.name) - end + before do + # Retry job_b + job_b.update!(retried: true) - let!(:deployment_a) do - create(:deployment, :success, project: project, environment: environment, deployable: build_a) - end + # New successful job after retry. + create(factory_type, :success, project: project, pipeline: pipeline, environment: environment_b.name) + end - let!(:deployment_b) do - create(:deployment, :success, project: project, environment: environment_b, deployable: build_b) + it { expect(subject_method(environment_b)).not_to be_nil } end + end - before do - # Retry build_b - build_b.update!(retried: true) - - # New successful build after retry. - create(:ci_build, :success, project: project, pipeline: pipeline, environment: environment_b.name) - end + it_behaves_like 'find last deployment group for environment' do + let(:factory_type) { :ci_build } + end - it { expect(subject_method(environment_b)).not_to be_nil } + it_behaves_like 'find last deployment group for environment' do + let(:factory_type) { :ci_bridge } end end end @@ -873,31 +887,41 @@ def subject_method(environment) end describe '#stop_action' do - let(:build) { create(:ci_build) } - subject { deployment.stop_action } - context 'when no other actions' do - let(:deployment) { FactoryBot.build(:deployment, deployable: build) } - - it { is_expected.to be_nil } - end - - context 'with other actions' do - let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } + shared_examples_for 'stop action for a job' do + let(:job) { create(factory_type) } # rubocop:disable Rails/SaveBang - context 'when matching action is defined' do - let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_other_app') } + context 'when no other actions' do + let(:deployment) { FactoryBot.build(:deployment, deployable: job) } it { is_expected.to be_nil } end - context 'when no matching action is defined' do - let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_app') } + context 'with other actions' do + let!(:close_action) { create(factory_type, :manual, pipeline: job.pipeline, name: 'close_app') } + + context 'when matching action is defined' do + let(:deployment) { FactoryBot.build(:deployment, deployable: job, on_stop: 'close_other_app') } + + it { is_expected.to be_nil } + end + + context 'when no matching action is defined' do + let(:deployment) { FactoryBot.build(:deployment, deployable: job, on_stop: 'close_app') } - it { is_expected.to eq(close_action) } + it { is_expected.to eq(close_action) } + end end end + + it_behaves_like 'stop action for a job' do + let(:factory_type) { :ci_build } + end + + it_behaves_like 'stop action for a job' do + let(:factory_type) { :ci_bridge } + end end describe '#deployed_by' do @@ -908,10 +932,18 @@ def subject_method(environment) expect(deployment.deployed_by).to eq(deployment_user) end - it 'returns the deployment user if the deployable have no user' do + it 'returns the deployment user if the deployable is build and have no user' do deployment_user = create(:user) - build = create(:ci_build, user: nil) - deployment = create(:deployment, deployable: build, user: deployment_user) + job = create(:ci_build, user: nil) + deployment = create(:deployment, deployable: job, user: deployment_user) + + expect(deployment.deployed_by).to eq(deployment_user) + end + + it 'returns the deployment user if the deployable is bridge and have no user' do + deployment_user = create(:user) + job = create(:ci_bridge, user: nil) + deployment = create(:deployment, deployable: job, user: deployment_user) expect(deployment.deployed_by).to eq(deployment_user) end @@ -919,8 +951,8 @@ def subject_method(environment) it 'returns the deployable user if there is one' do build_user = create(:user) deployment_user = create(:user) - build = create(:ci_build, user: build_user) - deployment = create(:deployment, deployable: build, user: deployment_user) + job = create(:ci_build, user: build_user) + deployment = create(:deployment, deployable: job, user: deployment_user) expect(deployment.deployed_by).to eq(build_user) end @@ -954,14 +986,14 @@ def subject_method(environment) end end - describe '.builds' do + describe '.jobs' do let!(:deployment1) { create(:deployment) } let!(:deployment2) { create(:deployment) } let!(:deployment3) { create(:deployment) } - subject { described_class.builds } + subject { described_class.jobs } - it 'retrieves builds for the deployments' do + it 'retrieves jobs for the deployments' do is_expected.to match_array( [deployment1.deployable, deployment2.deployable, deployment3.deployable]) end @@ -974,16 +1006,16 @@ def subject_method(environment) end end - describe '#build' do + describe '#job' do let!(:deployment) { create(:deployment) } - subject { deployment.build } + subject { deployment.job } - it 'retrieves build for the deployment' do + it 'retrieves job for the deployment' do is_expected.to eq(deployment.deployable) end - it 'returns nil when the associated build is not found' do + it 'returns nil when the associated job is not found' do deployment.update!(deployable_id: nil, deployable_type: nil) is_expected.to be_nil @@ -1088,22 +1120,30 @@ def subject_method(environment) end end - describe '#playable_build' do - subject { deployment.playable_build } + describe '#playable_job' do + subject { deployment.playable_job } - context 'when there is a deployable build' do - let(:deployment) { create(:deployment, deployable: build) } + context 'when there is a deployable job' do + let(:deployment) { create(:deployment, deployable: job) } - context 'when the deployable build is playable' do - let(:build) { create(:ci_build, :playable) } + context 'when the deployable job is build and playable' do + let(:job) { create(:ci_build, :playable) } - it 'returns that build' do - is_expected.to eq(build) + it 'returns that job' do + is_expected.to eq(job) end end - context 'when the deployable build is not playable' do - let(:build) { create(:ci_build) } + context 'when the deployable job is bridge and playable' do + let(:job) { create(:ci_bridge, :playable) } + + it 'returns that job' do + is_expected.to eq(job) + end + end + + context 'when the deployable job is not playable' do + let(:job) { create(:ci_build) } it 'returns nil' do is_expected.to be_nil @@ -1111,7 +1151,7 @@ def subject_method(environment) end end - context 'when there is no deployable build' do + context 'when there is no deployable job' do let(:deployment) { create(:deployment) } it 'returns nil' do @@ -1207,193 +1247,203 @@ def subject_method(environment) end describe '#sync_status_with' do - subject { deployment.sync_status_with(ci_build) } + subject { deployment.sync_status_with(job) } let_it_be(:project) { create(:project, :repository) } - let(:deployment) { create(:deployment, project: project, status: deployment_status) } - let(:ci_build) { create(:ci_build, project: project, status: build_status) } + shared_examples_for 'sync status with a job' do + let(:deployment) { create(:deployment, project: project, status: deployment_status) } + let(:job) { create(factory_type, project: project, status: job_status) } - shared_examples_for 'synchronizing deployment' do - let(:expected_deployment_status) { build_status.to_s } + shared_examples_for 'synchronizing deployment' do + let(:expected_deployment_status) { job_status.to_s } - it 'changes deployment status' do - expect(Gitlab::ErrorTracking).not_to receive(:track_exception) + it 'changes deployment status' do + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - is_expected.to eq(true) + is_expected.to eq(true) - expect(deployment.status).to eq(expected_deployment_status) - expect(deployment.errors).to be_empty + expect(deployment.status).to eq(expected_deployment_status) + expect(deployment.errors).to be_empty + end end - end - shared_examples_for 'gracefully handling error' do - it 'tracks an exception' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - instance_of(described_class::StatusSyncError), - deployment_id: deployment.id, - build_id: ci_build.id) + shared_examples_for 'gracefully handling error' do + it 'tracks an exception' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(described_class::StatusSyncError), + deployment_id: deployment.id, + job_id: job.id) - is_expected.to eq(false) + is_expected.to eq(false) - expect(deployment.status).to eq(deployment_status.to_s) - expect(deployment.errors.full_messages).to include(error_message) + expect(deployment.status).to eq(deployment_status.to_s) + expect(deployment.errors.full_messages).to include(error_message) + end end - end - shared_examples_for 'ignoring build' do - it 'does not change deployment status' do - expect(Gitlab::ErrorTracking).not_to receive(:track_exception) + shared_examples_for 'ignoring job' do + it 'does not change deployment status' do + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - is_expected.to eq(false) + is_expected.to eq(false) - expect(deployment.status).to eq(deployment_status.to_s) - expect(deployment.errors).to be_empty + expect(deployment.status).to eq(deployment_status.to_s) + expect(deployment.errors).to be_empty + end end - end - - context 'with created deployment' do - let(:deployment_status) { :created } - context 'with created build' do - let(:build_status) { :created } - - it_behaves_like 'ignoring build' - end + context 'with created deployment' do + let(:deployment_status) { :created } - context 'with manual build' do - let(:build_status) { :manual } + context 'with created job' do + let(:job_status) { :created } - it_behaves_like 'synchronizing deployment' do - let(:expected_deployment_status) { 'blocked' } + it_behaves_like 'ignoring job' end - context 'when track_manual_deployments feature flag is disabled' do - before do - stub_feature_flags(track_manual_deployments: false) + context 'with manual job' do + let(:job_status) { :manual } + + it_behaves_like 'synchronizing deployment' do + let(:expected_deployment_status) { 'blocked' } end - it_behaves_like 'ignoring build' + context 'when track_manual_deployments feature flag is disabled' do + before do + stub_feature_flags(track_manual_deployments: false) + end + + it_behaves_like 'ignoring job' + end end - end - context 'with running build' do - let(:build_status) { :running } + context 'with running job' do + let(:job_status) { :running } - it_behaves_like 'synchronizing deployment' - end + it_behaves_like 'synchronizing deployment' + end - context 'with finished build' do - let(:build_status) { :success } + context 'with finished job' do + let(:job_status) { :success } - it_behaves_like 'synchronizing deployment' - end + it_behaves_like 'synchronizing deployment' + end - context 'with unrelated build' do - let(:build_status) { :waiting_for_resource } + context 'with unrelated job' do + let(:job_status) { :waiting_for_resource } - it_behaves_like 'ignoring build' + it_behaves_like 'ignoring job' + end end - end - context 'with running deployment' do - let(:deployment_status) { :running } + context 'with running deployment' do + let(:deployment_status) { :running } - context 'with created build' do - let(:build_status) { :created } + context 'with created job' do + let(:job_status) { :created } - it_behaves_like 'gracefully handling error' do - let(:error_message) { %{Status cannot transition via \"create\"} } + it_behaves_like 'gracefully handling error' do + let(:error_message) { %{Status cannot transition via \"create\"} } + end end - end - context 'with manual build' do - let(:build_status) { :manual } - - it_behaves_like 'gracefully handling error' do - let(:error_message) { %{Status cannot transition via \"block\"} } - end + context 'with manual job' do + let(:job_status) { :manual } - context 'when track_manual_deployments feature flag is disabled' do - before do - stub_feature_flags(track_manual_deployments: false) + it_behaves_like 'gracefully handling error' do + let(:error_message) { %{Status cannot transition via \"block\"} } end - it_behaves_like 'ignoring build' + context 'when track_manual_deployments feature flag is disabled' do + before do + stub_feature_flags(track_manual_deployments: false) + end + + it_behaves_like 'ignoring job' + end end - end - context 'with running build' do - let(:build_status) { :running } + context 'with running job' do + let(:job_status) { :running } - it_behaves_like 'ignoring build' - end + it_behaves_like 'ignoring job' + end - context 'with finished build' do - let(:build_status) { :success } + context 'with finished job' do + let(:job_status) { :success } - it_behaves_like 'synchronizing deployment' - end + it_behaves_like 'synchronizing deployment' + end - context 'with unrelated build' do - let(:build_status) { :waiting_for_resource } + context 'with unrelated job' do + let(:job_status) { :waiting_for_resource } - it_behaves_like 'ignoring build' + it_behaves_like 'ignoring job' + end end - end - context 'with finished deployment' do - let(:deployment_status) { :success } + context 'with finished deployment' do + let(:deployment_status) { :success } - context 'with created build' do - let(:build_status) { :created } + context 'with created job' do + let(:job_status) { :created } - it_behaves_like 'gracefully handling error' do - let(:error_message) { %{Status cannot transition via \"create\"} } + it_behaves_like 'gracefully handling error' do + let(:error_message) { %{Status cannot transition via \"create\"} } + end end - end - context 'with manual build' do - let(:build_status) { :manual } + context 'with manual job' do + let(:job_status) { :manual } - it_behaves_like 'gracefully handling error' do - let(:error_message) { %{Status cannot transition via \"block\"} } - end + it_behaves_like 'gracefully handling error' do + let(:error_message) { %{Status cannot transition via \"block\"} } + end - context 'when track_manual_deployments feature flag is disabled' do - before do - stub_feature_flags(track_manual_deployments: false) + context 'when track_manual_deployments feature flag is disabled' do + before do + stub_feature_flags(track_manual_deployments: false) + end + + it_behaves_like 'ignoring job' end + end - it_behaves_like 'ignoring build' + context 'with running job' do + let(:job_status) { :running } + + it_behaves_like 'gracefully handling error' do + let(:error_message) { %{Status cannot transition via \"run\"} } + end end - end - context 'with running build' do - let(:build_status) { :running } + context 'with finished job' do + let(:job_status) { :success } - it_behaves_like 'gracefully handling error' do - let(:error_message) { %{Status cannot transition via \"run\"} } + it_behaves_like 'ignoring job' end - end - context 'with finished build' do - let(:build_status) { :success } + context 'with failed job' do + let(:job_status) { :failed } - it_behaves_like 'ignoring build' - end + it_behaves_like 'synchronizing deployment' + end - context 'with failed build' do - let(:build_status) { :failed } + context 'with unrelated job' do + let(:job_status) { :waiting_for_resource } - it_behaves_like 'synchronizing deployment' + it_behaves_like 'ignoring job' + end end + end - context 'with unrelated build' do - let(:build_status) { :waiting_for_resource } + it_behaves_like 'sync status with a job' do + let(:factory_type) { :ci_build } + end - it_behaves_like 'ignoring build' - end + it_behaves_like 'sync status with a job' do + let(:factory_type) { :ci_bridge } end end @@ -1467,6 +1517,14 @@ def subject_method(environment) expect(subject.tier_in_yaml).to eq('testing') end + context 'when deployable is a bridge job' do + let(:deployable) { create(:ci_bridge, :success, :environment_with_deployment_tier) } + + it 'returns the tier' do + expect(subject.tier_in_yaml).to eq('testing') + end + end + context 'when tier is not specified' do let(:deployable) { create(:ci_build, :success) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 066763645abf1889588ee029efd58520f8771966..2ad92907d7733b9834e4927a3a974038c5639711 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -690,178 +690,214 @@ subject { environment.stop_with_actions!(user) } - before do - expect(environment).to receive(:available?).and_call_original - end - - context 'when no other actions' do - context 'environment is available' do - before do - environment.update!(state: :available) - end - - it do - actions = subject - - expect(environment).to be_stopped - expect(actions).to match_array([]) - end + shared_examples_for 'stop with playing a teardown job' do + before do + expect(environment).to receive(:available?).and_call_original end - context 'environment is already stopped' do - before do - environment.update!(state: :stopped) - end + context 'when no other actions' do + context 'environment is available' do + before do + environment.update!(state: :available) + end - it do - subject + it do + actions = subject - expect(environment).to be_stopped + expect(environment).to be_stopped + expect(actions).to match_array([]) + end end - end - end - - context 'when matching action is defined' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } - before do - create(:deployment, :success, - environment: environment, - deployable: build_a, - on_stop: 'close_app_a') - end + context 'environment is already stopped' do + before do + environment.update!(state: :stopped) + end - context 'when user is not allowed to stop environment' do - let!(:close_action) do - create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') - end + it do + subject - it 'raises an exception' do - expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError) + expect(environment).to be_stopped + end end end - context 'when user is allowed to stop environment' do - before do - project.add_developer(user) + context 'when matching action is defined' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:job_a) { create(factory_type, :success, pipeline: pipeline, **factory_options) } - create(:protected_branch, :developers_can_merge, name: 'master', project: project) + before do + create(:deployment, :success, + environment: environment, + deployable: job_a, + on_stop: 'close_app_a') end - context 'when action did not yet finish' do + context 'when user is not allowed to stop environment' do let!(:close_action) do - create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') + create(factory_type, :manual, pipeline: pipeline, name: 'close_app_a', **factory_options) end - it 'returns the same action' do - action = subject.first - expect(action).to eq(close_action) - expect(action.user).to eq(user) + it 'raises an exception' do + expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError) end + end - it 'environment is not stopped' do - subject + context 'when user is allowed to stop environment' do + before do + project.add_developer(user) - expect(environment).not_to be_stopped + create(:protected_branch, :developers_can_merge, name: 'master', project: project) end - end - context 'if action did finish' do - let!(:close_action) do - create(:ci_build, :manual, :success, pipeline: pipeline, name: 'close_app_a') - end + context 'when action did not yet finish' do + let!(:close_action) do + create(factory_type, :manual, pipeline: pipeline, name: 'close_app_a', **factory_options) + end - it 'returns a new action of the same type' do - action = subject.first + it 'returns the same action' do + action = subject.first + expect(action).to eq(close_action) + expect(action.user).to eq(user) + end - expect(action).to be_persisted - expect(action.name).to eq(close_action.name) - expect(action.user).to eq(user) - end - end + it 'environment is not stopped' do + subject - context 'close action does not raise ActiveRecord::StaleObjectError' do - let!(:close_action) do - create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a') + expect(environment).not_to be_stopped + end end - before do - # preload the build - environment.stop_actions + context 'if action did finish' do + let!(:close_action) do + create(factory_type, :manual, :success, pipeline: pipeline, name: 'close_app_a', **factory_options) + end - # Update record as the other process. This makes `environment.stop_action` stale. - close_action.drop! - end + it 'returns a new action of the same type when build job' do + skip unless factory_type == :ci_build - it 'successfully plays the build even if the build was a stale object' do - # Since build is droped. - expect(close_action.processed).to be_falsey + action = subject.first + + expect(action).to be_persisted + expect(action.name).to eq(close_action.name) + expect(action.user).to eq(user) + end - # it encounters the StaleObjectError at first, but reloads the object and runs `build.play` - expect { subject }.not_to raise_error + it 'does nothing when bridge job' do + skip unless factory_type == :ci_bridge - # Now the build should be processed. - expect(close_action.reload.processed).to be_truthy + action = subject.first + + expect(action).to be_nil + end end - end - end - end - context 'when there are more then one stop action for the environment' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build_a) { create(:ci_build, :success, pipeline: pipeline) } - let(:build_b) { create(:ci_build, :success, pipeline: pipeline) } + context 'close action does not raise ActiveRecord::StaleObjectError' do + let!(:close_action) do + create(factory_type, :manual, pipeline: pipeline, name: 'close_app_a', **factory_options) + end - let!(:close_actions) do - [ - create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_a'), - create(:ci_build, :manual, pipeline: pipeline, name: 'close_app_b') - ] - end + before do + # preload the job + environment.stop_actions - before do - project.add_developer(user) + # Update record as the other process. This makes `environment.stop_action` stale. + close_action.drop! + end - create(:deployment, :success, - environment: environment, - deployable: build_a, - finished_at: 5.minutes.ago, - on_stop: 'close_app_a') + it 'successfully plays the job even if the job was a stale object when build job' do + skip unless factory_type == :ci_build - create(:deployment, :success, - environment: environment, - deployable: build_b, - finished_at: 1.second.ago, - on_stop: 'close_app_b') - end + # Since job is droped. + expect(close_action.processed).to be_falsey + + # it encounters the StaleObjectError at first, but reloads the object and runs `job.play` + expect { subject }.not_to raise_error + + # Now the job should be processed. + expect(close_action.reload.processed).to be_truthy + end + + it 'does nothing when bridge job' do + skip unless factory_type == :ci_bridge + + expect(close_action.processed).to be_falsey - it 'returns the same actions' do - actions = subject + # it encounters the StaleObjectError at first, but reloads the object and runs `job.play` + expect { subject }.not_to raise_error - expect(actions.count).to eq(close_actions.count) - expect(actions.pluck(:id)).to match_array(close_actions.pluck(:id)) - expect(actions.pluck(:user)).to match_array(close_actions.pluck(:user)) + # Bridge is not retried currently. + expect(close_action.processed).to be_falsey + end + end + end end - context 'when there are failed builds' do + context 'when there are more then one stop action for the environment' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:job_a) { create(factory_type, :success, pipeline: pipeline, **factory_options) } + let(:job_b) { create(factory_type, :success, pipeline: pipeline, **factory_options) } + + let!(:close_actions) do + [ + create(factory_type, :manual, pipeline: pipeline, name: 'close_app_a', **factory_options), + create(factory_type, :manual, pipeline: pipeline, name: 'close_app_b', **factory_options) + ] + end + before do - create(:ci_build, :failed, pipeline: pipeline, name: 'close_app_c') + project.add_developer(user) + + create(:deployment, :success, + environment: environment, + deployable: job_a, + finished_at: 5.minutes.ago, + on_stop: 'close_app_a') - create(:deployment, :failed, + create(:deployment, :success, environment: environment, - deployable: create(:ci_build, pipeline: pipeline), - on_stop: 'close_app_c') + deployable: job_b, + finished_at: 1.second.ago, + on_stop: 'close_app_b') end - it 'returns only stop actions from successful builds' do + it 'returns the same actions' do actions = subject - expect(actions).to match_array(close_actions) - expect(actions.count).to eq(pipeline.latest_successful_builds.count) + expect(actions.count).to eq(close_actions.count) + expect(actions.pluck(:id)).to match_array(close_actions.pluck(:id)) + expect(actions.pluck(:user)).to match_array(close_actions.pluck(:user)) + end + + context 'when there are failed builds' do + before do + create(factory_type, :failed, pipeline: pipeline, name: 'close_app_c', **factory_options) + + create(:deployment, :failed, + environment: environment, + deployable: create(factory_type, pipeline: pipeline, **factory_options), + on_stop: 'close_app_c') + end + + it 'returns only stop actions from successful builds' do + actions = subject + + expect(actions).to match_array(close_actions) + expect(actions.count).to eq(pipeline.latest_successful_jobs.count) + end end end end + + it_behaves_like 'stop with playing a teardown job' do + let(:factory_type) { :ci_build } + let(:factory_options) { {} } + end + + it_behaves_like 'stop with playing a teardown job' do + let(:factory_type) { :ci_bridge } + let(:factory_options) { { downstream: project } } + end end describe '#stop_actions' do @@ -1814,13 +1850,23 @@ let_it_be(:project) { create(:project, :repository) } let_it_be(:environment, reload: true) { create(:environment, project: project) } - let!(:deployment) { create(:deployment, project: project, environment: environment, deployable: build) } - let!(:build) { create(:ci_build, :running, project: project, environment: environment) } + let!(:deployment) { create(:deployment, project: project, environment: environment, deployable: job) } + let!(:job) { create(:ci_build, :running, project: project, environment: environment) } it 'cancels an active deployment job' do subject - expect(build.reset).to be_canceled + expect(job.reset).to be_canceled + end + + context 'when deployment job is bridge' do + let!(:job) { create(:ci_bridge, :running, project: project, environment: environment) } + + it 'does not cancel an active deployment job' do + subject + + expect(job.reset).to be_running + end end context 'when deployable does not exist' do @@ -1831,7 +1877,7 @@ it 'does not raise an error' do expect { subject }.not_to raise_error - expect(build.reset).to be_running + expect(job.reset).to be_running end end end diff --git a/spec/requests/api/graphql/environments/deployments_spec.rb b/spec/requests/api/graphql/environments/deployments_spec.rb index 0022a38d2d3437e51f98ad7869aee10556530952..a4abf3f583a7edb3f75b3a0c63ad6763c0f06d17 100644 --- a/spec/requests/api/graphql/environments/deployments_spec.rb +++ b/spec/requests/api/graphql/environments/deployments_spec.rb @@ -314,14 +314,17 @@ 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) + deployments = create_list(:deployment, 2, environment: environment, project: project) + set_deployment_attributes(deployments.first, :ci_build) + set_deployment_attributes(deployments.second, :ci_bridge) + deployments.each(&:save!) + end - deployment.save! - end + def set_deployment_attributes(deployment, factory_type) + deployment.user = create(:user).tap { |u| project.add_developer(u) } + deployment.deployable = + create(factory_type, project: project, environment: environment.name, deployment: deployment, + user: deployment.user) end end @@ -432,7 +435,7 @@ def create_deployments 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']) + expect(deployment_in_record.job.to_global_id.to_s).to eq(deployment['job']['id']) end end end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 0746e68d7c5b4a62277ef6f4cdc7fca171c4487d..b0f3f328a4fc66d73ceaf4493df9e53f7b42b301 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -116,20 +116,28 @@ describe 'playable_build' do before do - deployment.update!(deployable: build) + deployment.update!(deployable: job) end context 'when the deployment has a playable deployable' do - context 'when this build is ready to be played' do - let(:build) { create(:ci_build, :playable, :scheduled, pipeline: pipeline) } + context 'when this job is build and ready to be played' do + let(:job) { create(:ci_build, :playable, :scheduled, pipeline: pipeline) } + + it 'exposes only the play_path' do + expect(subject[:playable_build].keys).to contain_exactly(:play_path) + end + end + + context 'when this job is bridge and ready to be played' do + let(:job) { create(:ci_bridge, :playable, :manual, pipeline: pipeline, downstream: project) } it 'exposes only the play_path' do expect(subject[:playable_build].keys).to contain_exactly(:play_path) end end - context 'when this build has failed' do - let(:build) { create(:ci_build, :playable, :failed, pipeline: pipeline) } + context 'when this job has failed' do + let(:job) { create(:ci_build, :playable, :failed, pipeline: pipeline) } it 'exposes the play_path and the retry_path' do expect(subject[:playable_build].keys).to contain_exactly(:play_path, :retry_path) @@ -138,7 +146,7 @@ end context 'when the deployment does not have a playable deployable' do - let(:build) { create(:ci_build, pipeline: pipeline) } + let(:job) { create(:ci_build, pipeline: pipeline) } it 'is not exposed' do expect(subject[:playable_build]).to be_nil diff --git a/spec/services/deployments/older_deployments_drop_service_spec.rb b/spec/services/deployments/older_deployments_drop_service_spec.rb deleted file mode 100644 index 7e3074a16888ffc0d136defb3923aef769944cfb..0000000000000000000000000000000000000000 --- a/spec/services/deployments/older_deployments_drop_service_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Deployments::OlderDeploymentsDropService, feature_category: :continuous_delivery do - let(:environment) { create(:environment) } - let(:deployment) { create(:deployment, environment: environment) } - let(:service) { described_class.new(deployment) } - - describe '#execute' do - subject { service.execute } - - shared_examples 'it does not drop any build' do - it do - expect { subject }.to not_change(Ci::Build.failed, :count) - end - end - - context 'when deployment is nil' do - let(:deployment) { nil } - - it_behaves_like 'it does not drop any build' - end - - context 'when a deployment is passed in' do - context 'and there is no active deployment for the related environment' do - let(:deployment) { create(:deployment, :canceled, environment: environment) } - let(:deployment2) { create(:deployment, :canceled, environment: environment) } - - before do - deployment - deployment2 - end - - it_behaves_like 'it does not drop any build' - end - - context 'and there are active deployment for the related environment' do - let(:deployment) { create(:deployment, :running, environment: environment) } - let(:deployment2) { create(:deployment, :running, environment: environment) } - - context 'and there is no older deployment than "deployment"' do - before do - deployment - deployment2 - end - - it_behaves_like 'it does not drop any build' - end - - context 'and there is an older deployment than "deployment"' do - let(:older_deployment) { create(:deployment, :running, environment: environment) } - - before do - older_deployment - deployment - deployment2 - end - - it 'drops that older deployment' do - deployable = older_deployment.deployable - expect(deployable.failed?).to be_falsey - - subject - - expect(deployable.reload.failed?).to be_truthy - end - - context 'when older deployable is a manual job' do - let(:older_deployment) { create(:deployment, :created, environment: environment, deployable: build) } - let(:build) { create(:ci_build, :manual) } - - # Manual jobs should not be accounted as outdated deployment jobs. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/255978 for more information. - it 'does not drop any builds nor track the exception' do - expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - - expect { subject }.not_to change { Ci::Build.failed.count } - end - end - - context 'when deployable.drop raises RuntimeError' do - before do - allow_any_instance_of(Ci::Build).to receive(:drop).and_raise(RuntimeError) - end - - it 'does not drop an older deployment and tracks the exception' do - expect(Gitlab::ErrorTracking).to receive(:track_exception) - .with(kind_of(RuntimeError), subject_id: deployment.id, build_id: older_deployment.deployable_id) - - expect { subject }.not_to change { Ci::Build.failed.count } - end - end - - context 'when ActiveRecord::StaleObjectError is raised' do - before do - allow_any_instance_of(Ci::Build) - .to receive(:drop).and_raise(ActiveRecord::StaleObjectError) - end - - it 'resets the object via Gitlab::OptimisticLocking' do - allow_any_instance_of(Ci::Build).to receive(:reset).at_least(:once) - - subject - end - end - - context 'and there is no deployable for that older deployment' do - let(:older_deployment) { create(:deployment, :running, environment: environment, deployable: nil) } - - it_behaves_like 'it does not drop any build' - end - end - end - end - end -end diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb index 75ddc763bc6554a9c3cab7ea07e50d79b92c031e..b51a8fa20e2c2a84a234484015691f46847deb7a 100644 --- a/spec/support/shared_examples/ci/deployable_shared_examples.rb +++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb @@ -124,7 +124,7 @@ it 'does not change deployment status and tracks an error' do expect(Gitlab::ErrorTracking) .to receive(:track_exception).with( - instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, build_id: job.id) + instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, job_id: job.id) with_cross_database_modification_prevented do expect { subject }.not_to change { deployment.reload.status }