Commit 42ca9c6f authored by Tiger Watson's avatar Tiger Watson

Add :preparing status to HasStatus

Introduces a new status for builds between :created and :pending
that will be used when builds require one or more prerequisite
actions to be completed before being picked up by a runner
(such as creating Kubernetes resources before deploying).

The existing :created > :pending transition is unchanged, so
only builds that require preparation will use the :preparing
status.
parent 250f6ad2
......@@ -31,6 +31,7 @@
}
}
.ci-status-icon-preparing,
.ci-status-icon-running {
svg {
fill: $blue-400;
......
......@@ -166,6 +166,7 @@
float: left;
.accept-merge-request {
&.ci-preparing,
&.ci-pending,
&.ci-running {
@include btn-blue;
......
......@@ -793,6 +793,7 @@
@include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700);
}
&.ci-status-icon-preparing,
&.ci-status-icon-running {
@include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
}
......
......@@ -44,6 +44,7 @@
}
&.ci-info,
&.ci-preparing,
&.ci-running {
@include status-color($blue-100, $blue-500, $blue-600);
}
......
......@@ -82,10 +82,14 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition [:created, :skipped, :scheduled] => :pending
transition [:created, :preparing, :skipped, :scheduled] => :pending
transition [:success, :failed, :canceled] => :running
end
event :prepare do
transition any - [:preparing] => :preparing
end
event :run do
transition any - [:running] => :running
end
......@@ -118,7 +122,7 @@ module Ci
# Do not add any operations to this state_machine
# Create a separate worker for each new operation
before_transition [:created, :pending] => :running do |pipeline|
before_transition [:created, :preparing, :pending] => :running do |pipeline|
pipeline.started_at = Time.now
end
......@@ -141,7 +145,7 @@ module Ci
end
end
after_transition [:created, :pending] => :running do |pipeline|
after_transition [:created, :preparing, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
......@@ -149,7 +153,7 @@ module Ci
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
after_transition [:created, :pending, :running] => :success do |pipeline|
after_transition [:created, :preparing, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
end
......@@ -597,6 +601,7 @@ module Ci
retry_optimistic_lock(self) do
case latest_builds_status.to_s
when 'created' then nil
when 'preparing' then prepare
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
......
......@@ -39,10 +39,14 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
transition [:created, :preparing] => :pending
transition [:success, :failed, :canceled, :skipped] => :running
end
event :prepare do
transition any - [:preparing] => :preparing
end
event :run do
transition any - [:running] => :running
end
......@@ -76,6 +80,7 @@ module Ci
retry_optimistic_lock(self) do
case statuses.latest.status
when 'created' then nil
when 'preparing' then prepare
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
......
......@@ -66,7 +66,7 @@ class CommitStatus < ActiveRecord::Base
end
event :enqueue do
transition [:created, :skipped, :manual, :scheduled] => :pending
transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending
end
event :run do
......@@ -74,26 +74,26 @@ class CommitStatus < ActiveRecord::Base
end
event :skip do
transition [:created, :pending] => :skipped
transition [:created, :preparing, :pending] => :skipped
end
event :drop do
transition [:created, :pending, :running, :scheduled] => :failed
transition [:created, :preparing, :pending, :running, :scheduled] => :failed
end
event :success do
transition [:created, :pending, :running] => :success
transition [:created, :preparing, :pending, :running] => :success
end
event :cancel do
transition [:created, :pending, :running, :manual, :scheduled] => :canceled
transition [:created, :preparing, :pending, :running, :manual, :scheduled] => :canceled
end
before_transition [:created, :skipped, :manual, :scheduled] => :pending do |commit_status|
before_transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
commit_status.queued_at = Time.now
end
before_transition [:created, :pending] => :running do |commit_status|
before_transition [:created, :preparing, :pending] => :running do |commit_status|
commit_status.started_at = Time.now
end
......
......@@ -5,14 +5,14 @@ module HasStatus
DEFAULT_STATUS = 'created'.freeze
BLOCKED_STATUS = %w[manual scheduled].freeze
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual scheduled].freeze
AVAILABLE_STATUSES = %w[created preparing pending running success failed canceled skipped manual scheduled].freeze
STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze
ACTIVE_STATUSES = %w[pending running].freeze
ACTIVE_STATUSES = %w[preparing pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed pending running manual scheduled canceled success skipped created].freeze
ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8 }.freeze
scheduled: 8, preparing: 9 }.freeze
UnknownStatusError = Class.new(StandardError)
......@@ -26,6 +26,7 @@ module HasStatus
success = scope_relevant.success.select('count(*)').to_sql
manual = scope_relevant.manual.select('count(*)').to_sql
scheduled = scope_relevant.scheduled.select('count(*)').to_sql
preparing = scope_relevant.preparing.select('count(*)').to_sql
pending = scope_relevant.pending.select('count(*)').to_sql
running = scope_relevant.running.select('count(*)').to_sql
skipped = scope_relevant.skipped.select('count(*)').to_sql
......@@ -37,12 +38,14 @@ module HasStatus
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{preparing}) THEN 'preparing'
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
WHEN (#{scheduled})>0 THEN 'scheduled'
WHEN (#{preparing})>0 THEN 'preparing'
WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
......@@ -70,6 +73,7 @@ module HasStatus
state_machine :status, initial: :created do
state :created, value: 'created'
state :preparing, value: 'preparing'
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
......@@ -81,6 +85,7 @@ module HasStatus
end
scope :created, -> { where(status: 'created') }
scope :preparing, -> { where(status: 'preparing') }
scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) }
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
......@@ -90,14 +95,14 @@ module HasStatus
scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') }
scope :scheduled, -> { where(status: 'scheduled') }
scope :alive, -> { where(status: [:created, :pending, :running]) }
scope :alive, -> { where(status: [:created, :preparing, :pending, :running]) }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
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, :scheduled])
where(status: [:running, :preparing, :pending, :created, :scheduled])
end
end
......
......@@ -15,6 +15,7 @@ module Gitlab
failed: '#e05d44',
running: '#dfb317',
pending: '#dfb317',
preparing: '#dfb317',
canceled: '#9f9f9f',
skipped: '#9f9f9f',
unknown: '#9f9f9f'
......
......@@ -11,6 +11,7 @@ module Gitlab
Status::Build::Manual,
Status::Build::Canceled,
Status::Build::Created,
Status::Build::Preparing,
Status::Build::Pending,
Status::Build::Skipped],
[Status::Build::Cancelable,
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
module Build
class Preparing < Status::Extended
##
# TODO: image is shared with 'pending'
# until we get a dedicated one
#
def illustration
{
image: 'illustrations/job_not_triggered.svg',
size: 'svg-306',
title: _('This job is preparing to start'),
content: _('This job is performing tasks that must complete before it can start')
}
end
def self.matches?(build, _)
build.preparing?
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Status
class Preparing < Status::Core
def text
s_('CiStatusText|preparing')
end
def label
s_('CiStatusLabel|preparing')
end
##
# TODO: shared with 'created'
# until we get one for 'preparing'
#
def icon
'status_created'
end
##
# TODO: shared with 'created'
# until we get one for 'preparing'
#
def favicon
'favicon_status_created'
end
end
end
end
end
......@@ -1512,6 +1512,9 @@ msgstr ""
msgid "CiStatusLabel|pending"
msgstr ""
msgid "CiStatusLabel|preparing"
msgstr ""
msgid "CiStatusLabel|skipped"
msgstr ""
......@@ -1545,6 +1548,9 @@ msgstr ""
msgid "CiStatusText|pending"
msgstr ""
msgid "CiStatusText|preparing"
msgstr ""
msgid "CiStatusText|skipped"
msgstr ""
......@@ -7864,6 +7870,12 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
msgid "This job is performing tasks that must complete before it can start"
msgstr ""
msgid "This job is preparing to start"
msgstr ""
msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:"
msgstr ""
......
......@@ -75,6 +75,10 @@ FactoryBot.define do
status 'created'
end
trait :preparing do
status 'preparing'
end
trait :scheduled do
schedulable
status 'scheduled'
......
......@@ -50,6 +50,14 @@ FactoryBot.define do
failure_reason :config_error
end
trait :created do
status :created
end
trait :preparing do
status :preparing
end
trait :blocked do
status :manual
end
......
......@@ -33,6 +33,10 @@ FactoryBot.define do
status 'pending'
end
trait :preparing do
status 'preparing'
end
trait :created do
status 'created'
end
......
......@@ -41,6 +41,24 @@ describe 'Pipeline Badge' do
end
end
context 'when the pipeline is preparing' do
let!(:job) { create(:ci_build, status: 'created', pipeline: pipeline) }
before do
# Prevent skipping directly to 'pending'
allow(Ci::BuildPrepareWorker).to receive(:perform_async)
end
it 'displays the preparing badge' do
job.prepare
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
expect(page.status_code).to eq(200)
expect_badge('preparing')
end
end
context 'when the pipeline is running' do
it 'shows displays so on the badge' do
create(:ci_build, pipeline: pipeline, name: 'second build', status_event: 'run')
......
......@@ -21,6 +21,11 @@ describe 'Pipeline', :js do
pipeline: pipeline, stage: 'test', name: 'test')
end
let!(:build_preparing) do
create(:ci_build, :preparing,
pipeline: pipeline, stage: 'deploy', name: 'prepare')
end
let!(:build_running) do
create(:ci_build, :running,
pipeline: pipeline, stage: 'deploy', name: 'deploy')
......@@ -97,6 +102,24 @@ describe 'Pipeline', :js do
end
end
context 'when pipeline has preparing builds' do
it 'shows a preparing icon and a cancel action' do
page.within('#ci-badge-prepare') do
expect(page).to have_selector('.js-ci-status-icon-preparing')
expect(page).to have_selector('.js-icon-cancel')
expect(page).to have_content('prepare')
end
end
it 'cancels the preparing build and shows retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
expect(page).to have_css('.js-icon-retry')
end
end
end
context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
......
......@@ -282,6 +282,30 @@ describe 'Pipelines', :js do
end
context 'for generic statuses' do
context 'when preparing' do
let!(:pipeline) do
create(:ci_empty_pipeline,
status: 'preparing', project: project)
end
let!(:status) do
create(:generic_commit_status,
:preparing, pipeline: pipeline)
end
before do
visit_project_pipelines
end
it 'is cancelable' do
expect(page).to have_selector('.js-pipelines-cancel-button')
end
it 'shows the pipeline as preparing' do
expect(page).to have_selector('.ci-preparing')
end
end
context 'when running' do
let!(:running) do
create(:generic_commit_status,
......
......@@ -59,6 +59,16 @@ describe Gitlab::Badge::Pipeline::Template do
end
end
context 'when status is preparing' do
before do
allow(badge).to receive(:status).and_return('preparing')
end
it 'has expected color' do
expect(template.value_color).to eq '#dfb317'
end
end
context 'when status is unknown' do
before do
allow(badge).to receive(:status).and_return('unknown')
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Status::Build::Preparing do
subject do
described_class.new(double('subject'))
end
describe '#illustration' do
it { expect(subject.illustration).to include(:image, :size, :title, :content) }
end
describe '.matches?' do
subject { described_class.matches?(build, nil) }
context 'when build is preparing' do
let(:build) { create(:ci_build, :preparing) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not preparing' do
let(:build) { create(:ci_build, :success) }
it 'does not match' do
expect(subject).to be false
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Status::Preparing do
subject do
described_class.new(double('subject'), nil)
end
describe '#text' do
it { expect(subject.text).to eq 'preparing' }
end
describe '#label' do
it { expect(subject.label).to eq 'preparing' }
end
describe '#icon' do
it { expect(subject.icon).to eq 'status_created' }
end
describe '#favicon' do
it { expect(subject.favicon).to eq 'favicon_status_created' }
end
describe '#group' do
it { expect(subject.group).to eq 'preparing' }
end
end
......@@ -1201,16 +1201,28 @@ describe Ci::Pipeline, :mailer do
end
describe '#started_at' do
it 'updates on transitioning to running' do
build.run
let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
%i[created preparing pending].each do |status|
context "from #{status}" do
let(:from_status) { status }
expect(pipeline.reload.started_at).not_to be_nil
it 'updates on transitioning to running' do
pipeline.run
expect(pipeline.started_at).not_to be_nil
end
end
end
it 'does not update on transitioning to success' do
build.success
context 'from created' do
let(:from_status) { :created }
it 'does not update on transitioning to success' do
pipeline.succeed
expect(pipeline.reload.started_at).to be_nil
expect(pipeline.started_at).to be_nil
end
end
end
......@@ -1229,27 +1241,49 @@ describe Ci::Pipeline, :mailer do
end
describe 'merge request metrics' do
let(:project) { create(:project, :repository) }
let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
before do
expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
end
context 'when transitioning to running' do
it 'schedules metrics workers' do
pipeline.run
%i[created preparing pending].each do |status|
context "from #{status}" do
let(:from_status) { status }
it 'schedules metrics workers' do
pipeline.run
end
end
end
end
context 'when transitioning to success' do
let(:from_status) { 'created' }
it 'schedules metrics workers' do
pipeline.succeed
end
end
end
describe 'merge on success' do
let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
%i[created preparing pending running].each do |status|
context "from #{status}" do
let(:from_status) { status }
it 'schedules pipeline success worker' do
expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id)
pipeline.succeed
end
end
end
end
describe 'pipeline caching' do
it 'performs ExpirePipelinesCacheWorker' do
expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
......@@ -1768,6 +1802,14 @@ describe Ci::Pipeline, :mailer do
subject { pipeline.reload.status }
context 'on prepare' do
before do
build.prepare
end
it { is_expected.to eq('preparing') }
end
context 'on queuing' do
before do
build.enqueue
......
......@@ -49,6 +49,16 @@ describe CommitStatus do
commit_status.success!
end
describe 'transitioning to running' do
let(:commit_status) { create(:commit_status, :pending, started_at: nil) }
it 'records the started at time' do
commit_status.run!
expect(commit_status.started_at).to be_present
end
end
end
describe '#started?' do
......@@ -555,6 +565,7 @@ describe CommitStatus do
before do
allow(Time).to receive(:now).and_return(current_time)
expect(commit_status.any_unmet_prerequisites?).to eq false
end
shared_examples 'commit status enqueued' do
......@@ -569,6 +580,12 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :preparing' do
let(:commit_status) { create(:commit_status, :preparing) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :skipped' do
let(:commit_status) { create(:commit_status, :skipped) }
......
......@@ -34,6 +34,22 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
context 'all preparing' do
let!(:statuses) do
[create(type, status: :preparing), create(type, status: :preparing)]
end
it { is_expected.to eq 'preparing' }
end
context 'at least one preparing' do
let!(:statuses) do
[create(type, status: :success), create(type, status: :preparing)]
end
it { is_expected.to eq 'preparing' }
end
context 'success and failed but allowed to fail' do
let!(:statuses) do
[create(type, status: :success),
......@@ -188,7 +204,7 @@ describe HasStatus do