summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo.yml1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock4
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_services/hipchat_service.rb311
-rw-r--r--app/models/service.rb1
-rw-r--r--config/initializers/hipchat_client_patch.rb14
-rw-r--r--db/migrate/20190107151020_add_services_type_index.rb20
-rw-r--r--db/migrate/20190107151029_remove_hipchat_services.rb16
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/services.md39
-rw-r--r--doc/integration/README.md4
-rw-r--r--doc/project_services/hipchat.md1
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/user/index.md4
-rw-r--r--doc/user/project/integrations/hipchat.md53
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--lib/api/helpers/services_helpers.rb40
-rw-r--r--spec/factories/services.rb6
-rw-r--r--spec/features/projects/services/disable_triggers_spec.rb5
-rw-r--r--spec/features/projects/services/user_activates_hipchat_spec.rb38
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/project.json22
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb408
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--vendor/licenses.csv1
27 files changed, 953 insertions, 48 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 97e39ce99cb..77ad4753c84 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -268,6 +268,7 @@ Rails/Presence:
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb'
- 'app/models/concerns/token_authenticatable.rb'
+ - 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/jira_service.rb'
- 'app/models/project_services/kubernetes_service.rb'
diff --git a/Gemfile b/Gemfile
index 6052018754a..1c7ad5abcb5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -201,6 +201,9 @@ gem 'connection_pool', '~> 2.0'
# Discord integration
gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
+# HipChat integration
+gem 'hipchat', '~> 1.5.0'
+
# JIRA integration
gem 'jira-ruby', '~> 1.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index b522aa85b39..3314a769949 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -366,6 +366,9 @@ GEM
hashie (>= 3.0)
health_check (2.6.0)
rails (>= 4.0)
+ hipchat (1.5.2)
+ httparty
+ mimemagic
html-pipeline (2.8.4)
activesupport (>= 2)
nokogiri (>= 1.4)
@@ -1040,6 +1043,7 @@ DEPENDENCIES
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
+ hipchat (~> 1.5.0)
html-pipeline (~> 2.8)
html2text
httparty (~> 0.16.4)
diff --git a/app/models/project.rb b/app/models/project.rb
index 66fc83113ea..cb9b2a02eef 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -146,6 +146,7 @@ class Project < ApplicationRecord
has_one :pipelines_email_service
has_one :irker_service
has_one :pivotaltracker_service
+ has_one :hipchat_service
has_one :flowdock_service
has_one :assembla_service
has_one :asana_service
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
new file mode 100644
index 00000000000..a69b7b4c4b6
--- /dev/null
+++ b/app/models/project_services/hipchat_service.rb
@@ -0,0 +1,311 @@
+# frozen_string_literal: true
+
+class HipchatService < Service
+ include ActionView::Helpers::SanitizeHelper
+
+ MAX_COMMITS = 3
+ HIPCHAT_ALLOWED_TAGS = %w[
+ a b i strong em br img pre code
+ table th tr td caption colgroup col thead tbody tfoot
+ ul ol li dl dt dd
+ ].freeze
+
+ prop_accessor :token, :room, :server, :color, :api_version
+ boolean_accessor :notify_only_broken_pipelines, :notify
+ validates :token, presence: true, if: :activated?
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_pipelines = true
+ end
+ end
+
+ def title
+ 'HipChat'
+ end
+
+ def description
+ 'Private group chat and IM'
+ end
+
+ def self.to_param
+ 'hipchat'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: 'Room token', required: true },
+ { type: 'text', name: 'room', placeholder: 'Room name or ID' },
+ { type: 'checkbox', name: 'notify' },
+ { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
+ { type: 'text', name: 'api_version',
+ placeholder: 'Leave blank for default (v2)' },
+ { type: 'text', name: 'server',
+ placeholder: 'Leave blank for default. https://hipchat.example.com' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ message = create_message(data)
+ return unless message.present?
+
+ gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result }
+ end
+
+ private
+
+ def gate
+ options = { api_version: api_version.present? ? api_version : 'v2' }
+ options[:server_url] = server unless server.blank?
+ @gate ||= HipChat::Client.new(token, options)
+ end
+
+ def message_options(data = nil)
+ { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
+ end
+
+ def create_message(data)
+ object_kind = data[:object_kind]
+
+ case object_kind
+ when "push", "tag_push"
+ create_push_message(data)
+ when "issue"
+ create_issue_message(data) unless update?(data)
+ when "merge_request"
+ create_merge_request_message(data) unless update?(data)
+ when "note"
+ create_note_message(data)
+ when "pipeline"
+ create_pipeline_message(data) if should_pipeline_be_notified?(data)
+ end
+ end
+
+ def render_line(text)
+ markdown(text.lines.first.chomp, pipeline: :single_line) if text
+ end
+
+ def create_push_message(push)
+ ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
+ ref = Gitlab::Git.ref_name(push[:ref])
+
+ before = push[:before]
+ after = push[:after]
+
+ message = []
+ message << "#{push[:user_name]} "
+
+ if Gitlab::Git.blank_ref?(before)
+ message << "pushed new #{ref_type} <a href=\""\
+ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\
+ " to #{project_link}\n"
+ elsif Gitlab::Git.blank_ref?(after)
+ message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
+ else
+ message << "pushed to #{ref_type} <a href=\""\
+ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
+ message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> "
+ message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
+
+ push[:commits].take(MAX_COMMITS).each do |commit|
+ message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
+ end
+
+ if push[:commits].count > MAX_COMMITS
+ message << "<br />... #{push[:commits].count - MAX_COMMITS} more commits"
+ end
+ end
+
+ message.join
+ end
+
+ def markdown(text, options = {})
+ return "" unless text
+
+ context = {
+ project: project,
+ pipeline: :email
+ }
+
+ Banzai.render(text, context)
+
+ context.merge!(options)
+
+ html = Banzai.render_and_post_process(text, context)
+ sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
+
+ sanitized_html.truncate(200, separator: ' ', omission: '...')
+ end
+
+ def create_issue_message(data)
+ user_name = data[:user][:name]
+
+ obj_attr = data[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ title = render_line(obj_attr[:title])
+ state = obj_attr[:state]
+ issue_iid = obj_attr[:iid]
+ issue_url = obj_attr[:url]
+ description = obj_attr[:description]
+
+ issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>"
+
+ message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"]
+ message << "<pre>#{markdown(description)}</pre>"
+
+ message.join
+ end
+
+ def create_merge_request_message(data)
+ user_name = data[:user][:name]
+
+ obj_attr = data[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ merge_request_id = obj_attr[:iid]
+ state = obj_attr[:state]
+ description = obj_attr[:description]
+ title = render_line(obj_attr[:title])
+
+ merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
+ merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
+ message = ["#{user_name} #{state} #{merge_request_link} in " \
+ "#{project_link}: <b>#{title}</b>"]
+
+ message << "<pre>#{markdown(description)}</pre>"
+ message.join
+ end
+
+ def format_title(title)
+ "<b>#{render_line(title)}</b>"
+ end
+
+ def create_note_message(data)
+ data = HashWithIndifferentAccess.new(data)
+ user_name = data[:user][:name]
+
+ obj_attr = HashWithIndifferentAccess.new(data[:object_attributes])
+ note = obj_attr[:note]
+ note_url = obj_attr[:url]
+ noteable_type = obj_attr[:noteable_type]
+ commit_id = nil
+
+ case noteable_type
+ when "Commit"
+ commit_attr = HashWithIndifferentAccess.new(data[:commit])
+ commit_id = commit_attr[:id]
+ subject_desc = commit_id
+ subject_desc = Commit.truncate_sha(subject_desc)
+ subject_type = "commit"
+ title = format_title(commit_attr[:message])
+ when "Issue"
+ subj_attr = HashWithIndifferentAccess.new(data[:issue])
+ subject_id = subj_attr[:iid]
+ subject_desc = "##{subject_id}"
+ subject_type = "issue"
+ title = format_title(subj_attr[:title])
+ when "MergeRequest"
+ subj_attr = HashWithIndifferentAccess.new(data[:merge_request])
+ subject_id = subj_attr[:iid]
+ subject_desc = "!#{subject_id}"
+ subject_type = "merge request"
+ title = format_title(subj_attr[:title])
+ when "Snippet"
+ subj_attr = HashWithIndifferentAccess.new(data[:snippet])
+ subject_id = subj_attr[:id]
+ subject_desc = "##{subject_id}"
+ subject_type = "snippet"
+ title = format_title(subj_attr[:title])
+ end
+
+ subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>"
+ message = ["#{user_name} commented on #{subject_html} in #{project_link}: "]
+ message << title
+
+ message << "<pre>#{markdown(note, ref: commit_id)}</pre>"
+ message.join
+ end
+
+ def create_pipeline_message(data)
+ pipeline_attributes = data[:object_attributes]
+ pipeline_id = pipeline_attributes[:id]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ ref = pipeline_attributes[:ref]
+ user_name = (data[:user] && data[:user][:name]) || 'API'
+ status = pipeline_attributes[:status]
+ duration = pipeline_attributes[:duration]
+
+ branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
+ pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
+
+ "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ end
+
+ def message_color(data)
+ pipeline_status_color(data) || color || 'yellow'
+ end
+
+ def pipeline_status_color(data)
+ return unless data && data[:object_kind] == 'pipeline'
+
+ case data[:object_attributes][:status]
+ when 'success'
+ 'green'
+ else
+ 'red'
+ end
+ end
+
+ def project_name
+ project.full_name.gsub(/\s/, '')
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def project_link
+ "<a href=\"#{project_url}\">#{project_name}</a>"
+ end
+
+ def update?(data)
+ data[:object_attributes][:action] == 'update'
+ end
+
+ def humanized_status(status)
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index c6d5eb353dc..de549becf71 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -255,6 +255,7 @@ class Service < ApplicationRecord
external_wiki
flowdock
hangouts_chat
+ hipchat
irker
jira
kubernetes
diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb
new file mode 100644
index 00000000000..aec265312bb
--- /dev/null
+++ b/config/initializers/hipchat_client_patch.rb
@@ -0,0 +1,14 @@
+# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
+module HipChat
+ class Client
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class Room
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class User
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+end
diff --git a/db/migrate/20190107151020_add_services_type_index.rb b/db/migrate/20190107151020_add_services_type_index.rb
deleted file mode 100644
index 26b5bd58750..00000000000
--- a/db/migrate/20190107151020_add_services_type_index.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class AddServicesTypeIndex < ActiveRecord::Migration[5.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- add_concurrent_index :services, :type unless index_exists?(:services, :type)
- end
-
- def down
- remove_concurrent_index :services, :type if index_exists?(:services, :type)
- end
-end
diff --git a/db/migrate/20190107151029_remove_hipchat_services.rb b/db/migrate/20190107151029_remove_hipchat_services.rb
deleted file mode 100644
index 4741ec88907..00000000000
--- a/db/migrate/20190107151029_remove_hipchat_services.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
-class RemoveHipchatServices < ActiveRecord::Migration[5.0]
- DOWNTIME = false
-
- def up
- execute "DELETE FROM services WHERE type = 'HipchatService'"
- end
-
- def down
- # no-op
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index d1b3672725d..df47f988901 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1996,7 +1996,6 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.boolean "confidential_note_events", default: true
t.index ["project_id"], name: "index_services_on_project_id", using: :btree
t.index ["template"], name: "index_services_on_template", using: :btree
- t.index ["type"], name: "index_services_on_type", using: :btree
end
create_table "shards", force: :cascade do |t|
diff --git a/doc/api/services.md b/doc/api/services.md
index 1f84e2de7de..e8ae7ff78f4 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -449,6 +449,45 @@ Get Hangouts Chat service settings for a project.
GET /projects/:id/services/hangouts-chat
```
+## HipChat
+
+Private group chat and IM
+
+### Create/Edit HipChat service
+
+Set HipChat service for a project.
+
+```
+PUT /projects/:id/services/hipchat
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `token` | string | true | Room token |
+| `color` | string | false | The room color |
+| `notify` | boolean | false | Enable notifications |
+| `room` | string | false |Room name or ID |
+| `api_version` | string | false | Leave blank for default (v2) |
+| `server` | string | false | Leave blank for default. For example, `https://hipchat.example.com`. |
+
+### Delete HipChat service
+
+Delete HipChat service for a project.
+
+```
+DELETE /projects/:id/services/hipchat
+```
+
+### Get HipChat service settings
+
+Get HipChat service settings for a project.
+
+```
+GET /projects/:id/services/hipchat
+```
+
## Irker (IRC gateway)
Send IRC messages, on update, to a list of recipients through an Irker gateway.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index f5bc0693b84..a539933f223 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -29,8 +29,8 @@ See the documentation below for details on how to configure these services.
## Project services
-Integration with services such as Campfire, Flowdock, Pivotal Tracker, and Slack
-are available in the form of a [Project Service][].
+Integration with services such as Campfire, Flowdock, HipChat,
+Pivotal Tracker, and Slack are available in the form of a [Project Service][].
[Project Service]: ../user/project/integrations/project_services.md
diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md
new file mode 100644
index 00000000000..4ae9f6c6b2e
--- /dev/null
+++ b/doc/project_services/hipchat.md
@@ -0,0 +1 @@
+This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md).
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 254e234a22c..0af2f8d2f54 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -41,7 +41,7 @@ Objects (usually binary and large) created by a build process. These can include
### Atlassian
-A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Confluence, Bamboo.
+A [company](https://www.atlassian.com) that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo.
### Audit Log
diff --git a/doc/user/index.md b/doc/user/index.md
index 626246447f3..8164b31c37e 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -65,9 +65,7 @@ With GitLab Enterprise Edition, you can also:
- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
- Leverage continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html).
-You can also [integrate](project/integrations/project_services.md) GitLab with
-numerous third-party applications, such as Mattermost, Microsoft Teams, Trello,
-Slack, Bamboo CI, JIRA, and a lot more.
+You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more.
## Projects
diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md
new file mode 100644
index 00000000000..0fd847d415f
--- /dev/null
+++ b/doc/user/project/integrations/hipchat.md
@@ -0,0 +1,53 @@
+# Atlassian HipChat
+
+GitLab provides a way to send HipChat notifications upon a number of events,
+such as when a user pushes code, creates a branch or tag, adds a comment, and
+creates a merge request.
+
+## Setup
+
+GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
+not supported at this time. Note the differences between v1 and v2 tokens:
+
+HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
+token is allowed to send messages to *any* room.
+
+HipChat v2 API has tokens that are can be created using the Integrations tab
+in the Group or Room admin page. By design, these are lightweight tokens that
+allow GitLab to send messages only to *one* room.
+
+### Complete these steps in HipChat
+
+1. Go to: <https://admin.hipchat.com/admin>
+1. Click on "Group Admin" -> "Integrations".
+1. Find "Build Your Own!" and click "Create".
+1. Select the desired room, name the integration "GitLab", and click "Create".
+1. In the "Send messages to this room by posting this URL" column, you should
+see a URL in the format:
+
+```
+https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+```
+
+HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "HipChat".
+1. Select the "Active" checkbox.
+1. Insert the `token` field from the URL into the `Token` field on the Web page.
+1. Insert the `room` field from the URL into the `Room` field on the Web page.
+1. Save or optionally click "Test Settings".
+
+## Troubleshooting
+
+If you do not see notifications, make sure you are using a HipChat v2 API
+token, not a v1 token.
+
+Note that the v2 token is tied to a specific room. If you want to be able to
+specify arbitrary rooms, you can create an API token for a specific user in
+HipChat under "Account settings" and "API access". Use the `XXX` value under
+`auth_token=XXX`.
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index e2f23827360..42c7824a125 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -36,6 +36,7 @@ Click on the service links to see further configuration instructions and details
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
| [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat |
+| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 8582c45798f..953be7f3798 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
# frozen_string_literal: true
module API
@@ -386,6 +387,44 @@ module API
},
chat_notification_events
].flatten,
+ 'hipchat' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The room token'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'The room name or ID'
+ },
+ {
+ required: false,
+ name: :color,
+ type: String,
+ desc: 'The room color'
+ },
+ {
+ required: false,
+ name: :notify,
+ type: Boolean,
+ desc: 'Enable notifications'
+ },
+ {
+ required: false,
+ name: :api_version,
+ type: String,
+ desc: 'Leave blank for default (v2)'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'Leave blank for default. https://hipchat.example.com'
+ }
+ ],
'irker' => [
{
required: true,
@@ -690,6 +729,7 @@ module API
::ExternalWikiService,
::FlowdockService,
::HangoutsChatService,
+ ::HipchatService,
::IrkerService,
::JiraService,
::KubernetesService,
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 70c34f8640b..0d8c26a2ee9 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -62,4 +62,10 @@ FactoryBot.define do
project_key: 'jira-key'
)
end
+
+ factory :hipchat_service do
+ project
+ type 'HipchatService'
+ token 'test_token'
+ end
end
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb
index 65b597da269..1a13fe03a67 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/services/disable_triggers_spec.rb
@@ -14,11 +14,10 @@ describe 'Disable individual triggers' do
end
context 'service has multiple supported events' do
- let(:service_name) { 'JIRA' }
+ let(:service_name) { 'HipChat' }
it 'shows trigger checkboxes' do
- event_count = JiraService.supported_events.count
- expect(event_count).to be > 1
+ event_count = HipchatService.supported_events.count
expect(page).to have_content "Trigger"
expect(page).to have_css(checkbox_selector, count: event_count)
diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb
new file mode 100644
index 00000000000..2f5313c91f9
--- /dev/null
+++ b/spec/features/projects/services/user_activates_hipchat_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'User activates HipChat' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('HipChat')
+ end
+
+ context 'with standart settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+
+ context 'with custom settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab_custom')
+ fill_in('Token', with: 'secretCustom')
+ fill_in('Server', with: 'https://chat.example.com')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index b0a838a7d2b..e9c8cf0fe34 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -14,6 +14,7 @@ describe 'User views services' do
it 'shows the list of available services' do
expect(page).to have_content('Project services')
expect(page).to have_content('Campfire')
+ expect(page).to have_content('HipChat')
expect(page).to have_content('Assembla')
expect(page).to have_content('Pushover')
expect(page).to have_content('Atlassian Bamboo')
@@ -21,7 +22,5 @@ describe 'User views services' do
expect(page).to have_content('Asana')
expect(page).to have_content('Irker (IRC gateway)')
expect(page).to have_content('Packagist')
- expect(page).to have_content('Mattermost')
- expect(page).to have_content('Slack')
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ed557ffd4e3..54369ff75f4 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -223,6 +223,7 @@ project:
- packagist_service
- pivotaltracker_service
- prometheus_service
+- hipchat_service
- flowdock_service
- assembla_service
- asana_service
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 773651dd226..4a7accc4c52 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6795,6 +6795,28 @@
"wiki_page_events": true
},
{
+ "id": 93,
+ "title": "HipChat",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.219Z",
+ "updated_at": "2016-06-14T15:01:51.219Z",
+ "active": false,
+ "properties": {
+ "notify_only_broken_pipelines": true
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "pipeline_events": true,
+ "type": "HipchatService",
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
"id": 91,
"title": "Flowdock",
"project_id": 5,
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
new file mode 100644
index 00000000000..b0fd2ceead0
--- /dev/null
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -0,0 +1,408 @@
+require 'spec_helper'
+
+describe HipchatService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:token) }
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe "Execute" do
+ let(:hipchat) { described_class.new }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' }
+ let(:project_name) { project.full_name.gsub(/\s/, '') }
+ let(:token) { 'verySecret' }
+ let(:server_url) { 'https://hipchat.example.com'}
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ before do
+ allow(hipchat).to receive_messages(
+ project_id: project.id,
+ project: project,
+ room: 123456,
+ server: server_url,
+ token: token
+ )
+ WebMock.stub_request(:post, api_url)
+ end
+
+ it 'tests and return errors' do
+ allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
+ result = hipchat.test(push_sample_data)
+
+ expect(result[:success]).to be_falsey
+ expect(result[:result].to_s).to eq('no such room')
+ end
+
+ it 'uses v1 if version is provided' do
+ allow(hipchat).to receive(:api_version).and_return('v1')
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v1',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
+ hipchat.execute(push_sample_data)
+ end
+
+ it 'uses v2 as the version when nothing is provided' do
+ allow(hipchat).to receive(:api_version).and_return('')
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v2',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
+ hipchat.execute(push_sample_data)
+ end
+
+ context 'push events' do
+ it "calls Hipchat API for push events" do
+ hipchat.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a push message" do
+ message = hipchat.send(:create_push_message, push_sample_data)
+
+ push_sample_data[:object_attributes]
+ branch = push_sample_data[:ref].gsub('refs/heads/', '')
+ expect(message).to include("#{user.name} pushed to branch " \
+ "<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>")
+ end
+ end
+
+ context 'tag_push events' do
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build(
+ project,
+ user,
+ Gitlab::Git::BLANK_SHA,
+ '1' * 40,
+ 'refs/tags/test',
+ [])
+ end
+
+ it "calls Hipchat API for tag push events" do
+ hipchat.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a tag push message" do
+ message = hipchat.send(:create_push_message, push_sample_data)
+
+ push_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} pushed new tag " \
+ "<a href=\"#{project.web_url}/commits/test\">test</a> to " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>\n")
+ end
+ end
+
+ context 'issue events' do
+ let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') }
+ let(:issue_service) { Issues::CreateService.new(project, user) }
+ let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
+
+ it "calls Hipchat API for issue events" do
+ hipchat.execute(issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates an issue message" do
+ message = hipchat.send(:create_issue_message, issues_sample_data)
+
+ obj_attr = issues_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} opened " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>Awesome issue</b>" \
+ "<pre><strong>please</strong> fix</pre>")
+ end
+ end
+
+ context 'merge request events' do
+ let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) }
+ let(:merge_service) { MergeRequests::CreateService.new(project, user) }
+ let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
+
+ it "calls Hipchat API for merge requests events" do
+ hipchat.execute(merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a merge request message" do
+ message = hipchat.send(:create_merge_request_message,
+ merge_sample_data)
+
+ obj_attr = merge_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} opened " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>Awesome merge request</b>" \
+ "<pre><strong>please</strong> fix</pre>")
+ end
+ end
+
+ context "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ context 'when commit comment event triggered' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user, project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Hipchat API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ commit_id = Commit.truncate_sha(data[:commit][:id])
+ title = hipchat.send(:format_title, data[:commit][:message])
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "#{title}" \
+ "<pre>a comment on a commit</pre>")
+ end
+ end
+
+ context 'when merge request comment event triggered' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project,
+ target_project: project)
+ end
+
+ let(:merge_request_note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project,
+ note: "merge request **note**")
+ end
+
+ it "calls Hipchat API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ merge_id = data[:merge_request]['iid']
+ title = data[:merge_request]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>merge request <strong>note</strong></pre>")
+ end
+ end
+
+ context 'when issue comment event triggered' do
+ let(:issue) { create(:issue, project: project) }
+ let(:issue_note) do
+ create(:note_on_issue, noteable: issue, project: project,
+ note: "issue **note**")
+ end
+
+ it "calls Hipchat API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ hipchat.execute(data)
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ issue_id = data[:issue]['iid']
+ title = data[:issue]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>issue <strong>note</strong></pre>")
+ end
+
+ context 'with confidential issue' do
+ before do
+ issue.update!(confidential: true)
+ end
+
+ it 'calls Hipchat API with issue comment' do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ hipchat.execute(data)
+
+ message = hipchat.send(:create_message, data)
+
+ expect(message).to include("<pre>issue <strong>note</strong></pre>")
+ end
+ end
+ end
+
+ context 'when snippet comment event triggered' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:snippet_note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project,
+ note: "snippet note")
+ end
+
+ it "calls Hipchat API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ snippet_id = data[:snippet]['id']
+ title = data[:snippet]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>snippet note</pre>")
+ end
+ end
+ end
+
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ context 'for failed' do
+ before do
+ pipeline.drop
+ end
+
+ it "calls Hipchat API" do
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a build message" do
+ message = hipchat.__send__(:create_pipeline_message, data)
+
+ project_url = project.web_url
+ project_name = project.full_name.gsub(/\s/, '')
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
+
+ expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
+ "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
+ "by #{user_name} failed in #{duration} second(s)")
+ end
+ end
+
+ context 'for succeeded' do
+ before do
+ pipeline.succeed
+ end
+
+ it "calls Hipchat API" do
+ hipchat.notify_only_broken_pipelines = false
+ hipchat.execute(data)
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "notifies only broken" do
+ hipchat.notify_only_broken_pipelines = true
+ hipchat.execute(data)
+ expect(WebMock).not_to have_requested(:post, api_url).once
+ end
+ end
+ end
+
+ context "#message_options" do
+ it "is set to the defaults" do
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' })
+ end
+
+ it "sets notify to true" do
+ allow(hipchat).to receive(:notify).and_return('1')
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' })
+ end
+
+ it "sets the color" do
+ allow(hipchat).to receive(:color).and_return('red')
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' })
+ end
+
+ context 'with a successful build' do
+ it 'uses the green color' do
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
+
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
+ end
+ end
+
+ context 'with a failed build' do
+ it 'uses the red color' do
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
+
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
+ end
+ end
+ end
+ end
+
+ context 'with UrlBlocker' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:hipchat) { described_class.new(project: project) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+
+ describe '#execute' do
+ before do
+ hipchat.server = 'http://localhost:9123'
+ end
+
+ it 'raises UrlBlocker for localhost' do
+ expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original
+ expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 7f8d2ff91fd..9f6a0b53281 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -44,6 +44,7 @@ describe Project do
it { is_expected.to have_one(:pipelines_email_service) }
it { is_expected.to have_one(:irker_service) }
it { is_expected.to have_one(:pivotaltracker_service) }
+ it { is_expected.to have_one(:hipchat_service) }
it { is_expected.to have_one(:flowdock_service) }
it { is_expected.to have_one(:assembla_service) }
it { is_expected.to have_one(:slack_slash_commands_service) }
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index de6e32cb998..0c52cb5a947 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -520,6 +520,7 @@ hashie-forbidden_attributes,0.1.1,MIT
he,1.1.1,MIT
health_check,2.6.0,MIT
highlight.js,9.13.1,New BSD
+hipchat,1.5.2,MIT
hmac-drbg,1.0.1,MIT
hoopy,0.1.4,MIT
html-pipeline,2.8.4,MIT