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
This commit is contained in:
commit
4c833a1d4e
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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
|
||||||
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
|
||||||
{ 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
|
||||||
# '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
|
||||||
|
|
||||||
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
|
||||||
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"
|
||||||
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
|
||||||
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]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
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
|
||||||
}
|
}
|
||||||
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
|
def build_message(status_text = status)
|
||||||
let(:status) { 'failed' }
|
"<example.gitlab.com|project_name>:" \
|
||||||
let(:color) { 'danger' }
|
" Commit <example.gitlab.com/commit/" \
|
||||||
let(:duration) { 1 }
|
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
|
||||||
|
" of <example.gitlab.com/commits/develop|develop> branch" \
|
||||||
it 'returns seconds as singular when there is only one' do
|
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
|
||||||
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'
|
|
||||||
expect(subject.pretext).to be_empty
|
|
||||||
expect(subject.fallback).to eq(message)
|
|
||||||
expect(subject.attachments).to eq([text: message, color: color])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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)
|
||||||
|
"<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 @@
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
Loading…
Reference in New Issue