diff --git a/CHANGELOG b/CHANGELOG index f709758abf6..cc87ff3ecb7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -66,6 +66,7 @@ v 8.12.0 (unreleased) - Align add button on repository view (ClemMakesApps) - Fix contributions calendar month label truncation (ClemMakesApps) - Added tests for diff notes + - Add pipeline events to Slack integration !5525 - Add a button to download latest successful artifacts for branches and tags !5142 - Remove redundant pipeline tooltips (ClemMakesApps) - Expire commit info views after one day, instead of two weeks, to allow for user email updates diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index e6c943db2bf..e1b937817f4 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,6 +1,6 @@ class SlackService < Service prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_builds + boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines validates :webhook, presence: true, url: true, if: :activated? def initialize_properties @@ -10,6 +10,7 @@ class SlackService < Service if properties.nil? self.properties = {} self.notify_only_broken_builds = true + self.notify_only_broken_pipelines = true end end @@ -38,13 +39,15 @@ class SlackService < Service { type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'channel', placeholder: "#general" }, { type: 'checkbox', name: 'notify_only_broken_builds' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] default_fields + build_event_channels end def supported_events - %w(push issue confidential_issue merge_request note tag_push build wiki_page) + %w[push issue confidential_issue merge_request note tag_push + build pipeline wiki_page] end def execute(data) @@ -62,32 +65,22 @@ class SlackService < Service # 'close' action. Ignore update events for now to prevent duplicate # messages from arriving. - message = \ - case object_kind - when "push", "tag_push" - PushMessage.new(data) - when "issue" - IssueMessage.new(data) unless is_update?(data) - when "merge_request" - MergeMessage.new(data) unless is_update?(data) - when "note" - NoteMessage.new(data) - when "build" - BuildMessage.new(data) if should_build_be_notified?(data) - when "wiki_page" - WikiPageMessage.new(data) - end - - opt = {} - - event_channel = get_channel_field(object_kind) || channel - - opt[:channel] = event_channel if event_channel - opt[:username] = username if username + message = get_message(object_kind, data) if message + opt = {} + + event_channel = get_channel_field(object_kind) || channel + + opt[:channel] = event_channel if event_channel + opt[:username] = username if username + notifier = Slack::Notifier.new(webhook, opt) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) + + true + else + false end end @@ -105,6 +98,25 @@ class SlackService < Service private + def get_message(object_kind, data) + case object_kind + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "pipeline" + PipelineMessage.new(data) if should_pipeline_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + def get_channel_field(event) field_name = event_channel_name(event) self.public_send(field_name) @@ -142,6 +154,17 @@ class SlackService < Service false end end + + def should_pipeline_be_notified?(data) + case data[:object_attributes][:status] + when 'success' + !notify_only_broken_pipelines? + when 'failed' + true + else + false + end + end end require "slack_service/issue_message" @@ -149,4 +172,5 @@ require "slack_service/push_message" require "slack_service/merge_message" require "slack_service/note_message" require "slack_service/build_message" +require "slack_service/pipeline_message" require "slack_service/wiki_page_message" diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb index 69c21b3fc38..0fca4267bad 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/slack_service/build_message.rb @@ -9,7 +9,7 @@ class SlackService attr_reader :user_name attr_reader :duration - def initialize(params, commit = true) + def initialize(params) @sha = params[:sha] @ref_type = params[:tag] ? 'tag' : 'branch' @ref = params[:ref] @@ -36,7 +36,7 @@ class SlackService def message "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" - end + end def format(string) Slack::Notifier::LinkFormatter.format(string) diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb new file mode 100644 index 00000000000..f06b3562965 --- /dev/null +++ b/app/models/project_services/slack_service/pipeline_message.rb @@ -0,0 +1,79 @@ +class SlackService + class PipelineMessage < BaseMessage + attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url, + :user_name, :duration, :pipeline_id + + def initialize(data) + pipeline_attributes = data[:object_attributes] + @sha = pipeline_attributes[:sha] + @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' + @ref = pipeline_attributes[:ref] + @status = pipeline_attributes[:status] + @duration = pipeline_attributes[:duration] + @pipeline_id = pipeline_attributes[:id] + + @project_name = data[:project][:path_with_namespace] + @project_url = data[:project][:web_url] + @user_name = data[:commit] && data[:commit][:author_name] + end + + def pretext + '' + end + + def fallback + format(message) + end + + def attachments + [{ text: format(message), color: attachment_color }] + end + + private + + def message + "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def attachment_color + if status == 'success' + 'good' + else + 'danger' + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def pipeline_url + "#{project_url}/pipelines/#{pipeline_id}" + end + + def pipeline_link + "[#{Commit.truncate_sha(sha)}](#{pipeline_url})" + end + end +end diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 03f87df7a60..11dc7f580f0 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps page.check('Issue') page.check('Merge request') page.check('Build') + page.check('Pipeline') click_on 'Save' end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb index 7fcfdf0eacd..452f4e2782c 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -10,7 +10,7 @@ describe SlackService::BuildMessage do tag: false, project_name: 'project_name', - project_url: 'somewhere.com', + project_url: 'example.gitlab.com', commit: { status: status, @@ -20,42 +20,38 @@ describe SlackService::BuildMessage do } end - context 'succeeded' do + let(:message) { build_message } + + context 'build succeeded' do let(:status) { 'success' } let(:color) { 'good' } let(:duration) { 10 } - + let(:message) { build_message('passed') } + it 'returns a message with information about succeeded build' do - message = ': Commit of branch by hacker passed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end end - context 'failed' do + context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } let(:duration) { 10 } it 'returns a message with information about failed build' do - message = ': Commit of branch by hacker failed in 10 seconds' - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) - end - end - - describe '#seconds_name' do - let(:status) { 'failed' } - let(:color) { 'danger' } - let(:duration) { 1 } - - it 'returns seconds as singular when there is only one' do - message = ': Commit of branch by hacker failed in 1 second' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) end end + + def build_message(status_text = status) + ":" \ + " Commit " \ + " of branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end end diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb new file mode 100644 index 00000000000..babb3909f56 --- /dev/null +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe SlackService::PipelineMessage do + subject { SlackService::PipelineMessage.new(args) } + + let(:args) do + { + object_attributes: { + id: 123, + sha: '97de212e80737a608d939f648d959671fb0a0142', + tag: false, + ref: 'develop', + status: status, + duration: duration + }, + project: { path_with_namespace: 'project_name', + web_url: 'example.gitlab.com' }, + commit: { author_name: 'hacker' } + } + end + + let(:message) { build_message } + + context 'pipeline succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + let(:message) { build_message('passed') } + + it 'returns a message with information about succeeded build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'pipeline failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + def build_message(status_text = status) + ":" \ + " Pipeline " \ + " of branch" \ + " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 28af68d13b4..5afdc4b2f7b 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -21,6 +21,9 @@ require 'spec_helper' describe SlackService, models: true do + let(:slack) { SlackService.new } + let(:webhook_url) { 'https://example.gitlab.com/' } + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -42,15 +45,14 @@ describe SlackService, models: true do end describe "Execute" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project) } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } + let(:push_sample_data) do Gitlab::DataBuilder::Push.build_sample(project, user) end - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } - let(:username) { 'slack_username' } - let(:channel) { 'slack_channel' } before do allow(slack).to receive_messages( @@ -212,10 +214,8 @@ describe SlackService, models: true do end describe "Note events" do - let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id) } - let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } before do allow(slack).to receive_messages( @@ -285,4 +285,63 @@ describe SlackService, models: true do end end end + + describe 'Pipeline events' do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: status, + sha: project.commit.sha, ref: project.default_branch) + end + + before do + allow(slack).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + end + + shared_examples 'call Slack API' do + before do + WebMock.stub_request(:post, webhook_url) + end + + it 'calls Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + + it_behaves_like 'call Slack API' + end + + context 'with succeeded pipeline' do + let(:status) { 'success' } + + context 'with default to notify_only_broken_pipelines' do + it 'does not call Slack API for pipeline events' do + data = Gitlab::DataBuilder::Pipeline.build(pipeline) + result = slack.execute(data) + + expect(result).to be_falsy + end + end + + context 'with setting notify_only_broken_pipelines to false' do + before do + slack.notify_only_broken_pipelines = false + end + + it_behaves_like 'call Slack API' + end + end + end end