Read about our upcoming Code of Conduct on this issue

Commit afe5d7d2 authored by Stan Hu's avatar Stan Hu Committed by Douwe Maan
Browse files

Issue #595: Support Slack notifications upon issue and merge request events

1) Adds a DB migration for all services to toggle on push, issue, and merge events.

2) Upon an issue or merge request event, fire service hooks.

3) Slack service supports custom messages for each of these events. Other services
not supported at the moment.

4) Label merge request hooks with their corresponding actions.
parent 7fb58d4fc844
require 'slack-notifier'
class SlackMessage
attr_reader :after
attr_reader :before
attr_reader :commits
attr_reader :project_name
attr_reader :project_url
attr_reader :ref
attr_reader :username
def initialize(params)
@after = params.fetch(:after)
@before = params.fetch(:before)
@commits = params.fetch(:commits, [])
@project_name = params.fetch(:project_name)
@project_url = params.fetch(:project_url)
@ref = params.fetch(:ref).gsub('refs/heads/', '')
@username = params.fetch(:user_name)
end
def pretext
format(message)
end
def attachments
return [] if new_branch? || removed_branch?
commit_message_attachments
end
private
def message
if new_branch?
new_branch_message
elsif removed_branch?
removed_branch_message
else
push_message
end
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def new_branch_message
"#{username} pushed new branch #{branch_link} to #{project_link}"
end
def removed_branch_message
"#{username} removed branch #{ref} from #{project_link}"
end
def push_message
"#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})"
end
def commit_messages
commits.each_with_object('') do |commit, str|
str << compose_commit_message(commit)
end.chomp
end
def commit_message_attachments
[{ text: format(commit_messages), color: attachment_color }]
end
def compose_commit_message(commit)
author = commit.fetch(:author).fetch(:name)
id = commit.fetch(:id)[0..8]
message = commit.fetch(:message)
url = commit.fetch(:url)
"[#{id}](#{url}): #{message} - #{author}\n"
end
def new_branch?
before.include?('000000')
end
def removed_branch?
after.include?('000000')
end
def branch_url
"#{project_url}/commits/#{ref}"
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
def branch_link
"[#{ref}](#{branch_url})"
end
def project_link
"[#{project_name}](#{project_url})"
end
def compare_link
"[Compare changes](#{compare_url})"
end
def attachment_color
'#345'
end
end
require 'slack-notifier'
module SlackMessages
class SlackBaseMessage
def initialize(params)
raise NotImplementedError
end
def pretext
format(message)
end
def attachments
raise NotImplementedError
end
private
def message
raise NotImplementedError
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def attachment_color
'#345'
end
end
end
module SlackMessages
class SlackIssueMessage < SlackBaseMessage
attr_reader :username
attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :issue_iid
attr_reader :issue_url
attr_reader :action
attr_reader :state
attr_reader :description
def initialize(params)
@username = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@issue_iid = obj_attr[:iid]
@issue_url = obj_attr[:url]
@action = obj_attr[:action]
@state = obj_attr[:state]
@description = obj_attr[:description]
end
def attachments
return [] unless opened_issue?
description_message
end
private
def message
"#{username} #{state} issue #{issue_link} in #{project_link}: #{title}"
end
def opened_issue?
action == "open"
end
def description_message
[{ text: format(description), color: attachment_color }]
end
def project_link
"[#{project_name}](#{project_url})"
end
def issue_link
"[##{issue_iid}](#{issue_url})"
end
end
end
module SlackMessages
class SlackMergeMessage < SlackBaseMessage
attr_reader :username
attr_reader :project_name
attr_reader :project_url
attr_reader :merge_request_id
attr_reader :source_branch
attr_reader :target_branch
attr_reader :state
def initialize(params)
@username = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@merge_request_id = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
@state = obj_attr[:state]
end
def pretext
format(message)
end
def attachments
[]
end
private
def message
merge_request_message
end
def project_link
"[#{project_name}](#{project_url})"
end
def merge_request_message
"#{username} #{state} merge request #{merge_request_link} in #{project_link}"
end
def merge_request_link
"[##{merge_request_id}](#{merge_request_url})"
end
def merge_request_url
"#{project_url}/merge_requests/#{merge_request_id}"
end
end
end
require 'slack-notifier'
module SlackMessages
class SlackPushMessage < SlackBaseMessage
attr_reader :after
attr_reader :before
attr_reader :commits
attr_reader :project_name
attr_reader :project_url
attr_reader :ref
attr_reader :username
def initialize(params)
@after = params[:after]
@before = params[:before]
@commits = params.fetch(:commits, [])
@project_name = params[:project_name]
@project_url = params[:project_url]
@ref = params[:ref].gsub('refs/heads/', '')
@username = params[:user_name]
end
def pretext
format(message)
end
def attachments
return [] if new_branch? || removed_branch?
commit_message_attachments
end
private
def message
if new_branch?
new_branch_message
elsif removed_branch?
removed_branch_message
else
push_message
end
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def new_branch_message
"#{username} pushed new branch #{branch_link} to #{project_link}"
end
def removed_branch_message
"#{username} removed branch #{ref} from #{project_link}"
end
def push_message
"#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})"
end
def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n")
end
def commit_message_attachments
[{ text: format(commit_messages), color: attachment_color }]
end
def compose_commit_message(commit)
author = commit[:author][:name]
id = Commit.truncate_sha(commit[:id])
message = commit[:message]
url = commit[:url]
"[#{id}](#{url}): #{message} - #{author}"
end
def new_branch?
before.include?('000000')
end
def removed_branch?
after.include?('000000')
end
def branch_url
"#{project_url}/commits/#{ref}"
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
def branch_link
"[#{ref}](#{branch_url})"
end
def project_link
"[#{project_name}](#{project_url})"
end
def compare_link
"[Compare changes](#{compare_url})"
end
def attachment_color
'#345'
end
end
end
......@@ -11,7 +11,14 @@
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
#
require "slack_messages/slack_issue_message"
require "slack_messages/slack_push_message"
require "slack_messages/slack_merge_message"
class SlackService < Service
prop_accessor :webhook, :username, :channel
......@@ -38,20 +45,37 @@ def fields
]
end
def execute(push_data)
def execute(data)
return unless webhook.present?
message = SlackMessage.new(push_data.merge(
object_kind = data[:object_kind]
data = data.merge(
project_url: project_url,
project_name: project_name
))
)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
message = case object_kind
when "push"
message = SlackMessages::SlackPushMessage.new(data)
when "issue"
message = SlackMessages::SlackIssueMessage.new(data) unless is_update?(data)
when "merge_request"
message = SlackMessages::SlackMergeMessage.new(data) unless is_update?(data)
end
opt = {}
opt[:channel] = channel if channel
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments)
if message
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments)
end
end
private
......@@ -63,4 +87,8 @@ def project_name
def project_url
project.web_url
end
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
end
......@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
#
class TeamcityService < CiService
......@@ -115,13 +119,16 @@ def commit_status(sha)
end
end
def execute(push)
def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
auth = {
username: username,
password: password,
}
branch = push[:ref].gsub('refs/heads/', '')
branch = data[:ref].gsub('refs/heads/', '')
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
body: "<build branchName=\"#{branch}\">"\
......
......@@ -11,6 +11,11 @@
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
#
# To add new service you should build a class inherited from Service
# and implement a set of methods
......@@ -19,6 +24,10 @@ class Service < ActiveRecord::Base
serialize :properties, JSON
default_value_for :active, false
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
after_initialize :initialize_properties
......@@ -29,6 +38,11 @@ class Service < ActiveRecord::Base
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
def activated?
active
end
......
......@@ -54,7 +54,7 @@ def execute(project, user, oldrev, newrev, ref)
@push_data = post_receive_data(oldrev, newrev, ref)
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup)
project.execute_services(@push_data.dup, :push_hooks)
end
end
......
module Issues
class BaseService < ::IssuableBaseService
private
def execute_hooks(issue, action = 'open')
def hook_data(issue, action)
issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
issue_data[:object_attributes].merge!(url: issue_url, action: action)
issue_data
end
private
def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action)
issue.project.execute_hooks(issue_data, :issue_hooks)
issue.project.execute_services(issue_data, :issue_hooks)
end
end
end
......@@ -5,13 +5,19 @@ def create_note(merge_request)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
def hook_data(merge_request, action)
hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
hook_data[:object_attributes][:url] = merge_request_url
hook_data[:object_attributes][:action] = action
hook_data
end
def execute_hooks(merge_request, action = 'open')
if merge_request.project
hook_data = merge_request.to_hook_data(current_user)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id)
hook_data[:object_attributes][:url] = merge_request_url
hook_data[:object_attributes][:action] = action
merge_request.project.execute_hooks(hook_data, :merge_request_hooks)
merge_data = hook_data(merge_request, action)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
end
end
......
......@@ -27,6 +27,38 @@
.col-sm-10
= f.check_box :active
.form-group
= f.label :url, "Trigger", class: 'control-label'
.col-sm-10
%div
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
%div
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This url will be triggered when an issue is created
%div
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created
- @service.fields.each do |field|
- name = field[:name]
- value = @service.send(name) unless field[:type] == 'password'
......
class AddEventsToServices < ActiveRecord::Migration
def change
add_column :services, :push_events, :boolean, :default => true