diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d9d4e2809..dd5841a0c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix extra space on Build sidebar on Firefox !7060 - Fix mobile layout issues in admin user overview page !7087 - Fix HipChat notifications rendering (airatshigapov, eisnerd) + - Refactor Jira service to use jira-ruby gem - Add hover to trash icon in notes !7008 (blackst0ne) - Fix sidekiq stats in admin area (blackst0ne) - Removed delete branch tooltip !6954 diff --git a/Gemfile b/Gemfile index b810f6e8685..5f16cc063e2 100644 --- a/Gemfile +++ b/Gemfile @@ -161,6 +161,9 @@ gem 'connection_pool', '~> 2.0' # HipChat integration gem 'hipchat', '~> 1.5.0' +# JIRA integration +gem 'jira-ruby', '~> 1.1.2' + # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 536435d3cf6..f8116e1894f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -356,6 +356,9 @@ GEM cause json ipaddress (0.8.3) + jira-ruby (1.1.2) + activesupport + oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) @@ -421,7 +424,7 @@ GEM mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) numerizer (0.1.1) - oauth (0.4.7) + oauth (0.5.1) oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) @@ -881,6 +884,7 @@ DEPENDENCIES html-pipeline (~> 1.11.0) httparty (~> 0.13.3) influxdb (~> 0.2) + jira-ruby (~> 1.1.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) jquery-turbolinks (~> 2.1.0) diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 4cb3be41064..c33d7eecb9f 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -18,7 +18,7 @@ module ServiceParams :add_pusher, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id] + :jira_issue_transition_id, :url, :project_key] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 81af55aa29a..338e685339a 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,4 +1,6 @@ class BugzillaService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index d9fba3d4a41..b2f426dc2ac 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,4 +1,6 @@ class CustomIssueTrackerService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 5d17c358330..6bd8d4ec568 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,6 +1,8 @@ class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing.url_helpers + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url default_value_for :default, true diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index b26ddd518d7..207bb816ad1 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,6 +1,4 @@ class IssueTrackerService < Service - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? - default_value_for :category, 'issue_tracker' # Pattern used to extract links from comments @@ -38,18 +36,24 @@ class IssueTrackerService < Service ] end - def initialize_properties - if properties.nil? - if enabled_in_gitlab_config + # Initialize with default properties values + # or receive a block with custom properties + def initialize_properties(&block) + return unless properties.nil? + + if enabled_in_gitlab_config + if block_given? + yield + else self.properties = { title: issues_tracker['title'], project_url: issues_tracker['project_url'], issues_url: issues_tracker['issues_url'], new_issue_url: issues_tracker['new_issue_url'] } - else - self.properties = {} end + else + self.properties = {} end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f81b66fd219..5bcf199d468 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,15 +1,32 @@ +# == 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 +# build_events :boolean default(FALSE), not null +# + class JiraService < IssueTrackerService - include HTTParty include Gitlab::Routing.url_helpers - DEFAULT_API_VERSION = 2 + validates :url, url: true, presence: true, if: :activated? + validates :project_key, presence: true, if: :activated? - prop_accessor :username, :password, :api_url, :jira_issue_transition_id, - :title, :description, :project_url, :issues_url, :new_issue_url - - validates :api_url, presence: true, url: true, if: :activated? - - before_validation :set_api_url, :set_jira_issue_transition_id + prop_accessor :username, :password, :url, :project_key, + :jira_issue_transition_id, :title, :description before_update :reset_password @@ -18,16 +35,46 @@ class JiraService < IssueTrackerService @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end + def initialize_properties + super do + self.properties = { + title: issues_tracker['title'], + url: issues_tracker['url'] + } + end + end + def reset_password # don't reset the password if a new one is provided - if api_url_changed? && !password_touched? + if url_changed? && !password_touched? self.password = nil end end + def options + url = URI.parse(self.url) + + { + username: self.username, + password: self.password, + site: URI.join(url, '/').to_s, + context_path: url.path, + auth_type: :basic, + read_timeout: 120, + use_ssl: url.scheme == 'https' + } + end + + def client + @client ||= JIRA::Client.new(options) + end + + def jira_project + @jira_project ||= client.Project.find(project_key) + end + def help - 'Setting `project_url`, `issues_url` and `new_issue_url` will '\ - 'allow a user to easily navigate to the Jira issue tracker. See the '\ + 'See the ' \ '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\ 'for details.' end @@ -53,12 +100,26 @@ class JiraService < IssueTrackerService end def fields - super.push( - { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' }, + [ + { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' }, + { type: 'text', name: 'project_key', placeholder: 'Project Key' }, { type: 'text', name: 'username', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' }, { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } - ) + ] + end + + # URLs to redirect from Gitlab issues pages to jira issue tracker + def project_url + "#{url}/issues/?jql=project=#{project_key}" + end + + def issues_url + "#{url}/browse/:id" + end + + def new_issue_url + "#{url}/secure/CreateIssue.jspa" end def execute(push, issue = nil) @@ -72,7 +133,7 @@ class JiraService < IssueTrackerService end def create_cross_reference_note(mentioned, noteable, author) - issue_name = mentioned.id + issue_key = mentioned.id project = self.project noteable_name = noteable.class.name.underscore.downcase noteable_id = if noteable.is_a?(Commit) @@ -99,58 +160,28 @@ class JiraService < IssueTrackerService } } - add_comment(data, issue_name) + add_comment(data, issue_key) end def test_settings - return unless api_url.present? - result = JiraService.get( - jira_api_test_url, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) + return unless url.present? + # Test settings by getting the project + jira_project - case result.code - when 201, 200 - Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.") - true - else - Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}") - false - end - rescue Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}." + rescue Errno::ECONNREFUSED, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}." false end private - def build_api_url_from_project_url - server = URI(project_url) - default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port]) - server_url = "#{server.scheme}://#{server.host}" - server_url.concat(":#{server.port}") unless default_ports - "#{server_url}/rest/api/#{DEFAULT_API_VERSION}" - rescue - "" # looks like project URL was not valid - end - - def set_api_url - self.api_url = build_api_url_from_project_url if self.api_url.blank? - end - - def set_jira_issue_transition_id - self.jira_issue_transition_id ||= "2" - end - def close_issue(entity, issue) commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) entity.diff_head_sha end + commit_url = build_entity_url(:commit, commit_id) # Depending on the JIRA project's workflow, a comment during transition @@ -161,24 +192,16 @@ class JiraService < IssueTrackerService end def transition_issue(issue) - message = { - transition: { - id: jira_issue_transition_id - } - } - send_message(close_issue_url(issue.iid), message.to_json) + issue = client.Issue.find(issue.iid) + issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end def add_issue_solved_comment(issue, commit_id, commit_url) - comment = { - body: "Issue solved with [#{commit_id}|#{commit_url}]." - } - - send_message(comment_url(issue.iid), comment.to_json) + comment = "Issue solved with [#{commit_id}|#{commit_url}]." + send_message(issue.iid, comment) end - def add_comment(data, issue_name) - url = comment_url(issue_name) + def add_comment(data, issue_key) user_name = data[:user][:name] user_url = data[:user][:url] entity_name = data[:entity][:name] @@ -186,68 +209,31 @@ class JiraService < IssueTrackerService entity_title = data[:entity][:title] project_name = data[:project][:name] - message = { - body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'} - } + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" - unless existing_comment?(issue_name, message[:body]) - send_message(url, message.to_json) + unless comment_exists?(issue_key, message) + send_message(issue_key, message) end end - def auth - require 'base64' - Base64.urlsafe_encode64("#{self.username}:#{self.password}") + def comment_exists?(issue_key, message) + comments = client.Issue.find(issue_key).comments + comments.map { |comment| comment.body.include?(message) }.any? end - def send_message(url, message) - return unless api_url.present? - result = JiraService.post( - url, - body: message, - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) + def send_message(issue_key, message) + return unless url.present? - message = case result.code - when 201, 200, 204 - "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}." - when 401 - "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again." - else - "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}" - end + issue = client.Issue.find(issue_key) - Rails.logger.info(message) - message - rescue URI::InvalidURIError, Errno::ECONNREFUSED => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}." - end - - def existing_comment?(issue_name, new_comment) - return unless api_url.present? - result = JiraService.get( - comment_url(issue_name), - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Basic #{auth}" - } - ) - - case result.code - when 201, 200 - existing_comments = JSON.parse(result.body)['comments'] - - if existing_comments.present? - return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any? - end + if issue.comments.build.save!(body: message) + result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." end - false - rescue JSON::ParserError - false + Rails.logger.info(result_message) + result_message + rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" end def resource_url(resource) @@ -267,16 +253,4 @@ class JiraService < IssueTrackerService ) ) end - - def close_issue_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/transitions" - end - - def comment_url(issue_name) - "#{self.api_url}/issue/#{issue_name}/comment" - end - - def jira_api_test_url - "#{self.api_url}/myself" - end end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f634e0772c0..f9da273cf08 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,4 +1,6 @@ class RedmineService < IssueTrackerService + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 114ceac8e1f..3451b68cea5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -547,6 +547,10 @@ test: project_url: "http://redmine/projects/:issues_tracker_id" issues_url: "http://redmine/:project_id/:issues_tracker_id/:id" new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" + jira: + title: "JIRA" + url: https://sample_company.atlasian.net + project_key: PROJECT ldap: enabled: false servers: diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb new file mode 100644 index 00000000000..319d86ac159 --- /dev/null +++ b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb @@ -0,0 +1,10 @@ +class RemoveInactiveJiraServiceProperties < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = "Removes all inactive jira_service properties" + + def up + execute("UPDATE services SET properties = '{}' WHERE services.type = 'JiraService' and services.active = false") + end +end diff --git a/db/migrate/20161025231710_migrate_jira_to_gem.rb b/db/migrate/20161025231710_migrate_jira_to_gem.rb new file mode 100644 index 00000000000..870b00411d2 --- /dev/null +++ b/db/migrate/20161025231710_migrate_jira_to_gem.rb @@ -0,0 +1,73 @@ +class MigrateJiraToGem < ActiveRecord::Migration + DOWNTIME = true + + DOWNTIME_REASON = <<-HEREDOC + Refactor all Jira services properties(serialized field) to use new jira-ruby gem. + There were properties on old Jira service that are not needed anymore after the + service refactoring: api_url, project_url, new_issue_url, issues_url. + We extract the new necessary some properties from old keys and delete them: + taking project_key from project_url and url from api_url + HEREDOC + + def up + active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true" + + select_all(active_services_query).each do |service| + id = service['id'] + properties = JSON.parse(service['properties']) + properties_was = properties.clone + + # Migrate `project_url` to `project_key` + # Ignore if `project_url` doesn't have jql project query with project key + if properties['project_url'].present? + jql = properties['project_url'].match('project=([A-Za-z]*)') + properties['project_key'] = jql.captures.first if jql + end + + # Migrate `api_url` to `url` + if properties['api_url'].present? + url = properties['api_url'].match('(.*)\/rest\/api') + properties['url'] = url.captures.first if url + end + + # Delete now unnecessary properties + properties.delete('api_url') + properties.delete('project_url') + properties.delete('new_issue_url') + properties.delete('issues_url') + + # Update changes properties + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end + + def down + active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true" + + select_all(active_services_query).each do |service| + id = service['id'] + properties = JSON.parse(service['properties']) + properties_was = properties.clone + + # Rebuild old properties based on sane defaults + if properties['url'].present? + properties['api_url'] = "#{properties['url']}/rest/api/2" + properties['project_url'] = + "#{properties['url']}/issues/?jql=project=#{properties['project_key']}" + properties['issues_url'] = "#{properties['url']}/browse/:id" + properties['new_issue_url'] = "#{properties['url']}/secure/CreateIssue.jspa" + end + + # Delete the new properties + properties.delete('url') + properties.delete('project_key') + + # Update changes properties + if properties != properties_was + execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}") + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 02282b0f666..5bbfba0d7dd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161024042317) do +ActiveRecord::Schema.define(version: 20161025231710) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/integration/README.md b/doc/integration/README.md index c2fd299db07..a928b74f9b8 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png similarity index 100% rename from doc/project_services/img/jira_add_user_to_group.png rename to doc/integration/img/jira_add_user_to_group.png diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png similarity index 100% rename from doc/project_services/img/jira_create_new_group.png rename to doc/integration/img/jira_create_new_group.png diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png similarity index 100% rename from doc/project_services/img/jira_create_new_group_name.png rename to doc/integration/img/jira_create_new_group_name.png diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png similarity index 100% rename from doc/project_services/img/jira_create_new_user.png rename to doc/integration/img/jira_create_new_user.png diff --git a/doc/project_services/img/jira_group_access.png b/doc/integration/img/jira_group_access.png similarity index 100% rename from doc/project_services/img/jira_group_access.png rename to doc/integration/img/jira_group_access.png diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png similarity index 100% rename from doc/project_services/img/jira_issue_reference.png rename to doc/integration/img/jira_issue_reference.png diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png new file mode 100644 index 00000000000..b8f6058a514 Binary files /dev/null and b/doc/integration/img/jira_merge_request_close.png differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/integration/img/jira_project_name.png similarity index 100% rename from doc/project_services/img/jira_project_name.png rename to doc/integration/img/jira_project_name.png diff --git a/doc/project_services/img/jira_service.png b/doc/integration/img/jira_service.png similarity index 100% rename from doc/project_services/img/jira_service.png rename to doc/integration/img/jira_service.png diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png similarity index 100% rename from doc/project_services/img/jira_service_close_issue.png rename to doc/integration/img/jira_service_close_issue.png diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png new file mode 100644 index 00000000000..0cc160bebe2 Binary files /dev/null and b/doc/integration/img/jira_service_page.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png similarity index 100% rename from doc/project_services/img/jira_user_management_link.png rename to doc/integration/img/jira_user_management_link.png diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png similarity index 100% rename from doc/project_services/img/jira_workflow_screenshot.png rename to doc/integration/img/jira_workflow_screenshot.png diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 78aa6634116..cf1557ddc44 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1,197 @@ # GitLab JIRA integration -This document was moved under [project_services/jira](../project_services/jira.md). +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Table of Contents +* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) +* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) +* [Configuration](#configuration) + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` + USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: + ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words, eg. +`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and +merge requests. When a commit which contains the trigger word in the commit +message is pushed, GitLab will add a comment in the mentioned JIRA issue. + +For example, for project named `PROJECT` in JIRA, we implemented a new feature +and created a merge request in GitLab. + +This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab +contains the improvement and in merge request description we say that this +merge request `Closes PROJECT-7` issue. + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) + +--- + +## Configuration + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +JIRA configuration in GitLab is done via a project's **Services**. + +#### GitLab 13.0 with JIRA v1000.x + +To enable JIRA integration in a project, navigate to the project's +and open the context menu clicking on the top right gear icon, then go to +**Services > JIRA**. + +Fill in the required details on the page as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | +| `Project key` | The short, all capital letter identifier for your JIRA project. | +| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +#### GitLab 6.x-7.7 with JIRA v6.x + +_**Note:** GitLab versions 13.0 and up contain various integration improvements. +We strongly recommend upgrading._ + +In `gitlab.yml` enable the JIRA issue tracker section by +[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all +issues within GitLab are pointing to the JIRA issue tracker. + +After you set this, you will be able to close issues in JIRA by a commit in +GitLab. + +Go to your project's **Settings** page and fill in the project name for the +JIRA project: + +![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) + +--- + +You can also enable the JIRA service that will allow you to interact with JIRA +issues. Go to the **Settings > Services > JIRA** and: + +1. Tick the active check box to enable the service +1. Supply the URL to JIRA server, for example http://JIRA.example.com +1. Supply the username of a user we created under `Configuring JIRA` section, + for example `gitlab` +1. Supply the password of the user +1. Optional: supply the JIRA API version, default is version `2` +1. Optional: supply the JIRA issue transition ID (issue transition to closed). + This is dependent on JIRA settings, default is `2` +1. Hit save + + +![JIRA services page](img/jira_service.png) + +[services-templates]: ../project_services/services_templates.md +[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png deleted file mode 100644 index aec472b9118..00000000000 Binary files a/doc/project_services/img/jira_add_gitlab_commit_message.png and /dev/null differ diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png deleted file mode 100644 index acdd83702d3..00000000000 Binary files a/doc/project_services/img/jira_issue_closed.png and /dev/null differ diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png deleted file mode 100644 index 0703081d77b..00000000000 Binary files a/doc/project_services/img/jira_issues_workflow.png and /dev/null differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png deleted file mode 100644 index 47785e3ba27..00000000000 Binary files a/doc/project_services/img/jira_merge_request_close.png and /dev/null differ diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png deleted file mode 100644 index fb270d85e3c..00000000000 Binary files a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png deleted file mode 100644 index a5b49c501ba..00000000000 Binary files a/doc/project_services/img/jira_service_page.png and /dev/null differ diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png deleted file mode 100644 index 77630d39d39..00000000000 Binary files a/doc/project_services/img/jira_submit_gitlab_merge_request.png and /dev/null differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index b626c746c79..2ea1c58cb31 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,246 +1 @@ -# GitLab JIRA integration - ->**Note:** -Full JIRA integration was previously exclusive to GitLab Enterprise Edition. -With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] -to GitLab Community Edition as well. - ---- - -GitLab can be configured to interact with [JIRA Core] either using an -on-premises instance or the SaaS solution that Atlassian offers. Configuration -happens via username and password on a per-project basis. Connecting to a JIRA -server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance or, in -case you have a single JIRA instance, you can pre-fill the JIRA service -settings page in GitLab with a default template. To configure the JIRA template, -see the [Services Templates documentation][services-templates]. - -Once the GitLab project is connected to JIRA, you can reference and close the -issues in JIRA directly from GitLab's merge requests. - -## Configuration - -The configuration consists of two parts: - -- [JIRA configuration](#configuring-jira) -- [GitLab configuration](#configuring-gitlab) - -### Configuring JIRA - -First things first, we need to create a user in JIRA which will have access to -all projects that need to integrate with GitLab. - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![JIRA create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - ->**Note:** -The currently supported JIRA versions are v6.x and v7.x. and GitLab -7.8 or higher is required. - ---- - -Assuming you [have already configured JIRA](#configuring-jira), now it's time -to configure GitLab. - -JIRA configuration in GitLab is done via a project's -[**Services**](../project_services/project_services.md). - -To enable JIRA integration in a project, navigate to the project's -**Settings > Services > JIRA**. - -Fill in the required details on the page, as described in the table below. - -| Setting | Description | -| ------- | ----------- | -| `Description` | A name for the issue tracker (to differentiate between instances, for example). | -| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https:///issues/?jql=project=`. | -| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | -| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https:///secure/CreateIssue.jspa` | -| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | -| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -For example, given the settings below: - -- the JIRA URL is `https://jira.example.com` -- the project is named `GITLAB` -- the user is named `gitlab` -- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) - -the following screenshot shows how the JIRA service settings should look like. - -![JIRA service page](img/jira_service_page.png) - -[trans]: img/jira_issues_workflow.png - ---- - -## JIRA issues - -By now you should have [configured JIRA](#configuring-jira) and enabled the -[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference and close JIRA issues by just mentioning their -ID in GitLab commits and merge requests. - -### Referencing JIRA Issues - -If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link -which points back to JIRA is created. - -The same works for comments in merge requests as well. - -![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) - ---- - -The mentioning action is two-fold, so a comment with a JIRA issue in GitLab -will automatically add a comment in that particular JIRA issue with the link -back to GitLab. - - -![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) - ---- - -The comment on the JIRA issue is of the form: - -> USER mentioned this issue in LINK_TO_THE_MENTION - -Where: - -| Format | Description | -| ------ | ----------- | -| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | -| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | - -### Closing JIRA issues - -JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit which contains the trigger word -followed by the JIRA issue ID in the commit message is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it (provided -the transition ID was set up correctly). - -There are currently three trigger words, and you can use either one to achieve -the same goal: - -- `Resolves GITLAB-1` -- `Closes GITLAB-1` -- `Fixes GITLAB-1` - -where `GITLAB-1` the issue ID of the JIRA project. - -### JIRA issue closing example - -Let's say for example that we submitted a bug fix and created a merge request -in GitLab. The workflow would be something like this: - -1. Create a new branch -1. Fix the bug -1. Commit the changes and push branch to GitLab -1. Open a new merge request and reference the JIRA issue including one of the - trigger words, e.g.: `Fixes GITLAB-1`, in the description -1. Submit the merge request -1. Ask someone to review -1. Merge the merge request -1. The JIRA issue is automatically closed - ---- - -In the following screenshot you can see what the link references to the JIRA -issue look like. - -![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) - ---- - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) - ---- - -You can see from the above image that there are four references to GitLab: - -- The first is from a comment in a specific commit -- The second is from the JIRA issue reference in the merge request description -- The third is from the actual commit that solved the issue -- And the fourth is from the commit that the merge request created - -[services-templates]: ../project_services/services_templates.md "Services templates documentation" -[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" -[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" -[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" - -## Troubleshooting - -### GitLab is unable to comment on a ticket - -Make sure that the user you set up for GitLab to communicate with JIRA has the -correct access permission to post comments on a ticket and to also transition the -ticket, if you'd like GitLab to also take care of closing them. - -### GitLab is unable to close a ticket - -Make sure the the `Transition ID` you set within the JIRA settings matches the -one your project needs to close a ticket. +GitLab JIRA integration documentation has moved to [here](../integration/jira.md). diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 4442b7c1742..8116a1ce976 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [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 | +| [JIRA](../integration/jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 536199ddb4f..bd6466f3686 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -178,17 +178,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I fill jira settings' do - fill_in 'Project url', with: 'http://jira.example' + fill_in 'URL', with: 'http://jira.example' fill_in 'Username', with: 'gitlab' fill_in 'Password', with: 'gitlab' - fill_in 'Api url', with: 'http://jira.example/rest/api/2' + fill_in 'Project Key', with: 'GITLAB' click_button 'Save' end step 'I should see jira service settings saved' do - expect(find_field('Project url').value).to eq 'http://jira.example' + expect(find_field('URL').value).to eq 'http://jira.example' expect(find_field('Username').value).to eq 'gitlab' - expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2' + expect(find_field('Project Key').value).to eq 'GITLAB' end step 'I click Atlassian Bamboo CI service link' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 4065e2defbc..dd4a86b1e31 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -118,10 +118,9 @@ FactoryGirl.define do project.create_jira_service( active: true, properties: { - 'title' => 'JIRA tracker', - 'project_url' => 'http://jira.example/issues/?jql=project=A', - 'issues_url' => 'http://jira.example/browse/:id', - 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa' + title: 'JIRA tracker', + url: 'http://jira.example.net', + project_key: 'JIRA' } ) end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 6ff32aea018..a9f637147d1 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -10,23 +10,15 @@ describe JiraService, models: true do context 'when service is active' do before { subject.active = true } - it { is_expected.to validate_presence_of(:api_url) } - it { is_expected.to validate_presence_of(:project_url) } - it { is_expected.to validate_presence_of(:issues_url) } - it { is_expected.to validate_presence_of(:new_issue_url) } - it_behaves_like 'issue tracker service URL attribute', :api_url - it_behaves_like 'issue tracker service URL attribute', :project_url - it_behaves_like 'issue tracker service URL attribute', :issues_url - it_behaves_like 'issue tracker service URL attribute', :new_issue_url + it { is_expected.to validate_presence_of(:url) } + it { is_expected.to validate_presence_of(:project_key) } + it_behaves_like 'issue tracker service URL attribute', :url end context 'when service is inactive' do before { subject.active = false } - it { is_expected.not_to validate_presence_of(:api_url) } - it { is_expected.not_to validate_presence_of(:project_url) } - it { is_expected.not_to validate_presence_of(:issues_url) } - it { is_expected.not_to validate_presence_of(:new_issue_url) } + it { is_expected.not_to validate_presence_of(:url) } end end @@ -50,23 +42,25 @@ describe JiraService, models: true do project_id: project.id, project: project, service_hook: true, - project_url: 'http://jira.example.com', + url: 'http://jira.example.com', username: 'gitlab_jira_username', password: 'gitlab_jira_password' ) - @jira_service.save # will build API URL, as api_url was not specified above - @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) - # https://github.com/bblimke/webmock#request-with-basic-authentication - @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' + + @jira_service.save + + project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' + @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' - WebMock.stub_request(:post, @api_url) + WebMock.stub_request(:get, project_url) + WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) end it "calls JIRA API" do - @jira_service.execute(merge_request, - ExternalIssue.new("JIRA-123", project)) + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + expect(WebMock).to have_requested(:post, @comment_url).with( body: /Issue solved with/ ).once @@ -74,9 +68,9 @@ describe JiraService, models: true do it "calls the api with jira_issue_transition_id" do @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' - @jira_service.execute(merge_request, - ExternalIssue.new("JIRA-123", project)) - expect(WebMock).to have_requested(:post, @api_url).with( + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).to have_requested(:post, @transitions_url).with( body: /this-is-a-custom-id/ ).once end @@ -90,7 +84,7 @@ describe JiraService, models: true do @jira_service = JiraService.create!( project: create(:project), properties: { - api_url: 'http://jira.example.com/rest/api/2', + url: 'http://jira.example.com/rest/api/2', username: 'mic', password: "password" } @@ -98,7 +92,7 @@ describe JiraService, models: true do end it "reset password if url changed" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.save expect(@jira_service.password).to be_nil end @@ -110,16 +104,16 @@ describe JiraService, models: true do end it "does not reset password if new url is set together with password, even if it's the same password" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save expect(@jira_service.password).to eq("password") - expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") end it "resets password if url changed, even if setter called multiple times" do - @jira_service.api_url = 'http://jira1.example.com/rest/api/2' - @jira_service.api_url = 'http://jira1.example.com/rest/api/2' + @jira_service.url = 'http://jira1.example.com/rest/api/2' + @jira_service.url = 'http://jira1.example.com/rest/api/2' @jira_service.save expect(@jira_service.password).to be_nil end @@ -130,18 +124,18 @@ describe JiraService, models: true do @jira_service = JiraService.create( project: create(:project), properties: { - api_url: 'http://jira.example.com/rest/api/2', + url: 'http://jira.example.com/rest/api/2', username: 'mic' } ) end it "saves password if new url is set together with password" do - @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2' + @jira_service.url = 'http://jira_edited.example.com/rest/api/2' @jira_service.password = 'password' @jira_service.save expect(@jira_service.password).to eq("password") - expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2") + expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2") end end end @@ -152,9 +146,7 @@ describe JiraService, models: true do subject.active = true end - it { is_expected.to validate_presence_of :project_url } - it { is_expected.to validate_presence_of :issues_url } - it { is_expected.to validate_presence_of :new_issue_url } + it { is_expected.to validate_presence_of :url } end end @@ -201,9 +193,7 @@ describe JiraService, models: true do settings = { "jira" => { "title" => "Jira", - "project_url" => "http://jira.sample/projects/project_a", - "issues_url" => "http://jira.sample/issues/:id", - "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" + "url" => "http://jira.sample/projects/project_a" } } allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) @@ -215,9 +205,8 @@ describe JiraService, models: true do end it 'is prepopulated with the settings' do - expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a') - expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id") - expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new") + expect(@service.properties["title"]).to eq('Jira') + expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a') end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index ad5170afc21..45bc44ba172 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -451,11 +451,7 @@ describe GitPushService, services: true do # project.create_jira_service doesn't seem to invalidate the cache here project.has_external_issue_tracker = true jira_service_settings - - WebMock.stub_request(:post, jira_api_transition_url) - WebMock.stub_request(:post, jira_api_comment_url) - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - WebMock.stub_request(:get, jira_api_test_url) + stub_jira_urls("JIRA-1") allow(closing_commit).to receive_messages({ issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), @@ -475,9 +471,9 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\nrelated to JIRA-1" } it "initiates one api call to jira server to mention the issue" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, @oldrev, @newrev, @ref) - expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: /mentioned this issue in/ ).once end @@ -485,22 +481,19 @@ describe GitPushService, services: true do context "closing an issue" do let(:message) { "this is some work.\n\ncloses JIRA-1" } - let(:transition_body) { { transition: { id: '2' } }.to_json } let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } context "using right markdown" do it "initiates one api call to jira server to close the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_transition_url).with( - body: transition_body - ).once + expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once end it "initiates one api call to jira server to comment on the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: comment_body ).once end @@ -512,15 +505,13 @@ describe GitPushService, services: true do it "does not initiates one api call to jira server to close the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with( - body: transition_body - ) + expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1')) end it "does not initiates one api call to jira server to comment on the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) - expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with( + expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: comment_body ).once end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b4ba28dfe8e..5bb107fdd85 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -531,53 +531,47 @@ describe SystemNoteService, services: true do include JiraServiceHelper describe 'JIRA integration' do - let(:project) { create(:jira_project) } - let(:author) { create(:user) } - let(:issue) { create(:issue, project: project) } - let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } - let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} - let(:jira_tracker) { project.jira_service } - let(:commit) { project.repository.commits('master').find { |commit| commit.id == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } } + let(:project) { create(:jira_project) } + let(:author) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) } + let(:jira_issue) { ExternalIssue.new("JIRA-1", project)} + let(:jira_tracker) { project.jira_service } + let(:commit) { project.commit } + let(:comment_url) { jira_api_comment_url(jira_issue.id) } + let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." } + + before { stub_jira_urls(jira_issue.id) } context 'in JIRA issue tracker' do - before do - jira_service_settings - WebMock.stub_request(:post, jira_api_comment_url) - end + before { jira_service_settings } describe "new reference" do - before do - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - end - subject { described_class.cross_reference(jira_issue, commit, author) } - it { is_expected.to eq(jira_status_message) } - end - - describe "existing reference" do - before do - message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'} - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]})) - end - - subject { described_class.cross_reference(jira_issue, commit, author) } - it { is_expected.not_to eq(jira_status_message) } + it { is_expected.to eq(success_message) } end end context 'issue from an issue' do context 'in JIRA issue tracker' do - before do - jira_service_settings - WebMock.stub_request(:post, jira_api_comment_url) - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments) - end + before { jira_service_settings } subject { described_class.cross_reference(jira_issue, issue, author) } - it { is_expected.to eq(jira_status_message) } + it { is_expected.to eq(success_message) } end end + + describe "existing reference" do + before do + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'" + allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) + end + + subject { described_class.cross_reference(jira_issue, commit, author) } + + it { is_expected.not_to eq(success_message) } + end end end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index f3ea206f387..96e0dad6b55 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -1,20 +1,17 @@ module JiraServiceHelper + JIRA_URL = "http://jira.example.net" + JIRA_API = JIRA_URL + "/rest/api/2" + def jira_service_settings properties = { - "title" => "JIRA tracker", - "project_url" => "http://jira.example/issues/?jql=project=A", - "issues_url" => "http://jira.example/browse/JIRA-1", - "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa", - "api_url" => "http://jira.example/rest/api/2" + title: "JIRA tracker", + url: JIRA_URL, + project_key: "JIRA" } jira_tracker.update_attributes(properties: properties, active: true) end - def jira_status_message - "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}." - end - def jira_issue_comments "{\"startAt\":0,\"maxResults\":11,\"total\":11, \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\", @@ -52,15 +49,32 @@ module JiraServiceHelper ]}" end - def jira_api_comment_url - 'http://jira.example/rest/api/2/issue/JIRA-1/comment' + def jira_project_url + JIRA_API + "/project/#{jira_tracker.project_key}" end - def jira_api_transition_url - 'http://jira.example/rest/api/2/issue/JIRA-1/transitions' + def jira_api_comment_url(issue_id) + JIRA_API + "/issue/#{issue_id}/comment" + end + + def jira_api_transition_url(issue_id) + JIRA_API + "/issue/#{issue_id}/transitions" end def jira_api_test_url - 'http://jira.example/rest/api/2/myself' + JIRA_API + "/myself" + end + + def jira_issue_url(issue_id) + JIRA_API + "/issue/#{issue_id}" + end + + def stub_jira_urls(issue_id) + WebMock.stub_request(:get, jira_project_url) + WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments) + WebMock.stub_request(:get, jira_issue_url(issue_id)) + WebMock.stub_request(:get, jira_api_test_url) + WebMock.stub_request(:post, jira_api_comment_url(issue_id)) + WebMock.stub_request(:post, jira_api_transition_url(issue_id)) end end