2018-11-20 20:47:30 +05:30
# frozen_string_literal: true
2021-03-11 19:13:27 +05:30
# Accessible as Project#external_issue_tracker
2015-04-26 12:48:37 +05:30
class JiraService < IssueTrackerService
2020-04-08 14:13:33 +05:30
extend :: Gitlab :: Utils :: Override
2017-09-10 17:25:29 +05:30
include Gitlab :: Routing
2018-03-27 19:54:05 +05:30
include ApplicationHelper
include ActionView :: Helpers :: AssetUrlHelper
2020-11-24 15:15:51 +05:30
include Gitlab :: Utils :: StrongMemoize
2015-04-26 12:48:37 +05:30
2020-06-23 00:09:42 +05:30
PROJECTS_PER_PAGE = 50
2020-10-24 23:57:45 +05:30
# TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
DEPLOYMENT_TYPES = {
server : 'SERVER' ,
cloud : 'CLOUD'
} . freeze
2018-11-08 19:23:39 +05:30
validates :url , public_url : true , presence : true , if : :activated?
validates :api_url , public_url : true , allow_blank : true
2018-03-17 18:26:18 +05:30
validates :username , presence : true , if : :activated?
validates :password , presence : true , if : :activated?
2015-12-23 02:04:40 +05:30
2018-11-18 11:00:15 +05:30
validates :jira_issue_transition_id ,
2019-07-31 22:56:46 +05:30
format : { with : Gitlab :: Regex . jira_transition_id_regex , message : s_ ( " JiraService|transition ids can have only numbers which can be split with , or ; " ) } ,
2018-11-18 11:00:15 +05:30
allow_blank : true
2019-09-30 21:07:59 +05:30
# Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
2019-12-04 20:38:33 +05:30
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
# TODO: we can probably just delegate as part of
2019-12-21 20:55:43 +05:30
# https://gitlab.com/gitlab-org/gitlab/issues/29404
2021-03-11 19:13:27 +05:30
data_field :username , :password , :url , :api_url , :jira_issue_transition_id , :project_key , :issues_enabled ,
:vulnerabilities_enabled , :vulnerabilities_issuetype , :proxy_address , :proxy_port , :proxy_username , :proxy_password
2015-12-23 02:04:40 +05:30
before_update :reset_password
2020-11-24 15:15:51 +05:30
after_commit :update_deployment_type , on : [ :create , :update ] , if : :update_deployment_type?
2015-12-23 02:04:40 +05:30
2020-05-24 23:13:21 +05:30
enum comment_detail : {
standard : 1 ,
all_details : 2
}
2018-03-27 19:54:05 +05:30
alias_method :project_url , :url
2018-05-09 12:01:36 +05:30
# When these are false GitLab does not create cross reference
2019-09-30 21:07:59 +05:30
# comments on Jira except when an issue gets transitioned.
2017-08-17 22:00:37 +05:30
def self . supported_events
%w( commit merge_request )
end
2020-01-01 13:55:28 +05:30
def self . supported_event_actions
%w( comment )
end
2016-11-03 12:29:30 +05:30
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
2017-09-10 17:25:29 +05:30
def self . reference_pattern ( only_long : true )
2019-10-12 21:52:04 +05:30
@reference_pattern || = / (?<issue> \ b #{ Gitlab :: Regex . jira_issue_key_regex } ) /
2016-11-03 12:29:30 +05:30
end
2017-08-17 22:00:37 +05:30
def initialize_properties
2019-12-04 20:38:33 +05:30
{ }
end
def data_fields
jira_tracker_data || self . build_jira_tracker_data
2017-08-17 22:00:37 +05:30
end
2015-12-23 02:04:40 +05:30
def reset_password
2019-12-04 20:38:33 +05:30
data_fields . password = nil if reset_password?
end
def set_default_data
return unless issues_tracker . present?
return if url
data_fields . url || = issues_tracker [ 'url' ]
data_fields . api_url || = issues_tracker [ 'api_url' ]
2015-12-23 02:04:40 +05:30
end
2015-04-26 12:48:37 +05:30
2017-08-17 22:00:37 +05:30
def options
2017-09-10 17:25:29 +05:30
url = URI . parse ( client_url )
2017-08-17 22:00:37 +05:30
{
2019-12-21 20:55:43 +05:30
username : username & . strip ,
2019-12-04 20:38:33 +05:30
password : password ,
2019-02-15 15:39:39 +05:30
site : URI . join ( url , '/' ) . to_s , # Intended to find the root
2019-10-12 21:52:04 +05:30
context_path : url . path ,
2017-08-17 22:00:37 +05:30
auth_type : :basic ,
read_timeout : 120 ,
2018-03-17 18:26:18 +05:30
use_cookies : true ,
additional_cookies : [ 'OBBasicAuth=fromDialog' ] ,
2017-08-17 22:00:37 +05:30
use_ssl : url . scheme == 'https'
}
end
def client
2019-09-04 21:01:54 +05:30
@client || = begin
JIRA :: Client . new ( options ) . tap do | client |
# Replaces JIRA default http client with our implementation
client . request_client = Gitlab :: Jira :: HttpClient . new ( client . options )
end
end
2017-08-17 22:00:37 +05:30
end
2015-04-26 12:48:37 +05:30
def help
2019-09-30 21:07:59 +05:30
" You need to configure Jira before enabling this service. For more details
2017-08-17 22:00:37 +05:30
read the
2019-09-30 21:07:59 +05:30
[ Jira service documentation ] ( #{help_page_url('user/project/integrations/jira')})."
2015-04-26 12:48:37 +05:30
end
2020-07-28 23:09:34 +05:30
def title
2019-09-30 21:07:59 +05:30
'Jira'
2015-04-26 12:48:37 +05:30
end
2020-07-28 23:09:34 +05:30
def description
2019-09-30 21:07:59 +05:30
s_ ( 'JiraService|Jira issue tracker' )
2015-04-26 12:48:37 +05:30
end
2017-08-17 22:00:37 +05:30
def self . to_param
2015-04-26 12:48:37 +05:30
'jira'
end
2015-12-23 02:04:40 +05:30
def fields
2021-02-22 17:27:13 +05:30
transition_id_help_path = help_page_path ( 'user/project/integrations/jira' , anchor : 'obtaining-a-transition-id' )
transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">' . html_safe % { transition_id_help_path : transition_id_help_path }
2017-08-17 22:00:37 +05:30
[
2019-07-31 22:56:46 +05:30
{ type : 'text' , name : 'url' , title : s_ ( 'JiraService|Web URL' ) , placeholder : 'https://jira.example.com' , required : true } ,
2019-09-30 21:07:59 +05:30
{ type : 'text' , name : 'api_url' , title : s_ ( 'JiraService|Jira API URL' ) , placeholder : s_ ( 'JiraService|If different from Web URL' ) } ,
2019-07-31 22:56:46 +05:30
{ type : 'text' , name : 'username' , title : s_ ( 'JiraService|Username or Email' ) , placeholder : s_ ( 'JiraService|Use a username for server version and an email for cloud version' ) , required : true } ,
{ type : 'password' , name : 'password' , title : s_ ( 'JiraService|Password or API token' ) , placeholder : s_ ( 'JiraService|Use a password for server version and an API token for cloud version' ) , required : true } ,
2021-02-22 17:27:13 +05:30
{ type : 'text' , name : 'jira_issue_transition_id' , title : s_ ( 'JiraService|Jira workflow transition IDs' ) , placeholder : s_ ( 'JiraService|For example, 12, 24' ) , help : s_ ( 'JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}' . html_safe % { link_start : transition_id_help_link_start , link_end : '</a>' . html_safe } ) }
2017-08-17 22:00:37 +05:30
]
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def issues_url
" #{ url } /browse/:id "
end
def new_issue_url
2020-07-28 23:09:34 +05:30
" #{ url } /secure/CreateIssue!default.jspa "
2017-08-17 22:00:37 +05:30
end
2019-10-12 21:52:04 +05:30
alias_method :original_url , :url
def url
2019-12-21 20:55:43 +05:30
original_url & . delete_suffix ( '/' )
end
alias_method :original_api_url , :api_url
def api_url
original_api_url & . delete_suffix ( '/' )
2019-10-12 21:52:04 +05:30
end
2017-08-17 22:00:37 +05:30
def execute ( push )
# This method is a no-op, because currently JiraService does not
# support any events.
end
2021-03-11 19:13:27 +05:30
def find_issue ( issue_key , rendered_fields : false )
options = { }
options = options . merge ( expand : 'renderedFields' ) if rendered_fields
jira_request { client . Issue . find ( issue_key , options ) }
2021-03-08 18:12:59 +05:30
end
2021-03-11 19:13:27 +05:30
def close_issue ( entity , external_issue , current_user )
2021-03-08 18:12:59 +05:30
issue = find_issue ( external_issue . iid )
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
return if issue . nil? || has_resolution? ( issue ) || ! jira_issue_transition_id . present?
2017-08-17 22:00:37 +05:30
2019-12-21 20:55:43 +05:30
commit_id = case entity
when Commit then entity . id
when MergeRequest then entity . diff_head_sha
2017-08-17 22:00:37 +05:30
end
commit_url = build_entity_url ( :commit , commit_id )
2019-09-30 21:07:59 +05:30
# Depending on the Jira project's workflow, a comment during transition
2017-08-17 22:00:37 +05:30
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
2021-03-08 18:12:59 +05:30
issue = find_issue ( issue . key ) if transition_issue ( issue )
2017-09-10 17:25:29 +05:30
add_issue_solved_comment ( issue , commit_id , commit_url ) if has_resolution? ( issue )
2021-03-11 19:13:27 +05:30
log_usage ( :close_issue , current_user )
2015-12-23 02:04:40 +05:30
end
def create_cross_reference_note ( mentioned , noteable , author )
2017-08-17 22:00:37 +05:30
unless can_cross_reference? ( noteable )
2019-07-31 22:56:46 +05:30
return s_ ( " JiraService|Events for %{noteable_model_name} are disabled. " ) % { noteable_model_name : noteable . model_name . plural . humanize ( capitalize : false ) }
2017-08-17 22:00:37 +05:30
end
2021-03-08 18:12:59 +05:30
jira_issue = find_issue ( mentioned . id )
2017-08-17 22:00:37 +05:30
return unless jira_issue . present?
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
noteable_id = noteable . respond_to? ( :iid ) ? noteable . iid : noteable . id
noteable_type = noteable_name ( noteable )
entity_url = build_entity_url ( noteable_type , noteable_id )
2020-05-24 23:13:21 +05:30
entity_meta = build_entity_meta ( noteable )
2015-12-23 02:04:40 +05:30
data = {
user : {
name : author . name ,
2017-09-10 17:25:29 +05:30
url : resource_url ( user_path ( author ) )
2015-12-23 02:04:40 +05:30
} ,
project : {
2017-09-10 17:25:29 +05:30
name : project . full_path ,
2020-05-24 23:13:21 +05:30
url : resource_url ( project_path ( project ) )
2015-12-23 02:04:40 +05:30
} ,
entity : {
2020-05-24 23:13:21 +05:30
id : entity_meta [ :id ] ,
2017-08-17 22:00:37 +05:30
name : noteable_type . humanize . downcase ,
2016-06-02 11:05:42 +05:30
url : entity_url ,
2020-05-24 23:13:21 +05:30
title : noteable . title ,
description : entity_meta [ :description ] ,
branch : entity_meta [ :branch ]
2015-12-23 02:04:40 +05:30
}
}
2021-03-11 19:13:27 +05:30
add_comment ( data , jira_issue ) . tap { log_usage ( :cross_reference , author ) }
2015-12-23 02:04:40 +05:30
end
2020-06-23 00:09:42 +05:30
def valid_connection?
test ( nil ) [ :success ]
end
2017-08-17 22:00:37 +05:30
def test ( _ )
2020-11-24 15:15:51 +05:30
result = server_info
2017-09-10 17:25:29 +05:30
success = result . present?
2020-03-13 15:44:24 +05:30
result = @error & . message unless success
2015-12-23 02:04:40 +05:30
2017-09-10 17:25:29 +05:30
{ success : success , result : result }
2015-12-23 02:04:40 +05:30
end
2020-04-08 14:13:33 +05:30
override :support_close_issue?
def support_close_issue?
true
end
override :support_cross_reference?
def support_cross_reference?
true
end
2020-03-13 15:44:24 +05:30
private
2020-11-24 15:15:51 +05:30
def server_info
strong_memoize ( :server_info ) do
client_url . present? ? jira_request { client . ServerInfo . all . attrs } : nil
end
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def can_cross_reference? ( noteable )
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
2015-12-23 02:04:40 +05:30
end
2018-11-18 11:00:15 +05:30
# jira_issue_transition_id can have multiple values split by , or ;
# the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence
2015-12-23 02:04:40 +05:30
def transition_issue ( issue )
2018-11-18 11:00:15 +05:30
jira_issue_transition_id . scan ( Gitlab :: Regex . jira_transition_id_regex ) . each do | transition_id |
2019-07-07 11:18:12 +05:30
issue . transitions . build . save! ( transition : { id : transition_id } )
rescue = > error
2020-04-08 14:13:33 +05:30
log_error (
" Issue transition failed " ,
error : {
exception_class : error . class . name ,
exception_message : error . message ,
exception_backtrace : Gitlab :: BacktraceCleaner . clean_backtrace ( error . backtrace )
} ,
client_url : client_url
)
2019-07-07 11:18:12 +05:30
return false
2018-11-18 11:00:15 +05:30
end
2015-12-23 02:04:40 +05:30
end
2021-03-11 19:13:27 +05:30
def log_usage ( action , user )
key = " i_ecosystem_jira_service_ #{ action } "
Gitlab :: UsageDataCounters :: HLLRedisCounter . track_event ( key , values : user . id )
end
2015-12-23 02:04:40 +05:30
def add_issue_solved_comment ( issue , commit_id , commit_url )
2019-07-07 11:18:12 +05:30
link_title = " Solved by commit #{ commit_id } . "
2017-08-17 22:00:37 +05:30
comment = " Issue solved with [ #{ commit_id } | #{ commit_url } ]. "
link_props = build_remote_link_props ( url : commit_url , title : link_title , resolved : true )
send_message ( issue , comment , link_props )
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def add_comment ( data , issue )
entity_name = data [ :entity ] [ :name ]
entity_url = data [ :entity ] [ :url ]
2016-06-02 11:05:42 +05:30
entity_title = data [ :entity ] [ :title ]
2015-12-23 02:04:40 +05:30
2020-05-24 23:13:21 +05:30
message = comment_message ( data )
2019-07-07 11:18:12 +05:30
link_title = " #{ entity_name . capitalize } - #{ entity_title } "
2017-08-17 22:00:37 +05:30
link_props = build_remote_link_props ( url : entity_url , title : link_title )
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
unless comment_exists? ( issue , message )
send_message ( issue , message , link_props )
2015-12-23 02:04:40 +05:30
end
end
2020-05-24 23:13:21 +05:30
def comment_message ( data )
user_link = build_jira_link ( data [ :user ] [ :name ] , data [ :user ] [ :url ] )
entity = data [ :entity ]
entity_ref = all_details? ? " #{ entity [ :name ] } #{ entity [ :id ] } " : " a #{ entity [ :name ] } "
entity_link = build_jira_link ( entity_ref , entity [ :url ] )
project_link = build_jira_link ( project . full_name , Gitlab :: Routing . url_helpers . project_url ( project ) )
branch =
if entity [ :branch ] . present?
s_ ( 'JiraService| on branch %{branch_link}' ) % {
branch_link : build_jira_link ( entity [ :branch ] , project_tree_url ( project , entity [ :branch ] ) )
}
end
entity_message = entity [ :description ] . presence if all_details?
entity_message || = entity [ :title ] . chomp
s_ ( 'JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}' ) % {
user_link : user_link ,
entity_link : entity_link ,
project_link : project_link ,
branch : branch ,
entity_message : entity_message
}
end
def build_jira_link ( title , url )
" [ #{ title } | #{ url } ] "
end
2017-09-10 17:25:29 +05:30
def has_resolution? ( issue )
issue . respond_to? ( :resolution ) && issue . resolution . present?
end
2017-08-17 22:00:37 +05:30
def comment_exists? ( issue , message )
comments = jira_request { issue . comments }
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
comments . present? && comments . any? { | comment | comment . body . include? ( message ) }
end
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
def send_message ( issue , message , remote_link_props )
2017-09-10 17:25:29 +05:30
return unless client_url . present?
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
jira_request do
2020-04-08 14:13:33 +05:30
remote_link = find_remote_link ( issue , remote_link_props [ :object ] [ :url ] )
create_issue_comment ( issue , message ) unless remote_link
remote_link || = issue . remotelink . build
remote_link . save! ( remote_link_props )
2017-08-17 22:00:37 +05:30
2018-11-20 20:47:30 +05:30
log_info ( " Successfully posted " , client_url : client_url )
2019-09-30 21:07:59 +05:30
" SUCCESS: Successfully posted to #{ client_url } . "
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
end
2015-12-23 02:04:40 +05:30
2020-01-01 13:55:28 +05:30
def create_issue_comment ( issue , message )
return unless comment_on_event_enabled
issue . comments . build . save! ( body : message )
end
2017-09-10 17:25:29 +05:30
def find_remote_link ( issue , url )
links = jira_request { issue . remotelink . all }
2019-09-04 21:01:54 +05:30
return unless links
2017-09-10 17:25:29 +05:30
links . find { | link | link . object [ " url " ] == url }
end
2017-08-17 22:00:37 +05:30
def build_remote_link_props ( url : , title : , resolved : false )
status = {
resolved : resolved
}
{
GlobalID : 'GitLab' ,
2019-07-07 11:18:12 +05:30
relationship : 'mentioned on' ,
2017-08-17 22:00:37 +05:30
object : {
url : url ,
title : title ,
status : status ,
2018-03-27 19:54:05 +05:30
icon : {
2019-12-21 20:55:43 +05:30
title : 'GitLab' , url16x16 : asset_url ( Gitlab :: Favicon . main , host : gitlab_config . base_url )
2018-03-27 19:54:05 +05:30
}
2017-08-17 22:00:37 +05:30
}
}
2015-12-23 02:04:40 +05:30
end
def resource_url ( resource )
2017-08-17 22:00:37 +05:30
" #{ Settings . gitlab . base_url . chomp ( " / " ) } #{ resource } "
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def build_entity_url ( noteable_type , entity_id )
polymorphic_url (
[
self . project ,
noteable_type . to_sym
] ,
id : entity_id ,
host : Settings . gitlab . base_url
)
2015-12-23 02:04:40 +05:30
end
2020-05-24 23:13:21 +05:30
def build_entity_meta ( noteable )
if noteable . is_a? ( Commit )
{
id : noteable . short_id ,
description : noteable . safe_message ,
branch : noteable . ref_names ( project . repository ) . first
}
elsif noteable . is_a? ( MergeRequest )
{
id : noteable . to_reference ,
branch : noteable . source_branch
}
else
{ }
end
end
2017-08-17 22:00:37 +05:30
def noteable_name ( noteable )
name = noteable . model_name . singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == " project_snippet " ? " snippet " : name
2015-12-23 02:04:40 +05:30
end
2019-09-30 21:07:59 +05:30
# Handle errors when doing Jira API calls
2017-08-17 22:00:37 +05:30
def jira_request
yield
2020-06-23 00:09:42 +05:30
rescue = > error
2020-03-13 15:44:24 +05:30
@error = error
2020-06-23 00:09:42 +05:30
log_error ( " Error sending message " , client_url : client_url , error : @error . message )
2017-08-17 22:00:37 +05:30
nil
2015-12-23 02:04:40 +05:30
end
2017-09-10 17:25:29 +05:30
def client_url
2019-12-21 20:55:43 +05:30
api_url . presence || url
2017-09-10 17:25:29 +05:30
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url . present?
url_changed?
end
2018-05-09 12:01:36 +05:30
2020-11-24 15:15:51 +05:30
def update_deployment_type?
( api_url_changed? || url_changed? || username_changed? || password_changed? ) &&
can_test?
end
def update_deployment_type
clear_memoization ( :server_info ) # ensure we run the request when we try to update deployment type
results = server_info
return data_fields . deployment_unknown! unless results . present?
case results [ 'deploymentType' ]
when 'Server'
data_fields . deployment_server!
when 'Cloud'
data_fields . deployment_cloud!
else
data_fields . deployment_unknown!
end
end
2018-05-09 12:01:36 +05:30
def self . event_description ( event )
case event
when " merge_request " , " merge_request_events "
2019-09-30 21:07:59 +05:30
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a merge request. " )
2018-05-09 12:01:36 +05:30
when " commit " , " commit_events "
2019-09-30 21:07:59 +05:30
s_ ( " JiraService|Jira comments will be created when an issue gets referenced in a commit. " )
2018-05-09 12:01:36 +05:30
end
end
2015-04-26 12:48:37 +05:30
end
2020-07-28 23:09:34 +05:30
JiraService . prepend_if_ee ( 'EE::JiraService' )