Commit 2988e1fb authored by Kamil Trzcinski's avatar Kamil Trzcinski

Migrate CI::Services and CI::WebHooks to Services and WebHooks

parent 4e5897f5
......@@ -16,6 +16,7 @@ v 8.3.0 (unreleased)
- Fire update hook from GitLab
- Style warning about mentioning many people in a comment
- Fix: sort milestones by due date once again (Greg Smethells)
- Migrate all CI::Services and CI::WebHooks to Services and WebHooks
- Don't show project fork event as "imported"
- Add API endpoint to fetch merge request commits list
- Expose events API with comment information and author info
......
class Projects::CiServicesController < Projects::ApplicationController
before_action :ci_project
before_action :authorize_admin_project!
layout "project_settings"
def index
@ci_project.build_missing_services
@services = @ci_project.services.reload
end
def edit
service
end
def update
if service.update_attributes(service_params)
redirect_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param)
else
render 'edit'
end
end
def test
last_build = @project.ci_builds.last
if service.execute(last_build)
message = { notice: 'We successfully tested the service' }
else
message = { alert: 'We tried to test the service but error occurred' }
end
redirect_back_or_default(options: message)
end
private
def service
@service ||= @ci_project.services.find { |service| service.to_param == params[:id] }
end
def service_params
params.require(:service).permit(
:type, :active, :webhook, :notify_only_broken_builds,
:email_recipients, :email_only_broken_builds, :email_add_pusher,
:hipchat_token, :hipchat_room, :hipchat_server
)
end
end
class Projects::CiWebHooksController < Projects::ApplicationController
before_action :ci_project
before_action :authorize_admin_project!
layout "project_settings"
def index
@web_hooks = @ci_project.web_hooks
@web_hook = Ci::WebHook.new
end
def create
@web_hook = @ci_project.web_hooks.new(web_hook_params)
@web_hook.save
if @web_hook.valid?
redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
else
@web_hooks = @ci_project.web_hooks.select(&:persisted?)
render :index
end
end
def test
Ci::TestHookService.new.execute(hook, current_user)
redirect_back_or_default(default: { action: 'index' })
end
def destroy
hook.destroy
redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
end
private
def hook
@web_hook ||= @ci_project.web_hooks.find(params[:id])
end
def web_hook_params
params.require(:web_hook).permit(:url)
end
end
......@@ -53,6 +53,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(:url, :push_events, :issues_events,
:merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification)
:merge_requests_events, :tag_push_events, :note_events,
:build_events, :enable_ssl_verification)
end
end
......@@ -6,7 +6,9 @@ class Projects::ServicesController < Projects::ApplicationController
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url,
:note_events, :build_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification]
......
module Ci
module Emails
module Builds
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
end
end
end
module Ci
class Notify < ActionMailer::Base
include Ci::Emails::Builds
add_template_helper Ci::GitlabHelper
default_url_options[:host] = Gitlab.config.gitlab.host
default_url_options[:protocol] = Gitlab.config.gitlab.protocol
default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
default from: Gitlab.config.gitlab.email_from
# Just send email with 3 seconds delay
def self.delay
delay_for(2.seconds)
end
private
# Formats arguments into a String suitable for use as an email subject
#
# extra - Extra Strings to be inserted into the subject
#
# Examples
#
# >> subject('Lorem ipsum')
# => "GitLab-CI | Lorem ipsum"
#
# # Automatically inserts Project name when @project is set
# >> @project = Project.last
# => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
# >> subject('Lorem ipsum')
# => "GitLab-CI | Ruby on Rails | Lorem ipsum "
#
# # Accepts multiple arguments
# >> subject('Lorem ipsum', 'Dolor sit amet')
# => "GitLab-CI | Lorem ipsum | Dolor sit amet"
def subject(*extra)
subject = "GitLab-CI"
subject << (@project ? " | #{@project.name}" : "")
subject << " | " + extra.join(' | ') if extra.present?
subject
end
end
end
module Emails
module Builds
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
end
end
......@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects
include Emails::Profile
include Emails::Groups
include Emails::Builds
add_template_helper MergeRequestsHelper
add_template_helper EmailsHelper
......
......@@ -96,21 +96,21 @@ module Ci
end
state_machine :status, initial: :pending do
after_transition pending: :running do |build, transition|
build.execute_hooks
end
after_transition any => [:success, :failed, :canceled] do |build, transition|
return unless build.gl_project
project = build.project
if project.web_hooks?
Ci::WebHookService.new.build_end(build)
end
build.commit.create_next_builds(build)
project.execute_services(build)
if project.coverage_enabled?
build.update_coverage
end
build.commit.create_next_builds(build)
build.execute_hooks
end
end
......@@ -275,6 +275,12 @@ module Ci
end
end
def execute_hooks
build_data = Gitlab::BuildDataBuilder.build(self)
gl_project.execute_hooks(build_data.dup, :build_hooks)
gl_project.execute_services(build_data.dup, :build_hooks)
end
private
def yaml_variables
......
......@@ -178,6 +178,10 @@ module Ci
duration_array.reduce(:+).to_i
end
def started_at
@started_at ||= statuses.order('started_at ASC').first.try(:started_at)
end
def finished_at
@finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
end
......
......@@ -35,17 +35,10 @@ module Ci
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
has_many :events, dependent: :destroy, class_name: 'Ci::Event'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
# Project services
has_many :services, dependent: :destroy, class_name: 'Ci::Service'
has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService'
has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService'
has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService'
accepts_nested_attributes_for :variables, allow_destroy: true
delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project
......@@ -122,14 +115,6 @@ module Ci
email_add_pusher || email_recipients.present?
end
def web_hooks?
web_hooks.any?
end
def services?
services.any?
end
def timeout_in_minutes
timeout / 60
end
......@@ -151,32 +136,6 @@ module Ci
end
end
def available_services_names
%w(slack mail hip_chat)
end
def build_missing_services
available_services_names.each do |service_name|
service = services.find { |service| service.to_param == service_name }
# If service is available but missing in db
# we should create an instance. Ex `create_gitlab_ci_service`
self.send :"create_#{service_name}_service" if service.nil?
end
end
def execute_services(data)
services.each do |service|
# Call service hook only if it is active
begin
service.execute(data) if service.active && service.can_execute?(data)
rescue => e
logger.error(e)
end
end
end
def setup_finished?
commits.any?
end
......
# == Schema Information
#
# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
# To add new service you should build a class inherited from Service
# and implement a set of methods
module Ci
class Service < ActiveRecord::Base
extend Ci::Model
serialize :properties, JSON
default_value_for :active, false
after_initialize :initialize_properties
belongs_to :project, class_name: 'Ci::Project'
validates :project_id, presence: true
def activated?
active
end
def category
:common
end
def initialize_properties
self.properties = {} if properties.nil?
end
def title
# implement inside child
end
def description
# implement inside child
end
def help
# implement inside child
end
def to_param
# implement inside child
end
def fields
# implement inside child
[]
end
def can_test?
project.builds.any?
end
def can_execute?(build)
true
end
def execute(build)
# implement inside child
end
# Provide convenient accessor methods
# for each serialized property.
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
def #{arg}
(properties || {})['#{arg}']
end
def #{arg}=(value)
self.properties ||= {}
self.properties['#{arg}'] = value
end
}
end
end
def self.boolean_accessor(*args)
self.prop_accessor(*args)
args.each do |arg|
class_eval %{
def #{arg}?
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
end
}
end
end
end
end
# == Schema Information
#
# Table name: ci_web_hooks
#
# id :integer not null, primary key
# url :string(255) not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
module Ci
class WebHook < ActiveRecord::Base
extend Ci::Model
include HTTParty
belongs_to :project, class_name: 'Ci::Project'
# HTTParty timeout
default_timeout 10
validates :url, presence: true, url: true
def execute(data)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
Ci::WebHook.post(post_url,
body: data.to_json,
headers: { "Content-Type" => "application/json" },
verify: false,
basic_auth: auth)
end
end
end
end
......@@ -25,4 +25,5 @@ class ProjectHook < WebHook
scope :issue_hooks, -> { where(issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
end
......@@ -26,6 +26,7 @@ class WebHook < ActiveRecord::Base
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
default_value_for :build_events, false
default_value_for :enable_ssl_verification, true
# HTTParty timeout
......
......@@ -81,6 +81,7 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# 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)
# note_events :boolean default(TRUE), not null
#
class BuildsEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_builds
validates :recipients, presence: true, if: :activated?
def title
'Builds emails'
end
def description
'Email the builds status to a list of recipients.'
end
def to_param
'builds_email'
end
def supported_events
%w(build)
end
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
if should_build_be_notified?(push_data)
BuildEmailWorker.perform_async(
push_data[:build_id],
all_recipients(push_data),
push_data,
)
end
end
def fields
[
{ type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' },
{ type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def should_build_be_notified?(data)
case data[:build_status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
def all_recipients(data)
if add_pusher? && data[:user][:email]
recipients + " #{data[:user][:email]}"
else
recipients
end
end
end
module Ci
class HipChatMessage
include Gitlab::Application.routes.url_helpers
attr_reader :build
def initialize(build)
@build = build
end
def to_s
lines = Array.new
lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ")
lines.push("<a href=\"#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}\">Commit ##{commit.id}</a></br>")
lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).")
lines.join('')
end
def status_color(build_or_commit=nil)
build_or_commit ||= commit_status
case build_or_commit
when :success
'green'
when :failed, :canceled
'red'
else # :pending, :running or unknown
'yellow'
end
end
def notify?
[:failed, :canceled].include?(commit_status)
end
private
def commit
build.commit
end
def project
commit.project
end
def build_status
build.status.to_sym
end
def commit_status
commit.status.to_sym
end
def humanized_status(build_or_commit=nil)
build_or_commit ||= commit_status
case build_or_commit
when :pending
"Pending"
when :running
"Running"
when :failed
"Failed"
when :success
"Successful"
when :canceled
"Canceled"
else
"Unknown"
end
end
end
end
# == Schema Information
#
# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
module Ci
class HipChatService < Ci::Service
prop_accessor :hipchat_token, :hipchat_room, :hipchat_server
boolean_accessor :notify_only_broken_builds
validates :hipchat_token, presence: true, if: :activated?
validates :hipchat_room, presence: true, if: :activated?
default_value_for :notify_only_broken_builds, true
def title
"HipChat"
end
def description
"Private group chat, video chat, instant messaging for teams"
end
def help
end
def to_param
'hip_chat'
end
def fields
[
{ type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' },
{ type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' },
{ type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' },
{ type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
]
end
def can_execute?(build)
return if build.allow_failure?
commit = build.commit
return unless commit
return unless commit.latest_builds.include? build
case commit.status.to_sym
when :failed
true
when :success
true unless notify_only_broken_builds?
else
false
end
end
def execute(build)
msg = Ci::HipChatMessage.new(build)
opts = default_options.merge(
token: hipchat_token,
room: hipchat_room,
server: server_url,
color: msg.status_color,
notify: msg.notify?
)
Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts)
end
private
def default_options
{
service_name: 'GitLab CI',
message_format: 'html'
}
end
def server_url
if hipchat_server.blank?
'https://api.hipchat.com'
else
hipchat_server
end
end
end
end
# == Schema Information
#
# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
module Ci
class MailService < Ci::Service
delegate :email_recipients, :email_recipients=,
:email_add_pusher, :email_add_pusher=,
:email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false
before_save :update_project
default_value_for :active, true
def title
'Mail'
end
def description
'Email notification'
end
def to_param
'mail'
end
def fields
[
{ type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' },
{ type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' },
{ type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' }
]
end
def can_execute?(build)
return if build.allow_failure?
# it doesn't make sense to send emails for retried builds
commit = build.commit
return unless commit
return unless commit.latest_builds.include?(build)
case build.status.to_sym
when :failed
true
when :success
true unless email_only_broken_builds
else
false
end
end
def execute(build)
build.project_recipients.each do |recipient|
case build.status.to_sym
when :success
mailer.build_success_email(build.id, recipient).deliver_later
when :failed
mailer.build_fail_email(build.id, recipient).deliver_later
end
end
end
private
def update_project
project.save!
end
def mailer
Ci::Notify
end
end
end
require 'slack-notifier'
module Ci
class SlackMessage
include Gitlab::Application.routes.url_helpers
def initialize(commit)
@commit = commit
end
def pretext
''
end
def color
attachment_color
end
def fallback
format(attachment_message)
end
def attachments
fields = []
commit.latest_builds.each do |build|
next if build.allow_failure?
next unless build.failed?
fields << {
title: build.name,
value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
}
end
[{
text: attachment_message,
color: attachment_color,
fields: fields
}]
end
private
attr_reader :commit
def attachment_message
out = "<#{ci_project_url(project)}|#{project_name}>: "
out << "Commit <#{builds_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> "
out << "(<#{commit_sha_link}|#{commit.short_sha}>) "
out << "of <#{commit_ref_link}|#{commit.ref}> "
out << "by #{commit.git_author_name} " if commit.git_author_name
out << "#{commit_status} in "
out << "#{commit.duration} second(s)"
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def project
commit.project
end
def project_name
project.name
end
def commit_sha_link
"#{project.gitlab_url}/commit/#{commit.sha}"
end
def commit_ref_link
"#{project.gitlab_url}/commits/#{commit.ref}"
end
def attachment_color
if commit.success?
'good'
else
'danger'
end
end
def commit_status
if commit.success?
'succeeded'
else
'failed'
end
end
end
end
# == Schema Information
#
# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
module Ci
class SlackService < Ci::Service
prop_accessor :webhook
boolean_accessor :notify_only_broken_builds
validates :webhook, presence: true, if: :activated?
default_value_for :notify_only_broken_builds, true
def title
'Slack'
end
def description
'A team communication tool for the 21st century'
end
def to_param
'slack'
end
def help
'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present?
end
def fields
[
{ type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' },
{ type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
]
end
def can_execute?(build)
return if build.allow_failure?
commit = build.commit
return unless commit
return unless commit.latest_builds.include?(build)
case commit.status.to_sym
when :failed
true
when :success
true unless notify_only_broken_builds?
else
false
end
end
def execute(build)
message = Ci::SlackMessage.new(build.commit)
options = default_options.merge(
color: message.color,
fallback: message.fallback,
attachments: message.attachments
)
Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options)
end
private
def default_options
{
username: 'GitLab CI'
}
end
end
end
......@@ -22,6 +22,7 @@ class HipchatService < Service
MAX_COMMITS = 3
prop_accessor :token, :room, :server, :notify, :color, :api_version
boolean_accessor :notify_only_broken_builds
validates :token, presence: true, if: :activated?
def title
......@@ -45,12 +46,13 @@ class HipchatService < Service
{ type: 'text', name: 'api_version',
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' }
placeholder: 'Leave blank for default. https://hipchat.example.com' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def supported_events
%w(push issue merge_request note tag_push)
%w(push issue merge_request note tag_push build)
end
def execute(data)
......@@ -94,6 +96,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
when "build"
create_build_message(data) if should_build_be_notified?(data)
end
end
......@@ -235,6 +239,20 @@ class HipchatService < Service
message
end
def create_build_message(data)
ref_type = data[:tag] ? 'tag' : 'branch'
ref = data[:ref]
sha = data[:sha]
user_name = data[:commit][:author_name]
status = data[:commit][:status]
duration = data[:commit][:duration]
branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"
commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
......@@ -250,4 +268,24 @@ class HipchatService < Service
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
def humanized_status(status)
case status
when 'success'
'passed'
else
status
end
end
def should_build_be_notified?(data)
case data[:commit][:status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
end
......@@ -20,6 +20,7 @@
class SlackService < Service
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds
validates :webhook, presence: true, if: :activated?
def title
......@@ -45,12 +46,13 @@ class SlackService < Service
{ type: 'text', name: 'webhook',
placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: '#channel' }
{ type: 'text', name: 'channel', placeholder: '#channel' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def supported_events
%w(push issue merge_request note tag_push)
%w(push issue merge_request note tag_push build)
end
def execute(data)
......@@ -78,6 +80,8 @@ class SlackService < Service
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
end
opt = {}
......@@ -86,7 +90,7 @@ class SlackService < Service
if message
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
end
end
......@@ -103,9 +107,21 @@ class SlackService < Service
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
def should_build_be_notified?(data)
case data[:commit][:status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
end
require "slack_service/issue_message"
require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
......@@ -10,6 +10,9 @@ class SlackService
format(message)
end
def fallback
end
def attachments
raise NotImplementedError
end
......
class SlackService
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :project_name
attr_reader :project_url
attr_reader :user_name
attr_reader :duration
def initialize(params, commit = true)
@sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref]
@project_name = params[:project_name]
@project_url = params[:project_url]
@status = params[:commit][:status]
@user_name = params[:commit][:author_name]
@duration = params[:commit][:duration]
end
def pretext
''
end
def fallback
format(message)
end
def attachments
[{ text: format(message), color: attachment_color }]
end
private
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)"
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 commit_url
"#{project_url}/commit/#{sha}/builds"
end
def commit_link
"[#{Commit.truncate_sha(sha)}](#{commit_url})"
end
end
end
......@@ -30,6 +30,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :build_events, true
after_initialize :initialize_properties
......@@ -47,6 +48,7 @@ class Service < ActiveRecord::Base
scope :issue_hooks, -> { where(issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
def activated?
active
......@@ -133,6 +135,21 @@ class Service < ActiveRecord::Base
end
end
# Provide convenient boolean accessor methods
# for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
def self.boolean_accessor(*args)
self.prop_accessor(*args)
args.each do |arg|
class_eval %{
def #{arg}?
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
end
}
end
end
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
......@@ -163,6 +180,7 @@ class Service < ActiveRecord::Base
assembla
bamboo
buildkite
builds_email
campfire
custom_issue_tracker
drone_ci
......
......@@ -31,7 +31,8 @@ module Ci
trigger_request: trigger_request,
user: user)
commit.builds.create!(build_attrs)
build = commit.builds.create!(build_attrs)
build.execute_hooks
end
end
end
......
......@@ -50,18 +50,8 @@
= icon('retweet fw')
%span
Triggers
= nav_link path: 'ci_web_hooks#index' do
= link_to namespace_project_ci_web_hooks_path(@project.namespace, @project), title: 'CI Web Hooks' do
= icon('link fw')
%span
CI Web Hooks
= nav_link path: 'ci_settings#edit' do
= link_to edit_namespace_project_ci_settings_path(@project.namespace, @project), title: 'CI Settings' do
= icon('building fw')
%span
CI Settings
= nav_link controller: 'ci_services' do
= link_to namespace_project_ci_services_path(@project.namespace, @project), title: 'CI Services' do
= icon('share fw')
%span
CI Services
- content_for :header do
%h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
GitLab CI (build failed)
GitLab (build failed)
%h3
Project:
= link_to ci_project_url(@project) do
......
- content_for :header do
%h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
GitLab CI (build successful)
GitLab (build successful)
%h3
Project:
......
%h3.page-title
= @service.title
= boolean_to_icon @service.activated?
%p= @service.description
%hr
= form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
- if @service.errors.any?
.alert.alert-danger
%ul
- @service.errors.full_messages.each do |msg|
%li= msg
- if @service.help.present?
.bs-callout
= @service.help
.form-group
= f.label :active, "Active", class: "control-label"
.col-sm-10
= f.check_box :active
- @service.fields.each do |field|
- name = field[:name]
- label = field[:label] || name
- value = @service.send(name)
- type = field[:type]
- placeholder = field[:placeholder]
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
.form-group
= f.label label, class: "control-label"
.col-sm-10
- if type == 'text'
= f.text_field name, class: "form-control", placeholder: placeholder
- elsif type == 'textarea'
= f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
- elsif type == 'checkbox'
= f.check_box name
- elsif type == 'select'
= f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- if help
.light #{help}
.form-actions
= f.submit 'Save', class: 'btn btn-save'
&nbsp;
- if @service.valid? && @service.activated? && @service.can_test?
= link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn'
- page_title @service.title, "CI Services"
= render 'form'
- page_title "CI Services"
%h3.page-title Project services
%p.light Project services allow you to integrate GitLab CI with other applications
%table.table
%thead
%tr
%th
%th Service
%th Description
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
%td
= boolean_to_icon service.activated?
%td
= link_to edit_namespace_project_ci_service_path(@project.namespace, @project, service.to_param) do
%strong= service.title
%td
= service.description
%td.light
= time_ago_in_words service.updated_at
ago
- page_title "CI Web Hooks"
%h3.page-title
CI Web hooks
%p.light
Web Hooks can be used for binding events when build completed.
%hr.clearfix
= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
-if @web_hook.errors.any?
.alert.alert-danger
- @web_hook.errors.full_messages.each do |msg|
%p= msg
.form-group
= f.label :url, "URL", class: 'control-label'
.col-sm-10
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
.form-actions
= f.submit "Add Web Hook", class: "btn btn-create"
-if @web_hooks.any?
%h4 Activated web hooks (#{@web_hooks.count})
.table-holder
%table.table
- @web_hooks.each do |hook|
%tr
%td
.clearfix
%span.monospace= hook.url
%td
.pull-right
- if @ci_project.commits.any?
= link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
= link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
%h4 Web Hook data example
:erb
<pre>
<code>
{
"build_id": 2,
"build_name":"rspec_linux"
"build_status": "failed",
"build_started_at": "2014-05-05T18:01:02.563Z",
"build_finished_at": "2014-05-05T18:01:07.611Z",
"project_id": 1,
"project_name": "Brightbox \/ Brightbox Cli",
"gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
"ref": "master",
"sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
"before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
"push_data": {
"before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
"after": "a26cf5de9ed9827746d4970872376b10d9325f40",
"ref": "refs\/heads\/master",
"user_id": 1,
"user_name": "Administrator",
"project_id": 5,
"repository": {
"name": "Brightbox Cli",
"url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
"description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
"homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
},
"commits": [
{
"id": "a26cf5de9ed9827746d4970872376b10d9325f40",
"message": "Release v1.2.2",
"timestamp": "2014-04-22T16:46:42+03:00",
"url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
"author": {
"name": "Paul Thornthwaite",
"email": "tokengeek@gmail.com"
}
},
{
"id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
"message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
"timestamp": "2014-04-11T18:17:26+03:00",
"url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
"author": {
"name": "Paul Thornthwaite",
"email": "tokengeek@gmail.com"
}
}
],
"total_commits_count": 2,
"ci_yaml_file":"rspec_linux:\r\n script: ls\r\n"
}
}
</code>
</pre>
......@@ -55,6 +55,13 @@
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created
%div
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Build events
%p.light
This url will be triggered when the build status changes
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
......@@ -78,7 +85,7 @@
.clearfix
%span.monospace= hook.url
%p
- %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger|
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
......@@ -59,6 +59,15 @@
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created
- if @service.supported_events.include?("build")
%div
= form.check_box :build_events, class: 'pull-left'
.prepend-left-20
= form.label :build_events, class: 'list-label' do
%strong Build events
%p.light
This url will be triggered when a build status changes
- @service.fields.each do |field|
- type = field[:type]
......
class BuildEmailWorker
include Sidekiq::Worker
def perform(build_id, recipients, push_data)
recipients.split(' ').each do |recipient|
begin
case push_data['build_status']
when 'success'
Notify.build_success_email(build_id, recipient).deliver_now
when 'failed'
Notify.build_fail_email(build_id, recipient).deliver_now
end
# These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
end
end
end
end
module Ci
class HipChatNotifierWorker
include Sidekiq::Worker
def perform(message, options={})
room = options.delete('room')
token = options.delete('token')
server = options.delete('server')
name = options.delete('service_name')
client_opts = {
api_version: 'v2',
server_url: server
}
client = HipChat::Client.new(token, client_opts)
client[room].send(name, message, options.symbolize_keys)
end
end
end
module Ci
class SlackNotifierWorker
include Sidekiq::Worker
def perform(webhook_url, message, options={})
notifier = Slack::Notifier.new(webhook_url)
notifier.ping(message, options)
end
end
end
module Ci
class WebHookWorker
include Sidekiq::Worker
def perform(hook_id, data)
Ci::WebHook.find(hook_id).execute data
end
end
end
......@@ -596,17 +596,6 @@ Rails.application.routes.draw do
resource :variables, only: [:show, :update]
resources :triggers, only: [:index, :create, :destroy]
resource :ci_settings, only: [:edit, :update, :destroy]
resources :ci_web_hooks, only: [:index, :create, :destroy] do
member do
get :test
end
end
resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
get :test
end
end
resources :builds, only: [:index, :show] do
collection do
......
class AddBuildEventsToServices < ActiveRecord::Migration
def up
add_column :services, :build_events, :boolean, default: false, null: false
add_column :web_hooks, :build_events, :boolean, default: false, null: false
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20151203162133) do
ActiveRecord::Schema.define(version: 20151203162134) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -706,6 +706,7 @@ ActiveRecord::Schema.define(version: 20151203162133) do
t.boolean "merge_requests_events", default: true
t.boolean "tag_push_events", default: true
t.boolean "note_events", default: true, null: false
t.boolean "build_events", default: false, null: false
end
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
......@@ -854,6 +855,7 @@ ActiveRecord::Schema.define(version: 20151203162133) do
t.boolean "tag_push_events", default: false
t.boolean "note_events", default: false, null: false
t.boolean "enable_ssl_verification", default: true
t.boolean "build_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
......
......@@ -55,11 +55,11 @@ Feature: Project Services
And I fill Pushover settings
Then I should see Pushover service settings saved
Scenario: Activate email on push service
Scenario: Activate email service
When I visit project "Shop" services page
And I click email on push service link
And I fill email on push settings
Then I should see email on push service settings saved
And I click email service link
And I fill email settings
Then I should see email service settings saved
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
......
......@@ -32,6 +32,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Comments')
page.check('Issues events')
page.check('Merge Request events')
page.check('Build events')
click_on 'Save'
end
......@@ -39,6 +40,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'Channel', with: '#test_channel'
page.check('Notify only broken builds')
end
step 'I should see service template settings saved' do
......
......@@ -118,16 +118,16 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Restrict to branch').value).to eq 'master'
end
step 'I click email on push service link' do
click_link 'Emails on push'
step 'I click email service link' do
click_link 'Emails'
end
step 'I fill email on push settings' do
step 'I fill email settings' do
fill_in 'Recipients', with: 'qa@company.name'
click_button 'Save'
end
step 'I should see email on push service settings saved' do
step 'I should see email service settings saved' do
expect(find_field('Recipients').value).to eq 'qa@company.name'
end
......
......@@ -45,7 +45,8 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification
expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
expose :enable_ssl_verification
end
class ForkedFromProject < Grape::Entity
......@@ -252,7 +253,7 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events
expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
......
......@@ -45,6 +45,7 @@ module API
:merge_requests_events,
:tag_push_events,
:note_events,
:build_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
......@@ -77,6 +78,7 @@ module API
:merge_requests_events,
:tag_push_events,
:note_events,
:build_events,
:enable_ssl_verification
]
......
......@@ -5,30 +5,6 @@ module Ci
before { authenticate! }
resource :projects do
# Register new webhook for project
#
# Parameters
# project_id (required) - The ID of a project
# web_hook (required) - WebHook URL
# Example Request
# POST /projects/:project_id/webhooks
post ":project_id/webhooks" do
required_attributes! [:web_hook]
project = Ci::Project.find(params[:project_id])
unauthorized! unless can?(current_user, :admin_project, project.gl_project)
web_hook = project.web_hooks.new({ url: params[:web_hook] })
if web_hook.save
present web_hook, with: Entities::WebHook
else
errors = web_hook.errors.full_messages.join(", ")
render_api_error!(errors, 400)
end
end
# Retrieve all Gitlab CI projects that the user has access to
#
# Example Request:
......@@ -121,20 +97,6 @@ module Ci
end
end
# Remove a Gitlab CI project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# DELETE /projects/:id
delete ":id" do
project = Ci::Project.find(params[:id])
unauthorized! unless can?(current_user, :admin_project, project.gl_project)
project.destroy
end
# Link a Gitlab CI project to a runner
#
# Parameters:
......
module Gitlab
class BuildDataBuilder
class << self
def build(build)
project = build.gl_project
commit = build.commit
user = build.user
data = {
object_kind: 'build',
ref: build.ref,
tag: build.tag,
before_sha: build.before_sha,
sha: build.sha,
# TODO: should this be not prefixed with build_?
# Leaving this way to have backward compatibility
build_id: build.id,
build_name: build.name,
build_stage: build.stage,
build_status: build.status,
build_started_at: build.started_at,
build_finished_at: build.finished_at,
build_duration: build.duration,
# TODO: do we still need it?
project_id: project.id,
project_name: project.name_with_namespace,
user: {
id: user.try(:id),
name: user.try(:name),
email: user.try(:email),
},
commit: {
id: commit.id,
sha: commit.sha,
message: commit.git_commit_message,
author_name: commit.git_author_name,
author_email: commit.git_author_email,
status: commit.status,
duration: commit.duration,
started_at: commit.started_at,
finished_at: commit.finished_at,
},
repository: {
name: project.name,
url: project.url_to_repo,
description: project.description,
homepage: project.web_url,
git_http_url: project.http_url_to_repo,
git_ssh_url: project.ssh_url_to_repo,
visibility_level: project.visibility_level
},
}
data
end
end
end
end
FactoryGirl.define do
factory :ci_web_hook, class: Ci::WebHook do
sequence(:url) { FFaker::Internet.uri('http') }
project factory: :ci_project
end
end
require 'spec_helper'
describe 'CI web hooks' do
let(:user) { create(:user) }
before { login_as(user) }
before do
@project = FactoryGirl.create :ci_project
@gl_project = @project.gl_project
@gl_project.team << [user, :master]
visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project)
end
context 'create a trigger' do
before do
fill_in 'web_hook_url', with: 'http://example.com'
click_on 'Add Web Hook'
end
it { expect(@project.web_hooks.count).to eq(1) }
it 'revokes the trigger' do
click_on 'Remove'
expect(@project.web_hooks.count).to eq(0)
end
end
end
require 'spec_helper'
describe 'Gitlab::BuildDataBuilder' do
let(:build) { create(:ci_build) }
describe :build do
let(:data) do
Gitlab::BuildDataBuilder.build(build)
end
it { expect(data).to be_a(Hash) }
it { expect(data[:ref]).to eq(build.ref) }
it { expect(data[:sha]).to eq(build.sha) }
it { expect(data[:tag]).to eq(build.tag) }
it { expect(data[:build_id]).to eq(build.id) }
it { expect(data[:build_status]).to eq(build.status) }
it { expect(data[:project_id]).to eq(build.gl_project.id) }
it { expect(data[:project_name]).to eq(build.gl_project.name_with_namespace) }
end
end
require 'spec_helper'
describe Ci::Notify do
include EmailSpec::Helpers
include EmailSpec::Matchers
before do
@commit = FactoryGirl.create :ci_commit
@build = FactoryGirl.create :ci_build, commit: @commit
end
describe 'build success' do
subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') }
it 'has the correct subject' do
should have_subject /Build success for/
end
it 'contains name of project' do
should have_body_text /build successful/
end
end
describe 'build fail' do
subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') }
it 'has the correct subject' do
should have_subject /Build failed for/
end
it 'contains name of project' do
should have_body_text /build failed/
end
end
end
......@@ -13,6 +13,7 @@ describe Notify do
let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
let(:recipient) { create(:user, email: 'recipient@example.com') }
let(:project) { create(:project) }
let(:build) { create(:ci_build) }
before(:each) do
ActionMailer::Base.deliveries.clear
......@@ -865,4 +866,32 @@ describe Notify do
is_expected.to have_body_text /#{diff_path}/
end
end
describe 'build success' do
before { build.success }
subject { Notify.build_success_email(build.id, 'wow@example.com') }
it 'has the correct subject' do
should have_subject /Build success for/
end
it 'contains name of project' do
should have_body_text build.project_name
end
end
describe 'build fail' do
before { build.drop }
subject { Notify.build_fail_email(build.id, 'wow@example.com') }
it 'has the correct subject' do
should have_subject /Build failed for/
end
it 'contains name of project' do
should have_body_text build.project_name
end
end
end
require 'spec_helper'
describe Ci::HipChatMessage, models: true do
subject { Ci::HipChatMessage.new(build) }
let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
let(:build) do
commit.builds.first
end
context 'when all matrix builds succeed' do
it 'returns a successful message' do
commit.create_builds('master', false, nil)
commit.builds.update_all(status: "success")
commit.reload
expect(subject.status_color).to eq 'green'
expect(subject.notify?).to be_falsey
expect(subject.to_s).to match(/Commit #\d+/)
expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./)
end
end
context 'when at least one matrix build fails' do
it 'returns a failure message' do
commit.create_builds('master', false, nil)
first_build = commit.builds.first
second_build = commit.builds.last
first_build.update(status: "success")
second_build.update(status: "failed")
expect(subject.status_color).to eq 'red'
expect(subject.notify?).to be_truthy
expect(subject.to_s).to match(/Commit #\d+/)
expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./)
end
end
end
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
require 'spec_helper'
describe Ci::HipChatService, models: true do
describe "Validations" do
context "active" do
before do
subject.active = true
end
it { is_expected.to validate_presence_of :hipchat_room }
it { is_expected.to validate_presence_of :hipchat_token }
end
end
describe "Execute" do
let(:service) { Ci::HipChatService.new }
let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' }
before do
allow(service).to receive_messages(
project: commit.project,
project_id: commit.project_id,
notify_only_broken_builds: false,
hipchat_room: 123,
hipchat_token: 'a1b2c3d4e5f6'
)
WebMock.stub_request(:post, api_url)
end
it "should call the HipChat API" do
service.execute(build)
Ci::HipChatNotifierWorker.drain
expect( WebMock ).to have_requested(:post, api_url).once
end
it "calls the worker with expected arguments" do
expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \
.with(an_instance_of(String), hash_including(
token: 'a1b2c3d4e5f6',
room: 123,
server: 'https://api.hipchat.com',
color: 'red',
notify: true
))
service.execute(build)
end
end
end
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
require 'spec_helper'
describe Ci::MailService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "Validations" do
context "active" do
before do
subject.active = true
end
end
end
describe 'Sends email for' do
let(:mail) { Ci::MailService.new }
let(:user) { User.new(notification_email: 'git@example.com')}
describe 'failed build' do
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
end
it do
perform_enqueued_jobs do
expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"])
end
end
end
describe 'successfull build' do
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
end
it do
perform_enqueued_jobs do
expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
expect(ActionMailer::Base.deliveries.last.to).to eq(["git@example.com"])
end
end
end
describe 'successfull build and project has email_recipients' do
let(:project) do
FactoryGirl.create(:ci_project,
email_add_pusher: true,
email_only_broken_builds: false,
email_recipients: "jeroen@example.com")
end
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
end
it do
perform_enqueued_jobs do
expect{ mail.execute(build) }.to change{ ActionMailer::Base.deliveries.size }.by(2)
expect(
ActionMailer::Base.deliveries.map(&:to).flatten
).to include("git@example.com", "jeroen@example.com")
end
end
end
describe 'successful build and notify only broken builds' do
let(:project) do
FactoryGirl.create(:ci_project,
email_add_pusher: true,
email_only_broken_builds: true,
email_recipients: "jeroen@example.com")
end
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
end
it do
perform_enqueued_jobs do
expect do
mail.execute(build) if mail.can_execute?(build)
end.to_not change{ ActionMailer::Base.deliveries.size }
end
end
end
describe 'successful build and can test service' do
let(:project) do
FactoryGirl.create(:ci_project,
email_add_pusher: true,
email_only_broken_builds: false,
email_recipients: "jeroen@example.com")
end
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
build
end
it do
expect(mail.can_test?).to eq(true)
end
end
describe 'retried build should not receive email' do
let(:project) do
FactoryGirl.create(:ci_project,
email_add_pusher: true,
email_only_broken_builds: true,
email_recipients: "jeroen@example.com")
end
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
before do
allow(mail).to receive_messages(
project: project
)
end
it do
Ci::Build.retry(build)
perform_enqueued_jobs do
expect do
mail.execute(build) if mail.can_execute?(build)
end.to_not change{ ActionMailer::Base.deliveries.size }
end
end
end
end
end
require 'spec_helper'
describe Ci::SlackMessage, models: true do
subject { Ci::SlackMessage.new(commit) }
let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
context 'when all matrix builds succeeded' do
let(:color) { 'good' }
it 'returns a message with success' do
commit.create_builds('master', false, nil)
commit.builds.update_all(status: "success")
commit.reload
expect(subject.color).to eq(color)
expect(subject.fallback).to include('Commit')
expect(subject.fallback).to include("\##{commit.id}")
expect(subject.fallback).to include('succeeded')
expect(subject.attachments.first[:fields]).to be_empty
end
end
context 'when one of matrix builds failed' do
let(:color) { 'danger' }
it 'returns a message with information about failed build' do
commit.create_builds('master', false, nil)
first_build = commit.builds.first
second_build = commit.builds.last
first_build.update(status: "success")
second_build.update(status: "failed")
expect(subject.color).to eq(color)
expect(subject.fallback).to include('Commit')
expect(subject.fallback).to include("\##{commit.id}")
expect(subject.fallback).to include('failed')
expect(subject.attachments.first[:fields].size).to eq(1)
expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
end
end
end
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
require 'spec_helper'
describe Ci::SlackService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "Validations" do
context "active" do
before do
subject.active = true
end
it { is_expected.to validate_presence_of :webhook }
end
end
describe "Execute" do
let(:slack) { Ci::SlackService.new }
let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:notify_only_broken_builds) { false }
before do
allow(slack).to receive_messages(
project: commit.project,
project_id: commit.project_id,
webhook: webhook_url,
notify_only_broken_builds: notify_only_broken_builds
)
WebMock.stub_request(:post, webhook_url)
end
it "should call Slack API" do
slack.execute(build)
Ci::SlackNotifierWorker.drain
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
end
......@@ -34,11 +34,9 @@ describe Ci::Project, models: true do
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:web_hooks) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:services) }
it { is_expected.to validate_presence_of :timeout }
it { is_expected.to validate_presence_of :gitlab_id }
......
# == Schema Information
#
# Table name: ci_services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
#
require 'spec_helper'
describe Ci::Service, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "Mass assignment" do
end
describe "Test Button" do
before do
@service = Ci::Service.new
end
describe "Testable" do
let(:commit) { FactoryGirl.create :ci_commit }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
before do
allow(@service).to receive_messages(
project: commit.project
)
build
@testable = @service.can_test?
end
describe :can_test do
it { expect(@testable).to eq(true) }
end
end
end
end
# == Schema Information
#
# Table name: ci_web_hooks
#
# id :integer not null, primary key
# url :string(255) not null
# project_id :integer not null
# created_at :datetime
# updated_at :datetime
#
require 'spec_helper'
describe Ci::WebHook, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe "Validations" do
it { is_expected.to validate_presence_of(:url) }
context "url format" do
it { is_expected.to allow_value("http://example.com").for(:url) }
it { is_expected.to allow_value("https://excample.com").for(:url) }
it { is_expected.to allow_value("http://test.com/api").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
it { is_expected.not_to allow_value("example.com").for(:url) }
it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
end
end
describe "execute" do
before(:each) do
@web_hook = FactoryGirl.create(:ci_web_hook)
@project = @web_hook.project
@data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
WebMock.stub_request(:post, @web_hook.url)
end
it "POSTs to the web hook URL" do
@web_hook.execute(@data)
expect(WebMock).to have_requested(:post, @web_hook.url).once
end
it "POSTs the data as JSON" do
json = @data.to_json
@web_hook.execute(@data)
expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once
end
it "catches exceptions" do
expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error")
expect{ @web_hook.execute(@data) }.
to raise_error(RuntimeError, 'Some HTTP Post error')
end
end
end
......@@ -247,6 +247,55 @@ describe HipchatService, models: true do
end
end
context 'build events' do
let(:build) { create(:ci_build) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
context 'for failed' do
before { build.drop }
it "should call Hipchat API" do
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "should create a build message" do
message = hipchat.send(:create_build_message, data)
project_url = project.web_url
project_name = project.name_with_namespace.gsub(/\s/, '')
sha = data[:sha]
ref = data[:ref]
ref_type = data[:tag] ? 'tag' : 'branch'
duration = data[:commit][:duration]
expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
"Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
"of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
"by #{data[:commit][:author_name]} failed in #{duration} second(s)")
end
end
context 'for succeeded' do
before do
build.success
end
it "should call Hipchat API" do
hipchat.notify_only_broken_builds = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "should notify only broken" do
hipchat.notify_only_broken_builds = true
hipchat.execute(data)
expect(WebMock).to_not have_requested(:post, api_url).once
end
end
end
context "#message_options" do
it "should be set to the defaults" do
expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' })
......
require 'spec_helper'
describe SlackService::BuildMessage do
subject { SlackService::BuildMessage.new(args) }
let(:args) do
{
sha: '97de212e80737a608d939f648d959671fb0a0142',
ref: 'develop',
tag: false,
project_name: 'project_name',
project_url: 'somewhere.com',
commit: {
status: status,
author_name: 'hacker',
duration: 10,
},
}
end
context 'succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
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 succeeded in 10 second(s)'
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
let(:status) { 'failed' }
let(:color) { 'danger' }
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 second(s)'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
end
require 'spec_helper'
describe API::API, 'ProjectHooks', api: true do
describe API::API, 'ProjectHooks', api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) { create(:project_hook, project: project, url: "http://example.com", push_events: true, merge_requests_events: true, tag_push_events: true, issues_events: true, note_events: true, enable_ssl_verification: true) }
let!(:hook) do
create(:project_hook,
project: project, url: "http://example.com",
push_events: true, merge_requests_events: true, tag_push_events: true,
issues_events: true, note_events: true, build_events: true,
enable_ssl_verification: true)
end
before do
project.team << [user, :master]
......@@ -26,6 +32,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
......@@ -83,6 +90,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
......
......@@ -58,57 +58,6 @@ describe Ci::API::API do
end
end
describe "POST /projects/:project_id/webhooks" do
let!(:project) { FactoryGirl.create(:ci_project) }
context "Valid Webhook URL" do
let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } }
before do
options.merge!(webhook)
end
it "should create webhook for specified project" do
project.gl_project.team << [user, :master]
post ci_api("/projects/#{project.id}/webhooks"), options
expect(response.status).to eq(201)
expect(json_response["url"]).to eq(webhook[:web_hook])
end
it "fails to create webhook for non existsing project" do
post ci_api("/projects/non-existant-id/webhooks"), options
expect(response.status).to eq(404)
end
it "non-manager is not authorized" do
post ci_api("/projects/#{project.id}/webhooks"), options
expect(response.status).to eq(401)
end
end
context "Invalid Webhook URL" do
let!(:webhook) { { web_hook: "ala_ma_kota" } }
before do
options.merge!(webhook)
end
it "fails to create webhook for not valid url" do
project.gl_project.team << [user, :master]
post ci_api("/projects/#{project.id}/webhooks"), options
expect(response.status).to eq(400)
end
end
context "Missed web_hook parameter" do
it "fails to create webhook for not provided url" do
project.gl_project.team << [user, :master]
post ci_api("/projects/#{project.id}/webhooks"), options
expect(response.status).to eq(400)
end
end
end
describe "GET /projects/:id" do
let!(:project) { FactoryGirl.create(:ci_project) }
......@@ -158,28 +107,6 @@ describe Ci::API::API do
end
end
describe "DELETE /projects/:id" do
let!(:project) { FactoryGirl.create(:ci_project) }
it "should delete a specific project" do
project.gl_project.team << [user, :master]
delete ci_api("/projects/#{project.id}"), options
expect(response.status).to eq(200)
expect { project.reload }.
to raise_error(ActiveRecord::RecordNotFound)
end
it "non-manager is not authorized" do
delete ci_api("/projects/#{project.id}"), options
expect(response.status).to eq(401)
end
it "is getting not found error" do
delete ci_api("/projects/not-existing_id"), options
expect(response.status).to eq(404)
end
end
describe "POST /projects/:id/runners/:id" do
let(:project) { FactoryGirl.create(:ci_project) }
let(:runner) { FactoryGirl.create(:ci_runner) }
......
require 'spec_helper'
describe Ci::WebHookService, services: true do
let(:project) { FactoryGirl.create :ci_project }
let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
let(:hook) { FactoryGirl.create :ci_web_hook, project: project }
describe :execute do
it "should execute successfully" do
stub_request(:post, hook.url).to_return(status: 200)
expect(Ci::WebHookService.new.build_end(build)).to be_truthy
end
end
context 'build_data' do
it "contains all needed fields" do
expect(build_data(build)).to include(
:build_id,
:project_id,
:ref,
:build_status,
:build_started_at,
:build_finished_at,
:before_sha,
:project_name,
:gitlab_url,
:build_name
)
end
end
def build_data(build)
Ci::WebHookService.new.send :build_data, build
end
end
require 'spec_helper'
describe BuildEmailWorker do
include RepoHelpers
let(:build) { create(:ci_build) }
let(:user) { create(:user) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
subject { BuildEmailWorker.new }
before do
allow(build).to receive(:execute_hooks).and_return(false)
build.success
end
describe "#perform" do
it "sends mail" do
subject.perform(build.id, user.email, data.stringify_keys)
email = ActionMailer::Base.deliveries.last
expect(email.subject).to include('Build success for')
expect(email.to).to eq([user.email])
end
it "gracefully handles an input SMTP error" do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
subject.perform(build.id, user.email, data.stringify_keys)
expect(ActionMailer::Base.deliveries.count).to eq(0)
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