diff options
29 files changed, 698 insertions, 7 deletions
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index abcf84b4d15..8e19752a8a1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -5,8 +5,9 @@ module CiStatusHelper end def ci_status_with_icon(status, target = nil) - content = ci_icon_for_status(status) + ci_label_for_status(status) + content = ci_icon_for_status(status) + ci_text_for_status(status) klass = "ci-status ci-#{status}" + if target link_to content, target, class: klass else @@ -14,7 +15,19 @@ module CiStatusHelper end end + def ci_text_for_status(status) + if detailed_status?(status) + status.text + else + status + end + end + def ci_label_for_status(status) + if detailed_status?(status) + return status.label + end + case status when 'success' 'passed' @@ -31,6 +44,10 @@ module CiStatusHelper end def ci_icon_for_status(status) + if detailed_status?(status) + return custom_icon(status.icon) + end + icon_name = case status when 'success' @@ -94,4 +111,10 @@ module CiStatusHelper class: klass, title: title, data: data end end + + def detailed_status?(status) + status.respond_to?(:text) && + status.respond_to?(:label) && + status.respond_to?(:icon) + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fabbf97d4db..caf6908505e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -320,6 +320,10 @@ module Ci .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id } end + def detailed_status + Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate! + end + private def pipeline_data diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 4c7b14a04db..0f08f4e8592 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -1,12 +1,13 @@ - status = pipeline.status +- detailed_status = pipeline.detailed_status - show_commit = local_assigns.fetch(:show_commit, true) - show_branch = local_assigns.fetch(:show_branch, true) %tr.commit %td.commit-link - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - = ci_label_for_status(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "ci-status ci-#{detailed_status}" do + = ci_icon_for_status(detailed_status) + = ci_text_for_status(detailed_status) %td = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6d9b91ad0e7..9ab7971b56c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -8,14 +8,13 @@ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for - - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API + - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 095bd254d6b..229bdfb0e8d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,6 +1,6 @@ .page-content-header .header-main-content - = ci_status_with_icon(@pipeline.status) + = ci_status_with_icon(@pipeline.detailed_status) %strong Pipeline ##{@commit.pipelines.last.id} triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb new file mode 100644 index 00000000000..dd6d99e9075 --- /dev/null +++ b/lib/gitlab/ci/status/canceled.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Canceled < Status::Core + def text + 'canceled' + end + + def label + 'canceled' + end + + def icon + 'icon_status_canceled' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb new file mode 100644 index 00000000000..ce4108fdcf2 --- /dev/null +++ b/lib/gitlab/ci/status/core.rb @@ -0,0 +1,58 @@ +module Gitlab + module Ci + module Status + # Base abstract class fore core status + # + class Core + include Gitlab::Routing.url_helpers + + def initialize(subject) + @subject = subject + end + + def icon + raise NotImplementedError + end + + def label + raise NotImplementedError + end + + def title + "#{@subject.class.name.demodulize}: #{label}" + end + + # Deprecation warning: this method is here because we need to maintain + # backwards compatibility with legacy statuses. We often do something + # like "ci-status ci-status-#{status}" to set CSS class. + # + # `to_s` method should be renamed to `group` at some point, after + # phasing legacy satuses out. + # + def to_s + self.class.name.demodulize.downcase.underscore + end + + def has_details? + raise NotImplementedError + end + + def details_path + raise NotImplementedError + end + + def has_action? + raise NotImplementedError + end + + def action_icon + raise NotImplementedError + end + + def action_path + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb new file mode 100644 index 00000000000..6596d7e01ca --- /dev/null +++ b/lib/gitlab/ci/status/created.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Created < Status::Core + def text + 'created' + end + + def label + 'created' + end + + def icon + 'icon_status_created' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb new file mode 100644 index 00000000000..6bfb5d38c1f --- /dev/null +++ b/lib/gitlab/ci/status/extended.rb @@ -0,0 +1,11 @@ +module Gitlab + module Ci + module Status + module Extended + def matches?(_subject) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb new file mode 100644 index 00000000000..c5b5e3203ad --- /dev/null +++ b/lib/gitlab/ci/status/failed.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Failed < Status::Core + def text + 'failed' + end + + def label + 'failed' + end + + def icon + 'icon_status_failed' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb new file mode 100644 index 00000000000..d30f35a59a2 --- /dev/null +++ b/lib/gitlab/ci/status/pending.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Pending < Status::Core + def text + 'pending' + end + + def label + 'pending' + end + + def icon + 'icon_status_pending' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb new file mode 100644 index 00000000000..25e52bec3da --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/common.rb @@ -0,0 +1,23 @@ +module Gitlab + module Ci + module Status + module Pipeline + module Common + def has_details? + true + end + + def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject) + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb new file mode 100644 index 00000000000..71d27bf7cf5 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -0,0 +1,39 @@ +module Gitlab + module Ci + module Status + module Pipeline + class Factory + EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings] + + def initialize(pipeline) + @pipeline = pipeline + @status = pipeline.status || :created + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def core_status + Gitlab::Ci::Status + .const_get(@status.capitalize) + .new(@pipeline) + .extend(Status::Pipeline::Common) + end + + def extended_status + @extended ||= EXTENDED_STATUSES.find do |status| + status.matches?(@pipeline) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb new file mode 100644 index 00000000000..4b040d60df8 --- /dev/null +++ b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb @@ -0,0 +1,31 @@ +module Gitlab + module Ci + module Status + module Pipeline + class SuccessWithWarnings < SimpleDelegator + extend Status::Extended + + def text + 'passed' + end + + def label + 'passed with warnings' + end + + def icon + 'icon_status_warning' + end + + def to_s + 'success_with_warnings' + end + + def self.matches?(pipeline) + pipeline.success? && pipeline.has_warnings? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb new file mode 100644 index 00000000000..2aba3c373c7 --- /dev/null +++ b/lib/gitlab/ci/status/running.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Running < Status::Core + def text + 'running' + end + + def label + 'running' + end + + def icon + 'icon_status_running' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb new file mode 100644 index 00000000000..16282aefd03 --- /dev/null +++ b/lib/gitlab/ci/status/skipped.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Skipped < Status::Core + def text + 'skipped' + end + + def label + 'skipped' + end + + def icon + 'icon_status_skipped' + end + end + end + end +end diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb new file mode 100644 index 00000000000..c09c5f006e3 --- /dev/null +++ b/lib/gitlab/ci/status/success.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Success < Status::Core + def text + 'passed' + end + + def label + 'passed' + end + + def icon + 'icon_status_success' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb new file mode 100644 index 00000000000..619ecbcba67 --- /dev/null +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Canceled do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#label' do + it { expect(subject.label).to eq 'canceled' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_canceled' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: canceled' } + end +end diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb new file mode 100644 index 00000000000..157302c65a8 --- /dev/null +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Created do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'created' } + end + + describe '#label' do + it { expect(subject.label).to eq 'created' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_created' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: created' } + end +end diff --git a/spec/lib/gitlab/ci/status/extended_spec.rb b/spec/lib/gitlab/ci/status/extended_spec.rb new file mode 100644 index 00000000000..120e121aae5 --- /dev/null +++ b/spec/lib/gitlab/ci/status/extended_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Extended do + subject do + Class.new.extend(described_class) + end + + it 'requires subclass to implement matcher' do + expect { subject.matches?(double) } + .to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb new file mode 100644 index 00000000000..0b3cb8168e6 --- /dev/null +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Failed do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'failed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_failed' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: failed' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb new file mode 100644 index 00000000000..57c901c1202 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pending do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#label' do + it { expect(subject.label).to eq 'pending' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_pending' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: pending' } + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb new file mode 100644 index 00000000000..21adee3f8e7 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Common do + let(:pipeline) { create(:ci_pipeline) } + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(pipeline).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb new file mode 100644 index 00000000000..d6243940f2e --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::Factory do + subject do + described_class.new(pipeline) + end + + let(:status) do + subject.fabricate! + end + + context 'when pipeline has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:pipeline) do + create(:ci_pipeline, status: core_status) + end + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path) + .to include "pipelines/#{pipeline.id}" + end + end + end + end + + context 'when pipeline has warnings' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'fabricates extended "success with warnings" status' do + expect(status) + .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings + end + + it 'extends core status with common pipeline methods' do + expect(status).to have_details + end + end +end diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb new file mode 100644 index 00000000000..02e526e3de2 --- /dev/null +++ b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do + subject do + described_class.new(double('status')) + end + + describe '#test' do + it { expect(subject.text).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed with warnings' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_warning' } + end + + describe '.matches?' do + context 'when pipeline is successful' do + let(:pipeline) do + create(:ci_pipeline, status: :success) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'is a correct match' do + expect(described_class.matches?(pipeline)).to eq true + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + + context 'when pipeline is not successful' do + let(:pipeline) do + create(:ci_pipeline, status: :skipped) + end + + context 'when pipeline has warnings' do + before do + allow(pipeline).to receive(:has_warnings?).and_return(true) + end + + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + + context 'when pipeline does not have warnings' do + it 'does not match' do + expect(described_class.matches?(pipeline)).to eq false + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb new file mode 100644 index 00000000000..c023f1872cc --- /dev/null +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Running do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'running' } + end + + describe '#label' do + it { expect(subject.label).to eq 'running' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_running' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: running' } + end +end diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb new file mode 100644 index 00000000000..d4f7f4b3b70 --- /dev/null +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Skipped do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#label' do + it { expect(subject.label).to eq 'skipped' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_skipped' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: skipped' } + end +end diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb new file mode 100644 index 00000000000..9e261a3aa5f --- /dev/null +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Success do + subject { described_class.new(double('subject')) } + + describe '#text' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#label' do + it { expect(subject.label).to eq 'passed' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_success' } + end + + describe '#title' do + it { expect(subject.title).to eq 'Double: passed' } + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 0d2b4920835..3f93d9ddf19 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -404,6 +404,76 @@ describe Ci::Pipeline, models: true do end end + describe '#detailed_status' do + context 'when pipeline is created' do + let(:pipeline) { create(:ci_pipeline, status: :created) } + + it 'returns detailed status for created pipeline' do + expect(pipeline.detailed_status.text).to eq 'created' + end + end + + context 'when pipeline is pending' do + let(:pipeline) { create(:ci_pipeline, status: :pending) } + + it 'returns detailed status for pending pipeline' do + expect(pipeline.detailed_status.text).to eq 'pending' + end + end + + context 'when pipeline is running' do + let(:pipeline) { create(:ci_pipeline, status: :running) } + + it 'returns detailed status for running pipeline' do + expect(pipeline.detailed_status.text).to eq 'running' + end + end + + context 'when pipeline is successful' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + it 'returns detailed status for successful pipeline' do + expect(pipeline.detailed_status.text).to eq 'passed' + end + end + + context 'when pipeline is failed' do + let(:pipeline) { create(:ci_pipeline, status: :failed) } + + it 'returns detailed status for failed pipeline' do + expect(pipeline.detailed_status.text).to eq 'failed' + end + end + + context 'when pipeline is canceled' do + let(:pipeline) { create(:ci_pipeline, status: :canceled) } + + it 'returns detailed status for canceled pipeline' do + expect(pipeline.detailed_status.text).to eq 'canceled' + end + end + + context 'when pipeline is skipped' do + let(:pipeline) { create(:ci_pipeline, status: :skipped) } + + it 'returns detailed status for skipped pipeline' do + expect(pipeline.detailed_status.text).to eq 'skipped' + end + end + + context 'when pipeline is successful but with warnings' do + let(:pipeline) { create(:ci_pipeline, status: :success) } + + before do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) + end + + it 'retruns detailed status for successful pipeline with warnings' do + expect(pipeline.detailed_status.label).to eq 'passed with warnings' + end + end + end + describe '#cancelable?' do %i[created running pending].each do |status0| context "when there is a build #{status0}" do |