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,6 +24,7 @@ v 6.7.0 | ||
24 | - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) | 24 | - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) |
25 | - Create and Update MR calls now support the description parameter (Greg Messner) | 25 | - Create and Update MR calls now support the description parameter (Greg Messner) |
26 | - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository | 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 | v 6.6.5 | 29 | v 6.6.5 |
29 | - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) | 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,6 +132,9 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2" | ||
132 | # Gemnasium integration | 132 | # Gemnasium integration |
133 | gem "gemnasium-gitlab-service", "~> 0.2" | 133 | gem "gemnasium-gitlab-service", "~> 0.2" |
134 | 134 | ||
135 | +# Slack integration | ||
136 | +gem "slack-notifier", "~> 0.2.0" | ||
137 | + | ||
135 | # d3 | 138 | # d3 |
136 | gem "d3_rails", "~> 3.1.4" | 139 | gem "d3_rails", "~> 3.1.4" |
137 | 140 |
Gemfile.lock
@@ -468,6 +468,7 @@ GEM | @@ -468,6 +468,7 @@ GEM | ||
468 | rack-protection (~> 1.4) | 468 | rack-protection (~> 1.4) |
469 | tilt (~> 1.3, >= 1.3.4) | 469 | tilt (~> 1.3, >= 1.3.4) |
470 | six (0.2.0) | 470 | six (0.2.0) |
471 | + slack-notifier (0.2.0) | ||
471 | slim (2.0.2) | 472 | slim (2.0.2) |
472 | temple (~> 0.6.6) | 473 | temple (~> 0.6.6) |
473 | tilt (>= 1.3.3, < 2.1) | 474 | tilt (>= 1.3.3, < 2.1) |
@@ -652,6 +653,7 @@ DEPENDENCIES | @@ -652,6 +653,7 @@ DEPENDENCIES | ||
652 | simplecov | 653 | simplecov |
653 | sinatra | 654 | sinatra |
654 | six | 655 | six |
656 | + slack-notifier (~> 0.2.0) | ||
655 | slim | 657 | slim |
656 | spinach-rails | 658 | spinach-rails |
657 | spork (~> 1.0rc) | 659 | spork (~> 1.0rc) |
app/models/project.rb
@@ -56,6 +56,7 @@ class Project < ActiveRecord::Base | @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base | ||
56 | has_one :flowdock_service, dependent: :destroy | 56 | has_one :flowdock_service, dependent: :destroy |
57 | has_one :assembla_service, dependent: :destroy | 57 | has_one :assembla_service, dependent: :destroy |
58 | has_one :gemnasium_service, dependent: :destroy | 58 | has_one :gemnasium_service, dependent: :destroy |
59 | + has_one :slack_service, dependent: :destroy | ||
59 | has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" | 60 | has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" |
60 | has_one :forked_from_project, through: :forked_project_link | 61 | has_one :forked_from_project, through: :forked_project_link |
61 | # Merge Requests for target project should be removed with it | 62 | # Merge Requests for target project should be removed with it |
@@ -304,7 +305,7 @@ class Project < ActiveRecord::Base | @@ -304,7 +305,7 @@ class Project < ActiveRecord::Base | ||
304 | end | 305 | end |
305 | 306 | ||
306 | def available_services_names | 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 | end | 309 | end |
309 | 310 | ||
310 | def gitlab_ci? | 311 | def gitlab_ci? |
@@ -0,0 +1,95 @@ | @@ -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 @@ | @@ -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,6 +37,12 @@ Feature: Project Services | ||
37 | And I fill Assembla settings | 37 | And I fill Assembla settings |
38 | Then I should see Assembla service settings saved | 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 | Scenario: Activate email on push service | 46 | Scenario: Activate email on push service |
41 | When I visit project "Shop" services page | 47 | When I visit project "Shop" services page |
42 | And I click email on push service link | 48 | And I click email on push service link |
features/steps/project/services.rb
@@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps | @@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps | ||
100 | step 'I should see email on push service settings saved' do | 100 | step 'I should see email on push service settings saved' do |
101 | find_field('Recipients').value.should == 'qa@company.name' | 101 | find_field('Recipients').value.should == 'qa@company.name' |
102 | end | 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 | end | 121 | end |
spec/models/project_spec.rb
@@ -47,6 +47,7 @@ describe Project do | @@ -47,6 +47,7 @@ describe Project do | ||
47 | it { should have_many(:hooks).dependent(:destroy) } | 47 | it { should have_many(:hooks).dependent(:destroy) } |
48 | it { should have_many(:protected_branches).dependent(:destroy) } | 48 | it { should have_many(:protected_branches).dependent(:destroy) } |
49 | it { should have_one(:forked_project_link).dependent(:destroy) } | 49 | it { should have_one(:forked_project_link).dependent(:destroy) } |
50 | + it { should have_one(:slack_service).dependent(:destroy) } | ||
50 | end | 51 | end |
51 | 52 | ||
52 | describe "Mass assignment" do | 53 | describe "Mass assignment" do |
@@ -0,0 +1,56 @@ | @@ -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 @@ | @@ -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 |