diff options
42 files changed, 971 insertions, 421 deletions
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f2989eff22d..d69643967a1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -63,6 +63,10 @@ module Ci end state_machine :status do + event :actionize do + transition created: :manual + end + after_transition any => [:pending] do |build| build.run_after_commit do BuildQueueWorker.perform_async(id) @@ -94,16 +98,21 @@ module Ci .fabricate! end - def manual? - self.when == 'manual' - end - def other_actions pipeline.manual_actions.where.not(name: name) end def playable? - project.builds_enabled? && commands.present? && manual? && skipped? + project.builds_enabled? && has_commands? && + action? && manual? + end + + def action? + self.when == 'manual' + end + + def has_commands? + commands.present? end def play(current_user) @@ -122,7 +131,7 @@ module Ci end def retryable? - project.builds_enabled? && commands.present? && + project.builds_enabled? && has_commands? && (success? || failed? || canceled?) end @@ -552,7 +561,7 @@ module Ci ] variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag? variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request - variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual? + variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action? variables end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 80e11a5b58f..67206415f7b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -49,6 +49,10 @@ module Ci transition any - [:canceled] => :canceled end + event :block do + transition any - [:manual] => :manual + end + # IMPORTANT # Do not add any operations to this state_machine # Create a separate worker for each new operation @@ -321,6 +325,7 @@ module Ci when 'failed' then drop when 'canceled' then cancel when 'skipped' then skip + when 'manual' then block end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index fc750a3e5e9..7e23e14794f 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -29,9 +29,11 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - # We want to ignore failed_but_allowed jobs + # We want to ignore failed but allowed to fail jobs. + # + # TODO, we also skip ignored optional manual actions. where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]) + false, all_state_names - [:failed, :canceled, :manual]) end scope :retried, -> { where.not(id: latest) } @@ -42,11 +44,11 @@ class CommitStatus < ActiveRecord::Base state_machine :status do event :enqueue do - transition [:created, :skipped] => :pending + transition [:created, :skipped, :manual] => :pending end event :process do - transition skipped: :created + transition [:skipped, :manual] => :created end event :run do @@ -66,7 +68,7 @@ class CommitStatus < ActiveRecord::Base end event :cancel do - transition [:created, :pending, :running] => :canceled + transition [:created, :pending, :running, :manual] => :canceled end before_transition created: [:pending, :running] do |commit_status| @@ -86,7 +88,7 @@ class CommitStatus < ActiveRecord::Base commit_status.run_after_commit do pipeline.try do |pipeline| - if complete? + if complete? || manual? PipelineProcessWorker.perform_async(pipeline.id) else PipelineUpdateWorker.perform_async(pipeline.id) diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index aea359e70bb..b819947c9e6 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -2,22 +2,21 @@ module HasStatus extend ActiveSupport::Concern DEFAULT_STATUS = 'created'.freeze - AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped].freeze - STARTED_STATUSES = %w[running success failed skipped].freeze + BLOCKED_STATUS = 'manual'.freeze + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze + STARTED_STATUSES = %w[running success failed skipped manual].freeze ACTIVE_STATUSES = %w[pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze - ORDERED_STATUSES = %w[failed pending running canceled success skipped].freeze + ORDERED_STATUSES = %w[manual failed pending running canceled success skipped].freeze class_methods do def status_sql - scope = if respond_to?(:exclude_ignored) - exclude_ignored - else - all - end + scope = respond_to?(:exclude_ignored) ? exclude_ignored : all + builds = scope.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql + manual = scope.manual.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql @@ -30,7 +29,8 @@ module HasStatus WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' - WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' + WHEN (#{running})+(#{pending})>0 THEN 'running' + WHEN (#{manual})>0 THEN 'manual' ELSE 'failed' END)" end @@ -63,6 +63,7 @@ module HasStatus state :success, value: 'success' state :canceled, value: 'canceled' state :skipped, value: 'skipped' + state :manual, value: 'manual' end scope :created, -> { where(status: 'created') } @@ -73,12 +74,13 @@ module HasStatus scope :failed, -> { where(status: 'failed') } scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created]) + where(status: [:running, :pending, :created, :manual]) end end @@ -94,6 +96,10 @@ module HasStatus COMPLETED_STATUSES.include?(status) end + def blocked? + BLOCKED_STATUS == status + end + private def calculate_duration diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index b5384e6462b..5bcbe285052 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity path_to(:retry_namespace_project_build, build) end - expose :play_path, if: ->(build, _) { build.manual? } do |build| + expose :play_path, if: ->(build, _) { build.playable? } do |build| path_to(:play_namespace_project_build, build) end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 79eb97b7b55..2935d00c075 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -22,6 +22,8 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) + return if HasStatus::BLOCKED_STATUS == current_status + if HasStatus::COMPLETED_STATUSES.include?(current_status) created_builds_in_stage(index).select do |build| Gitlab::OptimisticLocking.retry_lock(build) do |subject| @@ -33,7 +35,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.enqueue + build.action? ? build.actionize : build.enqueue true else build.skip @@ -49,6 +51,8 @@ module Ci %w[failed] when 'always' %w[success failed skipped] + when 'manual' + %w[success] else [] end diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 5ea85f9fd4c..09286a1b3c6 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -46,7 +46,7 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail - - if build.manual? + - if build.action? %span.label.label-info manual - if pipeline_link diff --git a/changelogs/unreleased/pipeline-blocking-actions.yml b/changelogs/unreleased/pipeline-blocking-actions.yml new file mode 100644 index 00000000000..6bde501de18 --- /dev/null +++ b/changelogs/unreleased/pipeline-blocking-actions.yml @@ -0,0 +1,4 @@ +--- +title: Make it possible to configure blocking manual actions +merge_request: 9585 +author: diff --git a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb new file mode 100644 index 00000000000..9020e0d054c --- /dev/null +++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb @@ -0,0 +1,19 @@ +class MigrateLegacyManualActions < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute <<-EOS + UPDATE ci_builds SET status = 'manual', allow_failure = true + WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped'; + EOS + end + + def down + execute <<-EOS + UPDATE ci_builds SET status = 'skipped', allow_failure = false + WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual'; + EOS + end +end diff --git a/db/schema.rb b/db/schema.rb index 911cb22c8e5..6f7dd3e4768 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170305203726) do +ActiveRecord::Schema.define(version: 20170306170512) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a586b095ef5..fd1171eff7e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -545,13 +545,30 @@ The above script will: Manual actions are a special type of job that are not executed automatically; they need to be explicitly started by a user. Manual actions can be started -from pipeline, build, environment, and deployment views. You can execute the -same manual action multiple times. +from pipeline, build, environment, and deployment views. An example usage of manual actions is deployment to production. Read more at the [environments documentation][env-manual]. +Manual actions can be either optional or blocking. Blocking manual action will +block execution of the pipeline at stage this action is defined in. It is +possible to resume execution of the pipeline when someone executes a blocking +manual actions by clicking a _play_ button. + +When pipeline is blocked it will not be merged if Merge When Pipeline Succeeds +is set. Blocked pipelines also do have a special status, called _manual_. + +Manual actions are non-blocking by default. If you want to make manual action +blocking, it is necessary to add `allow_failure: false` to the job's definition +in `.gitlab-ci.yml`. + +Optional manual actions have `allow_failure: true` set by default. + +**Statuses of optional actions do not contribute to overall pipeline status.** + +> Blocking manual actions were introduced in GitLab 9.0 + ### environment > diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index e390919ae1d..15a461a16dd 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -58,7 +58,7 @@ module Ci commands: job[:commands], tag_list: job[:tags] || [], name: job[:name].to_s, - allow_failure: job[:allow_failure] || false, + allow_failure: job[:ignore], when: job[:when] || 'on_success', environment: job[:environment_name], coverage_regex: job[:coverage], diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 7f7662f2776..176301bcca1 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -104,6 +104,14 @@ module Gitlab (before_script_value.to_a + script_value.to_a).join("\n") end + def manual_action? + self.when == 'manual' + end + + def ignored? + allow_failure.nil? ? manual_action? : allow_failure + end + private def inherit!(deps) @@ -135,7 +143,8 @@ module Gitlab environment_name: environment_defined? ? environment_value[:name] : nil, coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, - after_script: after_script_value } + after_script: after_script_value, + ignore: ignored? } end end end diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 0f4b7b24cef..3495b8d0448 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -5,22 +5,10 @@ module Gitlab class Play < SimpleDelegator include Status::Extended - def text - 'manual' - end - def label 'manual play action' end - def icon - 'icon_status_manual' - end - - def group - 'manual' - end - def has_action? can?(user, :update_build, subject) end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 90401cad0d2..e8530f2aaae 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -5,22 +5,10 @@ module Gitlab class Stop < SimpleDelegator include Status::Extended - def text - 'manual' - end - def label 'manual stop action' end - def icon - 'icon_status_manual' - end - - def group - 'manual' - end - def has_action? can?(user, :update_build, subject) end diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb new file mode 100644 index 00000000000..5f28521901d --- /dev/null +++ b/lib/gitlab/ci/status/manual.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Manual < Status::Core + def text + 'manual' + end + + def label + 'manual action' + end + + def icon + 'icon_status_manual' + end + end + end + end +end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index e50e54b6e99..182a30fd74d 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -39,7 +39,7 @@ module Gitlab started_at: build.started_at, finished_at: build.finished_at, when: build.when, - manual: build.manual?, + manual: build.action?, user: build.user.try(:hook_attrs), runner: build.runner && runner_hook_attrs(build.runner), artifacts_file: { diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index cabe128acf7..279583c2c44 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -57,7 +57,7 @@ FactoryGirl.define do end trait :manual do - status 'skipped' + status 'manual' self.when 'manual' end @@ -71,8 +71,11 @@ FactoryGirl.define do allow_failure true end + trait :ignored do + allowed_to_fail + end + trait :playable do - skipped manual end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 756b341ecba..169590deb8e 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -35,6 +35,10 @@ FactoryGirl.define do status 'created' end + trait :manual do + status 'manual' + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index c203e1f20c1..65373e3f77d 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -13,7 +13,7 @@ feature 'Environment', :feature do feature 'environment details page' do given!(:environment) { create(:environment, project: project) } given!(:deployment) { } - given!(:manual) { } + given!(:action) { } before do visit_environment(environment) @@ -69,17 +69,23 @@ feature 'Environment', :feature do end context 'with manual action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'deploy to production') + end scenario 'does show a play button' do - expect(page).to have_link(manual.name.humanize) + expect(page).to have_link(action.name.humanize) end scenario 'does allow to play manual action' do - expect(manual).to be_skipped - expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - expect(page).to have_content(manual.name) - expect(manual.reload).to be_pending + expect(action).to be_manual + + expect { click_link(action.name.humanize) } + .not_to change { Ci::Pipeline.count } + + expect(page).to have_content(action.name) + expect(action.reload).to be_pending end context 'with external_url' do @@ -130,8 +136,16 @@ feature 'Environment', :feature do context 'when environment is available' do context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'close_app') + end + + given(:deployment) do + create(:deployment, environment: environment, + deployable: build, + on_stop: 'close_app') + end scenario 'does show stop button' do expect(page).to have_link('Stop') diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 78be7d36f47..25f31b423b8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -12,7 +12,7 @@ feature 'Environments page', :feature, :js do given!(:environment) { } given!(:deployment) { } - given!(:manual) { } + given!(:action) { } before do visit_environments(project) @@ -90,7 +90,7 @@ feature 'Environments page', :feature, :js do given(:pipeline) { create(:ci_pipeline, project: project) } given(:build) { create(:ci_build, pipeline: pipeline) } - given(:manual) do + given(:action) do create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') end @@ -102,19 +102,19 @@ feature 'Environments page', :feature, :js do scenario 'does show a play button' do find('.js-dropdown-play-icon-container').click - expect(page).to have_content(manual.name.humanize) + expect(page).to have_content(action.name.humanize) end scenario 'does allow to play manual action', js: true do - expect(manual).to be_skipped + expect(action).to be_manual find('.js-dropdown-play-icon-container').click - expect(page).to have_content(manual.name.humanize) + expect(page).to have_content(action.name.humanize) - expect { click_link(manual.name.humanize) } + expect { click_link(action.name.humanize) } .not_to change { Ci::Pipeline.count } - expect(manual.reload).to be_pending + expect(action.reload).to be_pending end scenario 'does show build name and id' do @@ -144,8 +144,15 @@ feature 'Environments page', :feature, :js do end context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') + end + + given(:deployment) do + create(:deployment, environment: environment, + deployable: build, + on_stop: 'close_app') + end scenario 'does show stop button' do expect(page).to have_selector('.stop-env-link') diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 7145f0da1d3..53abc056602 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -15,9 +15,9 @@ module Ci end describe '#build_attributes' do - describe 'coverage entry' do - subject { described_class.new(config, path).build_attributes(:rspec) } + subject { described_class.new(config, path).build_attributes(:rspec) } + describe 'coverage entry' do describe 'code coverage regexp' do let(:config) do YAML.dump(rspec: { script: 'rspec', @@ -30,6 +30,56 @@ module Ci end end end + + describe 'allow failure entry' do + context 'when job is a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual' }) + end + + it 'is allowed to fail' do + expect(subject[:allow_failure]).to be true + end + end + end + + context 'when job is not a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + end + end end describe "#builds_for_ref" do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index ebd80ac5e1d..1f757f12a56 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -155,6 +155,7 @@ describe Gitlab::Ci::Config::Entry::Global do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: { VAR: 'value' }, + ignore: false, after_script: ['make clean'] }, spinach: { name: :spinach, before_script: [], @@ -165,6 +166,7 @@ describe Gitlab::Ci::Config::Entry::Global do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: {}, + ignore: false, after_script: ['make clean'] }, ) end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index d20f4ec207d..9249bb9c172 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -144,6 +144,7 @@ describe Gitlab::Ci::Config::Entry::Job do script: %w[rspec], commands: "ls\npwd\nrspec", stage: 'test', + ignore: false, after_script: %w[cleanup]) end end @@ -159,4 +160,82 @@ describe Gitlab::Ci::Config::Entry::Job do end end end + + describe '#manual_action?' do + context 'when job is a manual action' do + let(:config) { { script: 'deploy', when: 'manual' } } + + it 'is a manual action' do + expect(entry).to be_manual_action + end + end + + context 'when job is not a manual action' do + let(:config) { { script: 'deploy' } } + + it 'is not a manual action' do + expect(entry).not_to be_manual_action + end + end + end + + describe '#ignored?' do + context 'when job is a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual' } + end + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: true } + end + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is not allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: false } + end + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + end + + context 'when job is not a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) { { script: 'deploy' } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + + context 'when job is allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: true } } + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is not allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: false } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index aaebf783962..7d104372ac6 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -62,10 +62,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do rspec: { name: :rspec, script: %w[rspec], commands: 'rspec', + ignore: false, stage: 'test' }, spinach: { name: :spinach, script: %w[spinach], commands: 'spinach', + ignore: false, stage: 'test' }) end end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 0c40fca0c1a..8b3bd08cf13 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -192,7 +192,7 @@ describe Gitlab::Ci::Status::Build::Factory do let(:build) { create(:ci_build, :playable) } it 'matches correct core status' do - expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual end it 'matches correct extended statuses' do @@ -200,12 +200,13 @@ describe Gitlab::Ci::Status::Build::Factory do .to eq [Gitlab::Ci::Status::Build::Play] end - it 'fabricates a core skipped status' do + it 'fabricates a play detailed status' do expect(status).to be_a Gitlab::Ci::Status::Build::Play end it 'fabricates status with correct details' do expect(status.text).to eq 'manual' + expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' expect(status.label).to eq 'manual play action' expect(status).to have_details @@ -218,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do let(:build) { create(:ci_build, :playable, :teardown_environment) } it 'matches correct core status' do - expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual end it 'matches correct extended statuses' do @@ -226,12 +227,13 @@ describe Gitlab::Ci::Status::Build::Factory do .to eq [Gitlab::Ci::Status::Build::Stop] end - it 'fabricates a core skipped status' do + it 'fabricates a stop detailed status' do expect(status).to be_a Gitlab::Ci::Status::Build::Stop end it 'fabricates status with correct details' do expect(status.text).to eq 'manual' + expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' expect(status.label).to eq 'manual stop action' expect(status).to have_details diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f3e72ea1796..6c97a4fe5ca 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -6,22 +6,10 @@ describe Gitlab::Ci::Status::Build::Play do subject { described_class.new(status) } - describe '#text' do - it { expect(subject.text).to eq 'manual' } - end - describe '#label' do it { expect(subject.label).to eq 'manual play action' } end - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } - end - - describe '#group' do - it { expect(subject.group).to eq 'manual' } - end - describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 41c2b624774..8d021c35a69 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -8,22 +8,10 @@ describe Gitlab::Ci::Status::Build::Stop do described_class.new(status) end - describe '#text' do - it { expect(subject.text).to eq 'manual' } - end - describe '#label' do it { expect(subject.label).to eq 'manual stop action' } end - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } - end - - describe '#group' do - it { expect(subject.group).to eq 'manual' } - end - describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 38412fe2e4f..768f8926f1d 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Canceled do end describe '#text' do - it { expect(subject.label).to eq 'canceled' } + it { expect(subject.text).to eq 'canceled' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 6d847484693..e96c13aede3 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Created do end describe '#text' do - it { expect(subject.label).to eq 'created' } + it { expect(subject.text).to eq 'created' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 990d686d22c..e5da0a91159 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Failed do end describe '#text' do - it { expect(subject.label).to eq 'failed' } + it { expect(subject.text).to eq 'failed' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb new file mode 100644 index 00000000000..3fd3727b92d --- /dev/null +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Manual do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 7bb6579c317..8d09cf2a05a 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Pending do end describe '#text' do - it { expect(subject.label).to eq 'pending' } + it { expect(subject.text).to eq 'pending' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 852d6c06baf..10d3bf749c1 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Running do end describe '#text' do - it { expect(subject.label).to eq 'running' } + it { expect(subject.text).to eq 'running' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index e00b356a24b..10db93d3802 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Skipped do end describe '#text' do - it { expect(subject.label).to eq 'skipped' } + it { expect(subject.text).to eq 'skipped' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 4a89e1faf40..230f24b94a4 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Success do end describe '#text' do - it { expect(subject.label).to eq 'passed' } + it { expect(subject.text).to eq 'passed' } end describe '#label' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 5743c555cbe..2db42a94077 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -20,6 +20,30 @@ describe Ci::Build, :models do it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :trace_html } + describe '#actionize' do + context 'when build is a created' do + before do + build.update_column(:status, :created) + end + + it 'makes build a manual action' do + expect(build.actionize).to be true + expect(build.reload).to be_manual + end + end + + context 'when build is not created' do + before do + build.update_column(:status, :pending) + end + + it 'does not change build status' do + expect(build.actionize).to be false + expect(build.reload).to be_pending + end + end + end + describe '#any_runners_online?' do subject { build.any_runners_online? } @@ -587,13 +611,21 @@ describe Ci::Build, :models do it { is_expected.to be_falsey } end - context 'and build.status is failed' do + context 'and build status is failed' do before do build.status = 'failed' end it { is_expected.to be_truthy } end + + context 'when build is a manual action' do + before do + build.status = 'manual' + end + + it { is_expected.to be_falsey } + end end end @@ -682,12 +714,12 @@ describe Ci::Build, :models do end end - describe '#manual?' do + describe '#action?' do before do build.update(when: value) end - subject { build.manual? } + subject { build.action? } context 'when is set to manual' do let(:value) { 'manual' } @@ -703,14 +735,50 @@ describe Ci::Build, :models do end end + describe '#has_commands?' do + context 'when build has commands' do + let(:build) do + create(:ci_build, commands: 'rspec') + end + + it 'has commands' do + expect(build).to have_commands + end + end + + context 'when does not have commands' do + context 'when commands are an empty string' do + let(:build) do + create(:ci_build, commands: '') + end + + it 'has no commands' do + expect(build).not_to have_commands + end + end + + context 'when commands are not set at all' do + let(:build) do + create(:ci_build, commands: nil) + end + + it 'has no commands' do + expect(build).not_to have_commands + end + end + end + end + describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } end context 'when build does not have tags' do subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c2fc8c02bb3..dd5f7098d06 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -24,6 +24,14 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + describe '#block' do + it 'changes pipeline status to manual' do + expect(pipeline.block).to be true + expect(pipeline.reload).to be_manual + expect(pipeline.reload).to be_blocked + end + end + describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do @@ -635,6 +643,14 @@ describe Ci::Pipeline, models: true do end end + context 'when pipeline is blocked' do + let(:pipeline) { create(:ci_pipeline, status: :manual) } + + it 'returns detailed status for blocked pipeline' do + expect(subject.text).to eq 'manual' + end + end + context 'when pipeline is successful but with warnings' do let(:pipeline) { create(:ci_pipeline, status: :success) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 36533bdd11e..ea5e4e21039 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -158,7 +158,7 @@ describe CommitStatus, :models do end end - describe '.exclude_ignored' do + describe '.after_stage' do subject { described_class.after_stage(0) } let(:statuses) do @@ -185,11 +185,32 @@ describe CommitStatus, :models do create_status(allow_failure: true, status: 'success'), create_status(allow_failure: true, status: 'failed'), create_status(allow_failure: false, status: 'success'), - create_status(allow_failure: false, status: 'failed')] + create_status(allow_failure: false, status: 'failed'), + create_status(allow_failure: true, status: 'manual'), + create_status(allow_failure: false, status: 'manual')] + end + + it 'returns statuses without what we want to ignore' do + is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11)) + end + end + + describe '.failed_but_allowed' do + subject { described_class.failed_but_allowed.order(:id) } + + let(:statuses) do + [create_status(allow_failure: true, status: 'success'), + create_status(allow_failure: true, status: 'failed'), + create_status(allow_failure: false, status: 'success'), + create_status(allow_failure: false, status: 'failed'), + create_status(allow_failure: true, status: 'canceled'), + create_status(allow_failure: false, status: 'canceled'), + create_status(allow_failure: true, status: 'manual'), + create_status(allow_failure: false, status: 'manual')] end it 'returns statuses without what we want to ignore' do - is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9)) + is_expected.to eq(statuses.values_at(1, 4)) end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index dbfe3cd2d36..f134da441c2 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -109,6 +109,24 @@ describe HasStatus do it { is_expected.to eq 'running' } end + + context 'when one status is a blocking manual action' do + let!(:statuses) do + [create(type, status: :failed), + create(type, status: :manual, allow_failure: false)] + end + + it { is_expected.to eq 'manual' } + end + + context 'when one status is a non-blocking manual action' do + let!(:statuses) do + [create(type, status: :failed), + create(type, status: :manual, allow_failure: true)] + end + + it { is_expected.to eq 'failed' } + end end context 'ci build statuses' do @@ -218,6 +236,18 @@ describe HasStatus do it_behaves_like 'not containing the job', status end end + + describe '.manual' do + subject { CommitStatus.manual } + + %i[manual].each do |status| + it_behaves_like 'containing the job', status + end + + %i[failed success skipped canceled].each do |status| + it_behaves_like 'not containing the job', status + end + end end describe '::DEFAULT_STATUS' do @@ -225,4 +255,10 @@ describe HasStatus do expect(described_class::DEFAULT_STATUS).to eq 'created' end end + + describe '::BLOCKED_STATUS' do + it 'is a status manual' do + expect(described_class::BLOCKED_STATUS).to eq 'manual' + end + end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index de68fb64726..d93616c4f50 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, :services do +describe Ci::ProcessPipelineService, '#execute', :services do let(:user) { create(:user) } let(:project) { create(:empty_project) } @@ -12,379 +12,518 @@ describe Ci::ProcessPipelineService, :services do project.add_developer(user) end - describe '#execute' do - context 'start queuing next builds' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2) - end + context 'when simple pipeline is defined' do + before do + create_build('linux', stage_idx: 0) + create_build('mac', stage_idx: 0) + create_build('rspec', stage_idx: 1) + create_build('rubocop', stage_idx: 1) + create_build('deploy', stage_idx: 2) + end - it 'processes a pipeline' do - expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(2) + it 'processes a pipeline' do + expect(process_pipeline).to be_truthy - expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(4) + succeed_pending + + expect(builds.success.count).to eq(2) + expect(process_pipeline).to be_truthy + + succeed_pending + + expect(builds.success.count).to eq(4) + expect(process_pipeline).to be_truthy + + succeed_pending + + expect(builds.success.count).to eq(5) + expect(process_pipeline).to be_falsey + end + + it 'does not process pipeline if existing stage is running' do + expect(process_pipeline).to be_truthy + expect(builds.pending.count).to eq(2) + + expect(process_pipeline).to be_falsey + expect(builds.pending.count).to eq(2) + end + end + + context 'custom stage with first job allowed to fail' do + before do + create_build('clean_job', stage_idx: 0, allow_failure: true) + create_build('test_job', stage_idx: 1, allow_failure: true) + end + it 'automatically triggers a next stage when build finishes' do + expect(process_pipeline).to be_truthy + expect(builds_statuses).to eq ['pending'] + + fail_running_or_pending + + expect(builds_statuses).to eq %w(failed pending) + end + end + + context 'when optional manual actions are defined' do + before do + create_build('build', stage_idx: 0) + create_build('test', stage_idx: 1) + create_build('test_failure', stage_idx: 2, when: 'on_failure') + create_build('deploy', stage_idx: 3) + create_build('production', stage_idx: 3, when: 'manual', allow_failure: true) + create_build('cleanup', stage_idx: 4, when: 'always') + create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true) + end + + context 'when builds are successful' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(5) + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - expect(process_pipeline).to be_falsey + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production) + expect(builds_statuses).to eq %w(success success pending manual) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production cleanup clear:cache) + expect(builds_statuses).to eq %w(success success success manual pending manual) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success success success manual success manual) + expect(pipeline.reload.status).to eq 'success' end + end - it 'does not process pipeline if existing stage is running' do + context 'when test job fails' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - expect(builds.pending.count).to eq(2) + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - expect(process_pipeline).to be_falsey - expect(builds.pending.count).to eq(2) + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure) + expect(builds_statuses).to eq %w(success failed pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed success pending) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success failed success success) + expect(pipeline.reload.status).to eq 'failed' end end - context 'custom stage with first job allowed to fail' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true) - create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true) + context 'when test and test_failure jobs fail' do + it 'properly processes the pipeline' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure) + expect(builds_statuses).to eq %w(success failed pending) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed failed pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed failed success) + expect(pipeline.reload.status).to eq('failed') end + end - it 'automatically triggers a next stage when build finishes' do + context 'when deploy job fails' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - expect(builds.pluck(:status)).to contain_exactly('pending') + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending - pipeline.builds.running_or_pending.each(&:drop) - expect(builds.pluck(:status)).to contain_exactly('failed', 'pending') + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production) + expect(builds_statuses).to eq %w(success success pending manual) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test deploy production cleanup) + expect(builds_statuses).to eq %w(success success failed manual pending) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success success failed manual success) + expect(pipeline.reload).to be_failed end end - context 'properly creates builds when "when" is defined' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure') - create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3) - create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual') - create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always') - create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual') - end + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] - context 'when builds are successful' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('success') - end - end + succeed_running_or_pending - context 'when test job fails' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end + expect(builds.running_or_pending).not_to be_empty + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - context 'when test and test_failure jobs fail' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end + cancel_running_or_pending - context 'when deploy job fails' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end + expect(builds.running_or_pending).to be_empty + expect(builds_names).to eq %w[build test] + expect(builds_statuses).to eq %w[success canceled] + expect(pipeline.reload).to be_canceled end + end - context 'when build is canceled in the second stage' do - it 'does not schedule builds after build has been canceled' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) + context 'when listing optional manual actions' do + it 'returns only for skipped builds' do + # currently all builds are created + expect(process_pipeline).to be_truthy + expect(manual_actions).to be_empty - expect(builds.running_or_pending).not_to be_empty + # succeed stage build + succeed_running_or_pending - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:cancel) + expect(manual_actions).to be_empty - expect(builds.running_or_pending).to be_empty - expect(pipeline.reload.status).to eq('canceled') - end - end + # succeed stage test + succeed_running_or_pending + + expect(manual_actions).to be_one # production + + # succeed stage deploy + succeed_running_or_pending - context 'when listing manual actions' do - it 'returns only for skipped builds' do - # currently all builds are created - expect(process_pipeline).to be_truthy - expect(manual_actions).to be_empty + expect(manual_actions).to be_many # production and clear cache + end + end + end - # succeed stage build - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty + context 'when there are manual action in earlier stages' do + context 'when first stage has only optional manual actions' do + before do + create_build('build', stage_idx: 0, when: 'manual', allow_failure: true) + create_build('check', stage_idx: 1) + create_build('test', stage_idx: 2) - # succeed stage test - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_one # production + process_pipeline + end - # succeed stage deploy - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_many # production and clear cache - end + it 'starts from the second stage' do + expect(all_builds_statuses).to eq %w[manual pending created] end end - context 'when there are manual/on_failure jobs in earlier stages' do + context 'when second stage has only optional manual actions' do before do - builds + create_build('check', stage_idx: 0) + create_build('build', stage_idx: 1, when: 'manual', allow_failure: true) + create_build('test', stage_idx: 2) + process_pipeline - builds.each(&:reload) end - context 'when first stage has only manual jobs' do - let(:builds) do - [create_build('build', 0, 'manual'), - create_build('check', 1), - create_build('test', 2)] - end + it 'skips second stage and continues on third stage' do + expect(all_builds_statuses).to eq(%w[pending created created]) - it 'starts from the second stage' do - expect(builds.map(&:status)).to eq(%w[skipped pending created]) - end + builds.first.success + + expect(all_builds_statuses).to eq(%w[success manual pending]) end + end + end + + context 'when blocking manual actions are defined' do + before do + create_build('code:test', stage_idx: 0) + create_build('staging:deploy', stage_idx: 1, when: 'manual') + create_build('staging:test', stage_idx: 2, when: 'on_success') + create_build('production:deploy', stage_idx: 3, when: 'manual') + create_build('production:test', stage_idx: 4, when: 'always') + end - context 'when second stage has only manual jobs' do - let(:builds) do - [create_build('check', 0), - create_build('build', 1, 'manual'), - create_build('test', 2)] - end + context 'when first stage succeeds' do + it 'blocks pipeline on stage with first manual action' do + process_pipeline - it 'skips second stage and continues on third stage' do - expect(builds.map(&:status)).to eq(%w[pending created created]) + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + expect(pipeline.reload.status).to eq 'pending' - builds.first.success - builds.each(&:reload) + succeed_running_or_pending - expect(builds.map(&:status)).to eq(%w[success skipped pending]) - end + expect(builds_names).to eq %w[code:test staging:deploy] + expect(builds_statuses).to eq %w[success manual] + expect(pipeline.reload).to be_manual end + end + + context 'when first stage fails' do + it 'does not take blocking action into account' do + process_pipeline + + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + expect(pipeline.reload.status).to eq 'pending' - context 'when second stage has only on_failure jobs' do - let(:builds) do - [create_build('check', 0), - create_build('build', 1, 'on_failure'), - create_build('test', 2)] - end + fail_running_or_pending - it 'skips second stage and continues on third stage' do - expect(builds.map(&:status)).to eq(%w[pending created created]) + expect(builds_names).to eq %w[code:test production:test] + expect(builds_statuses).to eq %w[failed pending] - builds.first.success - builds.each(&:reload) + succeed_running_or_pending - expect(builds.map(&:status)).to eq(%w[success skipped pending]) - end + expect(builds_statuses).to eq %w[failed success] + expect(pipeline.reload).to be_failed end end - context 'when failed build in the middle stage is retried' do - context 'when failed build is the only unsuccessful build in the stage' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2) - end + context 'when pipeline is promoted sequentially up to the end' do + it 'properly processes entire pipeline' do + process_pipeline + + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy] + expect(builds_statuses).to eq %w[success manual] + expect(pipeline.reload).to be_manual + + play_manual_action('staging:deploy') + + expect(builds_statuses).to eq %w[success pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy staging:test] + expect(builds_statuses).to eq %w[success success pending] + + succeed_running_or_pending - it 'does trigger builds in the next stage' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy] + expect(builds_statuses).to eq %w[success success success manual] - pipeline.builds.running_or_pending.each(&:success) + expect(pipeline.reload).to be_manual + expect(pipeline.reload).to be_blocked + expect(pipeline.reload).not_to be_active + expect(pipeline.reload).not_to be_complete - expect(builds.pluck(:name)) - .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + play_manual_action('production:deploy') - pipeline.builds.find_by(name: 'test:1').success - pipeline.builds.find_by(name: 'test:2').drop + expect(builds_statuses).to eq %w[success success success pending] + expect(pipeline.reload).to be_running - expect(builds.pluck(:name)) - .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + succeed_running_or_pending - Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy production:test] + expect(builds_statuses).to eq %w[success success success success pending] + expect(pipeline.reload).to be_running - expect(builds.pluck(:name)).to contain_exactly( - 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') - end + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy production:test] + expect(builds_statuses).to eq %w[success success success success success] + expect(pipeline.reload).to be_success end end + end - context 'when there are builds that are not created yet' do - let(:pipeline) do - create(:ci_pipeline, config: config) - end + context 'when second stage has only on_failure jobs' do + before do + create_build('check', stage_idx: 0) + create_build('build', stage_idx: 1, when: 'on_failure') + create_build('test', stage_idx: 2) - let(:config) do - { rspec: { stage: 'test', script: 'rspec' }, - deploy: { stage: 'deploy', script: 'rsync' } } - end + process_pipeline + end + + it 'skips second stage and continues on third stage' do + expect(all_builds_statuses).to eq(%w[pending created created]) + + builds.first.success + + expect(all_builds_statuses).to eq(%w[success skipped pending]) + end + end + context 'when failed build in the middle stage is retried' do + context 'when failed build is the only unsuccessful build in the stage' do before do - create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + create_build('build:1', stage_idx: 0) + create_build('build:2', stage_idx: 0) + create_build('test:1', stage_idx: 1) + create_build('test:2', stage_idx: 1) + create_build('deploy:1', stage_idx: 2) + create_build('deploy:2', stage_idx: 2) end - it 'processes the pipeline' do - # Currently we have five builds with state created - # - expect(builds.count).to eq(0) - expect(all_builds.count).to eq(2) + it 'does trigger builds in the next stage' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build:1', 'build:2'] - # Process builds service will enqueue builds from the first stage. - # - process_pipeline + succeed_running_or_pending - expect(builds.count).to eq(2) - expect(all_builds.count).to eq(2) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2'] - # When builds succeed we will enqueue remaining builds. - # - # We will have 2 succeeded, 1 pending (from stage test), total 4 (two - # additional build from `.gitlab-ci.yml`). - # - succeed_pending - process_pipeline + pipeline.builds.find_by(name: 'test:1').success + pipeline.builds.find_by(name: 'test:2').drop - expect(builds.success.count).to eq(2) - expect(builds.pending.count).to eq(1) - expect(all_builds.count).to eq(4) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2'] - # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. - # - succeed_pending - process_pipeline + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success - expect(builds.pending.count).to eq(1) - expect(builds.success.count).to eq(3) - expect(all_builds.count).to eq(4) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2', + 'test:2', 'deploy:1', 'deploy:2'] + end + end + end - # When the last one succeeds we have 4 successful builds. - # - succeed_pending - process_pipeline + context 'when there are builds that are not created yet' do + let(:pipeline) do + create(:ci_pipeline, config: config) + end - expect(builds.success.count).to eq(4) - expect(all_builds.count).to eq(4) - end + let(:config) do + { rspec: { stage: 'test', script: 'rspec' }, + deploy: { stage: 'deploy', script: 'rsync' } } + end + + before do + create_build('linux', stage: 'build', stage_idx: 0) + create_build('mac', stage: 'build', stage_idx: 0) + end + + it 'processes the pipeline' do + # Currently we have five builds with state created + # + expect(builds.count).to eq(0) + expect(all_builds.count).to eq(2) + + # Process builds service will enqueue builds from the first stage. + # + process_pipeline + + expect(builds.count).to eq(2) + expect(all_builds.count).to eq(2) + + # When builds succeed we will enqueue remaining builds. + # + # We will have 2 succeeded, 1 pending (from stage test), total 4 (two + # additional build from `.gitlab-ci.yml`). + # + succeed_pending + process_pipeline + + expect(builds.success.count).to eq(2) + expect(builds.pending.count).to eq(1) + expect(all_builds.count).to eq(4) + + # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. + # + succeed_pending + process_pipeline + + expect(builds.pending.count).to eq(1) + expect(builds.success.count).to eq(3) + expect(all_builds.count).to eq(4) + + # When the last one succeeds we have 4 successful builds. + # + succeed_pending + process_pipeline + + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(4) end end + def process_pipeline + described_class.new(pipeline.project, user).execute(pipeline) + end + def all_builds - pipeline.builds + pipeline.builds.order(:stage_idx, :id) end def builds all_builds.where.not(status: [:created, :skipped]) end - def process_pipeline - described_class.new(pipeline.project, user).execute(pipeline) + def builds_names + builds.pluck(:name) + end + + def builds_statuses + builds.pluck(:status) + end + + def all_builds_statuses + all_builds.pluck(:status) end def succeed_pending builds.pending.update_all(status: 'success') end + def succeed_running_or_pending + pipeline.builds.running_or_pending.each(&:success) + end + + def fail_running_or_pending + pipeline.builds.running_or_pending.each(&:drop) + end + + def cancel_running_or_pending + pipeline.builds.running_or_pending.each(&:cancel) + end + + def play_manual_action(name) + builds.find_by(name: name).play(user) + end + delegate :manual_actions, to: :pipeline - def create_build(name, stage_idx, when_value = nil) - create(:ci_build, - :created, - pipeline: pipeline, - name: name, - stage_idx: stage_idx, - when: when_value) + def create_build(name, **opts) + create(:ci_build, :created, pipeline: pipeline, name: name, **opts) end end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 8b1ed6470e4..5445b65f4e8 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -89,35 +89,74 @@ describe Ci::RetryPipelineService, '#execute', :services do 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) + context 'when there are optional manual actions only' 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, when: :manual, allow_failure: true) + create_build('rspec 2', :canceled, 1) + end + + it 'retries failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_manual + expect(build('rspec 2')).to be_created + expect(pipeline.reload).to be_running + end end + end - it 'retries builds failed builds and marks subsequent for processing' do - service.execute(pipeline) + context 'when pipeline has blocking manual actions defined' do + context 'when pipeline retry should enqueue builds' do + before do + create_build('test', :failed, 0) + create_build('deploy', :canceled, 0, when: :manual, allow_failure: false) + create_build('verify', :canceled, 1) + end + + it 'retries failed builds' do + service.execute(pipeline) + + expect(build('test')).to be_pending + expect(build('deploy')).to be_manual + expect(build('verify')).to be_created + expect(pipeline.reload).to be_running + end + end - 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 + context 'when pipeline retry should block pipeline immediately' do + before do + create_build('test', :success, 0) + create_build('deploy:1', :success, 1, when: :manual, allow_failure: false) + create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false) + create_build('verify', :canceled, 2) + end + + it 'reprocesses blocking manual action and blocks pipeline' do + service.execute(pipeline) + + expect(build('deploy:1')).to be_success + expect(build('deploy:2')).to be_manual + expect(build('verify')).to be_created + expect(pipeline.reload).to be_blocked + end end end context 'when there is a skipped manual action in last stage' do before do create_build('rspec 1', :canceled, 0) - create_build('rspec 2', :skipped, 0, :manual) - create_build('staging', :skipped, 1, :manual) + create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true) + create_build('staging', :skipped, 1, when: :manual, allow_failure: true) end it 'retries canceled job and reprocesses manual actions' do service.execute(pipeline) expect(build('rspec 1')).to be_pending - expect(build('rspec 2')).to be_skipped + expect(build('rspec 2')).to be_manual expect(build('staging')).to be_created expect(pipeline.reload).to be_running end @@ -126,7 +165,7 @@ describe Ci::RetryPipelineService, '#execute', :services do 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) + create_build('staging', :created, 1, when: :manual, allow_failure: true) end it 'retries canceled job and does not update the manual action' do @@ -141,14 +180,14 @@ describe Ci::RetryPipelineService, '#execute', :services do 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) + create_build('staging', :created, 0, when: :manual, allow_failure: true) end - it 'retries canceled job and skipps the manual action' do + it 'retries canceled job and processes the manual action' do service.execute(pipeline) expect(build('rspec 1')).to be_pending - expect(build('staging')).to be_skipped + expect(build('staging')).to be_manual expect(pipeline.reload).to be_running end end @@ -183,13 +222,12 @@ describe Ci::RetryPipelineService, '#execute', :services do statuses.latest.find_by(name: name) end - def create_build(name, status, stage_num, on = 'on_success') + def create_build(name, status, stage_num, **opts) create(:ci_build, name: name, status: status, stage: "stage_#{stage_num}", stage_idx: stage_num, - when: on, - pipeline: pipeline) do |build| + pipeline: pipeline, **opts) do |build| pipeline.update_status end end |