tefoji 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 888097f6ae0c3877e00fa2a2fc0318fb094f5e340621b445f510ffab51b01f85
4
- data.tar.gz: 65af396135aa1f22b1385a3c2ed7738db817733429d2243651b3ced884a2a9fe
3
+ metadata.gz: f8859212ce735cdc0292cbbf58899931446e7a0b90e330899c15f277d071e723
4
+ data.tar.gz: 41baa8c0faa84d4c058adf946eb0a9944a8a997a464a9c83c172b7eb191e19d7
5
5
  SHA512:
6
- metadata.gz: 9d5ce9983a3401f0fe6644fa35725a9c892733d7ad1bdb7ba6051e058fb6aa0a911a2a5cd30aba783c2260c19067fc1112c49c1bf4bf64501b7188096b5ae64e
7
- data.tar.gz: 171807f4bf998fd42dd6296de339934c6a22dcd73722749886abf474a795a2e6dfdb2b6faea95a6a8bd5cf7956bd5f9c6d81d786b1c30c554756c7faba7d18db
6
+ metadata.gz: 8e4ca95fd24a97cbe1d24c25885abf7cad21a1ec135c2c346bd5e349b583403419c10c98d07ada2ffd5a44bcbc64aedfbce7ff97c754e9ce77c93b2a96133443
7
+ data.tar.gz: bd8c63324127e9d2a291af0d489fcb739d99d587c95360aacb1b5164003ec267c832bd30ad98dd57da399c071f17929dc6aa0605375f3b0554bfe9f6e165d44b
@@ -3,7 +3,7 @@ module Logging
3
3
  # Another cheap hack to wrap 'logger' above. Called as 'fatal' rather than 'logger.fatal'
4
4
  # directly, does the exit for us so we can be lazy.
5
5
  def fatal(message)
6
- logger.fatal(message)
6
+ @logger.fatal(message)
7
7
  exit 1
8
8
  end
9
9
  end
@@ -34,7 +34,6 @@ module UserFunctions
34
34
  # Maps of Winston-style Jira names (projects, sprints, to the actual assets
35
35
  def jira_projects
36
36
  {
37
- BOLT: 'BOLT',
38
37
  BUILD_TOOLS: 'BUILD',
39
38
  CLIENT_TOOLS: 'CT',
40
39
  CODE_MANAGEMENT: 'CODEMGMT',
@@ -48,16 +47,15 @@ module UserFunctions
48
47
  MODULES: 'MODULES',
49
48
  MODULES_INTERNAL: 'FM',
50
49
  OPERATIONS: 'OPS',
51
- PDK: 'PDK',
50
+ PDK: 'CONT',
52
51
  PE_INTERNAL: 'PE',
52
+ POOLER: 'POOLER',
53
53
  PROJECT_CENTRAL: 'PC',
54
54
  PUPPETDB: 'PDB',
55
55
  PUPPETSERVER: 'SERVER',
56
56
  PUPPET_AGENT: 'PA',
57
57
  PUPPET: 'PUP',
58
58
  QUALITY_ENGINEERING: 'QENG',
59
- DIO: 'DIO',
60
- RAZOR: 'RAZOR',
61
59
  RELEASE_ENGINEERING: 'RE',
62
60
  SLV: 'SLV',
63
61
  SUPPORT: 'SUP',
@@ -89,12 +87,12 @@ module UserFunctions
89
87
  CD4PE: 'CD4PE',
90
88
  CODE_MANAGEMENT: 'Dumpling',
91
89
  DUMPLING: 'Dumpling',
92
- FACTER: "Phoenix",
90
+ FACTER: 'Phoenix',
93
91
  INSTALLER: 'Installer and Management',
94
92
  NETWORKING: 'Network Automation',
95
93
  OPERATIONS: 'Operations',
96
94
  PE: 'Dumpling',
97
- PLATFORM_OS: "Phoenix",
95
+ PLATFORM_OS: 'Phoenix',
98
96
  PUPPETDB: 'Dumpling',
99
97
  PUPPETSERVER: 'Dumpling',
100
98
  QE: 'Quality Engineering',
data/lib/tefoji/cli.rb CHANGED
@@ -8,6 +8,9 @@ module Tefoji
8
8
  DEFAULT_JIRA_URL = 'https://tickets.puppetlabs.com'
9
9
  JIRA_TEST_URL = 'https://jira-app-dev-1.ops.puppetlabs.net'
10
10
  DEFAULT_JIRA_AUTH_FILE = "#{ENV['HOME']}/.tefoji-auth.yaml"
11
+ JIRA_CLOUD_URLS = [
12
+ 'https://puppet.atlassian.net'
13
+ ]
11
14
 
12
15
  DOCUMENTATION = <<~DOCOPT
13
16
  Generate Jira issues from YAML files.
@@ -43,23 +46,28 @@ module Tefoji
43
46
  @user_options = parse_options(argv)
44
47
 
45
48
  if @user_options['--version']
46
- warn "tefoji version #{Gem.loaded_specs['tefoji'].version.to_s}"
49
+ warn "tefoji version #{Gem.loaded_specs['tefoji'].version}"
47
50
  exit 0
48
51
  end
49
52
 
53
+ @user_options['--jira'] = JIRA_TEST_URL if @user_options['--jira-test']
54
+ @user_options['--jira'] = DEFAULT_JIRA_URL if @user_options['--jira'].nil?
55
+
50
56
  @user_options['jira-auth-string'] = nil
51
57
  @user_options['jira-auth-file'] = @user_options['--jira-auth'] || DEFAULT_JIRA_AUTH_FILE
52
58
 
59
+ @user_options['jira-cloud'] = false
60
+ @user_options['jira-cloud'] = true if JIRA_CLOUD_URLS.include?(@user_options['--jira'])
61
+
53
62
  jira_auth_file = @user_options['jira-auth-file']
54
- if File.file?(jira_auth_file)
55
- authentication = YAML.load_file(jira_auth_file)
56
- if authentication.key?('jira-auth')
57
- @user_options['jira-auth-string'] = authentication['jira-auth']
58
- end
59
- end
63
+ return unless File.file?(jira_auth_file)
60
64
 
61
- @user_options['--jira'] = JIRA_TEST_URL if @user_options['--jira-test']
62
- @user_options['--jira'] = DEFAULT_JIRA_URL if @user_options['--jira'].nil?
65
+ authentication = YAML.load_file(jira_auth_file)
66
+ if @user_options['jira-cloud'] && authentication.key?('jira-cloud-auth')
67
+ @user_options['jira-auth-string'] = authentication['jira-cloud-auth']
68
+ elsif authentication.key?('jira-auth')
69
+ @user_options['jira-auth-string'] = authentication['jira-auth']
70
+ end
63
71
  end
64
72
 
65
73
  # Iterate through issue templates, validating each. If that goes well, generate
@@ -2,6 +2,7 @@ require 'base64'
2
2
  require 'io/console'
3
3
  require 'json'
4
4
  require 'rest-client'
5
+ RestClient.log = 'stdout'
5
6
 
6
7
  module Tefoji
7
8
  ## An interface to send API request to Jira using the rest-client gem.
@@ -14,6 +15,7 @@ module Tefoji
14
15
  include Logging
15
16
 
16
17
  ## Jira field keys
18
+ FIELD_ACCOUNT_ID = 'accountId'
17
19
  FIELD_ID = 'id'
18
20
  FIELD_KEY = 'key'
19
21
  FIELD_NAME = 'name'
@@ -74,19 +76,26 @@ module Tefoji
74
76
 
75
77
  # Do this so we can inform the user quickly that their credentials didn't work
76
78
  # There may be a better test, but this is the one the original Winston uses
77
- def test_authentication
78
- get_username
79
+ def test_authentication(jira_cloud)
80
+ get_username(@jira_username, jira_cloud)
79
81
  rescue RestClient::Forbidden
80
82
  fatal 'Forbidden: either the authentication is incorrect or ' \
81
83
  'Jira might be requiring a CAPTCHA response from the web interface.'
82
84
  end
83
85
 
84
- def get_username
85
- jira_get("user?username=#{@jira_username}")
86
+ # Get information about user in Jira
87
+ def get_username(username, jira_cloud, fail_if_not_found = true)
88
+ search_parameters = if jira_cloud
89
+ "user/search?query=#{username}"
90
+ else
91
+ "user?username=#{username}"
92
+ end
93
+
94
+ jira_get(search_parameters, fail_if_not_found)
86
95
  end
87
96
 
88
97
  # Save authentication YAML to the a file for reuse.
89
- def save_authentication(save_file_name)
98
+ def save_authentication(save_file_name, jira_cloud)
90
99
  backup_file_name = "#{save_file_name}.bak"
91
100
 
92
101
  if File.readable?(save_file_name)
@@ -94,18 +103,22 @@ module Tefoji
94
103
  @logger.info "Saved #{save_file_name} to #{backup_file_name}"
95
104
  end
96
105
 
97
- File.open(save_file_name, 'w') do |f|
98
- f.puts "jira-auth: #{@jira_auth_string}"
99
- end
106
+ jira_auth_string = if jira_cloud
107
+ "jira-auth: #{@jira_auth_string}\n"
108
+ else
109
+ "jira-cloud-auth: #{@jira_auth_string}\n"
110
+ end
111
+ File.write(save_file_name, jira_auth_string)
112
+
100
113
  File.chmod(0o600, save_file_name)
101
114
 
102
115
  @logger.info "Saved Jira authentication to #{save_file_name}"
103
116
  end
104
117
 
105
118
  # Create a Jira issue
106
- def create_issue(issue_data)
119
+ def create_issue(issue_data, jira_cloud)
107
120
  request_path = 'issue'
108
- jira_fields = issue_data_to_jira_fields(issue_data)
121
+ jira_fields = issue_data_to_jira_fields(issue_data, jira_cloud)
109
122
  jira_post(request_path, jira_fields)
110
123
  rescue RestClient::Forbidden
111
124
  fatal "Forbidden: could not send #{request_path} request to Jira. "\
@@ -159,15 +172,28 @@ module Tefoji
159
172
  end
160
173
 
161
174
  # https://www.youtube.com/watch?v=JsntlJZ9h1U
162
- def add_watcher(issue_key, watcher)
175
+ def add_watcher(issue_key, watcher, jira_cloud)
163
176
  request_path = "issue/#{issue_key}/watchers"
177
+ watcher = useremail_to_id(watcher) if jira_cloud
164
178
  request_data = watcher
165
179
  jira_post(request_path, request_data)
166
180
  end
167
181
 
182
+ # jira cloud api calls now require an accountId rather than a username. This method will convert
183
+ # emails or display names to accountIds.
184
+ def useremail_to_id(email)
185
+ response = get_username(email, true, true)[0]
186
+ id = response['accountId']
187
+ if id.nil? || id.empty?
188
+ @logger.error "accountId not found for #{email}."
189
+ exit 1
190
+ end
191
+ return id
192
+ end
193
+
168
194
  private
169
195
 
170
- def jira_get(jira_request_path)
196
+ def jira_get(jira_request_path, fail_if_not_found = true)
171
197
  # Jira likes to send complete URLs for responses. Handle
172
198
  # the case where we've received a 'self' query from Jira with
173
199
  # fully formed url
@@ -176,7 +202,6 @@ module Tefoji
176
202
  url = "#{@jira_base_rest_url}/#{jira_request_path}"
177
203
  end
178
204
  headers = @authentication_header
179
-
180
205
  begin
181
206
  response = RestClient.get(url, headers)
182
207
  rescue RestClient::MovedPermanently,
@@ -191,7 +216,9 @@ module Tefoji
191
216
  rescue RestClient::NotFound,
192
217
  SocketError,
193
218
  Errno::ECONNREFUSED => e
194
- fatal "Cannot connect to #{@jira_base_rest_url}: #{e.message}"
219
+ # Return False if not found rather than fail to allow for checking the existence of users.
220
+ fatal "Cannot connect to #{@jira_base_rest_url}: #{e.message}" if fail_if_not_found
221
+ return false
195
222
  end
196
223
 
197
224
  return JSON.parse(response.body)
@@ -241,7 +268,7 @@ module Tefoji
241
268
  # Provide the needed translation of user-created data to required format
242
269
  # of Jira requests. This is mostly a list of fussing with the Jira field
243
270
  # names.
244
- def issue_data_to_jira_fields(issue_data)
271
+ def issue_data_to_jira_fields(issue_data, jira_cloud)
245
272
  # Check to ensure we have what we need to create a issue
246
273
 
247
274
  unless issue_data['summary']
@@ -254,64 +281,130 @@ module Tefoji
254
281
  end
255
282
 
256
283
  # build the jira_fields hash describing the issue
257
-
258
284
  # These are required for all issues
259
285
  jira_fields = {
260
286
  'summary' => issue_data['summary'],
261
287
  'project' => { 'key' => issue_data['project'] }
262
288
  }
263
289
 
290
+ set_common_jira_fields(issue_data, jira_fields)
291
+
292
+ if jira_cloud
293
+ set_cloud_jira_fields(issue_data, jira_fields)
294
+ else
295
+ set_onprem_jira_fields(issue_data, jira_fields)
296
+ end
297
+
298
+ return { 'fields' => jira_fields }
299
+ end
300
+
301
+ def set_common_jira_fields(issue_data, jira_fields)
264
302
  # The following are optional
265
303
  if issue_data['description']
266
304
  jira_fields['description'] = issue_data['description']
267
305
  end
306
+
307
+ if issue_data['labels']
308
+ labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
309
+ jira_fields['labels'] = labels unless labels.empty?
310
+ end
311
+ if issue_data['duedate']
312
+ jira_fields['duedate'] = issue_data['duedate']
313
+ end
314
+ if issue_data['fix_version']
315
+ jira_fields['fixVersions'] = [issue_data['fix_version']]
316
+ end
317
+
318
+ if issue_data['components']
319
+ components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
320
+ unless components.empty?
321
+ jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
322
+ end
323
+ end
324
+
325
+ # Default issue type to ISSUE_TASK if it isn't already set
326
+ jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
327
+
328
+ security = ENV['SECURITY'] || issue_data['security']
329
+ return unless security
330
+
331
+ case security.downcase
332
+ when 'confidential'
333
+ jira_fields['security'] = { FIELD_ID => '10002' }
334
+ when 'internal'
335
+ jira_fields['security'] = { FIELD_ID => '10001' }
336
+ when 'public'
337
+ # Nothing to do here - public is default
338
+ else
339
+ @logger.fatal "Unknown security type: #{security}"
340
+ exit 1
341
+ end
342
+ end
343
+
344
+ def set_cloud_jira_fields(issue_data, jira_fields)
268
345
  if issue_data['assignee']
269
- jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
346
+ assignee = issue_data['assignee']
347
+ assignee = useremail_to_id(assignee)
348
+ jira_fields['assignee'] = { FIELD_ACCOUNT_ID => assignee }
349
+ end
350
+
351
+ if issue_data['type']
352
+ jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
353
+ # If this is an epic, we need to add an epic name
354
+ if issue_data['type'].casecmp?(ISSUE_EPIC)
355
+ jira_fields['customfield_10011'] = issue_data['epic_name'] || issue_data['summary']
356
+ end
270
357
  end
358
+
271
359
  if issue_data['story_points']
272
- jira_fields['customfield_10002'] = issue_data['story_points'].to_i
360
+ jira_fields['customfield_10038'] = issue_data['story_points'].to_i
273
361
  end
274
362
  if issue_data['team']
275
- jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
363
+ jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
276
364
  end
277
365
  if issue_data['teams']
278
366
  teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
279
367
  unless teams.empty?
280
- jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
368
+ jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
281
369
  end
282
370
  end
283
371
  if issue_data['subteam']
284
- jira_fields['customfield_11700'] = [issue_data['subteam']]
372
+ jira_fields['customfield_10045'] = [issue_data['subteam']]
285
373
  end
286
374
  if issue_data['sprint']
287
- jira_fields['customfield_10005'] = issue_data['sprint'].to_i
375
+ jira_fields['customfield_10020'] = issue_data['sprint'].to_i
288
376
  end
289
377
  if issue_data['acceptance']
290
- jira_fields['customfield_11501'] = issue_data['acceptance']
291
- end
292
- if issue_data['labels']
293
- labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
294
- jira_fields['labels'] = labels unless labels.empty?
295
- end
296
- if issue_data['duedate']
297
- jira_fields['duedate'] = issue_data['duedate']
298
- end
299
- if issue_data['fix_version']
300
- jira_fields['fixVersions'] = [issue_data['fix_version']]
378
+ jira_fields['customfield_10062'] = issue_data['acceptance']
301
379
  end
302
380
  if issue_data['release_notes']
303
- jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
381
+ jira_fields['customfield_10043'] = { FIELD_VALUE => issue_data['release_notes'] }
304
382
  end
305
383
 
306
- if issue_data['components']
307
- components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
308
- unless components.empty?
309
- jira_fields['components'] = components.map { |component| { FIELD_NAME => component } }
384
+ # If a issue has a specified parent issue, prefer that. The parent issue *should* already
385
+ # be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
386
+ # either be an epic linked to the main epic or the main epic itself.
387
+
388
+ if issue_data['parent']
389
+ unless issue_data['type'].casecmp?(ISSUE_SUB_TASK) || !issue_data['type']
390
+ @logger.fatal "A issue with a parent must be classified as a Sub-issue\n\n#{issue_data}"
391
+ exit 1
392
+ end
393
+ jira_fields['issuetype'] = { FIELD_NAME => ISSUE_SUB_TASK }
394
+ jira_fields['parent'] = { FIELD_KEY => issue_data['parent'] }
395
+ elsif issue_data['epic_parent']
396
+ if issue_data['type'].casecmp?(ISSUE_SUB_TASK)
397
+ @logger.fatal "This issue cannot be a subtask of an epic\n\n#{issue_data}"
398
+ exit 1
310
399
  end
400
+ jira_fields['customfield_10018'] = issue_data['epic_parent']
311
401
  end
402
+ end
312
403
 
313
- # Default issue type to ISSUE_TASK if it isn't already set
314
- jira_fields['issuetype'] = { FIELD_NAME => ISSUE_TASK }
404
+ def set_onprem_jira_fields(issue_data, jira_fields)
405
+ if issue_data['assignee']
406
+ jira_fields['assignee'] = { FIELD_NAME => issue_data['assignee'] }
407
+ end
315
408
 
316
409
  if issue_data['type']
317
410
  jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
@@ -321,6 +414,31 @@ module Tefoji
321
414
  end
322
415
  end
323
416
 
417
+ if issue_data['story_points']
418
+ jira_fields['customfield_10002'] = issue_data['story_points'].to_i
419
+ end
420
+ if issue_data['team']
421
+ jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
422
+ end
423
+ if issue_data['teams']
424
+ teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
425
+ unless teams.empty?
426
+ jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
427
+ end
428
+ end
429
+ if issue_data['subteam']
430
+ jira_fields['customfield_11700'] = [issue_data['subteam']]
431
+ end
432
+ if issue_data['sprint']
433
+ jira_fields['customfield_10005'] = issue_data['sprint'].to_i
434
+ end
435
+ if issue_data['acceptance']
436
+ jira_fields['customfield_11501'] = issue_data['acceptance']
437
+ end
438
+ if issue_data['release_notes']
439
+ jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
440
+ end
441
+
324
442
  # If a issue has a specified parent issue, prefer that. The parent issue *should* already
325
443
  # be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
326
444
  # either be an epic linked to the main epic or the main epic itself.
@@ -339,23 +457,6 @@ module Tefoji
339
457
  end
340
458
  jira_fields['customfield_10006'] = issue_data['epic_parent']
341
459
  end
342
-
343
- security = ENV['SECURITY'] || issue_data['security']
344
- if security
345
- case security.downcase
346
- when 'confidential'
347
- jira_fields['security'] = { FIELD_ID => '10002' }
348
- when 'internal'
349
- jira_fields['security'] = { FIELD_ID => '10001' }
350
- when 'public'
351
- # Nothing to do here - public is default
352
- else
353
- @logger.fatal "Unknown security type: #{security}"
354
- exit 1
355
- end
356
- end
357
-
358
- return { 'fields' => jira_fields }
359
460
  end
360
461
  end
361
462
 
data/lib/tefoji.rb CHANGED
@@ -28,6 +28,7 @@ module Tefoji
28
28
  @jira_auth_string = user_options['jira-auth-string']
29
29
  @jira_auth_file = user_options['jira-auth-file']
30
30
  @jira_mock = user_options['--jira-mock']
31
+ @jira_cloud = user_options['jira-cloud']
31
32
 
32
33
  @no_notes = user_options['--no-notes']
33
34
  @template_data = {}
@@ -74,6 +75,8 @@ module Tefoji
74
75
  resolve_variables(main_template_data['declare'])
75
76
  @logger.debug "Declarations: #{@declarations}"
76
77
 
78
+ check_assignees(main_template_data)
79
+
77
80
  # Process 'before' templates
78
81
  main_template.dig(:includes, :before)&.each do |before|
79
82
  default_epic_saved = @default_target_epic
@@ -270,13 +273,13 @@ module Tefoji
270
273
  @jira_api.logger = @logger
271
274
 
272
275
  @jira_api.authenticate(@jira_url, @jira_user, @jira_auth_string)
273
- @jira_api.test_authentication unless @jira_mock
276
+ @jira_api.test_authentication(@jira_cloud) unless @jira_mock
274
277
  end
275
278
 
276
279
  # Save Jira auth data
277
280
  def save_authentication
278
281
  authenticate unless @jira_api
279
- @jira_api.save_authentication(@jira_auth_file)
282
+ @jira_api.save_authentication(@jira_auth_file, @jira_cloud)
280
283
  end
281
284
 
282
285
  private
@@ -296,7 +299,7 @@ module Tefoji
296
299
 
297
300
  feature = variable_substitute(feature_to_do)
298
301
  feature['type'] = JiraApi::ISSUE_FEATURE
299
- @feature_issue = @jira_api.create_issue(feature)
302
+ @feature_issue = @jira_api.create_issue(feature, @jira_cloud)
300
303
  @logger.info "Feature issue: #{@feature_issue['key']}"
301
304
  @feature_issue
302
305
  end
@@ -358,7 +361,7 @@ module Tefoji
358
361
  epic['security'] = 'internal'
359
362
  end
360
363
 
361
- epic_issue = @jira_api.create_issue(epic)
364
+ epic_issue = @jira_api.create_issue(epic, @jira_cloud)
362
365
  epic_issue['short_name'] = short_name
363
366
  @logger.info 'Epic: %16s [%s]' % [epic_issue['key'], short_name]
364
367
  @jira_api.link_issues(@feature_issue['key'], epic_issue['key']) if @feature_issue
@@ -398,7 +401,7 @@ module Tefoji
398
401
  jira_ready_data, raw_issue_data = prepare_jira_ready_data(issue, issue_defaults)
399
402
  next if jira_ready_data.nil? || raw_issue_data.nil?
400
403
 
401
- response_data = @jira_api.create_issue(jira_ready_data)
404
+ response_data = @jira_api.create_issue(jira_ready_data, @jira_cloud)
402
405
  jira_issue = @jira_api.retrieve_issue(response_data['self'])
403
406
  jira_issue['short_name'] = raw_issue_data['short_name']
404
407
 
@@ -608,7 +611,7 @@ module Tefoji
608
611
  issue_key = jira_issue_data['key']
609
612
  watchers = raw_issue_data[deferred_tag].value
610
613
  watchers.each do |watcher|
611
- @jira_api.add_watcher(issue_key, watcher)
614
+ @jira_api.add_watcher(issue_key, watcher, @jira_cloud)
612
615
  @logger.info '%14s: watched by %s' % [issue_key, watcher]
613
616
  end
614
617
  end
@@ -641,12 +644,17 @@ module Tefoji
641
644
  def user_function_call(function_definition)
642
645
  @logger.debug "function_definition is: #{function_definition}"
643
646
  function_name = function_definition['name']
644
- if function_definition.key?('arguments')
645
- function_arguments = function_definition['arguments'].map do |a|
647
+ argument = function_definition['argument']
648
+ arguments = function_definition['arguments']
649
+
650
+ if argument.is_a?(Array) || arguments.is_a?(Array)
651
+ arguments = argument || function_definition['arguments']
652
+ function_arguments = arguments.map do |a|
646
653
  expand_right_value(a).value
647
654
  end
648
- elsif function_definition.key?('argument')
649
- function_arguments = [expand_right_value(function_definition['argument']).value]
655
+ elsif argument.is_a?(String) || arguments.is_a?(String)
656
+ argument = function_definition['argument'] || function_definition['arguments']
657
+ function_arguments = [expand_right_value(argument).value]
650
658
  else
651
659
  fatal("No arguments supplied to function \"#{function_name}\"")
652
660
  end
@@ -814,6 +822,124 @@ module Tefoji
814
822
  return true
815
823
  end
816
824
 
825
+ # Iterates through the assignees in a template and checks if they exist on jira. If any assignee in
826
+ # the template does not exist, it will fail, outputting a message saying which users failed and
827
+ # what epics/issues they are associated with.
828
+ # NOTE: This check does not guarantee a user is assignable, so there may be cases when a user passes
829
+ # the check but still fails when being assigned a ticket/epic. If we find a reasonable way to
830
+ # check assignability prior to the epic/issue being created, we should implement it in place of
831
+ # using the get_username method.
832
+ def check_assignees(main_template_data)
833
+ valid_users = []
834
+
835
+ invalid_users_to_features = {}
836
+ main_template_data['features']&.each do |feature|
837
+ next unless feature['assignee']
838
+
839
+ assignee_username = get_username_from_assignee(feature['assignee'])
840
+ next if valid_users.include?(assignee_username)
841
+
842
+ update_user_validity(assignee_username, feature['summary'], valid_users, invalid_users_to_features)
843
+ end
844
+
845
+ if main_template_data.dig('feature', 'assignee')
846
+ assignee_username = get_username_from_assignee(main_template_data['feature']['assignee'])
847
+ unless valid_users.include?(assignee_username)
848
+ update_user_validity(assignee_username, main_template_data['feature']['summary'], valid_users,
849
+ invalid_users_to_features)
850
+ end
851
+ end
852
+
853
+ invalid_users_to_epics = {}
854
+ main_template_data['epics']&.each do |epic|
855
+ next unless epic['assignee']
856
+
857
+ assignee_username = get_username_from_assignee(epic['assignee'])
858
+ next if valid_users.include?(assignee_username)
859
+
860
+ update_user_validity(assignee_username, epic['summary'], valid_users, invalid_users_to_epics)
861
+ end
862
+
863
+ if main_template_data.dig('epic', 'assignee')
864
+ assignee_username = get_username_from_assignee(main_template_data['epic']['assignee'])
865
+ unless valid_users.include?(assignee_username)
866
+ update_user_validity(assignee_username, main_template_data['epic']['summary'], valid_users,
867
+ invalid_users_to_epics)
868
+ end
869
+ end
870
+
871
+ invalid_users_to_issue_defaults = {}
872
+ if main_template_data.dig('issue_defaults', 'assignee')
873
+ assignee_username = get_username_from_assignee(main_template_data['issue_defaults']['assignee'])
874
+ unless valid_users.include?(assignee_username)
875
+ update_user_validity(assignee_username, 'issue_defaults', valid_users, invalid_users_to_issue_defaults)
876
+ end
877
+ end
878
+
879
+ invalid_users_to_issues = {}
880
+ main_template_data['issues'].each do |issue|
881
+ next unless issue['assignee']
882
+
883
+ assignee_username = get_username_from_assignee(issue['assignee'])
884
+ next if valid_users.include?(assignee_username)
885
+
886
+ update_user_validity(assignee_username, issue['summary'], valid_users, invalid_users_to_issues)
887
+ end
888
+ unless invalid_users_to_features.empty? &&
889
+ invalid_users_to_epics.empty? &&
890
+ invalid_users_to_issue_defaults.empty? &&
891
+ invalid_users_to_issues.empty?
892
+ invalid_user_message = "Invalid assignees:\n"
893
+ end
894
+ unless invalid_users_to_features.empty?
895
+ invalid_users_to_features.each do |assignee, feature|
896
+ invalid_user_message += "Assignee #{assignee} associated with features #{feature}\n"
897
+ end
898
+ end
899
+ unless invalid_users_to_epics.empty?
900
+ invalid_users_to_epics.each do |assignee, epic|
901
+ invalid_user_message += "Assignee #{assignee} associated with epics #{epic}\n"
902
+ end
903
+ end
904
+ unless invalid_users_to_issue_defaults.empty?
905
+ invalid_users_to_issue_defaults.each do |assignee, _|
906
+ invalid_user_message += "Assignee #{assignee} associated with issue_defaults\n"
907
+ end
908
+ end
909
+ unless invalid_users_to_issues.empty?
910
+ invalid_users_to_issues.each do |assignee, issues|
911
+ invalid_user_message += "Assignee #{assignee} associated with issues #{issues}\n"
912
+ end
913
+ end
914
+ fatal invalid_user_message if invalid_user_message
915
+ end
916
+
917
+ def get_username_from_assignee(assignee)
918
+ # Assignee is a variable or direct input
919
+ username = expand_string(assignee) if assignee.is_a?(String)
920
+ # Assignee is a function
921
+ if assignee.is_a?(Hash) && assignee.key?('function')
922
+ username = user_function_call(assignee['function'])
923
+ end
924
+
925
+ unless username
926
+ fatal "invalid assignee format for issue: #{issue['short_name']} failed: must be a function or a string"
927
+ end
928
+
929
+ username = username.value unless username.is_a?(String)
930
+ username
931
+ end
932
+
933
+ def update_user_validity(username, issue_name, valid_users, invalid_users)
934
+ if invalid_users.key?(username)
935
+ invalid_users[username] += [issue_name]
936
+ elsif @jira_api.get_username(username, @jira_cloud, false)
937
+ valid_users << username
938
+ else
939
+ invalid_users[username] = invalid_users.fetch(username, []) + [issue_name]
940
+ end
941
+ end
942
+
817
943
  def logger_initialize(log_location)
818
944
  logger = Logger.new(log_location, progname: 'tefoji', level: @log_level)
819
945
  logger.formatter = proc do |severity, _, script_name, message|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tefoji
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet Labs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-11 00:00:00.000000000 Z
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry-byebug