tefoji 1.0.8 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
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