Commit e3fc01b3eaa13a51aef380dc21f1e7d259706227
1 parent
acaf2978
Exists in
spb-stable
and in
3 other branches
Added Slack service integration.
Showing
11 changed files
with
320 additions
and
1 deletions
Show diff stats
CHANGELOG
... | ... | @@ -24,6 +24,7 @@ v 6.7.0 |
24 | 24 | - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) |
25 | 25 | - Create and Update MR calls now support the description parameter (Greg Messner) |
26 | 26 | - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository |
27 | + - Added Slack service integration (Federico Ravasio) | |
27 | 28 | |
28 | 29 | v 6.6.5 |
29 | 30 | - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) | ... | ... |
Gemfile
... | ... | @@ -132,6 +132,9 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2" |
132 | 132 | # Gemnasium integration |
133 | 133 | gem "gemnasium-gitlab-service", "~> 0.2" |
134 | 134 | |
135 | +# Slack integration | |
136 | +gem "slack-notifier", "~> 0.2.0" | |
137 | + | |
135 | 138 | # d3 |
136 | 139 | gem "d3_rails", "~> 3.1.4" |
137 | 140 | ... | ... |
Gemfile.lock
... | ... | @@ -468,6 +468,7 @@ GEM |
468 | 468 | rack-protection (~> 1.4) |
469 | 469 | tilt (~> 1.3, >= 1.3.4) |
470 | 470 | six (0.2.0) |
471 | + slack-notifier (0.2.0) | |
471 | 472 | slim (2.0.2) |
472 | 473 | temple (~> 0.6.6) |
473 | 474 | tilt (>= 1.3.3, < 2.1) |
... | ... | @@ -652,6 +653,7 @@ DEPENDENCIES |
652 | 653 | simplecov |
653 | 654 | sinatra |
654 | 655 | six |
656 | + slack-notifier (~> 0.2.0) | |
655 | 657 | slim |
656 | 658 | spinach-rails |
657 | 659 | spork (~> 1.0rc) | ... | ... |
app/models/project.rb
... | ... | @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base |
56 | 56 | has_one :flowdock_service, dependent: :destroy |
57 | 57 | has_one :assembla_service, dependent: :destroy |
58 | 58 | has_one :gemnasium_service, dependent: :destroy |
59 | + has_one :slack_service, dependent: :destroy | |
59 | 60 | has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" |
60 | 61 | has_one :forked_from_project, through: :forked_project_link |
61 | 62 | # Merge Requests for target project should be removed with it |
... | ... | @@ -304,7 +305,7 @@ class Project < ActiveRecord::Base |
304 | 305 | end |
305 | 306 | |
306 | 307 | def available_services_names |
307 | - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium) | |
308 | + %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack) | |
308 | 309 | end |
309 | 310 | |
310 | 311 | def gitlab_ci? | ... | ... |
... | ... | @@ -0,0 +1,95 @@ |
1 | +require 'slack-notifier' | |
2 | + | |
3 | +class SlackMessage | |
4 | + def initialize(params) | |
5 | + @after = params.fetch(:after) | |
6 | + @before = params.fetch(:before) | |
7 | + @commits = params.fetch(:commits, []) | |
8 | + @project_name = params.fetch(:project_name) | |
9 | + @project_url = params.fetch(:project_url) | |
10 | + @ref = params.fetch(:ref).gsub('refs/heads/', '') | |
11 | + @username = params.fetch(:user_name) | |
12 | + end | |
13 | + | |
14 | + def compose | |
15 | + format(message) | |
16 | + end | |
17 | + | |
18 | + private | |
19 | + | |
20 | + attr_reader :after | |
21 | + attr_reader :before | |
22 | + attr_reader :commits | |
23 | + attr_reader :project_name | |
24 | + attr_reader :project_url | |
25 | + attr_reader :ref | |
26 | + attr_reader :username | |
27 | + | |
28 | + def message | |
29 | + if new_branch? | |
30 | + new_branch_message | |
31 | + elsif removed_branch? | |
32 | + removed_branch_message | |
33 | + else | |
34 | + push_message << commit_messages | |
35 | + end | |
36 | + end | |
37 | + | |
38 | + def format(string) | |
39 | + Slack::Notifier::LinkFormatter.format(string) | |
40 | + end | |
41 | + | |
42 | + def new_branch_message | |
43 | + "#{username} pushed new branch #{branch_link} to #{project_link}" | |
44 | + end | |
45 | + | |
46 | + def removed_branch_message | |
47 | + "#{username} removed branch #{ref} from #{project_link}" | |
48 | + end | |
49 | + | |
50 | + def push_message | |
51 | + "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" | |
52 | + end | |
53 | + | |
54 | + def commit_messages | |
55 | + commits.each_with_object('') do |commit, str| | |
56 | + str << compose_commit_message(commit) | |
57 | + end | |
58 | + end | |
59 | + | |
60 | + def compose_commit_message(commit) | |
61 | + id = commit.fetch(:id)[0..5] | |
62 | + message = commit.fetch(:message) | |
63 | + url = commit.fetch(:url) | |
64 | + | |
65 | + "\n - #{message} ([#{id}](#{url}))" | |
66 | + end | |
67 | + | |
68 | + def new_branch? | |
69 | + before =~ /000000/ | |
70 | + end | |
71 | + | |
72 | + def removed_branch? | |
73 | + after =~ /000000/ | |
74 | + end | |
75 | + | |
76 | + def branch_url | |
77 | + "#{project_url}/commits/#{ref}" | |
78 | + end | |
79 | + | |
80 | + def compare_url | |
81 | + "#{project_url}/compare/#{before}...#{after}" | |
82 | + end | |
83 | + | |
84 | + def branch_link | |
85 | + "[#{ref}](#{branch_url})" | |
86 | + end | |
87 | + | |
88 | + def project_link | |
89 | + "[#{project_name}](#{project_url})" | |
90 | + end | |
91 | + | |
92 | + def compare_link | |
93 | + "[Compare changes](#{compare_url})" | |
94 | + end | |
95 | +end | ... | ... |
... | ... | @@ -0,0 +1,67 @@ |
1 | +# == Schema Information | |
2 | +# | |
3 | +# Table name: services | |
4 | +# | |
5 | +# id :integer not null, primary key | |
6 | +# type :string(255) | |
7 | +# title :string(255) | |
8 | +# token :string(255) | |
9 | +# project_id :integer not null | |
10 | +# created_at :datetime not null | |
11 | +# updated_at :datetime not null | |
12 | +# active :boolean default(FALSE), not null | |
13 | +# project_url :string(255) | |
14 | +# subdomain :string(255) | |
15 | +# room :string(255) | |
16 | +# api_key :string(255) | |
17 | +# | |
18 | + | |
19 | +class SlackService < Service | |
20 | + attr_accessible :room | |
21 | + attr_accessible :subdomain | |
22 | + | |
23 | + validates :room, presence: true, if: :activated? | |
24 | + validates :subdomain, presence: true, if: :activated? | |
25 | + validates :token, presence: true, if: :activated? | |
26 | + | |
27 | + def title | |
28 | + 'Slack' | |
29 | + end | |
30 | + | |
31 | + def description | |
32 | + 'A team communication tool for the 21st century' | |
33 | + end | |
34 | + | |
35 | + def to_param | |
36 | + 'slack' | |
37 | + end | |
38 | + | |
39 | + def fields | |
40 | + [ | |
41 | + { type: 'text', name: 'subdomain', placeholder: '' }, | |
42 | + { type: 'text', name: 'token', placeholder: '' }, | |
43 | + { type: 'text', name: 'room', placeholder: '' }, | |
44 | + ] | |
45 | + end | |
46 | + | |
47 | + def execute(push_data) | |
48 | + message = SlackMessage.new(push_data.merge( | |
49 | + project_url: project_url, | |
50 | + project_name: project_name | |
51 | + )) | |
52 | + | |
53 | + notifier = Slack::Notifier.new(subdomain, token) | |
54 | + notifier.channel = room | |
55 | + notifier.ping(message.compose) | |
56 | + end | |
57 | + | |
58 | + private | |
59 | + | |
60 | + def project_name | |
61 | + project.name_with_namespace.gsub(/\s/, '') | |
62 | + end | |
63 | + | |
64 | + def project_url | |
65 | + project.web_url | |
66 | + end | |
67 | +end | ... | ... |
features/project/service.feature
... | ... | @@ -37,6 +37,12 @@ Feature: Project Services |
37 | 37 | And I fill Assembla settings |
38 | 38 | Then I should see Assembla service settings saved |
39 | 39 | |
40 | + Scenario: Activate Slack service | |
41 | + When I visit project "Shop" services page | |
42 | + And I click Slack service link | |
43 | + And I fill Slack settings | |
44 | + Then I should see Slack service settings saved | |
45 | + | |
40 | 46 | Scenario: Activate email on push service |
41 | 47 | When I visit project "Shop" services page |
42 | 48 | And I click email on push service link | ... | ... |
features/steps/project/services.rb
... | ... | @@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps |
100 | 100 | step 'I should see email on push service settings saved' do |
101 | 101 | find_field('Recipients').value.should == 'qa@company.name' |
102 | 102 | end |
103 | + | |
104 | + step 'I click Slack service link' do | |
105 | + click_link 'Slack' | |
106 | + end | |
107 | + | |
108 | + step 'I fill Slack settings' do | |
109 | + check 'Active' | |
110 | + fill_in 'Subdomain', with: 'gitlab' | |
111 | + fill_in 'Room', with: '#gitlab' | |
112 | + fill_in 'Token', with: 'verySecret' | |
113 | + click_button 'Save' | |
114 | + end | |
115 | + | |
116 | + step 'I should see Slack service settings saved' do | |
117 | + find_field('Subdomain').value.should == 'gitlab' | |
118 | + find_field('Room').value.should == '#gitlab' | |
119 | + find_field('Token').value.should == 'verySecret' | |
120 | + end | |
103 | 121 | end | ... | ... |
spec/models/project_spec.rb
... | ... | @@ -47,6 +47,7 @@ describe Project do |
47 | 47 | it { should have_many(:hooks).dependent(:destroy) } |
48 | 48 | it { should have_many(:protected_branches).dependent(:destroy) } |
49 | 49 | it { should have_one(:forked_project_link).dependent(:destroy) } |
50 | + it { should have_one(:slack_service).dependent(:destroy) } | |
50 | 51 | end |
51 | 52 | |
52 | 53 | describe "Mass assignment" do | ... | ... |
... | ... | @@ -0,0 +1,56 @@ |
1 | +require_relative '../../app/models/project_services/slack_message' | |
2 | + | |
3 | +describe SlackMessage do | |
4 | + subject { SlackMessage.new(args) } | |
5 | + | |
6 | + let(:args) { | |
7 | + { | |
8 | + after: 'after', | |
9 | + before: 'before', | |
10 | + project_name: 'project_name', | |
11 | + ref: 'refs/heads/master', | |
12 | + user_name: 'user_name', | |
13 | + project_url: 'url' | |
14 | + } | |
15 | + } | |
16 | + | |
17 | + context 'push' do | |
18 | + before do | |
19 | + args[:commits] = [ | |
20 | + { message: 'message1', url: 'url1', id: 'abcdefghi' }, | |
21 | + { message: 'message2', url: 'url2', id: '123456789' }, | |
22 | + ] | |
23 | + end | |
24 | + | |
25 | + it 'returns a message regarding pushes' do | |
26 | + subject.compose.should == | |
27 | + 'user_name pushed to branch <url/commits/master|master> of ' << | |
28 | + '<url|project_name> (<url/compare/before...after|Compare changes>)' << | |
29 | + "\n - message1 (<url1|abcdef>)" << | |
30 | + "\n - message2 (<url2|123456>)" | |
31 | + end | |
32 | + end | |
33 | + | |
34 | + context 'new branch' do | |
35 | + before do | |
36 | + args[:before] = '000000' | |
37 | + end | |
38 | + | |
39 | + it 'returns a message regarding a new branch' do | |
40 | + subject.compose.should == | |
41 | + 'user_name pushed new branch <url/commits/master|master> to ' << | |
42 | + '<url|project_name>' | |
43 | + end | |
44 | + end | |
45 | + | |
46 | + context 'removed branch' do | |
47 | + before do | |
48 | + args[:after] = '000000' | |
49 | + end | |
50 | + | |
51 | + it 'returns a message regarding a removed branch' do | |
52 | + subject.compose.should == | |
53 | + 'user_name removed branch master from <url|project_name>' | |
54 | + end | |
55 | + end | |
56 | +end | ... | ... |
... | ... | @@ -0,0 +1,69 @@ |
1 | +# == Schema Information | |
2 | +# | |
3 | +# Table name: services | |
4 | +# | |
5 | +# id :integer not null, primary key | |
6 | +# type :string(255) | |
7 | +# title :string(255) | |
8 | +# token :string(255) | |
9 | +# project_id :integer not null | |
10 | +# created_at :datetime not null | |
11 | +# updated_at :datetime not null | |
12 | +# active :boolean default(FALSE), not null | |
13 | +# project_url :string(255) | |
14 | +# subdomain :string(255) | |
15 | +# room :string(255) | |
16 | +# api_key :string(255) | |
17 | +# | |
18 | + | |
19 | +require 'spec_helper' | |
20 | + | |
21 | +describe SlackService do | |
22 | + describe "Associations" do | |
23 | + it { should belong_to :project } | |
24 | + it { should have_one :service_hook } | |
25 | + end | |
26 | + | |
27 | + describe "Validations" do | |
28 | + context "active" do | |
29 | + before do | |
30 | + subject.active = true | |
31 | + end | |
32 | + | |
33 | + it { should validate_presence_of :room } | |
34 | + it { should validate_presence_of :subdomain } | |
35 | + it { should validate_presence_of :token } | |
36 | + end | |
37 | + end | |
38 | + | |
39 | + describe "Execute" do | |
40 | + let(:slack) { SlackService.new } | |
41 | + let(:user) { create(:user) } | |
42 | + let(:project) { create(:project) } | |
43 | + let(:sample_data) { GitPushService.new.sample_data(project, user) } | |
44 | + let(:subdomain) { 'gitlab' } | |
45 | + let(:token) { 'verySecret' } | |
46 | + let(:api_url) { | |
47 | + "https://#{subdomain}.slack.com/services/hooks/incoming-webhook?token=#{token}" | |
48 | + } | |
49 | + | |
50 | + before do | |
51 | + slack.stub( | |
52 | + project: project, | |
53 | + project_id: project.id, | |
54 | + room: '#gitlab', | |
55 | + service_hook: true, | |
56 | + subdomain: subdomain, | |
57 | + token: token | |
58 | + ) | |
59 | + | |
60 | + WebMock.stub_request(:post, api_url) | |
61 | + end | |
62 | + | |
63 | + it "should call Slack API" do | |
64 | + slack.execute(sample_data) | |
65 | + | |
66 | + WebMock.should have_requested(:post, api_url).once | |
67 | + end | |
68 | + end | |
69 | +end | ... | ... |