diff options
author | Valery Sizov <valery@gitlab.com> | 2015-05-04 11:30:47 +0000 |
---|---|---|
committer | Valery Sizov <valery@gitlab.com> | 2015-05-04 11:30:47 +0000 |
commit | f749a221520ef1e8013059910a98f69f4c1d08da (patch) | |
tree | 381e049168afd0e6d127314ccc3a1bf9d8847ceb | |
parent | 3bbd2f55a299b31a702b7ae6aa2624e5116bc8e8 (diff) | |
parent | f2e65ba17b6b707111d9848d54f97f4fc4febe21 (diff) | |
download | gitlab-ci-f749a221520ef1e8013059910a98f69f4c1d08da.tar.gz |
Merge branch 'hipchat_service' into 'master'
Add HipChat Notification Service
My company is looking for a secure, private, stable and hopefully less-expensive alternative to our existing GitHub & Jenkins setup, and is currently focused on GitLab and GitLab CI after trying a number of other solutions. The biggest hurdle is integrations.
I saw that GitLab CI was lacking a service to notify HipChat of builds, and in my search to see if there was a workaround I saw [someone ask about it](http://feedback.gitlab.com/forums/176466-general/suggestions/5350117-gitlab-ci-should-push-notifications-to-configured), and figured it just hadn't been done yet. So, I did it.
* Move existing Slack service spec into a subdir, mirroring /app
* Wire up HipChat service to the project and services controller.
* Split the message building into own class.
* 'namespace' room and token variables.
* Enforce v2 client (bug in HipChat gem v1.5.0. fixed in 1.5.1). Note
that I'm using the same version string as GitLab-CE, for shared
installations. This does prevent 'old' room tokens from being reused.
'v1' is more compatible, but there is rumblings about finally deprecating
it and moving to v2 only on their GitHub issue tracker for this gem.
* Defer execution to a notifier worker, like the Slack service.
* Ensure passing specs (basically a Slack service spec copy, fwiw)
* Added change to the CHANGELOG
I'm not sure exactly how your feedback's "Accepting Merge Requests" tag is supposed to work, but I'm happy to learn and change my contribution procedure if anything is wrong here. Thanks!

See merge request !83
-rw-r--r-- | CHANGELOG | 1 | ||||
-rw-r--r-- | Gemfile | 3 | ||||
-rw-r--r-- | Gemfile.lock | 5 | ||||
-rw-r--r-- | app/controllers/services_controller.rb | 3 | ||||
-rw-r--r-- | app/models/project.rb | 3 | ||||
-rw-r--r-- | app/models/project_services/hip_chat_message.rb | 76 | ||||
-rw-r--r-- | app/models/project_services/hip_chat_service.rb | 80 | ||||
-rw-r--r-- | app/workers/hip_chat_notifier_worker.rb | 18 | ||||
-rw-r--r-- | spec/models/project_services/hip_chat_message_spec.rb | 65 | ||||
-rw-r--r-- | spec/models/project_services/hip_chat_service_spec.rb | 61 | ||||
-rw-r--r-- | spec/models/project_services/slack_message_spec.rb (renamed from spec/models/slack_message_spec.rb) | 0 | ||||
-rw-r--r-- | spec/models/project_services/slack_service_spec.rb (renamed from spec/models/slack_service_spec.rb) | 0 |
12 files changed, 313 insertions, 2 deletions
@@ -4,6 +4,7 @@ v7.11.0 - Improved runners page - Running and Pending tabs on admin builds page - Fix [ci skip] tag, so you can skip CI triggering now + - Add HipChat notifications v7.10.1 - Fix failing migration when update to 7.10 from 7.8 and older versions @@ -63,6 +63,9 @@ gem "default_value_for", "~> 3.0.0" # Slack integration gem "slack-notifier", "~> 1.0.0" +# HipChat integration +gem 'hipchat', '~> 1.5.0' + # Other gem 'rake' gem 'foreman' diff --git a/Gemfile.lock b/Gemfile.lock index 41bc118..d2c9158 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,6 +156,9 @@ GEM hashie (2.0.5) highline (1.6.21) hike (1.2.3) + hipchat (1.5.0) + httparty + mimemagic httparty (0.11.0) multi_json (~> 1.0) multi_xml (>= 0.5.2) @@ -186,6 +189,7 @@ GEM mime-types (>= 1.16, < 3) method_source (0.8.2) mime-types (2.4.3) + mimemagic (0.3.0) mini_portile (0.5.2) minitest (5.5.1) multi_json (1.11.0) @@ -390,6 +394,7 @@ DEPENDENCIES growl guard-rspec haml-rails (~> 0.5.3) + hipchat (~> 1.5.0) httparty (= 0.11.0) jquery-rails jquery-turbolinks diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb index 39cf306..64bc698 100644 --- a/app/controllers/services_controller.rb +++ b/app/controllers/services_controller.rb @@ -50,7 +50,8 @@ class ServicesController < ApplicationController def service_params params.require(:service).permit( :type, :active, :webhook, :notify_only_broken_builds, - :email_recipients, :email_only_broken_builds, :email_add_pusher + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 31ce6ab..c48b079 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,7 @@ class Project < ActiveRecord::Base # Project services has_many :services, dependent: :destroy + has_one :hip_chat_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :mail_service, dependent: :destroy @@ -210,7 +211,7 @@ ls -la end def available_services_names - %w(slack mail) + %w(slack mail hip_chat) end def build_missing_services diff --git a/app/models/project_services/hip_chat_message.rb b/app/models/project_services/hip_chat_message.rb new file mode 100644 index 0000000..8350bb6 --- /dev/null +++ b/app/models/project_services/hip_chat_message.rb @@ -0,0 +1,76 @@ +class HipChatMessage + attr_reader :build + + def initialize(build) + @build = build + end + + def to_s + lines = Array.new + lines.push("<a href=\"#{RoutesHelper.project_url(project)}\">#{project.name}</a> - ") + if commit.matrix? + lines.push("<a href=\"#{RoutesHelper.project_ref_commit_url(project, commit.ref, commit.sha)}\">Commit ##{commit.id}</a></br>") + else + first_build = commit.builds_without_retry.first + lines.push("<a href=\"#{RoutesHelper.project_build_url(project, first_build)}\">Build '#{first_build.job_name}' ##{first_build.id}</a></br>") + end + 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 + diff --git a/app/models/project_services/hip_chat_service.rb b/app/models/project_services/hip_chat_service.rb new file mode 100644 index 0000000..8e5f024 --- /dev/null +++ b/app/models/project_services/hip_chat_service.rb @@ -0,0 +1,80 @@ +# == 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 +# + +class HipChatService < 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 execute build + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include? build + return if commit.success? and notify_only_broken_builds? + return if commit.running? + + msg = HipChatMessage.new(build) + opts = default_options.merge( + token: hipchat_token, + room: hipchat_room, + server: server_url, + color: msg.status_color, + notify: msg.notify? + ) + 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 diff --git a/app/workers/hip_chat_notifier_worker.rb b/app/workers/hip_chat_notifier_worker.rb new file mode 100644 index 0000000..0403578 --- /dev/null +++ b/app/workers/hip_chat_notifier_worker.rb @@ -0,0 +1,18 @@ + +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 diff --git a/spec/models/project_services/hip_chat_message_spec.rb b/spec/models/project_services/hip_chat_message_spec.rb new file mode 100644 index 0000000..6096b5c --- /dev/null +++ b/spec/models/project_services/hip_chat_message_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe HipChatMessage do + subject { HipChatMessage.new(build) } + + let(:project) { FactoryGirl.create(:project) } + let(:commit) { FactoryGirl.create(:commit, project: project) } + let(:job) { FactoryGirl.create(:job, project: project) } + let(:build) { FactoryGirl.create(:build, commit: commit, job: job, status: 'success') } + + context 'when build succeeds' do + + before { build.save } + + it 'returns a successful message' do + expect( subject.status_color ).to eq 'green' + expect( subject.notify? ).to be_false + expect( subject.to_s ).to match(/Build '[^']+' #\d+/) + expect( subject.to_s ).to match(/Successful in \d+ second\(s\)\./) + end + end + + context 'when build fails' do + + before do + build.status = 'failed' + build.save + end + + it 'returns a failure message' do + expect( subject.status_color ).to eq 'red' + expect( subject.notify? ).to be_true + expect( subject.to_s ).to match(/Build '[^']+' #\d+/) + expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./) + end + end + + context 'when all matrix builds succeed' do + let(:job2) { FactoryGirl.create(:job, project: project, name: 'Another Job') } + let(:build2) { FactoryGirl.create(:build, id: 10, commit: commit, job: job2, status: 'success') } + + before { build.save; build2.save } + + it 'returns a successful message' do + expect( subject.status_color ).to eq 'green' + expect( subject.notify? ).to be_false + 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 + let(:job2) { FactoryGirl.create(:job, project: project, name: 'Another Job') } + let(:build2) { FactoryGirl.create(:build, id: 10, commit: commit, job: job2, status: 'failed') } + + before { build.save; build2.save } + + it 'returns a failure message' do + expect( subject.status_color ).to eq 'red' + expect( subject.notify? ).to be_true + expect( subject.to_s ).to match(/Commit #\d+/) + expect( subject.to_s ).to match(/Failed in \d+ second\(s\)\./) + end + end +end diff --git a/spec/models/project_services/hip_chat_service_spec.rb b/spec/models/project_services/hip_chat_service_spec.rb new file mode 100644 index 0000000..5642221 --- /dev/null +++ b/spec/models/project_services/hip_chat_service_spec.rb @@ -0,0 +1,61 @@ + +require 'spec_helper' + +describe HipChatService do + + describe "Validations" do + + context "active" do + before do + subject.active = true + end + + it { should validate_presence_of :hipchat_room } + it { should validate_presence_of :hipchat_token } + + end + end + + describe "Execute" do + + let(:service) { HipChatService.new } + let(:project) { FactoryGirl.create :project } + let(:commit) { FactoryGirl.create :commit, project: project } + let(:build) { FactoryGirl.create :build, commit: commit, status: 'failed' } + let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' } + + before do + service.stub( + project: project, + project_id: 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) + HipChatNotifierWorker.drain + + expect( WebMock ).to have_requested(:post, api_url).once + end + + it "calls the worker with expected arguments" do + expect( 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 + diff --git a/spec/models/slack_message_spec.rb b/spec/models/project_services/slack_message_spec.rb index 1fa2e31..1fa2e31 100644 --- a/spec/models/slack_message_spec.rb +++ b/spec/models/project_services/slack_message_spec.rb diff --git a/spec/models/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index e1c1428..e1c1428 100644 --- a/spec/models/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb |