tefoji 1.0.9 → 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: 8279bb8ebda1af1e674da5c4d16932b790e69210a5c163563746a80d44aca92b
4
- data.tar.gz: f17bbc221ed1ec7c4fde90d2e64caddb45078ee0ee2d5cb59e7226d90df14317
3
+ metadata.gz: f8859212ce735cdc0292cbbf58899931446e7a0b90e330899c15f277d071e723
4
+ data.tar.gz: 41baa8c0faa84d4c058adf946eb0a9944a8a997a464a9c83c172b7eb191e19d7
5
5
  SHA512:
6
- metadata.gz: d790551fe749db5eea41ce6889479c3f23e8278ad94744b8be4561d0cceadb33b609949c612224e49bbc21587b8b3cf50501a6eeef1e2d3059c860420e583c15
7
- data.tar.gz: a0b4bd8875027f141a87f2bc960c43143813500db372e0764df428896fc04f34fd3f5b91d3a4d4ca305e9949b88dd3329f4363a1981905f6b4b7d7fe6c7b9fe9
6
+ metadata.gz: 8e4ca95fd24a97cbe1d24c25885abf7cad21a1ec135c2c346bd5e349b583403419c10c98d07ada2ffd5a44bcbc64aedfbce7ff97c754e9ce77c93b2a96133443
7
+ data.tar.gz: bd8c63324127e9d2a291af0d489fcb739d99d587c95360aacb1b5164003ec267c832bd30ad98dd57da399c071f17929dc6aa0605375f3b0554bfe9f6e165d44b
@@ -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',
@@ -50,14 +49,13 @@ module UserFunctions
50
49
  OPERATIONS: 'OPS',
51
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',
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.
@@ -47,19 +50,24 @@ module Tefoji
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,20 +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(@jira_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
86
  # Get information about user in Jira
85
- def get_username(username, fail_if_not_found = true)
86
- jira_get("user?username=#{username}", fail_if_not_found)
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)
87
95
  end
88
96
 
89
97
  # Save authentication YAML to the a file for reuse.
90
- def save_authentication(save_file_name)
98
+ def save_authentication(save_file_name, jira_cloud)
91
99
  backup_file_name = "#{save_file_name}.bak"
92
100
 
93
101
  if File.readable?(save_file_name)
@@ -95,18 +103,22 @@ module Tefoji
95
103
  @logger.info "Saved #{save_file_name} to #{backup_file_name}"
96
104
  end
97
105
 
98
- File.open(save_file_name, 'w') do |f|
99
- f.puts "jira-auth: #{@jira_auth_string}"
100
- 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
+
101
113
  File.chmod(0o600, save_file_name)
102
114
 
103
115
  @logger.info "Saved Jira authentication to #{save_file_name}"
104
116
  end
105
117
 
106
118
  # Create a Jira issue
107
- def create_issue(issue_data)
119
+ def create_issue(issue_data, jira_cloud)
108
120
  request_path = 'issue'
109
- jira_fields = issue_data_to_jira_fields(issue_data)
121
+ jira_fields = issue_data_to_jira_fields(issue_data, jira_cloud)
110
122
  jira_post(request_path, jira_fields)
111
123
  rescue RestClient::Forbidden
112
124
  fatal "Forbidden: could not send #{request_path} request to Jira. "\
@@ -160,12 +172,25 @@ module Tefoji
160
172
  end
161
173
 
162
174
  # https://www.youtube.com/watch?v=JsntlJZ9h1U
163
- def add_watcher(issue_key, watcher)
175
+ def add_watcher(issue_key, watcher, jira_cloud)
164
176
  request_path = "issue/#{issue_key}/watchers"
177
+ watcher = useremail_to_id(watcher) if jira_cloud
165
178
  request_data = watcher
166
179
  jira_post(request_path, request_data)
167
180
  end
168
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
+
169
194
  private
170
195
 
171
196
  def jira_get(jira_request_path, fail_if_not_found = true)
@@ -177,7 +202,6 @@ module Tefoji
177
202
  url = "#{@jira_base_rest_url}/#{jira_request_path}"
178
203
  end
179
204
  headers = @authentication_header
180
-
181
205
  begin
182
206
  response = RestClient.get(url, headers)
183
207
  rescue RestClient::MovedPermanently,
@@ -244,7 +268,7 @@ module Tefoji
244
268
  # Provide the needed translation of user-created data to required format
245
269
  # of Jira requests. This is mostly a list of fussing with the Jira field
246
270
  # names.
247
- def issue_data_to_jira_fields(issue_data)
271
+ def issue_data_to_jira_fields(issue_data, jira_cloud)
248
272
  # Check to ensure we have what we need to create a issue
249
273
 
250
274
  unless issue_data['summary']
@@ -257,64 +281,130 @@ module Tefoji
257
281
  end
258
282
 
259
283
  # build the jira_fields hash describing the issue
260
-
261
284
  # These are required for all issues
262
285
  jira_fields = {
263
286
  'summary' => issue_data['summary'],
264
287
  'project' => { 'key' => issue_data['project'] }
265
288
  }
266
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)
267
302
  # The following are optional
268
303
  if issue_data['description']
269
304
  jira_fields['description'] = issue_data['description']
270
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)
271
345
  if issue_data['assignee']
272
- 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 }
273
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
357
+ end
358
+
274
359
  if issue_data['story_points']
275
- jira_fields['customfield_10002'] = issue_data['story_points'].to_i
360
+ jira_fields['customfield_10038'] = issue_data['story_points'].to_i
276
361
  end
277
362
  if issue_data['team']
278
- jira_fields['customfield_14200'] = { FIELD_VALUE => issue_data['team'] }
363
+ jira_fields['customfield_10052'] = { FIELD_VALUE => issue_data['team'] }
279
364
  end
280
365
  if issue_data['teams']
281
366
  teams = issue_data['teams'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
282
367
  unless teams.empty?
283
- jira_fields['customfield_14201'] = teams.map { |team| { FIELD_VALUE => team } }
368
+ jira_fields['customfield_10066'] = teams.map { |team| { FIELD_VALUE => team } }
284
369
  end
285
370
  end
286
371
  if issue_data['subteam']
287
- jira_fields['customfield_11700'] = [issue_data['subteam']]
372
+ jira_fields['customfield_10045'] = [issue_data['subteam']]
288
373
  end
289
374
  if issue_data['sprint']
290
- jira_fields['customfield_10005'] = issue_data['sprint'].to_i
375
+ jira_fields['customfield_10020'] = issue_data['sprint'].to_i
291
376
  end
292
377
  if issue_data['acceptance']
293
- jira_fields['customfield_11501'] = issue_data['acceptance']
294
- end
295
- if issue_data['labels']
296
- labels = issue_data['labels'].to_a.flatten.reject { |t| t =~ /^\s*$/ }
297
- jira_fields['labels'] = labels unless labels.empty?
298
- end
299
- if issue_data['duedate']
300
- jira_fields['duedate'] = issue_data['duedate']
301
- end
302
- if issue_data['fix_version']
303
- jira_fields['fixVersions'] = [issue_data['fix_version']]
378
+ jira_fields['customfield_10062'] = issue_data['acceptance']
304
379
  end
305
380
  if issue_data['release_notes']
306
- jira_fields['customfield_11100'] = { FIELD_VALUE => issue_data['release_notes'] }
381
+ jira_fields['customfield_10043'] = { FIELD_VALUE => issue_data['release_notes'] }
307
382
  end
308
383
 
309
- if issue_data['components']
310
- components = issue_data[:components].to_a.flatten.reject { |t| t =~ /^\s*$/ }
311
- unless components.empty?
312
- 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
313
399
  end
400
+ jira_fields['customfield_10018'] = issue_data['epic_parent']
314
401
  end
402
+ end
315
403
 
316
- # Default issue type to ISSUE_TASK if it isn't already set
317
- 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
318
408
 
319
409
  if issue_data['type']
320
410
  jira_fields['issuetype'] = { FIELD_NAME => issue_data['type'] }
@@ -324,6 +414,31 @@ module Tefoji
324
414
  end
325
415
  end
326
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
+
327
442
  # If a issue has a specified parent issue, prefer that. The parent issue *should* already
328
443
  # be linked to the main epic. Otherwise, we need to set it to have an epic_parent. This can
329
444
  # either be an epic linked to the main epic or the main epic itself.
@@ -342,23 +457,6 @@ module Tefoji
342
457
  end
343
458
  jira_fields['customfield_10006'] = issue_data['epic_parent']
344
459
  end
345
-
346
- security = ENV['SECURITY'] || issue_data['security']
347
- if security
348
- case security.downcase
349
- when 'confidential'
350
- jira_fields['security'] = { FIELD_ID => '10002' }
351
- when 'internal'
352
- jira_fields['security'] = { FIELD_ID => '10001' }
353
- when 'public'
354
- # Nothing to do here - public is default
355
- else
356
- @logger.fatal "Unknown security type: #{security}"
357
- exit 1
358
- end
359
- end
360
-
361
- return { 'fields' => jira_fields }
362
460
  end
363
461
  end
364
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 = {}
@@ -272,13 +273,13 @@ module Tefoji
272
273
  @jira_api.logger = @logger
273
274
 
274
275
  @jira_api.authenticate(@jira_url, @jira_user, @jira_auth_string)
275
- @jira_api.test_authentication unless @jira_mock
276
+ @jira_api.test_authentication(@jira_cloud) unless @jira_mock
276
277
  end
277
278
 
278
279
  # Save Jira auth data
279
280
  def save_authentication
280
281
  authenticate unless @jira_api
281
- @jira_api.save_authentication(@jira_auth_file)
282
+ @jira_api.save_authentication(@jira_auth_file, @jira_cloud)
282
283
  end
283
284
 
284
285
  private
@@ -298,7 +299,7 @@ module Tefoji
298
299
 
299
300
  feature = variable_substitute(feature_to_do)
300
301
  feature['type'] = JiraApi::ISSUE_FEATURE
301
- @feature_issue = @jira_api.create_issue(feature)
302
+ @feature_issue = @jira_api.create_issue(feature, @jira_cloud)
302
303
  @logger.info "Feature issue: #{@feature_issue['key']}"
303
304
  @feature_issue
304
305
  end
@@ -360,7 +361,7 @@ module Tefoji
360
361
  epic['security'] = 'internal'
361
362
  end
362
363
 
363
- epic_issue = @jira_api.create_issue(epic)
364
+ epic_issue = @jira_api.create_issue(epic, @jira_cloud)
364
365
  epic_issue['short_name'] = short_name
365
366
  @logger.info 'Epic: %16s [%s]' % [epic_issue['key'], short_name]
366
367
  @jira_api.link_issues(@feature_issue['key'], epic_issue['key']) if @feature_issue
@@ -400,7 +401,7 @@ module Tefoji
400
401
  jira_ready_data, raw_issue_data = prepare_jira_ready_data(issue, issue_defaults)
401
402
  next if jira_ready_data.nil? || raw_issue_data.nil?
402
403
 
403
- response_data = @jira_api.create_issue(jira_ready_data)
404
+ response_data = @jira_api.create_issue(jira_ready_data, @jira_cloud)
404
405
  jira_issue = @jira_api.retrieve_issue(response_data['self'])
405
406
  jira_issue['short_name'] = raw_issue_data['short_name']
406
407
 
@@ -610,7 +611,7 @@ module Tefoji
610
611
  issue_key = jira_issue_data['key']
611
612
  watchers = raw_issue_data[deferred_tag].value
612
613
  watchers.each do |watcher|
613
- @jira_api.add_watcher(issue_key, watcher)
614
+ @jira_api.add_watcher(issue_key, watcher, @jira_cloud)
614
615
  @logger.info '%14s: watched by %s' % [issue_key, watcher]
615
616
  end
616
617
  end
@@ -932,7 +933,7 @@ module Tefoji
932
933
  def update_user_validity(username, issue_name, valid_users, invalid_users)
933
934
  if invalid_users.key?(username)
934
935
  invalid_users[username] += [issue_name]
935
- elsif @jira_api.get_username(username, false)
936
+ elsif @jira_api.get_username(username, @jira_cloud, false)
936
937
  valid_users << username
937
938
  else
938
939
  invalid_users[username] = invalid_users.fetch(username, []) + [issue_name]
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.9
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: 2023-02-01 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