Commit 4c833a1d authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'pipeline-hooks' into 'master'

Implement Slack integration for pipeline hooks

## What does this MR do?

Add pipeline events to Slack integration

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- Tests
  - [x] Added for this feature/bug

See merge request !5525
parents 1579cc74 c2bcfab1
...@@ -66,6 +66,7 @@ v 8.12.0 (unreleased) ...@@ -66,6 +66,7 @@ v 8.12.0 (unreleased)
- Align add button on repository view (ClemMakesApps) - Align add button on repository view (ClemMakesApps)
- Fix contributions calendar month label truncation (ClemMakesApps) - Fix contributions calendar month label truncation (ClemMakesApps)
- Added tests for diff notes - 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 - Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps) - Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates - Expire commit info views after one day, instead of two weeks, to allow for user email updates
......
class SlackService < Service class SlackService < Service
prop_accessor :webhook, :username, :channel 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? validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties def initialize_properties
...@@ -10,6 +10,7 @@ class SlackService < Service ...@@ -10,6 +10,7 @@ class SlackService < Service
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.notify_only_broken_builds = true self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end end
end end
...@@ -38,13 +39,15 @@ class SlackService < Service ...@@ -38,13 +39,15 @@ class SlackService < Service
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" }, { type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
default_fields + build_event_channels default_fields + build_event_channels
end end
def supported_events 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 end
def execute(data) def execute(data)
...@@ -62,32 +65,22 @@ class SlackService < Service ...@@ -62,32 +65,22 @@ class SlackService < Service
# 'close' action. Ignore update events for now to prevent duplicate # 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving. # messages from arriving.
message = \ message = 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 "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
if message 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 = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
else
false
end end
end end
...@@ -105,6 +98,25 @@ class SlackService < Service ...@@ -105,6 +98,25 @@ class SlackService < Service
private 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) def get_channel_field(event)
field_name = event_channel_name(event) field_name = event_channel_name(event)
self.public_send(field_name) self.public_send(field_name)
...@@ -142,6 +154,17 @@ class SlackService < Service ...@@ -142,6 +154,17 @@ class SlackService < Service
false false
end end
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 end
require "slack_service/issue_message" require "slack_service/issue_message"
...@@ -149,4 +172,5 @@ require "slack_service/push_message" ...@@ -149,4 +172,5 @@ require "slack_service/push_message"
require "slack_service/merge_message" require "slack_service/merge_message"
require "slack_service/note_message" require "slack_service/note_message"
require "slack_service/build_message" require "slack_service/build_message"
require "slack_service/pipeline_message"
require "slack_service/wiki_page_message" require "slack_service/wiki_page_message"
...@@ -9,7 +9,7 @@ class SlackService ...@@ -9,7 +9,7 @@ class SlackService
attr_reader :user_name attr_reader :user_name
attr_reader :duration attr_reader :duration
def initialize(params, commit = true) def initialize(params)
@sha = params[:sha] @sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch' @ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref] @ref = params[:ref]
...@@ -36,7 +36,7 @@ class SlackService ...@@ -36,7 +36,7 @@ class SlackService
def message def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" "#{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) def format(string)
Slack::Notifier::LinkFormatter.format(string) Slack::Notifier::LinkFormatter.format(string)
......
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
...@@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps ...@@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Issue') page.check('Issue')
page.check('Merge request') page.check('Merge request')
page.check('Build') page.check('Build')
page.check('Pipeline')
click_on 'Save' click_on 'Save'
end end
......
...@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do ...@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do
tag: false, tag: false,
project_name: 'project_name', project_name: 'project_name',
project_url: 'somewhere.com', project_url: 'example.gitlab.com',
commit: { commit: {
status: status, status: status,
...@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do ...@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do
} }
end end
context 'succeeded' do let(:message) { build_message }
context 'build succeeded' do
let(:status) { 'success' } let(:status) { 'success' }
let(:color) { 'good' } let(:color) { 'good' }
let(:duration) { 10 } let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do it 'returns a message with information about succeeded build' do
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message) expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color]) expect(subject.attachments).to eq([text: message, color: color])
end end
end end
context 'failed' do context 'build failed' do
let(:status) { 'failed' } let(:status) { 'failed' }
let(:color) { 'danger' } let(:color) { 'danger' }
let(:duration) { 10 } let(:duration) { 10 }
it 'returns a message with information about failed build' do it 'returns a message with information about failed build' do
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
expect(subject.pretext).to be_empty expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message) expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color]) expect(subject.attachments).to eq([text: message, color: color])
end end
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 def build_message(status_text = status)
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second' "<example.gitlab.com|project_name>:" \
expect(subject.pretext).to be_empty " Commit <example.gitlab.com/commit/" \
expect(subject.fallback).to eq(message) "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
expect(subject.attachments).to eq([text: message, color: color]) " of <example.gitlab.com/commits/develop|develop> branch" \
end " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end end
end end
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)
"<example.gitlab.com|project_name>:" \
" Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
" of <example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
...@@ -21,6 +21,9 @@ ...@@ -21,6 +21,9 @@
require 'spec_helper' require 'spec_helper'
describe SlackService, models: true do describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
...@@ -42,15 +45,14 @@ describe SlackService, models: true do ...@@ -42,15 +45,14 @@ describe SlackService, models: true do
end end
describe "Execute" do describe "Execute" do
let(:slack) { SlackService.new }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
let(:push_sample_data) do let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user) Gitlab::DataBuilder::Push.build_sample(project, user)
end end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
before do before do
allow(slack).to receive_messages( allow(slack).to receive_messages(
...@@ -212,10 +214,8 @@ describe SlackService, models: true do ...@@ -212,10 +214,8 @@ describe SlackService, models: true do
end end
describe "Note events" do describe "Note events" do
let(:slack) { SlackService.new }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) } let(:project) { create(:project, creator_id: user.id) }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do before do
allow(slack).to receive_messages( allow(slack).to receive_messages(
...@@ -285,4 +285,63 @@ describe SlackService, models: true do ...@@ -285,4 +285,63 @@ describe SlackService, models: true do
end end
end 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 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