Commit 79b8f02b authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'pipeline-blocking-actions' into 'master'

Make manual actions blocking

Closes #26360 and #22628

See merge request !9585
parents b696cbc5 3b35d1f8
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
---
title: Make it possible to configure blocking manual actions
merge_request: 9585
author:
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
......@@ -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"
......
......@@ -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
>
......
......@@ -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],
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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
......@@ -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: {
......
......@@ -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
......
......@@ -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
......
......@@ -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')
......
......@@ -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')
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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) }
......
......@@ -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) }
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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) }
......
......@@ -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
......
......@@ -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
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
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment