tefoji 1.0.9 → 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: 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