Merge branch 'issue_1008_1' into 'master'
Jira refactoring to jira gem part of gitlab-org/gitlab-ee#1008 Original MR: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2572/commits See merge request !6598
|
@ -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
|
||||
|
|
3
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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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{(?<issue>\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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
73
db/migrate/20161025231710_migrate_jira_to_gem.rb
Normal file
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
BIN
doc/integration/img/jira_merge_request_close.png
Normal file
After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
BIN
doc/integration/img/jira_service_page.png
Normal file
After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 51 KiB |
|
@ -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://<jira_host_url>/issues/?jql=project=<jira_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://<jira_host_url>/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://<jira_host_url>/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://<jira_host_url>/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).
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|