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

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 2f4656b5
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 7.9.0 (unreleased)
- Added issue and merge request events to Slack service (Stan Hu)
- Fix broken access control for note attachments (Hannes Rosenögger)
v 7.9.0 (unreleased) v 7.9.0 (unreleased)
- Move labels/milestones tabs to sidebar - Move labels/milestones tabs to sidebar
......
...@@ -51,7 +51,8 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -51,7 +51,8 @@ class Projects::ServicesController < Projects::ApplicationController
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :build_type, :build_key, :server, :teamcity_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels :colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events
) )
end end
end end
...@@ -479,8 +479,9 @@ class Project < ActiveRecord::Base ...@@ -479,8 +479,9 @@ class Project < ActiveRecord::Base
end end
end end
def execute_services(data) def execute_services(data, hooks_scope = :push_hooks)
services.select(&:active).each do |service| # Call only service hooks that are active for this scope
services.send(hooks_scope).each do |service|
service.async_execute(data) service.async_execute(data)
end end
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 'asana' require 'asana'
...@@ -62,6 +66,9 @@ automatically inspected. Leave blank to include all branches.' ...@@ -62,6 +66,9 @@ automatically inspected. Leave blank to include all branches.'
end end
def execute(push) def execute(push)
object_kind = push[:object_kind]
return unless object_kind == "push"
Asana.configure do |client| Asana.configure do |client|
client.api_key = api_key client.api_key = api_key
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 AssemblaService < Service class AssemblaService < Service
...@@ -38,8 +42,11 @@ class AssemblaService < Service ...@@ -38,8 +42,11 @@ class AssemblaService < Service
] ]
end end
def execute(push) def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' }) AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
end end
end end
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 BambooService < CiService class BambooService < CiService
...@@ -118,7 +122,10 @@ class BambooService < CiService ...@@ -118,7 +122,10 @@ class BambooService < CiService
end end
end end
def execute(_data) def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
# Bamboo requires a GET and does not take any data. # Bamboo requires a GET and does not take any data.
self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}",
verify: false) verify: false)
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 "addressable/uri" require "addressable/uri"
...@@ -33,6 +37,9 @@ class BuildboxService < CiService ...@@ -33,6 +37,9 @@ class BuildboxService < CiService
end end
def execute(data) def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
service_hook.execute(data) service_hook.execute(data)
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 CampfireService < Service class CampfireService < Service
...@@ -38,6 +42,9 @@ class CampfireService < Service ...@@ -38,6 +42,9 @@ class CampfireService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data[:object_kind]
return unless object_kind == "push"
room = gate.find_room_by_name(self.room) room = gate.find_room_by_name(self.room)
return true unless room return true unless room
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
# Base class for CI services # Base class for CI services
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
class EmailsOnPushService < Service class EmailsOnPushService < Service
...@@ -30,6 +34,9 @@ class EmailsOnPushService < Service ...@@ -30,6 +34,9 @@ class EmailsOnPushService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data[:object_kind]
return unless object_kind == "push"
EmailsOnPushWorker.perform_async(project_id, recipients, push_data) EmailsOnPushWorker.perform_async(project_id, recipients, push_data)
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require "flowdock-git-hook" require "flowdock-git-hook"
...@@ -38,6 +42,9 @@ class FlowdockService < Service ...@@ -38,6 +42,9 @@ class FlowdockService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data[:object_kind]
return unless object_kind == "push"
Flowdock::Git.post( Flowdock::Git.post(
push_data[:ref], push_data[:ref],
push_data[:before], push_data[:before],
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require "gemnasium/gitlab_service" require "gemnasium/gitlab_service"
...@@ -39,6 +43,9 @@ class GemnasiumService < Service ...@@ -39,6 +43,9 @@ class GemnasiumService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data[:object_kind]
return unless object_kind == "push"
Gemnasium::GitlabService.execute( Gemnasium::GitlabService.execute(
ref: push_data[:ref], ref: push_data[:ref],
before: push_data[:before], before: push_data[:before],
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
class GitlabCiService < CiService class GitlabCiService < CiService
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 GitlabIssueTrackerService < IssueTrackerService class GitlabIssueTrackerService < IssueTrackerService
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
class HipchatService < Service class HipchatService < Service
...@@ -41,6 +45,9 @@ class HipchatService < Service ...@@ -41,6 +45,9 @@ class HipchatService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data.fetch(:object_kind)
return unless object_kind == "push"
gate[room].send('GitLab', create_message(push_data)) gate[room].send('GitLab', create_message(push_data))
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 IssueTrackerService < Service class IssueTrackerService < Service
...@@ -66,6 +70,9 @@ class IssueTrackerService < Service ...@@ -66,6 +70,9 @@ class IssueTrackerService < Service
end end
def execute(data) def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again."
result = false result = false
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 JiraService < IssueTrackerService class JiraService < IssueTrackerService
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 PivotaltrackerService < Service class PivotaltrackerService < Service
...@@ -38,6 +42,9 @@ class PivotaltrackerService < Service ...@@ -38,6 +42,9 @@ class PivotaltrackerService < Service
end end
def execute(push) def execute(push)
object_kind = push[:object_kind]
return unless object_kind == "push"
url = 'https://www.pivotaltracker.com/services/v5/source_commits' url = 'https://www.pivotaltracker.com/services/v5/source_commits'
push[:commits].each do |commit| push[:commits].each do |commit|
message = { message = {
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 PushoverService < Service class PushoverService < Service
...@@ -77,6 +81,9 @@ class PushoverService < Service ...@@ -77,6 +81,9 @@ class PushoverService < Service
end end
def execute(push_data) def execute(push_data)
object_kind = push_data[:object_kind]
return unless object_kind == "push"
ref = push_data[:ref].gsub('refs/heads/', '') ref = push_data[:ref].gsub('refs/heads/', '')
before = push_data[:before] before = push_data[:before]
after = push_data[:after] after = push_data[:after]
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 RedmineService < IssueTrackerService class RedmineService < IssueTrackerService
......
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 @@ ...@@ -11,7 +11,14 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 class SlackService < Service
prop_accessor :webhook, :username, :channel prop_accessor :webhook, :username, :channel
...@@ -38,20 +45,37 @@ class SlackService < Service ...@@ -38,20 +45,37 @@ class SlackService < Service
] ]
end end
def execute(push_data) def execute(data)
return unless webhook.present? return unless webhook.present?
message = SlackMessage.new(push_data.merge( object_kind = data[:object_kind]
data = data.merge(
project_url: project_url, project_url: project_url,
project_name: project_name 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 = {}
opt[:channel] = channel if channel opt[:channel] = channel if channel
opt[:username] = username if username opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt) if message
notifier.ping(message.pretext, attachments: message.attachments) notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments)
end
end end
private private
...@@ -63,4 +87,8 @@ class SlackService < Service ...@@ -63,4 +87,8 @@ class SlackService < Service
def project_url def project_url
project.web_url project.web_url
end end
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
end end
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 class TeamcityService < CiService
...@@ -115,13 +119,16 @@ class TeamcityService < CiService ...@@ -115,13 +119,16 @@ class TeamcityService < CiService
end end
end end
def execute(push) def execute(data)
object_kind = data[:object_kind]
return unless object_kind == "push"
auth = { auth = {
username: username, username: username,
password: password, password: password,
} }
branch = push[:ref].gsub('refs/heads/', '') branch = data[:ref].gsub('refs/heads/', '')
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
body: "<build branchName=\"#{branch}\">"\ body: "<build branchName=\"#{branch}\">"\
......
...@@ -11,6 +11,11 @@ ...@@ -11,6 +11,11 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # 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 # To add new service you should build a class inherited from Service
# and implement a set of methods # and implement a set of methods
...@@ -19,6 +24,10 @@ class Service < ActiveRecord::Base ...@@ -19,6 +24,10 @@ class Service < ActiveRecord::Base
serialize :properties, JSON serialize :properties, JSON
default_value_for :active, false 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 after_initialize :initialize_properties
...@@ -29,6 +38,11 @@ class Service < ActiveRecord::Base ...@@ -29,6 +38,11 @@ class Service < ActiveRecord::Base
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } 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? def activated?
active active
end end
......
...@@ -54,7 +54,7 @@ class GitPushService ...@@ -54,7 +54,7 @@ class GitPushService
@push_data = post_receive_data(oldrev, newrev, ref) @push_data = post_receive_data(oldrev, newrev, ref)
EventCreateService.new.push(project, user, @push_data) EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks) project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup) project.execute_services(@push_data.dup, :push_hooks)
end end
end end
......
module Issues module Issues
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
private def hook_data(issue, action)
def execute_hooks(issue, action = 'open')
issue_data = issue.to_hook_data(current_user) issue_data = issue.to_hook_data(current_user)
issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id)
issue_data[:object_attributes].merge!(url: issue_url, action: action) 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_hooks(issue_data, :issue_hooks)
issue.project.execute_services(issue_data, :issue_hooks)
end end
end end
end end
...@@ -5,13 +5,19 @@ module MergeRequests ...@@ -5,13 +5,19 @@ module MergeRequests
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end 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') def execute_hooks(merge_request, action = 'open')
if merge_request.project if merge_request.project
hook_data = merge_request.to_hook_data(current_user) merge_data = hook_data(merge_request, action)
merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
hook_data[:object_attributes][:url] = merge_request_url merge_request.project.execute_services(merge_data, :merge_request_hooks)
hook_data[:object_attributes][:action] = action
merge_request.project.execute_hooks(hook_data, :merge_request_hooks)
end end
end end
end end
......
...@@ -27,6 +27,38 @@ ...@@ -27,6 +27,38 @@
.col-sm-10 .col-sm-10
= f.check_box :active = 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| - @service.fields.each do |field|
- name = field[:name] - name = field[:name]
- value = @service.send(name) unless field[:type] == 'password' - value = @service.send(name) unless field[:type] == 'password'
......
class AddEventsToServices < ActiveRecord::Migration
def change
add_column :services, :push_events, :boolean, :default => true
add_column :services, :issues_events, :boolean, :default => true
add_column :services, :merge_requests_events, :boolean, :default => true
add_column :services, :tag_push_events, :boolean, :default => true
end
end
...@@ -364,9 +364,13 @@ ActiveRecord::Schema.define(version: 20150223022001) do ...@@ -364,9 +364,13 @@ ActiveRecord::Schema.define(version: 20150223022001) do
t.integer "project_id" t.integer "project_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "active", default: false, null: false t.boolean "active", default: false, null: false
t.text "properties" t.text "properties"
t.boolean "template", default: false t.boolean "template", default: false
t.boolean "push_events", default: true
t.boolean "issues_events", default: true
t.boolean "merge_requests_events", default: true
t.boolean "tag_push_events", default: true
end end
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
......
...@@ -29,6 +29,7 @@ module Gitlab ...@@ -29,6 +29,7 @@ module Gitlab
# Hash to be passed as post_receive_data # Hash to be passed as post_receive_data
data = { data = {
object_kind: "push",
before: oldrev, before: oldrev,
after: newrev, after: newrev,
ref: ref, ref: ref,
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
require 'spec_helper'
describe SlackMessages::SlackIssueMessage do
subject { SlackMessages::SlackIssueMessage.new(args) }
let(:args) {
{
user: {
username: 'username'
},
project_name: 'project_name',
project_url: 'somewhere.com',
object_attributes: {
title: 'Issue title',
id: 10,
iid: 100,
assignee_id: 1,
url: 'url',
action: 'open',
state: 'opened',
description: 'issue description'
}
}
}
let(:color) { '#345' }
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
'username opened issue <url|#100> in <somewhere.com|project_name>: '\
'Issue title')
expect(subject.attachments).to eq([
{
text: "issue description",
color: color,
}
])
end
end
context 'close' do
before do
args[:object_attributes][:action] = 'close'
args[:object_attributes][:state] = 'closed'
end
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
'username closed issue <url|#100> in <somewhere.com|project_name>: '\
'Issue title')
expect(subject.attachments).to be_empty
end
end
end
require 'spec_helper'
describe SlackMessages::SlackMergeMessage do
subject { SlackMessages::SlackMergeMessage.new(args) }
let(:args) {
{
user: {
username: 'username'
},
project_name: 'project_name',
project_url: 'somewhere.com',
object_attributes: {
title: 'Issue title',
id: 10,
iid: 100,
assignee_id: 1,
url: 'url',
state: 'opened',
description: 'issue description',
source_branch: 'source_branch',
target_branch: 'target_branch',
}
}
}
let(:color) { '#345' }
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
'username opened merge request <somewhere.com/merge_requests/100|#100> '\
'in <somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
end
context 'close' do
before do
args[:object_attributes][:state] = 'closed'
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
'username closed merge request <somewhere.com/merge_requests/100|#100> '\
'in <somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
end
end
require 'spec_helper' require 'spec_helper'
describe SlackMessage do describe SlackMessages::SlackPushMessage do
subject { SlackMessage.new(args) } subject { SlackMessages::SlackPushMessage.new(args) }
let(:args) { let(:args) {
{ {
......
...@@ -2,14 +2,18 @@ ...@@ -2,14 +2,18 @@
# #
# Table name: services # Table name: services
# #
# id :integer not null, primary key # id :integer not null, primary key
# type :string(255) # type :string(255)
# title :string(255) # title :string(255)
# project_id :integer not null # project_id :integer not null
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
...@@ -34,7 +38,7 @@ describe SlackService do ...@@ -34,7 +38,7 @@ describe SlackService do
let(:slack) { SlackService.new } let(:slack) { SlackService.new }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' } let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' } let(:channel) { 'slack_channel' }
...@@ -48,10 +52,43 @@ describe SlackService do ...@@ -48,10 +52,43 @@ describe SlackService do
) )
WebMock.stub_request(:post, webhook_url) WebMock.stub_request(:post, webhook_url)
opts = {
title: 'Awesome issue',
description: 'please fix'
}
issue_service = Issues::CreateService.new(project, user, opts)
@issue = issue_service.execute
@issues_sample_data = issue_service.hook_data(@issue, 'open')
opts = {
title: 'Awesome merge_request',
description: 'please fix',
source_branch: 'stable',
target_branch: 'master'
}
merge_service = MergeRequests::CreateService.new(project,
user, opts)
@merge_request = merge_service.execute
@merge_sample_data = merge_service.hook_data(@merge_request,
'open')
end end
it "should call Slack API" do it "should call Slack API for pull requests" do
slack.execute(sample_data) slack.execute(push_sample_data)
WebMock.should have_requested(:post, webhook_url).once
end
it "should call Slack API for issue events" do
slack.execute(@issues_sample_data)
WebMock.should have_requested(:post, webhook_url).once
end
it "should call Slack API for merge requests events" do
slack.execute(@merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
......
...@@ -11,6 +11,10 @@ ...@@ -11,6 +11,10 @@
# active :boolean default(FALSE), not null # active :boolean default(FALSE), not null
# properties :text # properties :text
# template :boolean default(FALSE) # template :boolean default(FALSE)
# push_events :boolean
# issues_events :boolean
# merge_requests_events :boolean
# tag_push_events :boolean
# #
require 'spec_helper' require 'spec_helper'
......
...@@ -49,6 +49,7 @@ describe GitPushService do ...@@ -49,6 +49,7 @@ describe GitPushService do
subject { @push_data } subject { @push_data }
it { is_expected.to include(object_kind: 'push') }
it { is_expected.to include(before: @oldrev) } it { is_expected.to include(before: @oldrev) }
it { is_expected.to include(after: @newrev) } it { is_expected.to include(after: @newrev) }
it { is_expected.to include(ref: @ref) } it { is_expected.to include(ref: @ref) }
......
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