Class: Integrations::Jira

Inherits:
BaseIssueTracker show all
Extended by:
Gitlab::Utils::Override
Includes:
ActionView::Helpers::AssetUrlHelper, ApplicationHelper, Gitlab::Routing, Gitlab::Utils::StrongMemoize
Defined in:
app/models/integrations/jira.rb

Constant Summary collapse

PROJECTS_PER_PAGE =
50
JIRA_CLOUD_HOST =
'.atlassian.net'
ATLASSIAN_REFERRER_GITLAB_COM =
{ atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
ATLASSIAN_REFERRER_SELF_MANAGED =
{ atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
SECTION_TYPE_JIRA_TRIGGER =
'jira_trigger'
SECTION_TYPE_JIRA_ISSUES =
'jira_issues'

Constants inherited from Integration

Integration::BASE_CLASSES, Integration::DEV_INTEGRATION_NAMES, Integration::INTEGRATION_NAMES, Integration::PROJECT_SPECIFIC_INTEGRATION_NAMES, Integration::SECTION_TYPE_CONNECTION, Integration::UnknownType

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Instance Attribute Summary

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from ApplicationHelper

#active_when, #add_page_specific_style, #add_page_startup_api_call, #admin_section?, #asset_to_string, #autocomplete_data_sources, #body_data, #body_data_page, #client_class_list, #client_js_flags, #collapsed_sidebar?, #conditional_link_to, #current_action?, #current_controller?, #dispensable_render, #dispensable_render_if_exists, #edited_time_ago_with_tooltip, #external_storage_url_or_path, #extra_config, #gitlab_config, #gitlab_ui_form_for, #hexdigest, #instance_review_permitted?, #last_commit, #linkedin_url, #locale_path, #outdated_browser?, #page_class, #page_filter_path, #page_startup_api_calls, #partial_exists?, #path_to_key, #project_data, promo_host, #promo_host, #promo_url, #read_only_message, #registry_config, #render_if_exists, #show_callout?, #show_last_push_widget?, #simple_sanitize, #static_objects_external_storage_enabled?, #stylesheet_link_tag_defer, #support_url, #system_message_class, #template_exists?, #time_ago_with_tooltip, #truncate_first_line, #twitter_url

Methods included from Gitlab::Routing

add_helpers, includes_helpers, redirect_legacy_paths, url_helpers

Methods inherited from BaseIssueTracker

#activate_disabled_reason, #default?, #handle_properties, #issue_path, #issue_tracker_path, #issue_url, #legacy_properties_data, #new_issue_path, #supports_data_fields?

Methods inherited from Integration

#activate_disabled_reason, #activated?, #api_field_names, #async_execute, #attributes, available_integration_names, available_integration_types, boolean_accessor, build_from_integration, #category, #configurable_events, create_from_active_default_integrations, default_integration, #default_test_event, default_test_event, dev_integration_names, #dup, #editable?, #event_channel_names, event_description, #event_field, event_names, #event_names, field, fields, #fields, find_or_initialize_all_non_project_specific, find_or_initialize_non_project_specific_integration, #global_fields, #group_level?, #inheritable?, inherited_descendants_from_self_or_ancestors_from, #initialize_properties, instance_exists_for?, #instance_level?, integration_name_to_model, integration_name_to_type, integration_names, #json_fields, #operating?, #parent, #project_level?, project_specific_integration_names, prop_accessor, #properties=, #reencrypt_properties, #reset_updated_properties, #secret_fields, #show_active_box?, #supported_events, #supports_data_fields?, #testable?, #to_data_fields_hash, #to_integration_hash, #to_param, #updated_properties

Methods included from ResetSecretFields

#exposing_secrets_fields

Methods included from Loggable

#build_message, #log_error, #log_exception, #log_info, #logger

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Class Method Details

.reference_pattern(only_long: true) ⇒ Object

PROJECT-KEY-NUMBER Examples: JIRA-1, PROJECT-1


88
89
90
# File 'app/models/integrations/jira.rb', line 88

def self.reference_pattern(only_long: true)
  @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
end

.supported_eventsObject

When these are false GitLab does not create cross reference comments on Jira except when an issue gets transitioned.


83
84
85
# File 'app/models/integrations/jira.rb', line 83

def self.supported_events
  %w(commit merge_request)
end

.to_paramObject


148
149
150
# File 'app/models/integrations/jira.rb', line 148

def self.to_param
  'jira'
end

.valid_jira_cloud_url?(url) ⇒ Boolean

Returns:

  • (Boolean)

92
93
94
95
96
# File 'app/models/integrations/jira.rb', line 92

def self.valid_jira_cloud_url?(url)
  return false unless url.present?

  !!URI(url).hostname&.end_with?(JIRA_CLOUD_HOST)
end

Instance Method Details

#api_urlObject


212
213
214
# File 'app/models/integrations/jira.rb', line 212

def api_url
  original_api_url&.delete_suffix('/')
end

#clientObject


126
127
128
129
130
131
132
133
# File 'app/models/integrations/jira.rb', line 126

def client
  @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
end

#close_issue(entity, external_issue, current_user) ⇒ Object


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/models/integrations/jira.rb', line 230

def close_issue(entity, external_issue, current_user)
  issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)

  return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?

  commit_id = case entity
              when Commit then entity.id
              when MergeRequest then entity.diff_head_sha
              end

  commit_url = build_entity_url(:commit, commit_id)

  # Depending on the Jira project's workflow, a comment during transition
  # 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.
  issue = find_issue(issue.key) if transition_issue(issue)
  add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
  log_usage(:close_issue, current_user)
end

#configured?Boolean

Returns:

  • (Boolean)

291
292
293
# File 'app/models/integrations/jira.rb', line 291

def configured?
  active? && valid_connection?
end

#create_cross_reference_note(external_issue, mentioned_in, author) ⇒ Object


251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'app/models/integrations/jira.rb', line 251

def create_cross_reference_note(external_issue, mentioned_in, author)
  unless can_cross_reference?(mentioned_in)
    return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: mentioned_in.model_name.plural.humanize(capitalize: false) }
  end

  jira_issue = find_issue(external_issue.id)

  return unless jira_issue.present?

  mentioned_in_id = mentioned_in.respond_to?(:iid) ? mentioned_in.iid : mentioned_in.id
  mentioned_in_type = mentionable_name(mentioned_in)
  entity_url = build_entity_url(mentioned_in_type, mentioned_in_id)
  entity_meta = build_entity_meta(mentioned_in)

  data = {
    user: {
      name: author.name,
      url: resource_url(user_path(author))
    },
    project: {
      name: project.full_path,
      url: resource_url(project_path(project))
    },
    entity: {
      id: entity_meta[:id],
      name: mentioned_in_type.humanize.downcase,
      url: entity_url,
      title: mentioned_in.title,
      description: entity_meta[:description],
      branch: entity_meta[:branch]
    }
  }

  add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
end

#data_fieldsObject


98
99
100
# File 'app/models/integrations/jira.rb', line 98

def data_fields
  jira_tracker_data || self.build_jira_tracker_data
end

#descriptionObject


144
145
146
# File 'app/models/integrations/jira.rb', line 144

def description
  s_("JiraService|Use Jira as this project's issue tracker.")
end

#execute(push) ⇒ Object


216
217
218
219
# File 'app/models/integrations/jira.rb', line 216

def execute(push)
  # This method is a no-op, because currently Integrations::Jira does not
  # support any events.
end

#find_issue(issue_key, rendered_fields: false, transitions: false) ⇒ Object


221
222
223
224
225
226
227
228
# File 'app/models/integrations/jira.rb', line 221

def find_issue(issue_key, rendered_fields: false, transitions: false)
  expands = []
  expands << 'renderedFields' if rendered_fields
  expands << 'transitions' if transitions
  options = { expand: expands.join(',') } if expands.any?

  jira_request { client.Issue.find(issue_key, options || {}) }
end

#helpObject


135
136
137
138
# File 'app/models/integrations/jira.rb', line 135

def help
  jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('integration/jira/index') }
  s_("JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end

#issue_transition_enabled?Boolean

Returns:

  • (Boolean)

313
314
315
# File 'app/models/integrations/jira.rb', line 313

def issue_transition_enabled?
  jira_issue_transition_automatic || jira_issue_transition_id.present?
end

#issues_urlObject


198
199
200
# File 'app/models/integrations/jira.rb', line 198

def issues_url
  web_url('browse/:id')
end

#new_issue_urlObject


202
203
204
# File 'app/models/integrations/jira.rb', line 202

def new_issue_url
  web_url('secure/CreateIssue!default.jspa')
end

#optionsObject


111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/integrations/jira.rb', line 111

def options
  url = URI.parse(client_url)

  {
    username: username&.strip,
    password: password,
    site: URI.join(url, '/').to_s.delete_suffix('/'), # Intended to find the root
    context_path: (url.path.presence || '/').delete_suffix('/'),
    auth_type: :basic,
    use_cookies: true,
    additional_cookies: ['OBBasicAuth=fromDialog'],
    use_ssl: url.scheme == 'https'
  }
end

#original_api_urlObject


211
# File 'app/models/integrations/jira.rb', line 211

alias_method :original_api_url, :api_url

#original_urlObject


206
# File 'app/models/integrations/jira.rb', line 206

alias_method :original_url, :url

#sectionsObject


152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/integrations/jira.rb', line 152

def sections
  sections = [
    {
      type: SECTION_TYPE_CONNECTION,
      title: s_('Integrations|Connection details'),
      description: help
    },
    {
      type: SECTION_TYPE_JIRA_TRIGGER,
      title: _('Trigger'),
      description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
    }
  ]

  # Jira issues is currently only configurable on the project level.
  if project_level?
    sections.push({
      type: SECTION_TYPE_JIRA_ISSUES,
      title: _('Issues'),
      description: jira_issues_section_description
    })
  end

  sections
end

#set_default_dataObject


102
103
104
105
106
107
108
109
# File 'app/models/integrations/jira.rb', line 102

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']
end

#support_close_issue?Boolean

Returns:

  • (Boolean)

304
305
306
# File 'app/models/integrations/jira.rb', line 304

def support_close_issue?
  true
end

#support_cross_reference?Boolean

Returns:

  • (Boolean)

309
310
311
# File 'app/models/integrations/jira.rb', line 309

def support_cross_reference?
  true
end

#test(_) ⇒ Object


295
296
297
298
299
300
301
# File 'app/models/integrations/jira.rb', line 295

def test(_)
  result = server_info
  success = result.present?
  result = @error&.message unless success

  { success: success, result: result }
end

#titleObject


140
141
142
# File 'app/models/integrations/jira.rb', line 140

def title
  'Jira'
end

#urlObject


207
208
209
# File 'app/models/integrations/jira.rb', line 207

def url
  original_url&.delete_suffix('/')
end

#valid_connection?Boolean

Returns:

  • (Boolean)

287
288
289
# File 'app/models/integrations/jira.rb', line 287

def valid_connection?
  test(nil)[:success]
end

#web_url(path = nil, **params) ⇒ Object Also known as: project_url


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'app/models/integrations/jira.rb', line 178

def web_url(path = nil, **params)
  return '' unless url.present?

  if Gitlab.com?
    params.merge!(ATLASSIAN_REFERRER_GITLAB_COM) unless Gitlab.staging?
  else
    params.merge!(ATLASSIAN_REFERRER_SELF_MANAGED) unless Gitlab.dev_or_test_env?
  end

  url = Addressable::URI.parse(self.url)
  url.path = url.path.delete_suffix('/')
  url.path << "/#{path.delete_prefix('/').delete_suffix('/')}" if path.present?
  url.query_values = (url.query_values || {}).merge(params)
  url.query_values = nil if url.query_values.empty?

  url.to_s
end