Merge branch 'fix/gb/pipeline-retry-builds-started' into 'master'
Make pipeline retry take stages order into account Closes #27431, #23710, and #26551 See merge request !9021
This commit is contained in:
		
						commit
						761c0c86d1
					
				|  | @ -62,33 +62,10 @@ module Ci | ||||||
|         new_build.save |         new_build.save | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def retry(build, user = nil) |       def retry(build, current_user) | ||||||
|         new_build = Ci::Build.create( |         Ci::RetryBuildService | ||||||
|           ref: build.ref, |           .new(build.project, current_user) | ||||||
|           tag: build.tag, |           .execute(build) | ||||||
|           options: build.options, |  | ||||||
|           commands: build.commands, |  | ||||||
|           tag_list: build.tag_list, |  | ||||||
|           project: build.project, |  | ||||||
|           pipeline: build.pipeline, |  | ||||||
|           name: build.name, |  | ||||||
|           allow_failure: build.allow_failure, |  | ||||||
|           stage: build.stage, |  | ||||||
|           stage_idx: build.stage_idx, |  | ||||||
|           trigger_request: build.trigger_request, |  | ||||||
|           yaml_variables: build.yaml_variables, |  | ||||||
|           when: build.when, |  | ||||||
|           user: user, |  | ||||||
|           environment: build.environment, |  | ||||||
|           status_event: 'enqueue' |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         MergeRequests::AddTodoWhenBuildFailsService |  | ||||||
|           .new(build.project, nil) |  | ||||||
|           .close(new_build) |  | ||||||
| 
 |  | ||||||
|         build.pipeline.mark_as_processable_after_stage(build.stage_idx) |  | ||||||
|         new_build |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -136,7 +113,7 @@ module Ci | ||||||
|       project.builds_enabled? && commands.present? && manual? && skipped? |       project.builds_enabled? && commands.present? && manual? && skipped? | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def play(current_user = nil) |     def play(current_user) | ||||||
|       # Try to queue a current build |       # Try to queue a current build | ||||||
|       if self.enqueue |       if self.enqueue | ||||||
|         self.update(user: current_user) |         self.update(user: current_user) | ||||||
|  |  | ||||||
|  | @ -214,21 +214,17 @@ module Ci | ||||||
|     def cancel_running |     def cancel_running | ||||||
|       Gitlab::OptimisticLocking.retry_lock( |       Gitlab::OptimisticLocking.retry_lock( | ||||||
|         statuses.cancelable) do |cancelable| |         statuses.cancelable) do |cancelable| | ||||||
|           cancelable.each(&:cancel) |           cancelable.find_each(&:cancel) | ||||||
|         end |         end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def retry_failed(user) |     def retry_failed(current_user) | ||||||
|       Gitlab::OptimisticLocking.retry_lock( |       Ci::RetryPipelineService.new(project, current_user) | ||||||
|         builds.latest.failed_or_canceled) do |failed_or_canceled| |         .execute(self) | ||||||
|           failed_or_canceled.select(&:retryable?).each do |build| |  | ||||||
|             Ci::Build.retry(build, user) |  | ||||||
|           end |  | ||||||
|         end |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def mark_as_processable_after_stage(stage_idx) |     def mark_as_processable_after_stage(stage_idx) | ||||||
|       builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) |       builds.skipped.after_stage(stage_idx).find_each(&:process) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def latest? |     def latest? | ||||||
|  |  | ||||||
|  | @ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base | ||||||
|     where(id: max_id.group(:name, :commit_id)) |     where(id: max_id.group(:name, :commit_id)) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   scope :retried, -> { where.not(id: latest) } |  | ||||||
|   scope :ordered, -> { order(:name) } |  | ||||||
| 
 |  | ||||||
|   scope :failed_but_allowed, -> do |   scope :failed_but_allowed, -> do | ||||||
|     where(allow_failure: true, status: [:failed, :canceled]) |     where(allow_failure: true, status: [:failed, :canceled]) | ||||||
|   end |   end | ||||||
|  | @ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base | ||||||
|       false, all_state_names - [:failed, :canceled]) |       false, all_state_names - [:failed, :canceled]) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   scope :retried, -> { where.not(id: latest) } | ||||||
|  |   scope :ordered, -> { order(:name) } | ||||||
|   scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } |   scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } | ||||||
|   scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } |   scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } | ||||||
|  |   scope :after_stage, -> (index) { where('stage_idx > ?', index) } | ||||||
| 
 | 
 | ||||||
|   state_machine :status do |   state_machine :status do | ||||||
|     event :enqueue do |     event :enqueue do | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| class BaseService | class BaseService | ||||||
|  |   include Gitlab::Allowable | ||||||
|   include Gitlab::CurrentSettings |   include Gitlab::CurrentSettings | ||||||
| 
 | 
 | ||||||
|   attr_accessor :project, :current_user, :params |   attr_accessor :project, :current_user, :params | ||||||
|  | @ -7,10 +8,6 @@ class BaseService | ||||||
|     @project, @current_user, @params = project, user, params.dup |     @project, @current_user, @params = project, user, params.dup | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def can?(object, action, subject) |  | ||||||
|     Ability.allowed?(object, action, subject) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def notification_service |   def notification_service | ||||||
|     NotificationService.new |     NotificationService.new | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | module Ci | ||||||
|  |   class RetryBuildService < ::BaseService | ||||||
|  |     CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name | ||||||
|  |                           allow_failure stage stage_idx trigger_request | ||||||
|  |                           yaml_variables when environment coverage_regex] | ||||||
|  |                             .freeze | ||||||
|  | 
 | ||||||
|  |     REJECT_ATTRIBUTES = %i[id status user token coverage trace runner | ||||||
|  |                            artifacts_file artifacts_metadata artifacts_size | ||||||
|  |                            created_at updated_at started_at finished_at | ||||||
|  |                            queued_at erased_by erased_at].freeze | ||||||
|  | 
 | ||||||
|  |     IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url | ||||||
|  |                            deploy job_id description].freeze | ||||||
|  | 
 | ||||||
|  |     def execute(build) | ||||||
|  |       reprocess(build).tap do |new_build| | ||||||
|  |         build.pipeline.mark_as_processable_after_stage(build.stage_idx) | ||||||
|  | 
 | ||||||
|  |         new_build.enqueue! | ||||||
|  | 
 | ||||||
|  |         MergeRequests::AddTodoWhenBuildFailsService | ||||||
|  |           .new(project, current_user) | ||||||
|  |           .close(new_build) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def reprocess(build) | ||||||
|  |       unless can?(current_user, :update_build, build) | ||||||
|  |         raise Gitlab::Access::AccessDeniedError | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       attributes = CLONE_ATTRIBUTES.map do |attribute| | ||||||
|  |         [attribute, build.send(attribute)] | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       attributes.push([:user, current_user]) | ||||||
|  | 
 | ||||||
|  |       project.builds.create(Hash[attributes]) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | module Ci | ||||||
|  |   class RetryPipelineService < ::BaseService | ||||||
|  |     def execute(pipeline) | ||||||
|  |       unless can?(current_user, :update_pipeline, pipeline) | ||||||
|  |         raise Gitlab::Access::AccessDeniedError | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       pipeline.builds.failed_or_canceled.find_each do |build| | ||||||
|  |         next unless build.retryable? | ||||||
|  | 
 | ||||||
|  |         Ci::RetryBuildService.new(project, current_user) | ||||||
|  |           .reprocess(build) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       MergeRequests::AddTodoWhenBuildFailsService | ||||||
|  |         .new(project, current_user) | ||||||
|  |         .close_all(pipeline) | ||||||
|  | 
 | ||||||
|  |       pipeline.process! | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -18,5 +18,11 @@ module MergeRequests | ||||||
|         todo_service.merge_request_build_retried(merge_request) |         todo_service.merge_request_build_retried(merge_request) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     def close_all(pipeline) | ||||||
|  |       pipeline_merge_requests(pipeline) do |merge_request| | ||||||
|  |         todo_service.merge_request_build_retried(merge_request) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | --- | ||||||
|  | title: Fix CI/CD pipeline retry and take stages order into account | ||||||
|  | merge_request: 9021 | ||||||
|  | author: | ||||||
|  | @ -11,7 +11,7 @@ module SharedBuilds | ||||||
| 
 | 
 | ||||||
|   step 'project has a recent build' do |   step 'project has a recent build' do | ||||||
|     @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') |     @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') | ||||||
|     @build = create(:ci_build_with_coverage, pipeline: @pipeline) |     @build = create(:ci_build, :coverage, pipeline: @pipeline) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   step 'recent build is successful' do |   step 'recent build is successful' do | ||||||
|  |  | ||||||
|  | @ -89,8 +89,9 @@ FactoryGirl.define do | ||||||
|       tag true |       tag true | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     factory :ci_build_with_coverage do |     trait :coverage do | ||||||
|       coverage 99.9 |       coverage 99.9 | ||||||
|  |       coverage_regex '/(d+)/' | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     trait :trace do |     trait :trace do | ||||||
|  | @ -99,6 +100,16 @@ FactoryGirl.define do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     trait :erased do | ||||||
|  |       erased_at Time.now | ||||||
|  |       erased_by factory: :user | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     trait :queued do | ||||||
|  |       queued_at Time.now | ||||||
|  |       runner factory: :ci_runner | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     trait :artifacts do |     trait :artifacts do | ||||||
|       after(:create) do |build, _| |       after(:create) do |build, _| | ||||||
|         build.artifacts_file = |         build.artifacts_file = | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| describe Ci::Build, :models do | describe Ci::Build, :models do | ||||||
|  |   let(:user) { create(:user) } | ||||||
|   let(:project) { create(:project, :repository) } |   let(:project) { create(:project, :repository) } | ||||||
|   let(:build) { create(:ci_build, pipeline: pipeline) } |   let(:build) { create(:ci_build, pipeline: pipeline) } | ||||||
|   let(:test_trace) { 'This is a test' } |   let(:test_trace) { 'This is a test' } | ||||||
|  | @ -207,14 +208,16 @@ describe Ci::Build, :models do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'expects to have retried builds instead the original ones' do |     it 'expects to have retried builds instead the original ones' do | ||||||
|       retried_rspec = Ci::Build.retry(rspec_test) |       project.add_developer(user) | ||||||
|       expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) | 
 | ||||||
|  |       retried_rspec = Ci::Build.retry(rspec_test, user) | ||||||
|  | 
 | ||||||
|  |       expect(staging.depends_on_builds.map(&:id)) | ||||||
|  |         .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#detailed_status' do |   describe '#detailed_status' do | ||||||
|     let(:user) { create(:user) } |  | ||||||
| 
 |  | ||||||
|     it 'returns a detailed status' do |     it 'returns a detailed status' do | ||||||
|       expect(build.detailed_status(user)) |       expect(build.detailed_status(user)) | ||||||
|         .to be_a Gitlab::Ci::Status::Build::Cancelable |         .to be_a Gitlab::Ci::Status::Build::Cancelable | ||||||
|  | @ -813,12 +816,16 @@ describe Ci::Build, :models do | ||||||
| 
 | 
 | ||||||
|     subject { build.other_actions } |     subject { build.other_actions } | ||||||
| 
 | 
 | ||||||
|  |     before do | ||||||
|  |       project.add_developer(user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     it 'returns other actions' do |     it 'returns other actions' do | ||||||
|       is_expected.to contain_exactly(other_build) |       is_expected.to contain_exactly(other_build) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when build is retried' do |     context 'when build is retried' do | ||||||
|       let!(:new_build) { Ci::Build.retry(build) } |       let!(:new_build) { Ci::Build.retry(build, user) } | ||||||
| 
 | 
 | ||||||
|       it 'does not return any of them' do |       it 'does not return any of them' do | ||||||
|         is_expected.not_to include(build, new_build) |         is_expected.not_to include(build, new_build) | ||||||
|  | @ -826,7 +833,7 @@ describe Ci::Build, :models do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when other build is retried' do |     context 'when other build is retried' do | ||||||
|       let!(:retried_build) { Ci::Build.retry(other_build) } |       let!(:retried_build) { Ci::Build.retry(other_build, user) } | ||||||
| 
 | 
 | ||||||
|       it 'returns a retried build' do |       it 'returns a retried build' do | ||||||
|         is_expected.to contain_exactly(retried_build) |         is_expected.to contain_exactly(retried_build) | ||||||
|  | @ -857,21 +864,29 @@ describe Ci::Build, :models do | ||||||
|   describe '#play' do |   describe '#play' do | ||||||
|     let(:build) { create(:ci_build, :manual, pipeline: pipeline) } |     let(:build) { create(:ci_build, :manual, pipeline: pipeline) } | ||||||
| 
 | 
 | ||||||
|     subject { build.play } |     before do | ||||||
| 
 |       project.add_developer(user) | ||||||
|     it 'enqueues a build' do |  | ||||||
|       is_expected.to be_pending |  | ||||||
|       is_expected.to eq(build) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'for successful build' do |     context 'when build is manual' do | ||||||
|  |       it 'enqueues a build' do | ||||||
|  |         new_build = build.play(user) | ||||||
|  | 
 | ||||||
|  |         expect(new_build).to be_pending | ||||||
|  |         expect(new_build).to eq(build) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when build is passed' do | ||||||
|       before do |       before do | ||||||
|         build.update(status: 'success') |         build.update(status: 'success') | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates a new build' do |       it 'creates a new build' do | ||||||
|         is_expected.to be_pending |         new_build = build.play(user) | ||||||
|         is_expected.not_to eq(build) | 
 | ||||||
|  |         expect(new_build).to be_pending | ||||||
|  |         expect(new_build).not_to eq(build) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | @ -1246,12 +1261,9 @@ describe Ci::Build, :models do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when build has user' do |     context 'when build has user' do | ||||||
|       let(:user) { create(:user, username: 'starter') } |  | ||||||
|       let(:user_variables) do |       let(:user_variables) do | ||||||
|         [ |         [ { key: 'GITLAB_USER_ID',    value: user.id.to_s, public: true }, | ||||||
|           { key: 'GITLAB_USER_ID',    value: user.id.to_s, public: true }, |           { key: 'GITLAB_USER_EMAIL', value: user.email,   public: true } ] | ||||||
|           { key: 'GITLAB_USER_EMAIL', value: user.email,   public: true } |  | ||||||
|         ] |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       before do |       before do | ||||||
|  |  | ||||||
|  | @ -3,8 +3,12 @@ require 'spec_helper' | ||||||
| describe Ci::Pipeline, models: true do | describe Ci::Pipeline, models: true do | ||||||
|   include EmailHelpers |   include EmailHelpers | ||||||
| 
 | 
 | ||||||
|   let(:project) { FactoryGirl.create :empty_project } |   let(:user) { create(:user) } | ||||||
|   let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project } |   let(:project) { create(:empty_project) } | ||||||
|  | 
 | ||||||
|  |   let(:pipeline) do | ||||||
|  |     create(:ci_empty_pipeline, status: :created, project: project) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   it { is_expected.to belong_to(:project) } |   it { is_expected.to belong_to(:project) } | ||||||
|   it { is_expected.to belong_to(:user) } |   it { is_expected.to belong_to(:user) } | ||||||
|  | @ -503,7 +507,9 @@ describe Ci::Pipeline, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#status' do |   describe '#status' do | ||||||
|     let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } |     let(:build) do | ||||||
|  |       create(:ci_build, :created, pipeline: pipeline, name: 'test') | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|     subject { pipeline.reload.status } |     subject { pipeline.reload.status } | ||||||
| 
 | 
 | ||||||
|  | @ -545,13 +551,21 @@ describe Ci::Pipeline, models: true do | ||||||
|         build.cancel |         build.cancel | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it { is_expected.to eq('canceled') } |       context 'when build is pending' do | ||||||
|  |         let(:build) do | ||||||
|  |           create(:ci_build, :pending, pipeline: pipeline) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to eq('canceled') } | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'on failure and build retry' do |     context 'on failure and build retry' do | ||||||
|       before do |       before do | ||||||
|         build.drop |         build.drop | ||||||
|         Ci::Build.retry(build) |         project.add_developer(user) | ||||||
|  | 
 | ||||||
|  |         Ci::Build.retry(build, user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       # We are changing a state: created > failed > running |       # We are changing a state: created > failed > running | ||||||
|  | @ -563,8 +577,6 @@ describe Ci::Pipeline, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#detailed_status' do |   describe '#detailed_status' do | ||||||
|     let(:user) { create(:user) } |  | ||||||
| 
 |  | ||||||
|     subject { pipeline.detailed_status(user) } |     subject { pipeline.detailed_status(user) } | ||||||
| 
 | 
 | ||||||
|     context 'when pipeline is created' do |     context 'when pipeline is created' do | ||||||
|  | @ -720,7 +732,7 @@ describe Ci::Pipeline, models: true do | ||||||
|   describe '#cancel_running' do |   describe '#cancel_running' do | ||||||
|     let(:latest_status) { pipeline.statuses.pluck(:status) } |     let(:latest_status) { pipeline.statuses.pluck(:status) } | ||||||
| 
 | 
 | ||||||
|     context 'when there is a running external job and created build' do |     context 'when there is a running external job and a regular job' do | ||||||
|       before do |       before do | ||||||
|         create(:ci_build, :running, pipeline: pipeline) |         create(:ci_build, :running, pipeline: pipeline) | ||||||
|         create(:generic_commit_status, :running, pipeline: pipeline) |         create(:generic_commit_status, :running, pipeline: pipeline) | ||||||
|  | @ -733,7 +745,7 @@ describe Ci::Pipeline, models: true do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when builds are in different stages' do |     context 'when jobs are in different stages' do | ||||||
|       before do |       before do | ||||||
|         create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) |         create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) | ||||||
|         create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) |         create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) | ||||||
|  | @ -745,17 +757,34 @@ describe Ci::Pipeline, models: true do | ||||||
|         expect(latest_status).to contain_exactly('canceled', 'canceled') |         expect(latest_status).to contain_exactly('canceled', 'canceled') | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     context 'when there are created builds present in the pipeline' do | ||||||
|  |       before do | ||||||
|  |         create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) | ||||||
|  |         create(:ci_build, :created, stage_idx: 1, pipeline: pipeline) | ||||||
|  | 
 | ||||||
|  |         pipeline.cancel_running | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'cancels created builds' do | ||||||
|  |         expect(latest_status).to eq ['canceled', 'canceled'] | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#retry_failed' do |   describe '#retry_failed' do | ||||||
|     let(:latest_status) { pipeline.statuses.latest.pluck(:status) } |     let(:latest_status) { pipeline.statuses.latest.pluck(:status) } | ||||||
| 
 | 
 | ||||||
|  |     before do | ||||||
|  |       project.add_developer(user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'when there is a failed build and failed external status' do |     context 'when there is a failed build and failed external status' do | ||||||
|       before do |       before do | ||||||
|         create(:ci_build, :failed, name: 'build', pipeline: pipeline) |         create(:ci_build, :failed, name: 'build', pipeline: pipeline) | ||||||
|         create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) |         create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) | ||||||
| 
 | 
 | ||||||
|         pipeline.retry_failed(create(:user)) |         pipeline.retry_failed(user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'retries only build' do |       it 'retries only build' do | ||||||
|  | @ -768,11 +797,11 @@ describe Ci::Pipeline, models: true do | ||||||
|         create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) |         create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) | ||||||
|         create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) |         create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) | ||||||
| 
 | 
 | ||||||
|         pipeline.retry_failed(create(:user)) |         pipeline.retry_failed(user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'retries both builds' do |       it 'retries both builds' do | ||||||
|         expect(latest_status).to contain_exactly('pending', 'pending') |         expect(latest_status).to contain_exactly('pending', 'created') | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -781,11 +810,11 @@ describe Ci::Pipeline, models: true do | ||||||
|         create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) |         create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) | ||||||
|         create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) |         create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) | ||||||
| 
 | 
 | ||||||
|         pipeline.retry_failed(create(:user)) |         pipeline.retry_failed(user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'retries both builds' do |       it 'retries both builds' do | ||||||
|         expect(latest_status).to contain_exactly('pending', 'pending') |         expect(latest_status).to contain_exactly('pending', 'created') | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| describe CommitStatus, models: true do | describe CommitStatus, :models do | ||||||
|   let(:project) { create(:project, :repository) } |   let(:project) { create(:project, :repository) } | ||||||
| 
 | 
 | ||||||
|   let(:pipeline) do |   let(:pipeline) do | ||||||
|  | @ -127,7 +127,7 @@ describe CommitStatus, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.latest' do |   describe '.latest' do | ||||||
|     subject { CommitStatus.latest.order(:id) } |     subject { described_class.latest.order(:id) } | ||||||
| 
 | 
 | ||||||
|     let(:statuses) do |     let(:statuses) do | ||||||
|       [create_status(name: 'aa', ref: 'bb', status: 'running'), |       [create_status(name: 'aa', ref: 'bb', status: 'running'), | ||||||
|  | @ -143,7 +143,7 @@ describe CommitStatus, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.running_or_pending' do |   describe '.running_or_pending' do | ||||||
|     subject { CommitStatus.running_or_pending.order(:id) } |     subject { described_class.running_or_pending.order(:id) } | ||||||
| 
 | 
 | ||||||
|     let(:statuses) do |     let(:statuses) do | ||||||
|       [create_status(name: 'aa', ref: 'bb', status: 'running'), |       [create_status(name: 'aa', ref: 'bb', status: 'running'), | ||||||
|  | @ -159,7 +159,21 @@ describe CommitStatus, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.exclude_ignored' do |   describe '.exclude_ignored' do | ||||||
|     subject { CommitStatus.exclude_ignored.order(:id) } |     subject { described_class.after_stage(0) } | ||||||
|  | 
 | ||||||
|  |     let(:statuses) do | ||||||
|  |       [create_status(name: 'aa', stage_idx: 0), | ||||||
|  |        create_status(name: 'cc', stage_idx: 1), | ||||||
|  |        create_status(name: 'aa', stage_idx: 2)] | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns statuses from second and third stage' do | ||||||
|  |       is_expected.to eq(statuses.values_at(1, 2)) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.exclude_ignored' do | ||||||
|  |     subject { described_class.exclude_ignored.order(:id) } | ||||||
| 
 | 
 | ||||||
|     let(:statuses) do |     let(:statuses) do | ||||||
|       [create_status(when: 'manual', status: 'skipped'), |       [create_status(when: 'manual', status: 'skipped'), | ||||||
|  |  | ||||||
|  | @ -155,7 +155,7 @@ describe Environment, models: true do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#stop_with_action!' do |   describe '#stop_with_action!' do | ||||||
|     let(:user) { create(:user) } |     let(:user) { create(:admin) } | ||||||
| 
 | 
 | ||||||
|     subject { environment.stop_with_action!(user) } |     subject { environment.stop_with_action!(user) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do | ||||||
| 
 | 
 | ||||||
|   let!(:project) { create(:project, :repository) } |   let!(:project) { create(:project, :repository) } | ||||||
|   let(:commit) { project.repository.commit } |   let(:commit) { project.repository.commit } | ||||||
|   let(:commit_status) { create(:commit_status, pipeline: pipeline) } |  | ||||||
|   let(:guest) { create_user(:guest) } |   let(:guest) { create_user(:guest) } | ||||||
|   let(:reporter) { create_user(:reporter) } |   let(:reporter) { create_user(:reporter) } | ||||||
|   let(:developer) { create_user(:developer) } |   let(:developer) { create_user(:developer) } | ||||||
|   let(:sha) { commit.id } |   let(:sha) { commit.id } | ||||||
| 
 | 
 | ||||||
|  |   let(:commit_status) do | ||||||
|  |     create(:commit_status, status: :pending, pipeline: pipeline) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   describe "GET /projects/:id/repository/commits/:sha/statuses" do |   describe "GET /projects/:id/repository/commits/:sha/statuses" do | ||||||
|     let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } |     let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,16 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| describe Ci::ProcessPipelineService, services: true do | describe Ci::ProcessPipelineService, :services do | ||||||
|   let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') } |  | ||||||
|   let(:user) { create(:user) } |   let(:user) { create(:user) } | ||||||
|  |   let(:project) { create(:empty_project) } | ||||||
|  | 
 | ||||||
|  |   let(:pipeline) do | ||||||
|  |     create(:ci_empty_pipeline, ref: 'master', project: project) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   before do | ||||||
|  |     project.add_developer(user) | ||||||
|  |   end | ||||||
| 
 | 
 | ||||||
|   describe '#execute' do |   describe '#execute' do | ||||||
|     context 'start queuing next builds' do |     context 'start queuing next builds' do | ||||||
|  | @ -285,7 +293,7 @@ describe Ci::ProcessPipelineService, services: true do | ||||||
|           expect(builds.pluck(:name)) |           expect(builds.pluck(:name)) | ||||||
|             .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') |             .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') | ||||||
| 
 | 
 | ||||||
|           Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success |           Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success | ||||||
| 
 | 
 | ||||||
|           expect(builds.pluck(:name)).to contain_exactly( |           expect(builds.pluck(:name)).to contain_exactly( | ||||||
|             'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') |             'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') | ||||||
|  |  | ||||||
|  | @ -0,0 +1,117 @@ | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | describe Ci::RetryBuildService, :services do | ||||||
|  |   let(:user) { create(:user) } | ||||||
|  |   let(:project) { create(:empty_project) } | ||||||
|  |   let(:pipeline) { create(:ci_pipeline, project: project) } | ||||||
|  |   let(:build) { create(:ci_build, pipeline: pipeline) } | ||||||
|  | 
 | ||||||
|  |   let(:service) do | ||||||
|  |     described_class.new(project, user) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   shared_examples 'build duplication' do | ||||||
|  |     let(:build) do | ||||||
|  |       create(:ci_build, :failed, :artifacts, :erased, :trace, | ||||||
|  |              :queued, :coverage, pipeline: pipeline) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'clone attributes' do | ||||||
|  |       described_class::CLONE_ATTRIBUTES.each do |attribute| | ||||||
|  |         it "clones #{attribute} build attribute" do | ||||||
|  |           expect(new_build.send(attribute)).to eq build.send(attribute) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     describe 'reject attributes' do | ||||||
|  |       described_class::REJECT_ATTRIBUTES.each do |attribute| | ||||||
|  |         it "does not clone #{attribute} build attribute" do | ||||||
|  |           expect(new_build.send(attribute)).not_to eq build.send(attribute) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'has correct number of known attributes' do | ||||||
|  |       attributes = | ||||||
|  |         described_class::CLONE_ATTRIBUTES + | ||||||
|  |         described_class::IGNORE_ATTRIBUTES + | ||||||
|  |         described_class::REJECT_ATTRIBUTES | ||||||
|  | 
 | ||||||
|  |       expect(attributes.size).to eq build.attributes.size | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#execute' do | ||||||
|  |     let(:new_build) { service.execute(build) } | ||||||
|  | 
 | ||||||
|  |     context 'when user has ability to execute build' do | ||||||
|  |       before do | ||||||
|  |         project.add_developer(user) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'build duplication' | ||||||
|  | 
 | ||||||
|  |       it 'creates a new build that represents the old one' do | ||||||
|  |         expect(new_build.name).to eq build.name | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'enqueues the new build' do | ||||||
|  |         expect(new_build).to be_pending | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'resolves todos for old build that failed' do | ||||||
|  |         expect(MergeRequests::AddTodoWhenBuildFailsService) | ||||||
|  |           .to receive_message_chain(:new, :close) | ||||||
|  | 
 | ||||||
|  |         service.execute(build) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when there are subsequent builds that are skipped' do | ||||||
|  |         let!(:subsequent_build) do | ||||||
|  |           create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'resumes pipeline processing in subsequent stages' do | ||||||
|  |           service.execute(build) | ||||||
|  | 
 | ||||||
|  |           expect(subsequent_build.reload).to be_created | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user does not have ability to execute build' do | ||||||
|  |       it 'raises an error' do | ||||||
|  |         expect { service.execute(build) } | ||||||
|  |           .to raise_error Gitlab::Access::AccessDeniedError | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#reprocess' do | ||||||
|  |     let(:new_build) { service.reprocess(build) } | ||||||
|  | 
 | ||||||
|  |     context 'when user has ability to execute build' do | ||||||
|  |       before do | ||||||
|  |         project.add_developer(user) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'build duplication' | ||||||
|  | 
 | ||||||
|  |       it 'creates a new build that represents the old one' do | ||||||
|  |         expect(new_build.name).to eq build.name | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'does not enqueue the new build' do | ||||||
|  |         expect(new_build).to be_created | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when user does not have ability to execute build' do | ||||||
|  |       it 'raises an error' do | ||||||
|  |         expect { service.reprocess(build) } | ||||||
|  |           .to raise_error Gitlab::Access::AccessDeniedError | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,175 @@ | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | describe Ci::RetryPipelineService, '#execute', :services do | ||||||
|  |   let(:user) { create(:user) } | ||||||
|  |   let(:project) { create(:empty_project) } | ||||||
|  |   let(:pipeline) { create(:ci_pipeline, project: project) } | ||||||
|  |   let(:service) { described_class.new(project, user) } | ||||||
|  | 
 | ||||||
|  |   context 'when user has ability to modify pipeline' do | ||||||
|  |     let(:user) { create(:admin) } | ||||||
|  | 
 | ||||||
|  |     context 'when there are failed builds in the last stage' do | ||||||
|  |       before do | ||||||
|  |         create_build('rspec 1', :success, 0) | ||||||
|  |         create_build('rspec 2', :failed, 1) | ||||||
|  |         create_build('rspec 3', :canceled, 1) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'enqueues all builds in the last stage' do | ||||||
|  |         service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(build('rspec 2')).to be_pending | ||||||
|  |         expect(build('rspec 3')).to be_pending | ||||||
|  |         expect(pipeline.reload).to be_running | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when there are failed or canceled builds in the first stage' do | ||||||
|  |       before do | ||||||
|  |         create_build('rspec 1', :failed, 0) | ||||||
|  |         create_build('rspec 2', :canceled, 0) | ||||||
|  |         create_build('rspec 3', :canceled, 1) | ||||||
|  |         create_build('spinach 1', :canceled, 2) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'retries builds failed builds and marks subsequent for processing' do | ||||||
|  |         service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(build('rspec 1')).to be_pending | ||||||
|  |         expect(build('rspec 2')).to be_pending | ||||||
|  |         expect(build('rspec 3')).to be_created | ||||||
|  |         expect(build('spinach 1')).to be_created | ||||||
|  |         expect(pipeline.reload).to be_running | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when there is failed build present which was run on failure' do | ||||||
|  |       before do | ||||||
|  |         create_build('rspec 1', :failed, 0) | ||||||
|  |         create_build('rspec 2', :canceled, 0) | ||||||
|  |         create_build('rspec 3', :canceled, 1) | ||||||
|  |         create_build('report 1', :failed, 2) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'retries builds only in the first stage' do | ||||||
|  |         service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(build('rspec 1')).to be_pending | ||||||
|  |         expect(build('rspec 2')).to be_pending | ||||||
|  |         expect(build('rspec 3')).to be_created | ||||||
|  |         expect(build('report 1')).to be_created | ||||||
|  |         expect(pipeline.reload).to be_running | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'creates a new job for report job in this case' do | ||||||
|  |         service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(statuses.where(name: 'report 1').first).to be_retried | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when pipeline contains manual actions' do | ||||||
|  |       context 'when there is a canceled manual action in first stage' do | ||||||
|  |         before do | ||||||
|  |           create_build('rspec 1', :failed, 0) | ||||||
|  |           create_build('staging', :canceled, 0, :manual) | ||||||
|  |           create_build('rspec 2', :canceled, 1) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'retries builds failed builds and marks subsequent for processing' do | ||||||
|  |           service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |           expect(build('rspec 1')).to be_pending | ||||||
|  |           expect(build('staging')).to be_skipped | ||||||
|  |           expect(build('rspec 2')).to be_created | ||||||
|  |           expect(pipeline.reload).to be_running | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when there is a skipped manual action in last stage' do | ||||||
|  |         before do | ||||||
|  |           create_build('rspec 1', :canceled, 0) | ||||||
|  |           create_build('staging', :skipped, 1, :manual) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'retries canceled job and skips manual action' do | ||||||
|  |           service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |           expect(build('rspec 1')).to be_pending | ||||||
|  |           expect(build('staging')).to be_skipped | ||||||
|  |           expect(pipeline.reload).to be_running | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when there is a created manual action in the last stage' do | ||||||
|  |         before do | ||||||
|  |           create_build('rspec 1', :canceled, 0) | ||||||
|  |           create_build('staging', :created, 1, :manual) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'retries canceled job and does not update the manual action' do | ||||||
|  |           service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |           expect(build('rspec 1')).to be_pending | ||||||
|  |           expect(build('staging')).to be_created | ||||||
|  |           expect(pipeline.reload).to be_running | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'when there is a created manual action in the first stage' do | ||||||
|  |         before do | ||||||
|  |           create_build('rspec 1', :canceled, 0) | ||||||
|  |           create_build('staging', :created, 0, :manual) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'retries canceled job and skipps the manual action' do | ||||||
|  |           service.execute(pipeline) | ||||||
|  | 
 | ||||||
|  |           expect(build('rspec 1')).to be_pending | ||||||
|  |           expect(build('staging')).to be_skipped | ||||||
|  |           expect(pipeline.reload).to be_running | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'closes all todos about failed jobs for pipeline' do | ||||||
|  |       expect(MergeRequests::AddTodoWhenBuildFailsService) | ||||||
|  |         .to receive_message_chain(:new, :close_all) | ||||||
|  | 
 | ||||||
|  |       service.execute(pipeline) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'reprocesses the pipeline' do | ||||||
|  |       expect(pipeline).to receive(:process!) | ||||||
|  | 
 | ||||||
|  |       service.execute(pipeline) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   context 'when user is not allowed to retry pipeline' do | ||||||
|  |     it 'raises an error' do | ||||||
|  |       expect { service.execute(pipeline) } | ||||||
|  |         .to raise_error Gitlab::Access::AccessDeniedError | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def statuses | ||||||
|  |     pipeline.reload.statuses | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def build(name) | ||||||
|  |     statuses.latest.find_by(name: name) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def create_build(name, status, stage_num, on = 'on_success') | ||||||
|  |     create(:ci_build, name: name, | ||||||
|  |                       status: status, | ||||||
|  |                       stage: "stage_#{stage_num}", | ||||||
|  |                       stage_idx: stage_num, | ||||||
|  |                       when: on, | ||||||
|  |                       pipeline: pipeline) do |build| | ||||||
|  |       pipeline.update_status | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -234,7 +234,11 @@ describe CreateDeploymentService, services: true do | ||||||
| 
 | 
 | ||||||
|       context 'when build is retried' do |       context 'when build is retried' do | ||||||
|         it_behaves_like 'does create environment and deployment' do |         it_behaves_like 'does create environment and deployment' do | ||||||
|           let(:deployable) { Ci::Build.retry(build) } |           before do | ||||||
|  |             project.add_developer(user) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           let(:deployable) { Ci::Build.retry(build, user) } | ||||||
| 
 | 
 | ||||||
|           subject { deployable.success } |           subject { deployable.success } | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do | ||||||
|     described_class.new(project, user, commit_message: 'Awesome message') |     described_class.new(project, user, commit_message: 'Awesome message') | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   let(:todo_service) { TodoService.new } |   let(:todo_service) { spy('todo service') } | ||||||
| 
 | 
 | ||||||
|   let(:merge_request) do |   let(:merge_request) do | ||||||
|     create(:merge_request, merge_user: user, |     create(:merge_request, merge_user: user, | ||||||
|  | @ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#close_all' do | ||||||
|  |     context 'when using pipeline that belongs to merge request' do | ||||||
|  |       it 'resolves todos about failed builds for pipeline' do | ||||||
|  |         service.close_all(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(todo_service) | ||||||
|  |           .to have_received(:merge_request_build_retried) | ||||||
|  |           .with(merge_request) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when pipeline is not related to merge request' do | ||||||
|  |       let(:pipeline) { create(:ci_empty_pipeline) } | ||||||
|  | 
 | ||||||
|  |       it 'does not resolve any todos about failed builds' do | ||||||
|  |         service.close_all(pipeline) | ||||||
|  | 
 | ||||||
|  |         expect(todo_service) | ||||||
|  |           .not_to have_received(:merge_request_build_retried) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue